Run black

This commit is contained in:
Oskar Hahn 2019-01-06 16:22:33 +01:00
parent 800055a5ea
commit eddbd86d3a
186 changed files with 9061 additions and 6570 deletions

View File

@ -5,7 +5,7 @@ from parser import parser
if len(sys.argv) < 2: if len(sys.argv) < 2:
args = parser.parse_args(['--help']) args = parser.parse_args(["--help"])
else: else:
args = parser.parse_args() args = parser.parse_args()

View File

@ -2,47 +2,48 @@ from parser import command, argument, call
import yaml import yaml
import requirements import requirements
FAIL = '\033[91m' FAIL = "\033[91m"
SUCCESS = '\033[92m' SUCCESS = "\033[92m"
RESET = '\033[0m' 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): def check(args=None):
""" """
Checks for pep8 and other code styling conventions. Checks for pep8 and other code styling conventions.
""" """
value = call('flake8 --max-line-length=150 --statistics openslides tests') value = call("flake8 --max-line-length=150 --statistics openslides tests")
value += call('python -m mypy openslides/ tests/') value += call("python -m mypy openslides/ tests/")
return value return value
@command('travis', help='Runs the code that travis does') @command("travis", help="Runs the code that travis does")
def travis(args=None): def travis(args=None):
""" """
Runs all commands that travis tests. Runs all commands that travis tests.
""" """
return_codes = [] return_codes = []
with open('.travis.yml') as f: with open(".travis.yml") as f:
travis = yaml.load(f) travis = yaml.load(f)
for line in travis['script']: for line in travis["script"]:
print('Run: {}'.format(line)) print("Run: {}".format(line))
return_code = call(line) return_code = call(line)
return_codes.append(return_code) return_codes.append(return_code)
if return_code: if return_code:
print(FAIL + 'fail!\n' + RESET) print(FAIL + "fail!\n" + RESET)
else: else:
print(SUCCESS + 'success!\n' + RESET) print(SUCCESS + "success!\n" + RESET)
# Retuns True if one command exited with a different statuscode then 1 # Retuns True if one command exited with a different statuscode then 1
return bool(list(filter(bool, return_codes))) return bool(list(filter(bool, return_codes)))
@argument('-r', '--requirements', nargs='?', @argument("-r", "--requirements", nargs="?", default="requirements.txt")
default='requirements.txt') @command(
@command('min_requirements', "min_requirements",
help='Prints a pip line to install the minimum supported versions of ' help="Prints a pip line to install the minimum supported versions of "
'the requirements.') "the requirements.",
)
def min_requirements(args=None): def min_requirements(args=None):
""" """
Prints a pip install command to install the minimal supported versions of a 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) pip install $(python make min_requirements)
""" """
def get_lowest_versions(requirements_file): def get_lowest_versions(requirements_file):
with open(requirements_file) as f: with open(requirements_file) as f:
for req in requirements.parse(f): for req in requirements.parse(f):
@ -62,22 +64,20 @@ def min_requirements(args=None):
if spec == ">=": if spec == ">=":
yield "{}=={}".format(req.name, version) yield "{}=={}".format(req.name, version)
print(' '.join(get_lowest_versions(args.requirements))) print(" ".join(get_lowest_versions(args.requirements)))
@command('clean', @command("clean", help="Deletes unneeded files and folders")
help='Deletes unneeded files and folders')
def clean(args=None): def clean(args=None):
""" """
Deletes all .pyc and .orig files and empty folders. Deletes all .pyc and .orig files and empty folders.
""" """
call('find -name "*.pyc" -delete') call('find -name "*.pyc" -delete')
call('find -name "*.orig" -delete') call('find -name "*.orig" -delete')
call('find -type d -empty -delete') call("find -type d -empty -delete")
@command('format', @command("format", help="Format code with isort and black")
help='Format code with isort and black')
def isort(args=None): def isort(args=None):
call('isort --recursive openslides tests') call("isort --recursive openslides tests")
call('black --py36 openslides tests') call("black --py36 openslides tests")

View File

@ -1,7 +1,7 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from subprocess import call as _call from subprocess import call as _call
parser = ArgumentParser(description='Development scripts for OpenSlides') parser = ArgumentParser(description="Development scripts for OpenSlides")
subparsers = parser.add_subparsers() subparsers = parser.add_subparsers()
@ -12,6 +12,7 @@ def command(*args, **kwargs):
The arguments to this decorator are used as arguments for the argparse The arguments to this decorator are used as arguments for the argparse
command. command.
""" """
class decorator: class decorator:
def __init__(self, func): def __init__(self, func):
self.parser = subparsers.add_parser(*args, **kwargs) 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 Does only work if the decorated function was decorated with the
command-decorator before. command-decorator before.
""" """
def decorator(func): def decorator(func):
func.parser.add_argument(*args, **kwargs) func.parser.add_argument(*args, **kwargs)
def wrapper(*func_args, **func_kwargs): def wrapper(*func_args, **func_kwargs):
return func(*func_args, **func_kwargs) return func(*func_args, **func_kwargs)
return wrapper return wrapper
return decorator return decorator

View File

@ -1,7 +1,7 @@
__author__ = 'OpenSlides Team <support@openslides.org>' __author__ = "OpenSlides Team <support@openslides.org>"
__description__ = 'Presentation and assembly system' __description__ = "Presentation and assembly system"
__version__ = '3.0-dev' __version__ = "3.0-dev"
__license__ = 'MIT' __license__ = "MIT"
__url__ = 'https://openslides.org' __url__ = "https://openslides.org"
args = None args = None

View File

@ -42,7 +42,7 @@ def main():
else: else:
# Check for unknown_args. # Check for unknown_args.
if 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. # Save arguments, if one wants to access them later.
arguments.set_arguments(known_args) arguments.set_arguments(known_args)
@ -59,11 +59,11 @@ def get_parser():
if len(sys.argv) == 1: if len(sys.argv) == 1:
# Use start subcommand if called by openslides console script without # Use start subcommand if called by openslides console script without
# any other arguments. # any other arguments.
sys.argv.append('start') sys.argv.append("start")
# Init parser # Init parser
description = 'Start script for OpenSlides.' description = "Start script for OpenSlides."
if 'manage.py' not in sys.argv[0]: if "manage.py" not in sys.argv[0]:
description += """ description += """
If it is called without any argument, this will be treated as If it is called without any argument, this will be treated as
if it is called with the 'start' subcommand. That means 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 (without the two hyphen-minus characters) to list them all. Type
'%(prog)s help <subcommand>' for help on a specific subcommand. '%(prog)s help <subcommand>' for help on a specific subcommand.
""" """
parser = ExceptionArgumentParser( parser = ExceptionArgumentParser(description=description, epilog=epilog)
description=description,
epilog=epilog)
# Add version argument # Add version argument
parser.add_argument( parser.add_argument(
'--version', "--version",
action='version', action="version",
version=openslides.__version__, version=openslides.__version__,
help='Show version number and exit.') help="Show version number and exit.",
)
# Init subparsers # Init subparsers
subparsers = parser.add_subparsers( subparsers = parser.add_subparsers(
dest='subcommand', dest="subcommand",
title='Available subcommands', title="Available subcommands",
description="Type '%s <subcommand> --help' for help on a " description="Type '%s <subcommand> --help' for help on a "
"specific subcommand." % parser.prog, # type: ignore "specific subcommand." % parser.prog, # type: ignore
help='You can choose only one subcommand at once.', help="You can choose only one subcommand at once.",
metavar='') metavar="",
)
# Subcommand start # Subcommand start
start_help = ( start_help = (
'Setup settings and database, start webserver, launch the ' "Setup settings and database, start webserver, launch the "
'default web browser and open the webinterface. The environment ' "default web browser and open the webinterface. The environment "
'variable DJANGO_SETTINGS_MODULE is ignored.') "variable DJANGO_SETTINGS_MODULE is ignored."
)
subcommand_start = subparsers.add_parser( subcommand_start = subparsers.add_parser(
'start', "start", description=start_help, help=start_help
description=start_help, )
help=start_help)
subcommand_start.set_defaults(callback=start) subcommand_start.set_defaults(callback=start)
subcommand_start.add_argument( subcommand_start.add_argument(
'--no-browser', "--no-browser",
action='store_true', action="store_true",
help='Do not launch the default web browser.') help="Do not launch the default web browser.",
)
subcommand_start.add_argument( subcommand_start.add_argument(
'--debug-email', "--debug-email",
action='store_true', action="store_true",
help='Change the email backend to console output.') help="Change the email backend to console output.",
)
subcommand_start.add_argument( subcommand_start.add_argument(
'--no-template-caching', "--no-template-caching",
action='store_true', action="store_true",
default=False, default=False,
help='Disables caching of templates.') help="Disables caching of templates.",
)
subcommand_start.add_argument( subcommand_start.add_argument(
'--host', "--host",
action='store', action="store",
default='0.0.0.0', default="0.0.0.0",
help='IP address to listen on. Default is 0.0.0.0.') help="IP address to listen on. Default is 0.0.0.0.",
)
subcommand_start.add_argument( subcommand_start.add_argument(
'--port', "--port",
action='store', action="store",
default='8000', default="8000",
help='Port to listen on. Default is 8000.') help="Port to listen on. Default is 8000.",
)
subcommand_start.add_argument( subcommand_start.add_argument(
'--settings_dir', "--settings_dir", action="store", default=None, help="The settings directory."
action='store', )
default=None,
help='The settings directory.')
subcommand_start.add_argument( subcommand_start.add_argument(
'--settings_filename', "--settings_filename",
action='store', action="store",
default='settings.py', default="settings.py",
help='The used settings file name. The file is created, if it does not exist.') help="The used settings file name. The file is created, if it does not exist.",
)
subcommand_start.add_argument( subcommand_start.add_argument(
'--local-installation', "--local-installation",
action='store_true', action="store_true",
help='Store settings and user files in a local directory.') help="Store settings and user files in a local directory.",
)
# Subcommand createsettings # Subcommand createsettings
createsettings_help = 'Creates the settings file.' createsettings_help = "Creates the settings file."
subcommand_createsettings = subparsers.add_parser( subcommand_createsettings = subparsers.add_parser(
'createsettings', "createsettings", description=createsettings_help, help=createsettings_help
description=createsettings_help, )
help=createsettings_help)
subcommand_createsettings.set_defaults(callback=createsettings) subcommand_createsettings.set_defaults(callback=createsettings)
subcommand_createsettings.add_argument( subcommand_createsettings.add_argument(
'--settings_dir', "--settings_dir",
action='store', action="store",
default=None, 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( subcommand_createsettings.add_argument(
'--settings_filename', "--settings_filename",
action='store', action="store",
default='settings.py', default="settings.py",
help='The used settings file name. The file is created, if it does not exist.') help="The used settings file name. The file is created, if it does not exist.",
)
subcommand_createsettings.add_argument( subcommand_createsettings.add_argument(
'--local-installation', "--local-installation",
action='store_true', action="store_true",
help='Store settings and user files in a local directory.') help="Store settings and user files in a local directory.",
)
# Help text for several Django subcommands # Help text for several Django subcommands
django_subcommands = ( django_subcommands = (
('backupdb', 'Backups the SQLite3 database.'), ("backupdb", "Backups the SQLite3 database."),
('createsuperuser', 'Creates or resets the admin user.'), ("createsuperuser", "Creates or resets the admin user."),
('migrate', 'Updates database schema.'), ("migrate", "Updates database schema."),
('runserver', 'Starts the Tornado webserver.'), ("runserver", "Starts the Tornado webserver."),
) )
for django_subcommand, help_text in django_subcommands: for django_subcommand, help_text in django_subcommands:
subparsers._choices_actions.append( # type: ignore subparsers._choices_actions.append( # type: ignore
subparsers._ChoicesPseudoAction( # type: ignore subparsers._ChoicesPseudoAction( # type: ignore
django_subcommand, django_subcommand, (), help_text
(), )
help_text)) )
return parser return parser
@ -188,8 +195,10 @@ def start(args):
""" """
Starts OpenSlides: Runs migrations and runs runserver. Starts OpenSlides: Runs migrations and runs runserver.
""" """
raise OpenSlidesError('The start command does not work anymore. ' + raise OpenSlidesError(
'Please use `createsettings`, `migrate` and `runserver`.') "The start command does not work anymore. "
+ "Please use `createsettings`, `migrate` and `runserver`."
)
settings_dir = args.settings_dir settings_dir = args.settings_dir
settings_filename = args.settings_filename settings_filename = args.settings_filename
local_installation = is_local_installation() local_installation = is_local_installation()
@ -212,10 +221,10 @@ def start(args):
from django.conf import settings from django.conf import settings
if args.debug_email: if args.debug_email:
settings.EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' settings.EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# Migrate database # Migrate database
call_command('migrate') call_command("migrate")
# Open the browser # Open the browser
if not args.no_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. # Use flag --insecure to serve static files even if DEBUG is False.
call_command( call_command(
'runserver', "runserver",
'{}:{}'.format(args.host, args.port), "{}:{}".format(args.host, args.port),
noreload=False, # Means True, see above. noreload=False, # Means True, see above.
insecure=True, insecure=True,
) )
@ -248,11 +257,14 @@ def createsettings(args):
if settings_dir is None: if settings_dir is None:
settings_dir = get_local_settings_dir() settings_dir = get_local_settings_dir()
context = { context = {
'openslides_user_data_dir': repr(os.path.join(os.getcwd(), 'personal_data', 'var')), "openslides_user_data_dir": repr(
'debug': 'True'} os.path.join(os.getcwd(), "personal_data", "var")
),
"debug": "True",
}
settings_path = write_settings(settings_dir, args.settings_filename, **context) 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__": if __name__ == "__main__":

View File

@ -1 +1 @@
default_app_config = 'openslides.agenda.apps.AgendaAppConfig' default_app_config = "openslides.agenda.apps.AgendaAppConfig"

View File

@ -8,14 +8,14 @@ class ItemAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Item and ItemViewSet. 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 # TODO: In the following method we use full_data['is_hidden'] and
# full_data['is_internal'] but this can be out of date. # full_data['is_internal'] but this can be out of date.
async def get_restricted_data( async def get_restricted_data(
self, self, full_data: List[Dict[str, Any]], user_id: int
full_data: List[Dict[str, Any]], ) -> List[Dict[str, Any]]:
user_id: int) -> List[Dict[str, Any]]:
""" """
Returns the restricted serialized data for the instance prepared Returns the restricted serialized data for the instance prepared
for the user. for the user.
@ -25,6 +25,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
We remove comments for non admins/managers and a lot of fields of We remove comments for non admins/managers and a lot of fields of
internal items for users without permission to see internal items. internal items for users without permission to see internal items.
""" """
def filtered_data(full_data, blocked_keys): def filtered_data(full_data, blocked_keys):
""" """
Returns a new dict like full_data but with all blocked_keys removed. 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} return {key: full_data[key] for key in whitelist}
# Parse data. # Parse data.
if full_data and await async_has_perm(user_id, 'agenda.can_see'): 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 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. # Managers with special permission can see everything.
data = full_data 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 # Non managers with special permission can see everything but
# comments and hidden items. # comments and hidden items.
data = [full for full in full_data if not full['is_hidden']] # filter hidden items data = [
blocked_keys = ('comment',) full for full in full_data if not full["is_hidden"]
data = [filtered_data(full, blocked_keys) for full in data] # remove blocked_keys ] # filter hidden items
blocked_keys = ("comment",)
data = [
filtered_data(full, blocked_keys) for full in data
] # remove blocked_keys
else: else:
# Users without special permission for internal items. # Users without special permission for internal items.
# In internal and hidden case managers and non managers see only some fields # 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. # 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. # 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(( blocked_keys_internal_hidden_case = set(full_data[0].keys()) - set(
'id', ("id", "title", "speakers", "speaker_list_closed", "content_object")
'title', )
'speakers',
'speaker_list_closed',
'content_object'))
# In non internal case managers see everything and non managers see # In non internal case managers see everything and non managers see
# everything but comments. # 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] = [] blocked_keys_non_internal_hidden_case: Iterable[str] = []
can_see_hidden = True can_see_hidden = True
else: else:
blocked_keys_non_internal_hidden_case = ('comment',) blocked_keys_non_internal_hidden_case = ("comment",)
can_see_hidden = False can_see_hidden = False
data = [] data = []
for full in full_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 # Same filtering for internal and hidden items
data.append(filtered_data(full, blocked_keys_internal_hidden_case)) data.append(
elif full['is_internal']: 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 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: else:
data = [] data = []

View File

@ -6,8 +6,8 @@ from ..utils.projector import register_projector_elements
class AgendaAppConfig(AppConfig): class AgendaAppConfig(AppConfig):
name = 'openslides.agenda' name = "openslides.agenda"
verbose_name = 'OpenSlides Agenda' verbose_name = "OpenSlides Agenda"
angular_site_module = True angular_site_module = True
angular_projector_module = True angular_projector_module = True
@ -20,7 +20,8 @@ class AgendaAppConfig(AppConfig):
from .signals import ( from .signals import (
get_permission_change_data, get_permission_change_data,
listen_to_related_object_post_delete, listen_to_related_object_post_delete,
listen_to_related_object_post_save) listen_to_related_object_post_save,
)
from .views import ItemViewSet from .views import ItemViewSet
from . import serializers # noqa from . import serializers # noqa
from ..utils.access_permissions import required_user from ..utils.access_permissions import required_user
@ -31,22 +32,27 @@ class AgendaAppConfig(AppConfig):
# Connect signals. # Connect signals.
post_save.connect( post_save.connect(
listen_to_related_object_post_save, 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( pre_delete.connect(
listen_to_related_object_post_delete, 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( permission_change.connect(
get_permission_change_data, get_permission_change_data, dispatch_uid="agenda_get_permission_change_data"
dispatch_uid='agenda_get_permission_change_data') )
# Register viewsets. # 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 # 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): def get_config_variables(self):
from .config_variables import get_config_variables from .config_variables import get_config_variables
return get_config_variables() return get_config_variables()
def get_startup_elements(self): def get_startup_elements(self):
@ -54,11 +60,11 @@ class AgendaAppConfig(AppConfig):
Yields all Cachables required on startup i. e. opening the websocket Yields all Cachables required on startup i. e. opening the websocket
connection. connection.
""" """
yield self.get_model('Item') yield self.get_model("Item")
def required_users(element: Dict[str, Any]) -> Set[int]: def required_users(element: Dict[str, Any]) -> Set[int]:
""" """
Returns all user ids that are displayed as speaker in the given element. 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"])

View File

@ -10,97 +10,108 @@ def get_config_variables():
It has to be evaluated during app loading (see apps.py). It has to be evaluated during app loading (see apps.py).
""" """
yield ConfigVariable( yield ConfigVariable(
name='agenda_enable_numbering', name="agenda_enable_numbering",
label='Enable numbering for agenda items', label="Enable numbering for agenda items",
input_type='boolean', input_type="boolean",
default_value=True, default_value=True,
weight=200, weight=200,
group='Agenda', group="Agenda",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='agenda_number_prefix', name="agenda_number_prefix",
default_value='', default_value="",
label='Numbering prefix for agenda items', label="Numbering prefix for agenda items",
help_text='This prefix will be set if you run the automatic agenda numbering.', help_text="This prefix will be set if you run the automatic agenda numbering.",
weight=210, weight=210,
group='Agenda', group="Agenda",
subgroup='General', subgroup="General",
validators=(MaxLengthValidator(20),)) validators=(MaxLengthValidator(20),),
)
yield ConfigVariable( yield ConfigVariable(
name='agenda_numeral_system', name="agenda_numeral_system",
default_value='arabic', default_value="arabic",
input_type='choice', input_type="choice",
label='Numeral system for agenda items', label="Numeral system for agenda items",
choices=( choices=(
{'value': 'arabic', 'display_name': 'Arabic'}, {"value": "arabic", "display_name": "Arabic"},
{'value': 'roman', 'display_name': 'Roman'}), {"value": "roman", "display_name": "Roman"},
),
weight=215, weight=215,
group='Agenda', group="Agenda",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='agenda_start_event_date_time', name="agenda_start_event_date_time",
default_value=None, default_value=None,
input_type='datetimepicker', input_type="datetimepicker",
label='Begin of event', label="Begin of event",
help_text='Input format: DD.MM.YYYY HH:MM', help_text="Input format: DD.MM.YYYY HH:MM",
weight=220, weight=220,
group='Agenda', group="Agenda",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='agenda_hide_internal_items_on_projector', name="agenda_hide_internal_items_on_projector",
default_value=True, default_value=True,
input_type='boolean', input_type="boolean",
label='Hide internal items when projecting subitems', label="Hide internal items when projecting subitems",
weight=225, weight=225,
group='Agenda', group="Agenda",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='agenda_new_items_default_visibility', name="agenda_new_items_default_visibility",
default_value='2', default_value="2",
input_type='choice', input_type="choice",
choices=( choices=(
{'value': '1', 'display_name': 'Public item'}, {"value": "1", "display_name": "Public item"},
{'value': '2', 'display_name': 'Internal item'}, {"value": "2", "display_name": "Internal item"},
{'value': '3', 'display_name': 'Hidden item'}), {"value": "3", "display_name": "Hidden item"},
label='Default visibility for new agenda items (except topics)', ),
label="Default visibility for new agenda items (except topics)",
weight=227, weight=227,
group='Agenda', group="Agenda",
subgroup='General') subgroup="General",
)
# List of speakers # List of speakers
yield ConfigVariable( yield ConfigVariable(
name='agenda_show_last_speakers', name="agenda_show_last_speakers",
default_value=1, default_value=1,
input_type='integer', input_type="integer",
label='Number of last speakers to be shown on the projector', label="Number of last speakers to be shown on the projector",
weight=230, weight=230,
group='Agenda', group="Agenda",
subgroup='List of speakers', subgroup="List of speakers",
validators=(MinValueValidator(0),)) validators=(MinValueValidator(0),),
)
yield ConfigVariable( yield ConfigVariable(
name='agenda_countdown_warning_time', name="agenda_countdown_warning_time",
default_value=0, default_value=0,
input_type='integer', input_type="integer",
label='Show orange countdown in the last x seconds of speaking time', label="Show orange countdown in the last x seconds of speaking time",
help_text='Enter duration in seconds. Choose 0 to disable warning color.', help_text="Enter duration in seconds. Choose 0 to disable warning color.",
weight=235, weight=235,
group='Agenda', group="Agenda",
subgroup='List of speakers', subgroup="List of speakers",
validators=(MinValueValidator(0),)) validators=(MinValueValidator(0),),
)
yield ConfigVariable( yield ConfigVariable(
name='agenda_couple_countdown_and_speakers', name="agenda_couple_countdown_and_speakers",
default_value=False, default_value=False,
input_type='boolean', input_type="boolean",
label='Couple countdown with the list of speakers', label="Couple countdown with the list of speakers",
help_text='[Begin speech] starts the countdown, [End speech] stops the countdown.', help_text="[Begin speech] starts the countdown, [End speech] stops the countdown.",
weight=240, weight=240,
group='Agenda', group="Agenda",
subgroup='List of speakers') subgroup="List of speakers",
)

View File

@ -14,55 +14,109 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('contenttypes', '0002_remove_content_type_name'), ("contenttypes", "0002_remove_content_type_name"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Item', name="Item",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('item_number', models.CharField(blank=True, max_length=255)), "id",
('comment', models.TextField(blank=True, null=True)), models.AutoField(
('closed', models.BooleanField(default=False)), auto_created=True,
('type', models.IntegerField(choices=[(1, 'Agenda item'), (2, 'Hidden item')], default=2)), primary_key=True,
('duration', models.CharField(blank=True, max_length=5, null=True)), serialize=False,
('weight', models.IntegerField(default=10000)), verbose_name="ID",
('object_id', models.PositiveIntegerField(blank=True, null=True)), ),
('speaker_list_closed', models.BooleanField(default=False)), ),
('content_type', models.ForeignKey( ("item_number", models.CharField(blank=True, max_length=255)),
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contenttypes.ContentType')), ("comment", models.TextField(blank=True, null=True)),
('parent', models.ForeignKey( ("closed", models.BooleanField(default=False)),
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='agenda.Item')), (
"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={ options={
'permissions': ( "permissions": (
('can_see', 'Can see agenda'), ("can_see", "Can see agenda"),
('can_manage', 'Can manage agenda'), ("can_manage", "Can manage agenda"),
('can_see_hidden_items', 'Can see hidden items and time scheduling of agenda')), (
'default_permissions': (), "can_see_hidden_items",
"Can see hidden items and time scheduling of agenda",
),
),
"default_permissions": (),
}, },
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='Speaker', name="Speaker",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('begin_time', models.DateTimeField(null=True)), "id",
('end_time', models.DateTimeField(null=True)), models.AutoField(
('weight', models.IntegerField(null=True)), auto_created=True,
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='speakers', to='agenda.Item')), primary_key=True,
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 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={ options={
'permissions': (('can_be_speaker', 'Can put oneself on the list of speakers'),), "permissions": (
'default_permissions': (), ("can_be_speaker", "Can put oneself on the list of speakers"),
),
"default_permissions": (),
}, },
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='item', name="item", unique_together=set([("content_type", "object_id")])
unique_together=set([('content_type', 'object_id')]),
), ),
] ]

View File

@ -11,7 +11,7 @@ def convert_duration(apps, schema_editor):
IntegerField. It uses the temporary field for proper renaming the field IntegerField. It uses the temporary field for proper renaming the field
in the end. in the end.
""" """
Item = apps.get_model('agenda', 'Item') Item = apps.get_model("agenda", "Item")
for item in Item.objects.all(): for item in Item.objects.all():
duration = item.duration duration = item.duration
item.duration_tmp = None item.duration_tmp = None
@ -20,7 +20,7 @@ def convert_duration(apps, schema_editor):
item.duration_tmp = int(duration) item.duration_tmp = int(duration)
elif isinstance(duration, str): elif isinstance(duration, str):
# Assuming format (h)h:(m)m. If not, new value is None. # 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]): if len(split) == 2 and is_int(split[0]) and is_int(split[1]):
# Calculate new duration: hours * 60 + minutes. # Calculate new duration: hours * 60 + minutes.
item.duration_tmp = int(split[0]) * 60 + int(split[1]) item.duration_tmp = int(split[0]) * 60 + int(split[1])
@ -41,26 +41,17 @@ def is_int(s):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("agenda", "0001_initial")]
('agenda', '0001_initial'),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='item', model_name="item",
name='duration_tmp', name="duration_tmp",
field=models.IntegerField(blank=True, null=True), field=models.IntegerField(blank=True, null=True),
), ),
migrations.RunPython( migrations.RunPython(convert_duration),
convert_duration migrations.RemoveField(model_name="item", name="duration"),
),
migrations.RemoveField(
model_name='item',
name='duration',
),
migrations.RenameField( migrations.RenameField(
model_name='item', model_name="item", old_name="duration_tmp", new_name="duration"
old_name='duration_tmp',
new_name='duration',
), ),
] ]

View File

@ -11,24 +11,31 @@ from openslides.utils.migrations import (
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("agenda", "0002_item_duration")]
('agenda', '0002_item_duration'),
]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='item', name="item",
options={ options={
'default_permissions': (), "default_permissions": (),
'permissions': ( "permissions": (
('can_see', 'Can see agenda'), ("can_see", "Can see agenda"),
('can_manage', 'Can manage agenda'), ("can_manage", "Can manage agenda"),
('can_manage_list_of_speakers', 'Can manage list of speakers'), ("can_manage_list_of_speakers", "Can manage list of speakers"),
('can_see_hidden_items', 'Can see hidden items and time scheduling of agenda') (
) "can_see_hidden_items",
"Can see hidden items and time scheduling of agenda",
),
),
}, },
), ),
migrations.RunPython(add_permission_to_groups_based_on_existing_permission( migrations.RunPython(
'can_manage', 'item', 'agenda', 'can_manage_list_of_speakers', 'Can manage list of speakers' add_permission_to_groups_based_on_existing_permission(
)), "can_manage",
"item",
"agenda",
"can_manage_list_of_speakers",
"Can manage list of speakers",
)
),
] ]

View File

@ -7,14 +7,12 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("agenda", "0003_auto_20170818_1202")]
('agenda', '0003_auto_20170818_1202'),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='speaker', model_name="speaker",
name='marked', name="marked",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), )
] ]

View File

@ -11,44 +11,47 @@ from openslides.utils.migrations import (
def delete_old_can_see_hidden_permission(apps, schema_editor): 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): if len(perm):
perm = perm.delete() perm = perm.delete()
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("agenda", "0004_speaker_marked")]
('agenda', '0004_speaker_marked'),
]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='item', name="item",
options={ options={
'default_permissions': (), "default_permissions": (),
'permissions': ( "permissions": (
('can_see', 'Can see agenda'), ("can_see", "Can see agenda"),
('can_manage', 'Can manage agenda'), ("can_manage", "Can manage agenda"),
('can_manage_list_of_speakers', 'Can manage list of speakers'), ("can_manage_list_of_speakers", "Can manage list of speakers"),
('can_see_internal_items', 'Can see internal items and time scheduling of agenda') (
) "can_see_internal_items",
"Can see internal items and time scheduling of agenda",
),
),
}, },
), ),
migrations.AlterField( migrations.AlterField(
model_name='item', model_name="item",
name='type', name="type",
field=models.IntegerField( field=models.IntegerField(
choices=[ choices=[(1, "Agenda item"), (2, "Internal item"), (3, "Hidden item")],
(1, 'Agenda item'), default=3,
(2, 'Internal item'),
(3, 'Hidden item')
],
default=3
), ),
), ),
migrations.RunPython(add_permission_to_groups_based_on_existing_permission( migrations.RunPython(
'can_see_hidden_items', 'item', 'agenda', 'can_see_internal_items', 'Can see internal items and time scheduling of agenda' 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), migrations.RunPython(delete_old_can_see_hidden_permission),
] ]

View File

@ -24,13 +24,14 @@ class ItemManager(models.Manager):
Customized model manager with special methods for agenda tree and Customized model manager with special methods for agenda tree and
numbering. numbering.
""" """
def get_full_queryset(self): def get_full_queryset(self):
""" """
Returns the normal queryset with all items. In the background all Returns the normal queryset with all items. In the background all
speakers and related items (topics, motions, assignments) are speakers and related items (topics, motions, assignments) are
prefetched from the database. 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): 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. Generator that yields a list of items and their children.
""" """
for item in items: 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 item_is_not_public = True
yield item yield item
else: else:
item_is_not_public = False item_is_not_public = False
yield from yield_items( yield from yield_items(
item_children[item.pk], item_children[item.pk], parent_is_not_public=item_is_not_public
parent_is_not_public=item_is_not_public) )
yield from yield_items(root_items) 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 If only_item_type is given, the tree hides items with other types and
all of their children. all of their children.
""" """
queryset = self.order_by('weight') queryset = self.order_by("weight")
item_children: Dict[int, List[Item]] = defaultdict(list) item_children: Dict[int, List[Item]] = defaultdict(list)
root_items = [] root_items = []
for item in queryset: for item in queryset:
@ -88,7 +92,9 @@ class ItemManager(models.Manager):
If include_content is True, the yielded dictonaries have no key 'id' If include_content is True, the yielded dictonaries have no key 'id'
but a key 'item' with the entire object. 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): def get_children(items):
""" """
@ -98,7 +104,9 @@ class ItemManager(models.Manager):
if include_content: if include_content:
yield dict(item=item, children=get_children(item_children[item.pk])) yield dict(item=item, children=get_children(item_children[item.pk]))
else: 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) yield from get_children(root_items)
@ -110,6 +118,7 @@ class ItemManager(models.Manager):
The tree has to be a nested object. For example: The tree has to be a nested object. For example:
[{"id": 1}, {"id": 2, "children": [{"id": 3}]}] [{"id": 1}, {"id": 2, "children": [{"id": 3}]}]
""" """
def walk_items(tree, parent=None): def walk_items(tree, parent=None):
""" """
Generator that returns each item in the tree as tuple. Generator that returns each item in the tree as tuple.
@ -118,15 +127,17 @@ class ItemManager(models.Manager):
weight of the item. weight of the item.
""" """
for weight, element in enumerate(tree): for weight, element in enumerate(tree):
yield (element['id'], parent, weight) yield (element["id"], parent, weight)
yield from walk_items(element.get('children', []), element['id']) yield from walk_items(element.get("children", []), element["id"])
touched_items: Set[int] = set() touched_items: Set[int] = set()
db_items = dict((item.pk, item) for item in Item.objects.all()) db_items = dict((item.pk, item) for item in Item.objects.all())
for item_id, parent_id, weight in walk_items(tree): for item_id, parent_id, weight in walk_items(tree):
# Check that the item is only once in the tree to prevent invalid trees # Check that the item is only once in the tree to prevent invalid trees
if item_id in touched_items: 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) touched_items.add(item_id)
try: try:
@ -143,36 +154,40 @@ class ItemManager(models.Manager):
db_item.save() db_item.save()
@transaction.atomic @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 Auto numbering of the agenda according to the numeral_system. Manually
added item numbers will be overwritten. added item numbers will be overwritten.
""" """
def walk_tree(tree, number=None): def walk_tree(tree, number=None):
for index, tree_element in enumerate(tree): for index, tree_element in enumerate(tree):
# Calculate number of visable agenda items. # 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) item_number = to_roman(index + 1)
else: else:
item_number = str(index + 1) item_number = str(index + 1)
if number is not None: if number is not None:
item_number = '.'.join((number, item_number)) item_number = ".".join((number, item_number))
# Add prefix. # Add prefix.
if config['agenda_number_prefix']: if config["agenda_number_prefix"]:
item_number_tmp = "%s %s" % (config['agenda_number_prefix'], item_number) item_number_tmp = "%s %s" % (
config["agenda_number_prefix"],
item_number,
)
else: else:
item_number_tmp = item_number item_number_tmp = item_number
# Save the new value and go down the tree. # Save the new value and go down the tree.
tree_element['item'].item_number = item_number_tmp tree_element["item"].item_number = item_number_tmp
tree_element['item'].save() tree_element["item"].save()
walk_tree(tree_element['children'], item_number) walk_tree(tree_element["children"], item_number)
# Start numbering visable agenda items. # Start numbering visable agenda items.
walk_tree(self.get_tree(only_item_type=Item.AGENDA_ITEM, include_content=True)) walk_tree(self.get_tree(only_item_type=Item.AGENDA_ITEM, include_content=True))
# Reset number of hidden items. # Reset number of hidden items.
for item in self.get_only_non_public_items(): for item in self.get_only_non_public_items():
item.item_number = '' item.item_number = ""
item.save() item.save()
@ -180,18 +195,20 @@ class Item(RESTModelMixin, models.Model):
""" """
An Agenda Item An Agenda Item
""" """
access_permissions = ItemAccessPermissions() access_permissions = ItemAccessPermissions()
objects = ItemManager() objects = ItemManager()
can_see_permission = 'agenda.can_see' can_see_permission = "agenda.can_see"
AGENDA_ITEM = 1 AGENDA_ITEM = 1
INTERNAL_ITEM = 2 INTERNAL_ITEM = 2
HIDDEN_ITEM = 3 HIDDEN_ITEM = 3
ITEM_TYPE = ( ITEM_TYPE = (
(AGENDA_ITEM, ugettext_lazy('Agenda item')), (AGENDA_ITEM, ugettext_lazy("Agenda item")),
(INTERNAL_ITEM, ugettext_lazy('Internal item')), (INTERNAL_ITEM, ugettext_lazy("Internal item")),
(HIDDEN_ITEM, ugettext_lazy('Hidden item'))) (HIDDEN_ITEM, ugettext_lazy("Hidden item")),
)
item_number = models.CharField(blank=True, max_length=255) item_number = models.CharField(blank=True, max_length=255)
""" """
@ -208,9 +225,7 @@ class Item(RESTModelMixin, models.Model):
Flag, if the item is finished. Flag, if the item is finished.
""" """
type = models.IntegerField( type = models.IntegerField(choices=ITEM_TYPE, default=HIDDEN_ITEM)
choices=ITEM_TYPE,
default=HIDDEN_ITEM)
""" """
Type of the agenda item. Type of the agenda item.
@ -223,11 +238,12 @@ class Item(RESTModelMixin, models.Model):
""" """
parent = models.ForeignKey( parent = models.ForeignKey(
'self', "self",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
related_name='children') related_name="children",
)
""" """
The parent item in the agenda tree. The parent item in the agenda tree.
""" """
@ -238,10 +254,8 @@ class Item(RESTModelMixin, models.Model):
""" """
content_type = models.ForeignKey( content_type = models.ForeignKey(
ContentType, ContentType, on_delete=models.SET_NULL, null=True, blank=True
on_delete=models.SET_NULL, )
null=True,
blank=True)
""" """
Field for generic relation to a related object. Type of the object. 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. Field for generic relation to a related object. General field to the related object.
""" """
speaker_list_closed = models.BooleanField( speaker_list_closed = models.BooleanField(default=False)
default=False)
""" """
True, if the list of speakers is closed. True, if the list of speakers is closed.
""" """
@ -265,11 +278,15 @@ class Item(RESTModelMixin, models.Model):
class Meta: class Meta:
default_permissions = () default_permissions = ()
permissions = ( permissions = (
('can_see', 'Can see agenda'), ("can_see", "Can see agenda"),
('can_manage', 'Can manage agenda'), ("can_manage", "Can manage agenda"),
('can_manage_list_of_speakers', 'Can manage list of speakers'), ("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_internal_items",
"Can see internal items and time scheduling of agenda",
),
)
unique_together = ("content_type", "object_id")
def __str__(self): def __str__(self):
return self.title return self.title
@ -280,10 +297,11 @@ class Item(RESTModelMixin, models.Model):
list of speakers projector element is disabled. list of speakers projector element is disabled.
""" """
Projector.remove_any( Projector.remove_any(
skip_autoupdate=skip_autoupdate, skip_autoupdate=skip_autoupdate, name="agenda/list-of-speakers", id=self.pk
name='agenda/list-of-speakers', )
id=self.pk) return super().delete( # type: ignore
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore skip_autoupdate=skip_autoupdate, *args, **kwargs
)
@property @property
def title(self): def title(self):
@ -293,8 +311,10 @@ class Item(RESTModelMixin, models.Model):
try: try:
return self.content_object.get_agenda_title() return self.content_object.get_agenda_title()
except AttributeError: except AttributeError:
raise NotImplementedError('You have to provide a get_agenda_title ' raise NotImplementedError(
'method on your related model.') "You have to provide a get_agenda_title "
"method on your related model."
)
@property @property
def title_with_type(self): def title_with_type(self):
@ -304,8 +324,10 @@ class Item(RESTModelMixin, models.Model):
try: try:
return self.content_object.get_agenda_title_with_type() return self.content_object.get_agenda_title_with_type()
except AttributeError: except AttributeError:
raise NotImplementedError('You have to provide a get_agenda_title_with_type ' raise NotImplementedError(
'method on your related model.') "You have to provide a get_agenda_title_with_type "
"method on your related model."
)
def is_internal(self): def is_internal(self):
""" """
@ -314,8 +336,9 @@ class Item(RESTModelMixin, models.Model):
Attention! This executes one query for each ancestor of the item. Attention! This executes one query for each ancestor of the item.
""" """
return (self.type == self.INTERNAL_ITEM or return self.type == self.INTERNAL_ITEM or (
(self.parent is not None and self.parent.is_internal())) self.parent is not None and self.parent.is_internal()
)
def is_hidden(self): def is_hidden(self):
""" """
@ -324,15 +347,16 @@ class Item(RESTModelMixin, models.Model):
Attention! This executes one query for each ancestor of the item. Attention! This executes one query for each ancestor of the item.
""" """
return (self.type == self.HIDDEN_ITEM or return self.type == self.HIDDEN_ITEM or (
(self.parent is not None and self.parent.is_hidden())) self.parent is not None and self.parent.is_hidden()
)
def get_next_speaker(self): def get_next_speaker(self):
""" """
Returns the speaker object of the speaker who is next. Returns the speaker object of the speaker who is next.
""" """
try: 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: except IndexError:
# The list of speakers is empty. # The list of speakers is empty.
return None return None
@ -342,6 +366,7 @@ class SpeakerManager(models.Manager):
""" """
Manager for Speaker model. Provides a customized add method. Manager for Speaker model. Provides a customized add method.
""" """
def add(self, user, item, skip_autoupdate=False): def add(self, user, item, skip_autoupdate=False):
""" """
Customized manager method to prevent anonymous users to be on the 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(): if self.filter(user=user, item=item, begin_time=None).exists():
raise OpenSlidesError( 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): if isinstance(user, AnonymousUser):
raise OpenSlidesError( raise OpenSlidesError(
_('An anonymous user can not be on lists of speakers.')) _("An anonymous user can not be on lists of speakers.")
weight = (self.filter(item=item).aggregate( )
models.Max('weight'))['weight__max'] or 0) weight = (
self.filter(item=item).aggregate(models.Max("weight"))["weight__max"] or 0
)
speaker = self.model(item=item, user=user, weight=weight + 1) speaker = self.model(item=item, user=user, weight=weight + 1)
speaker.save(force_insert=True, skip_autoupdate=skip_autoupdate) speaker.save(force_insert=True, skip_autoupdate=skip_autoupdate)
return speaker return speaker
@ -368,17 +396,12 @@ class Speaker(RESTModelMixin, models.Model):
objects = SpeakerManager() objects = SpeakerManager()
user = models.ForeignKey( user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
""" """
ForeinKey to the user who speaks. ForeinKey to the user who speaks.
""" """
item = models.ForeignKey( item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name="speakers")
Item,
on_delete=models.CASCADE,
related_name='speakers')
""" """
ForeinKey to the agenda item to which the user want to speak. ForeinKey to the agenda item to which the user want to speak.
""" """
@ -405,9 +428,7 @@ class Speaker(RESTModelMixin, models.Model):
class Meta: class Meta:
default_permissions = () default_permissions = ()
permissions = ( permissions = (("can_be_speaker", "Can put oneself on the list of speakers"),)
('can_be_speaker', 'Can put oneself on the list of speakers'),
)
def __str__(self): def __str__(self):
return str(self.user) return str(self.user)
@ -420,8 +441,11 @@ class Speaker(RESTModelMixin, models.Model):
speaking, end his speech. speaking, end his speech.
""" """
try: try:
current_speaker = (Speaker.objects.filter(item=self.item, end_time=None) current_speaker = (
.exclude(begin_time=None).get()) Speaker.objects.filter(item=self.item, end_time=None)
.exclude(begin_time=None)
.get()
)
except Speaker.DoesNotExist: except Speaker.DoesNotExist:
pass pass
else: else:
@ -431,15 +455,21 @@ class Speaker(RESTModelMixin, models.Model):
self.weight = None self.weight = None
self.begin_time = timezone.now() self.begin_time = timezone.now()
self.save() # Here, the item is saved and causes an autoupdate. self.save() # Here, the item is saved and causes an autoupdate.
if config['agenda_couple_countdown_and_speakers']: if config["agenda_couple_countdown_and_speakers"]:
countdown, created = Countdown.objects.get_or_create(pk=1, defaults={ countdown, created = Countdown.objects.get_or_create(
'default_time': config['projector_default_countdown'], pk=1,
'countdown_time': config['projector_default_countdown']}) defaults={
"default_time": config["projector_default_countdown"],
"countdown_time": config["projector_default_countdown"],
},
)
if not created: if not created:
countdown.control(action='reset', skip_autoupdate=True) countdown.control(action="reset", skip_autoupdate=True)
countdown.control(action='start', 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): def end_speech(self, skip_autoupdate=False):
""" """
@ -447,13 +477,13 @@ class Speaker(RESTModelMixin, models.Model):
""" """
self.end_time = timezone.now() self.end_time = timezone.now()
self.save(skip_autoupdate=skip_autoupdate) self.save(skip_autoupdate=skip_autoupdate)
if config['agenda_couple_countdown_and_speakers']: if config["agenda_couple_countdown_and_speakers"]:
try: try:
countdown = Countdown.objects.get(pk=1) countdown = Countdown.objects.get(pk=1)
except Countdown.DoesNotExist: except Countdown.DoesNotExist:
pass # Do not create a new countdown on stop action pass # Do not create a new countdown on stop action
else: else:
countdown.control(action='reset', skip_autoupdate=skip_autoupdate) countdown.control(action="reset", skip_autoupdate=skip_autoupdate)
def get_root_rest_element(self): def get_root_rest_element(self):
""" """

View File

@ -16,14 +16,15 @@ class ItemListSlide(ProjectorElement):
Additionally set 'tree' to True to get also children of children. Additionally set 'tree' to True to get also children of children.
""" """
name = 'agenda/item-list'
name = "agenda/item-list"
def check_data(self): def check_data(self):
pk = self.config_entry.get('id') pk = self.config_entry.get("id")
if pk is not None: if pk is not None:
# Children slide. # Children slide.
if not Item.objects.filter(pk=pk).exists(): if not Item.objects.filter(pk=pk).exists():
raise ProjectorException('Item does not exist.') raise ProjectorException("Item does not exist.")
class ListOfSpeakersSlide(ProjectorElement): class ListOfSpeakersSlide(ProjectorElement):
@ -31,21 +32,23 @@ class ListOfSpeakersSlide(ProjectorElement):
Slide definitions for Item model. Slide definitions for Item model.
This is only for list of speakers slide. You have to set 'id'. 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): def check_data(self):
if not Item.objects.filter(pk=self.config_entry.get('id')).exists(): if not Item.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException('Item does not exist.') raise ProjectorException("Item does not exist.")
def update_data(self): 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): class CurrentListOfSpeakersSlide(ProjectorElement):
""" """
Slide for the current list of speakers. 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]: def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:

View File

@ -7,16 +7,17 @@ class SpeakerSerializer(ModelSerializer):
""" """
Serializer for agenda.models.Speaker objects. Serializer for agenda.models.Speaker objects.
""" """
class Meta: class Meta:
model = Speaker model = Speaker
fields = ( fields = (
'id', "id",
'user', "user",
'begin_time', "begin_time",
'end_time', "end_time",
'weight', "weight",
'marked', "marked",
'item', # js-data needs the item-id in the nested object to define relations. "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. A custom field to use for the content_object generic relationship.
""" """
def to_representation(self, value): def to_representation(self, value):
""" """
Returns info concerning the related object extracted from the api URL Returns info concerning the related object extracted from the api URL
of this object. 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): class ItemSerializer(ModelSerializer):
""" """
Serializer for agenda.models.Item objects. Serializer for agenda.models.Item objects.
""" """
content_object = RelatedItemRelatedField(read_only=True) content_object = RelatedItemRelatedField(read_only=True)
speakers = SpeakerSerializer(many=True, read_only=True) speakers = SpeakerSerializer(many=True, read_only=True)
class Meta: class Meta:
model = Item model = Item
fields = ( fields = (
'id', "id",
'item_number', "item_number",
'title', "title",
'title_with_type', "title_with_type",
'comment', "comment",
'closed', "closed",
'type', "type",
'is_internal', "is_internal",
'is_hidden', "is_hidden",
'duration', "duration",
'speakers', "speakers",
'speaker_list_closed', "speaker_list_closed",
'content_object', "content_object",
'weight', "weight",
'parent',) "parent",
)

View File

@ -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 Do not run caching and autoupdate if the instance has a key
skip_autoupdate in the agenda_item_update_information container. skip_autoupdate in the agenda_item_update_information container.
""" """
if hasattr(instance, 'get_agenda_title'): if hasattr(instance, "get_agenda_title"):
if created: if created:
attrs = {} 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): if instance.agenda_item_update_information.get(attr):
attrs[attr] = instance.agenda_item_update_information.get(attr) attrs[attr] = instance.agenda_item_update_information.get(attr)
Item.objects.create(content_object=instance, **attrs) Item.objects.create(content_object=instance, **attrs)
# If the object is created, the related_object has to be sent again. # 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) 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. # If the object has changed, then also the agenda item has to be sent.
inform_changed_data(instance.agenda_item) 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 Receiver function to delete agenda items. It is connected to the signal
django.db.models.signals.post_delete during app loading. 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) content_type = ContentType.objects.get_for_model(instance)
try: try:
# Attention: This delete() call is also necessary to remove # 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 Yields all necessary collections if 'agenda.can_see' or
'agenda.can_see_internal_items' permissions changes. '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: for permission in permissions:
# There could be only one 'agenda.can_see' and then we want to return data. # There could be only one 'agenda.can_see' and then we want to return data.
if (permission.content_type.app_label == agenda_app.label if (
and permission.codename in ('can_see', 'can_see_internal_items')): 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() yield from agenda_app.get_startup_elements()
break break

View File

@ -24,12 +24,14 @@ from .models import Item, Speaker
# Viewsets for the REST API # Viewsets for the REST API
class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericViewSet): class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
""" """
API endpoint for agenda items. API endpoint for agenda items.
There are some views, see check_view_permissions. There are some views, see check_view_permissions.
""" """
access_permissions = ItemAccessPermissions() access_permissions = ItemAccessPermissions()
queryset = Item.objects.all() queryset = Item.objects.all()
@ -37,22 +39,26 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
""" """
Returns True if the user has required permissions. 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) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('metadata', 'manage_speaker', 'tree'): elif self.action in ("metadata", "manage_speaker", "tree"):
result = has_perm(self.request.user, 'agenda.can_see') result = has_perm(self.request.user, "agenda.can_see")
# For manage_speaker and tree requests the rest of the check is # For manage_speaker and tree requests the rest of the check is
# done in the specific method. See below. # done in the specific method. See below.
elif self.action in ('partial_update', 'update', 'sort', 'assign'): elif self.action in ("partial_update", "update", "sort", "assign"):
result = (has_perm(self.request.user, 'agenda.can_see') and result = (
has_perm(self.request.user, 'agenda.can_see_internal_items') and has_perm(self.request.user, "agenda.can_see")
has_perm(self.request.user, 'agenda.can_manage')) and has_perm(self.request.user, "agenda.can_see_internal_items")
elif self.action in ('speak', 'sort_speakers'): and has_perm(self.request.user, "agenda.can_manage")
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 ("speak", "sort_speakers"):
elif self.action in ('numbering', ): result = has_perm(self.request.user, "agenda.can_see") and has_perm(
result = (has_perm(self.request.user, 'agenda.can_see') and self.request.user, "agenda.can_manage_list_of_speakers"
has_perm(self.request.user, 'agenda.can_manage')) )
elif self.action in ("numbering",):
result = has_perm(self.request.user, "agenda.can_see") and has_perm(
self.request.user, "agenda.can_manage"
)
else: else:
result = False result = False
return result return result
@ -82,7 +88,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
return response return response
@detail_route(methods=['POST', 'PATCH', 'DELETE']) @detail_route(methods=["POST", "PATCH", "DELETE"])
def manage_speaker(self, request, pk=None): def manage_speaker(self, request, pk=None):
""" """
Special view endpoint to add users to the list of speakers or remove 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. # Retrieve item.
item = self.get_object() item = self.get_object()
if request.method == 'POST': if request.method == "POST":
# Retrieve user_id # Retrieve user_id
user_id = request.data.get('user') user_id = request.data.get("user")
# Check permissions and other conditions. Get user instance. # Check permissions and other conditions. Get user instance.
if user_id is None: if user_id is None:
# Add oneself # 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) self.permission_denied(request)
if item.speaker_list_closed: 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 user = self.request.user
else: else:
# Add someone 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) self.permission_denied(request)
try: try:
user = get_user_model().objects.get(pk=int(user_id)) user = get_user_model().objects.get(pk=int(user_id))
except (ValueError, get_user_model().DoesNotExist): 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 # Try to add the user. This ensurse that a user is not twice in the
# list of coming speakers. # list of coming speakers.
try: try:
Speaker.objects.add(user, item) Speaker.objects.add(user, item)
except OpenSlidesError as e: except OpenSlidesError as e:
raise ValidationError({'detail': str(e)}) raise ValidationError({"detail": str(e)})
message = _('User %s was successfully added to the list of speakers.') % user message = (
_("User %s was successfully added to the list of speakers.") % user
)
# Send new speaker via autoupdate because users without permission # Send new speaker via autoupdate because users without permission
# to see users may not have it but can get it now. # to see users may not have it but can get it now.
inform_changed_data([user]) inform_changed_data([user])
# Toggle 'marked' for the speaker # Toggle 'marked' for the speaker
elif request.method == 'PATCH': elif request.method == "PATCH":
# Check permissions # 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) self.permission_denied(request)
# Retrieve user_id # Retrieve user_id
user_id = request.data.get('user') user_id = request.data.get("user")
try: try:
user = get_user_model().objects.get(pk=int(user_id)) user = get_user_model().objects.get(pk=int(user_id))
except (ValueError, get_user_model().DoesNotExist): 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): 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) queryset = Speaker.objects.filter(item=item, user=user)
try: try:
@ -160,37 +172,46 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
# there is only one speaker instance or none. # there is only one speaker instance or none.
speaker = queryset.get() speaker = queryset.get()
except Speaker.DoesNotExist: 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: else:
speaker.marked = marked speaker.marked = marked
speaker.save() speaker.save()
if speaker.marked: if speaker.marked:
message = _('You are successfully marked the speaker.') message = _("You are successfully marked the speaker.")
else: else:
message = _('You are successfully unmarked the speaker.') message = _("You are successfully unmarked the speaker.")
else: else:
# request.method == 'DELETE' # request.method == 'DELETE'
speaker_ids = request.data.get('speaker') speaker_ids = request.data.get("speaker")
# Check permissions and other conditions. Get speaker instance. # Check permissions and other conditions. Get speaker instance.
if speaker_ids is None: if speaker_ids is None:
# Remove oneself # Remove oneself
queryset = Speaker.objects.filter( queryset = Speaker.objects.filter(
item=item, user=self.request.user).exclude(weight=None) item=item, user=self.request.user
).exclude(weight=None)
try: try:
# We assume that there aren't multiple entries because this # We assume that there aren't multiple entries because this
# is forbidden by the Manager's add method. We assume that # is forbidden by the Manager's add method. We assume that
# there is only one speaker instance or none. # there is only one speaker instance or none.
speaker = queryset.get() speaker = queryset.get()
except Speaker.DoesNotExist: 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: else:
speaker.delete() speaker.delete()
message = _('You are successfully removed from the list of speakers.') message = _(
"You are successfully removed from the list of speakers."
)
else: else:
# Remove someone 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) self.permission_denied(request)
if type(speaker_ids) is int: if type(speaker_ids) is int:
speaker_ids = [speaker_ids] speaker_ids = [speaker_ids]
@ -209,15 +230,24 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
inform_changed_data(item) inform_changed_data(item)
if deleted_speaker_count > 1: 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: 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: 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. # 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): def speak(self, request, pk=None):
""" """
Special view endpoint to begin and end speech of speakers. Send PUT Special view endpoint to begin and end speech of speakers. Send PUT
@ -227,20 +257,22 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
# Retrieve item. # Retrieve item.
item = self.get_object() item = self.get_object()
if request.method == 'PUT': if request.method == "PUT":
# Retrieve speaker_id # Retrieve speaker_id
speaker_id = request.data.get('speaker') speaker_id = request.data.get("speaker")
if speaker_id is None: if speaker_id is None:
speaker = item.get_next_speaker() speaker = item.get_next_speaker()
if speaker is None: if speaker is None:
raise ValidationError({'detail': _('The list of speakers is empty.')}) raise ValidationError(
{"detail": _("The list of speakers is empty.")}
)
else: else:
try: try:
speaker = Speaker.objects.get(pk=int(speaker_id)) speaker = Speaker.objects.get(pk=int(speaker_id))
except (ValueError, Speaker.DoesNotExist): except (ValueError, Speaker.DoesNotExist):
raise ValidationError({'detail': _('Speaker does not exist.')}) raise ValidationError({"detail": _("Speaker does not exist.")})
speaker.begin_speech() speaker.begin_speech()
message = _('User is now speaking.') message = _("User is now speaking.")
else: else:
# request.method == 'DELETE' # request.method == 'DELETE'
@ -248,17 +280,27 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
# We assume that there aren't multiple entries because this # We assume that there aren't multiple entries because this
# is forbidden by the Model's begin_speech method. We assume that # is forbidden by the Model's begin_speech method. We assume that
# there is only one speaker instance or none. # 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: except Speaker.DoesNotExist:
raise ValidationError( 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() current_speaker.end_speech()
message = _('The speech is finished now.') message = _("The speech is finished now.")
# Initiate response. # 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): def sort_speakers(self, request, pk=None):
""" """
Special view endpoint to sort the list of speakers. Special view endpoint to sort the list of speakers.
@ -269,10 +311,9 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
item = self.get_object() item = self.get_object()
# Check data # Check data
speaker_ids = request.data.get('speakers') speaker_ids = request.data.get("speakers")
if not isinstance(speaker_ids, list): if not isinstance(speaker_ids, list):
raise ValidationError( raise ValidationError({"detail": _("Invalid data.")})
{'detail': _('Invalid data.')})
# Get all speakers # Get all speakers
speakers = {} speakers = {}
@ -283,8 +324,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
valid_speakers = [] valid_speakers = []
for speaker_id in speaker_ids: for speaker_id in speaker_ids:
if not isinstance(speaker_id, int) or speakers.get(speaker_id) is None: if not isinstance(speaker_id, int) or speakers.get(speaker_id) is None:
raise ValidationError( raise ValidationError({"detail": _("Invalid data.")})
{'detail': _('Invalid data.')})
valid_speakers.append(speakers[speaker_id]) valid_speakers.append(speakers[speaker_id])
weight = 0 weight = 0
with transaction.atomic(): with transaction.atomic():
@ -297,50 +337,57 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
inform_changed_data(item) inform_changed_data(item)
# Initiate response. # 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): def numbering(self, request):
""" """
Auto numbering of the agenda according to the config. Manually added Auto numbering of the agenda according to the config. Manually added
item numbers will be overwritten. item numbers will be overwritten.
""" """
if not config['agenda_enable_numbering']: if not config["agenda_enable_numbering"]:
raise ValidationError({'detail': _('Numbering of agenda items is deactivated.')}) raise ValidationError(
{"detail": _("Numbering of agenda items is deactivated.")}
)
Item.objects.number_all(numeral_system=config['agenda_numeral_system']) Item.objects.number_all(numeral_system=config["agenda_numeral_system"])
return Response({'detail': _('The agenda has been numbered.')}) return Response({"detail": _("The agenda has been numbered.")})
@list_route(methods=['post']) @list_route(methods=["post"])
def sort(self, request): def sort(self, request):
""" """
Sort agenda items. Also checks parent field to prevent hierarchical Sort agenda items. Also checks parent field to prevent hierarchical
loops. loops.
""" """
nodes = request.data.get('nodes', []) nodes = request.data.get("nodes", [])
parent_id = request.data.get('parent_id') parent_id = request.data.get("parent_id")
items = [] items = []
with transaction.atomic(): with transaction.atomic():
for index, node in enumerate(nodes): 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.parent_id = parent_id
item.weight = index item.weight = index
item.save(skip_autoupdate=True) item.save(skip_autoupdate=True)
items.append(item) items.append(item)
# Now check consistency. TODO: Try to use less DB queries. # 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 ancestor = item.parent
while ancestor is not None: while ancestor is not None:
if ancestor == item: if ancestor == item:
raise ValidationError({'detail': _( raise ValidationError(
'There must not be a hierarchical loop. Please reload the page.')}) {
"detail": _(
"There must not be a hierarchical loop. Please reload the page."
)
}
)
ancestor = ancestor.parent ancestor = ancestor.parent
inform_changed_data(items) 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 @transaction.atomic
def assign(self, request): def assign(self, request):
""" """
@ -359,9 +406,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
"items": { "items": {
"description": "An array of agenda item ids where the items should be assigned to the new parent id.", "description": "An array of agenda item ids where the items should be assigned to the new parent id.",
"type": "array", "type": "array",
"items": { "items": {"type": "integer"},
"type": "integer",
},
"minItems": 1, "minItems": 1,
"uniqueItems": True, "uniqueItems": True,
}, },
@ -377,13 +422,19 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
try: try:
jsonschema.validate(request.data, schema) jsonschema.validate(request.data, schema)
except jsonschema.ValidationError as err: except jsonschema.ValidationError as err:
raise ValidationError({'detail': str(err)}) raise ValidationError({"detail": str(err)})
# Check parent item # Check parent item
try: try:
parent = Item.objects.get(pk=request.data['parent_id']) parent = Item.objects.get(pk=request.data["parent_id"])
except Item.DoesNotExist: 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 # Collect ancestors
ancestors = [parent.pk] ancestors = [parent.pk]
@ -394,16 +445,24 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
# First validate all items before changeing them. # First validate all items before changeing them.
items = [] items = []
for item_id in request.data['items']: for item_id in request.data["items"]:
# Prevent hierarchical loops. # Prevent hierarchical loops.
if item_id in ancestors: 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 # Check every item
try: try:
items.append(Item.objects.get(pk=item_id)) items.append(Item.objects.get(pk=item_id))
except Item.DoesNotExist: 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. # OK, assign new parents.
for item in items: for item in items:
@ -415,6 +474,10 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
inform_changed_data(items) inform_changed_data(items)
# Send response. # Send response.
return Response({ return Response(
'detail': _('{number} items successfully assigned.').format(number=len(items)), {
}) "detail": _("{number} items successfully assigned.").format(
number=len(items)
)
}
)

View File

@ -1 +1 @@
default_app_config = 'openslides.assignments.apps.AssignmentsAppConfig' default_app_config = "openslides.assignments.apps.AssignmentsAppConfig"

View File

@ -8,31 +8,35 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Assignment and AssignmentViewSet. Access permissions container for Assignment and AssignmentViewSet.
""" """
base_permission = 'assignments.can_see'
base_permission = "assignments.can_see"
async def get_restricted_data( async def get_restricted_data(
self, self, full_data: List[Dict[str, Any]], user_id: int
full_data: List[Dict[str, Any]], ) -> List[Dict[str, Any]]:
user_id: int) -> List[Dict[str, Any]]:
""" """
Returns the restricted serialized data for the instance prepared Returns the restricted serialized data for the instance prepared
for the user. Removes unpublished polls for non admins so that they for the user. Removes unpublished polls for non admins so that they
only get a result like the AssignmentShortSerializer would give them. only get a result like the AssignmentShortSerializer would give them.
""" """
# Parse data. # 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 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. # Exclude unpublished poll votes.
data = [] data = []
for full in full_data: for full in full_data:
full_copy = full.copy() full_copy = full.copy()
polls = full_copy['polls'] polls = full_copy["polls"]
for poll in polls: for poll in polls:
if not poll['published']: if not poll["published"]:
for option in poll['options']: for option in poll["options"]:
option['votes'] = [] # clear votes for not published polls option["votes"] = [] # clear votes for not published polls
poll['has_votes'] = False # A user should see, if there are votes. poll[
"has_votes"
] = False # A user should see, if there are votes.
data.append(full_copy) data.append(full_copy)
else: else:
data = [] data = []

View File

@ -7,8 +7,8 @@ from ..utils.projector import register_projector_elements
class AssignmentsAppConfig(AppConfig): class AssignmentsAppConfig(AppConfig):
name = 'openslides.assignments' name = "openslides.assignments"
verbose_name = 'OpenSlides Assignments' verbose_name = "OpenSlides Assignments"
angular_site_module = True angular_site_module = True
angular_projector_module = True angular_projector_module = True
@ -28,17 +28,23 @@ class AssignmentsAppConfig(AppConfig):
# Connect signals. # Connect signals.
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data,
dispatch_uid='assignments_get_permission_change_data') dispatch_uid="assignments_get_permission_change_data",
)
# Register viewsets. # Register viewsets.
router.register(self.get_model('Assignment').get_collection_string(), AssignmentViewSet) router.register(
router.register('assignments/poll', AssignmentPollViewSet) self.get_model("Assignment").get_collection_string(), AssignmentViewSet
)
router.register("assignments/poll", AssignmentPollViewSet)
# Register required_users # 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): def get_config_variables(self):
from .config_variables import get_config_variables from .config_variables import get_config_variables
return get_config_variables() return get_config_variables()
def get_startup_elements(self): def get_startup_elements(self):
@ -46,18 +52,15 @@ class AssignmentsAppConfig(AppConfig):
Yields all Cachables required on startup i. e. opening the websocket Yields all Cachables required on startup i. e. opening the websocket
connection. connection.
""" """
yield self.get_model('Assignment') yield self.get_model("Assignment")
def get_angular_constants(self): def get_angular_constants(self):
assignment = self.get_model('Assignment') assignment = self.get_model("Assignment")
Item = TypedDict('Item', {'value': int, 'display_name': str}) Item = TypedDict("Item", {"value": int, "display_name": str})
phases: List[Item] = [] phases: List[Item] = []
for phase in assignment.PHASES: for phase in assignment.PHASES:
phases.append({ phases.append({"value": phase[0], "display_name": phase[1]})
'value': phase[0], return {"AssignmentPhases": phases}
'display_name': phase[1],
})
return {'AssignmentPhases': phases}
def required_users(element: Dict[str, Any]) -> Set[int]: 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 Returns all user ids that are displayed as candidates (including poll
options) in the assignment element. options) in the assignment element.
""" """
candidates = set(related_user['user_id'] for related_user in element['assignment_related_users']) candidates = set(
for poll in element['polls']: related_user["user_id"] for related_user in element["assignment_related_users"]
candidates.update(option['candidate_id'] for option in poll['options']) )
for poll in element["polls"]:
candidates.update(option["candidate_id"] for option in poll["options"])
return candidates return candidates

View File

@ -13,96 +13,118 @@ def get_config_variables():
""" """
# Ballot and ballot papers # Ballot and ballot papers
yield ConfigVariable( yield ConfigVariable(
name='assignments_poll_vote_values', name="assignments_poll_vote_values",
default_value='auto', default_value="auto",
input_type='choice', input_type="choice",
label='Election method', label="Election method",
choices=( choices=(
{'value': 'auto', 'display_name': 'Automatic assign of method'}, {"value": "auto", "display_name": "Automatic assign of method"},
{'value': 'votes', 'display_name': 'Always one option per candidate'}, {"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": "yesnoabstain",
"display_name": "Always Yes-No-Abstain per candidate",
},
{"value": "yesno", "display_name": "Always Yes/No per candidate"},
),
weight=410, weight=410,
group='Elections', group="Elections",
subgroup='Ballot and ballot papers') subgroup="Ballot and ballot papers",
)
yield ConfigVariable( yield ConfigVariable(
name='assignments_poll_100_percent_base', name="assignments_poll_100_percent_base",
default_value='YES_NO_ABSTAIN', default_value="YES_NO_ABSTAIN",
input_type='choice', input_type="choice",
label='The 100-%-base of an election result consists of', label="The 100-%-base of an election result consists of",
choices=( choices=(
{'value': 'YES_NO_ABSTAIN', 'display_name': 'Yes/No/Abstain per candidate'}, {"value": "YES_NO_ABSTAIN", "display_name": "Yes/No/Abstain per candidate"},
{'value': 'YES_NO', 'display_name': 'Yes/No per candidate'}, {"value": "YES_NO", "display_name": "Yes/No per candidate"},
{'value': 'VALID', 'display_name': 'All valid ballots'}, {"value": "VALID", "display_name": "All valid ballots"},
{'value': 'CAST', 'display_name': 'All casted ballots'}, {"value": "CAST", "display_name": "All casted ballots"},
{'value': 'DISABLED', 'display_name': 'Disabled (no percents)'}), {"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, ' help_text=(
'the sum of all votes of all candidates is 100 %. Otherwise for each ' "For Yes/No/Abstain per candidate and Yes/No per candidate the 100-%-base "
'candidate the sum of all votes is 100 %.'), "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, weight=420,
group='Elections', group="Elections",
subgroup='Ballot and ballot papers') subgroup="Ballot and ballot papers",
)
# TODO: Add server side validation of the choices. # TODO: Add server side validation of the choices.
yield ConfigVariable( yield ConfigVariable(
name='assignments_poll_default_majority_method', name="assignments_poll_default_majority_method",
default_value=majorityMethods[0]['value'], default_value=majorityMethods[0]["value"],
input_type='choice', input_type="choice",
choices=majorityMethods, choices=majorityMethods,
label='Required majority', label="Required majority",
help_text='Default method to check whether a candidate has reached the required majority.', help_text="Default method to check whether a candidate has reached the required majority.",
weight=425, weight=425,
group='Elections', group="Elections",
subgroup='Ballot and ballot papers') subgroup="Ballot and ballot papers",
)
yield ConfigVariable( yield ConfigVariable(
name='assignments_add_candidates_to_list_of_speakers', name="assignments_add_candidates_to_list_of_speakers",
default_value=True, default_value=True,
input_type='boolean', input_type="boolean",
label='Put all candidates on the list of speakers', label="Put all candidates on the list of speakers",
weight=428, weight=428,
group='Elections', group="Elections",
subgroup='Ballot and ballot papers') subgroup="Ballot and ballot papers",
)
yield ConfigVariable( yield ConfigVariable(
name='assignments_pdf_ballot_papers_selection', name="assignments_pdf_ballot_papers_selection",
default_value='CUSTOM_NUMBER', default_value="CUSTOM_NUMBER",
input_type='choice', input_type="choice",
label='Number of ballot papers (selection)', label="Number of ballot papers (selection)",
choices=( choices=(
{'value': 'NUMBER_OF_DELEGATES', 'display_name': 'Number of all delegates'}, {"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_ALL_PARTICIPANTS",
"display_name": "Number of all participants",
},
{
"value": "CUSTOM_NUMBER",
"display_name": "Use the following custom number",
},
),
weight=430, weight=430,
group='Elections', group="Elections",
subgroup='Ballot and ballot papers') subgroup="Ballot and ballot papers",
)
yield ConfigVariable( yield ConfigVariable(
name='assignments_pdf_ballot_papers_number', name="assignments_pdf_ballot_papers_number",
default_value=8, default_value=8,
input_type='integer', input_type="integer",
label='Custom number of ballot papers', label="Custom number of ballot papers",
weight=440, weight=440,
group='Elections', group="Elections",
subgroup='Ballot and ballot papers', subgroup="Ballot and ballot papers",
validators=(MinValueValidator(1),)) validators=(MinValueValidator(1),),
)
# PDF # PDF
yield ConfigVariable( yield ConfigVariable(
name='assignments_pdf_title', name="assignments_pdf_title",
default_value='Elections', default_value="Elections",
label='Title for PDF document (all elections)', label="Title for PDF document (all elections)",
weight=460, weight=460,
group='Elections', group="Elections",
subgroup='PDF') subgroup="PDF",
)
yield ConfigVariable( yield ConfigVariable(
name='assignments_pdf_preamble', name="assignments_pdf_preamble",
default_value='', default_value="",
label='Preamble text for PDF document (all elections)', label="Preamble text for PDF document (all elections)",
weight=470, weight=470,
group='Elections', group="Elections",
subgroup='PDF') subgroup="PDF",
)

View File

@ -15,104 +15,196 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('core', '0001_initial'), ("core", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Assignment', name="Assignment",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('title', models.CharField(max_length=100)), "id",
('description', models.TextField(blank=True)), models.AutoField(
('open_posts', models.PositiveSmallIntegerField()), auto_created=True,
('poll_description_default', models.CharField(blank=True, max_length=79)), primary_key=True,
('phase', models.IntegerField(choices=[(0, 'Searching for candidates'), (1, 'Voting'), (2, 'Finished')], default=0)), 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={ options={
'verbose_name': 'Election', "verbose_name": "Election",
'default_permissions': (), "default_permissions": (),
'permissions': ( "permissions": (
('can_see', 'Can see elections'), ("can_see", "Can see elections"),
('can_nominate_other', 'Can nominate another participant'), ("can_nominate_other", "Can nominate another participant"),
('can_nominate_self', 'Can nominate oneself'), ("can_nominate_self", "Can nominate oneself"),
('can_manage', 'Can manage elections')), ("can_manage", "Can manage elections"),
'ordering': ('title',), ),
"ordering": ("title",),
}, },
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='AssignmentOption', name="AssignmentOption",
fields=[ 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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='AssignmentPoll', name="AssignmentPoll",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('votesvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)), "id",
('votesinvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)), models.AutoField(
('votescast', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)), auto_created=True,
('published', models.BooleanField(default=False)), primary_key=True,
('yesnoabstain', models.BooleanField(default=False)), serialize=False,
('description', models.CharField(blank=True, max_length=79)), verbose_name="ID",
('assignment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polls', to='assignments.Assignment')), ),
),
(
"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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='AssignmentRelatedUser', name="AssignmentRelatedUser",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('elected', models.BooleanField(default=False)), "id",
('assignment', models.ForeignKey( models.AutoField(
on_delete=django.db.models.deletion.CASCADE, related_name='assignment_related_users', to='assignments.Assignment')), auto_created=True,
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='AssignmentVote', name="AssignmentVote",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('weight', models.IntegerField(default=1, null=True)), "id",
('value', models.CharField(max_length=255, null=True)), models.AutoField(
('option', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='assignments.AssignmentOption')), 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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.AddField( migrations.AddField(
model_name='assignmentoption', model_name="assignmentoption",
name='poll', name="poll",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='assignments.AssignmentPoll'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="options",
to="assignments.AssignmentPoll",
),
), ),
migrations.AddField( migrations.AddField(
model_name='assignment', model_name="assignment",
name='related_users', name="related_users",
field=models.ManyToManyField(through='assignments.AssignmentRelatedUser', to=settings.AUTH_USER_MODEL), field=models.ManyToManyField(
through="assignments.AssignmentRelatedUser", to=settings.AUTH_USER_MODEL
),
), ),
migrations.AddField( migrations.AddField(
model_name='assignment', model_name="assignment",
name='tags', name="tags",
field=models.ManyToManyField(blank=True, to='core.Tag'), field=models.ManyToManyField(blank=True, to="core.Tag"),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='assignmentrelateduser', name="assignmentrelateduser", unique_together=set([("assignment", "user")])
unique_together=set([('assignment', 'user')]),
), ),
] ]

View File

@ -7,18 +7,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("assignments", "0001_initial")]
('assignments', '0001_initial'),
]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(model_name="assignmentpoll", name="yesnoabstain"),
model_name='assignmentpoll',
name='yesnoabstain',
),
migrations.AddField( migrations.AddField(
model_name='assignmentpoll', model_name="assignmentpoll",
name='pollmethod', name="pollmethod",
field=models.CharField(default='yna', max_length=5), field=models.CharField(default="yna", max_length=5),
), ),
] ]

View File

@ -7,19 +7,17 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("assignments", "0002_assignmentpoll_pollmethod")]
('assignments', '0002_assignmentpoll_pollmethod'),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='assignmentrelateduser', model_name="assignmentrelateduser",
name='weight', name="weight",
field=models.IntegerField(default=0), field=models.IntegerField(default=0),
), ),
migrations.AddField( migrations.AddField(
model_name='assignmentoption', model_name="assignmentoption",
name='weight', name="weight",
field=models.IntegerField(default=0), field=models.IntegerField(default=0),
), ),
] ]

View File

@ -9,19 +9,17 @@ from openslides.utils.models import MinMaxIntegerField
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("assignments", "0003_candidate_weight")]
('assignments', '0003_candidate_weight'),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='assignmentpoll', model_name="assignmentpoll",
name='votesabstain', name="votesabstain",
field=MinMaxIntegerField(null=True, blank=True, min_value=-2), field=MinMaxIntegerField(null=True, blank=True, min_value=-2),
), ),
migrations.AddField( migrations.AddField(
model_name='assignmentpoll', model_name="assignmentpoll",
name='votesno', name="votesno",
field=MinMaxIntegerField(null=True, blank=True, min_value=-2), field=MinMaxIntegerField(null=True, blank=True, min_value=-2),
), ),
] ]

View File

@ -8,69 +8,73 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("assignments", "0004_auto_20180703_1523")]
('assignments', '0004_auto_20180703_1523'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='assignmentpoll', model_name="assignmentpoll",
name='votescast', name="votescast",
field=models.DecimalField( field=models.DecimalField(
blank=True, blank=True,
decimal_places=6, decimal_places=6,
max_digits=15, max_digits=15,
null=True, null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='assignmentpoll', model_name="assignmentpoll",
name='votesinvalid', name="votesinvalid",
field=models.DecimalField( field=models.DecimalField(
blank=True, blank=True,
decimal_places=6, decimal_places=6,
max_digits=15, max_digits=15,
null=True, null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='assignmentpoll', model_name="assignmentpoll",
name='votesvalid', name="votesvalid",
field=models.DecimalField( field=models.DecimalField(
blank=True, blank=True,
decimal_places=6, decimal_places=6,
max_digits=15, max_digits=15,
null=True, null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='assignmentvote', model_name="assignmentvote",
name='weight', name="weight",
field=models.DecimalField( field=models.DecimalField(
decimal_places=6, decimal_places=6,
default=Decimal('1'), default=Decimal("1"),
max_digits=15, max_digits=15,
null=True, null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='assignmentpoll', model_name="assignmentpoll",
name='votesabstain', name="votesabstain",
field=models.DecimalField( field=models.DecimalField(
blank=True, blank=True,
decimal_places=6, decimal_places=6,
max_digits=15, max_digits=15,
null=True, null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='assignmentpoll', model_name="assignmentpoll",
name='votesno', name="votesno",
field=models.DecimalField( field=models.DecimalField(
blank=True, blank=True,
decimal_places=6, decimal_places=6,
max_digits=15, max_digits=15,
null=True, null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
),
), ),
] ]

View File

@ -31,16 +31,13 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
""" """
assignment = models.ForeignKey( assignment = models.ForeignKey(
'Assignment', "Assignment", on_delete=models.CASCADE, related_name="assignment_related_users"
on_delete=models.CASCADE, )
related_name='assignment_related_users')
""" """
ForeinKey to the assignment. ForeinKey to the assignment.
""" """
user = models.ForeignKey( user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
""" """
ForeinKey to the user who is related to the assignment. ForeinKey to the user who is related to the assignment.
""" """
@ -57,7 +54,7 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
class Meta: class Meta:
default_permissions = () default_permissions = ()
unique_together = ('assignment', 'user') unique_together = ("assignment", "user")
def __str__(self): def __str__(self):
return "%s <-> %s" % (self.assignment, self.user) 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. Customized model manager to support our get_full_queryset method.
""" """
def get_full_queryset(self): def get_full_queryset(self):
""" """
Returns the normal queryset with all assignments. In the background Returns the normal queryset with all assignments. In the background
@ -80,18 +78,17 @@ class AssignmentManager(models.Manager):
polls are prefetched from the database. polls are prefetched from the database.
""" """
return self.get_queryset().prefetch_related( return self.get_queryset().prefetch_related(
'related_users', "related_users", "agenda_items", "polls", "tags"
'agenda_items', )
'polls',
'tags')
class Assignment(RESTModelMixin, models.Model): class Assignment(RESTModelMixin, models.Model):
""" """
Model for assignments. Model for assignments.
""" """
access_permissions = AssignmentAccessPermissions() access_permissions = AssignmentAccessPermissions()
can_see_permission = 'assignments.can_see' can_see_permission = "assignments.can_see"
objects = AssignmentManager() objects = AssignmentManager()
@ -100,19 +97,17 @@ class Assignment(RESTModelMixin, models.Model):
PHASE_FINISHED = 2 PHASE_FINISHED = 2
PHASES = ( PHASES = (
(PHASE_SEARCH, 'Searching for candidates'), (PHASE_SEARCH, "Searching for candidates"),
(PHASE_VOTING, 'Voting'), (PHASE_VOTING, "Voting"),
(PHASE_FINISHED, 'Finished'), (PHASE_FINISHED, "Finished"),
) )
title = models.CharField( title = models.CharField(max_length=100)
max_length=100)
""" """
Title of the assignment. Title of the assignment.
""" """
description = models.TextField( description = models.TextField(blank=True)
blank=True)
""" """
Text to describe the assignment. Text to describe the assignment.
""" """
@ -122,23 +117,19 @@ class Assignment(RESTModelMixin, models.Model):
The number of members to be elected. The number of members to be elected.
""" """
poll_description_default = models.CharField( poll_description_default = models.CharField(max_length=79, blank=True)
max_length=79,
blank=True)
""" """
Default text for the poll description. Default text for the poll description.
""" """
phase = models.IntegerField( phase = models.IntegerField(choices=PHASES, default=PHASE_SEARCH)
choices=PHASES,
default=PHASE_SEARCH)
""" """
Phase in which the assignment is. Phase in which the assignment is.
""" """
related_users = models.ManyToManyField( related_users = models.ManyToManyField(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL, through="AssignmentRelatedUser"
through='AssignmentRelatedUser') )
""" """
Users that are candidates or elected. 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 # In theory there could be one then more agenda_item. But we support only
# one. See the property agenda_item. # one. See the property agenda_item.
agenda_items = GenericRelation(Item, related_name='assignments') agenda_items = GenericRelation(Item, related_name="assignments")
class Meta: class Meta:
default_permissions = () default_permissions = ()
permissions = ( permissions = (
('can_see', 'Can see elections'), ("can_see", "Can see elections"),
('can_nominate_other', 'Can nominate another participant'), ("can_nominate_other", "Can nominate another participant"),
('can_nominate_self', 'Can nominate oneself'), ("can_nominate_self", "Can nominate oneself"),
('can_manage', 'Can manage elections'), ("can_manage", "Can manage elections"),
) )
ordering = ('title', ) ordering = ("title",)
verbose_name = ugettext_noop('Election') verbose_name = ugettext_noop("Election")
def __str__(self): def __str__(self):
return self.title return self.title
@ -174,26 +165,25 @@ class Assignment(RESTModelMixin, models.Model):
assignment projector element is disabled. assignment projector element is disabled.
""" """
Projector.remove_any( Projector.remove_any(
skip_autoupdate=skip_autoupdate, skip_autoupdate=skip_autoupdate, name="assignments/assignment", id=self.pk
name='assignments/assignment', )
id=self.pk) return super().delete( # type: ignore
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore # TODO fix typing skip_autoupdate=skip_autoupdate, *args, **kwargs
)
@property @property
def candidates(self): def candidates(self):
""" """
Queryset that represents the candidates for the assignment. Queryset that represents the candidates for the assignment.
""" """
return self.related_users.filter( return self.related_users.filter(assignmentrelateduser__elected=False)
assignmentrelateduser__elected=False)
@property @property
def elected(self): def elected(self):
""" """
Queryset that represents all elected users for the assignment. Queryset that represents all elected users for the assignment.
""" """
return self.related_users.filter( return self.related_users.filter(assignmentrelateduser__elected=True)
assignmentrelateduser__elected=True)
def is_candidate(self, user): def is_candidate(self, user):
""" """
@ -215,22 +205,22 @@ class Assignment(RESTModelMixin, models.Model):
""" """
Adds the user as candidate. Adds the user as candidate.
""" """
weight = self.assignment_related_users.aggregate( weight = (
models.Max('weight'))['weight__max'] or 0 self.assignment_related_users.aggregate(models.Max("weight"))["weight__max"]
defaults = { or 0
'elected': False, )
'weight': weight + 1} defaults = {"elected": False, "weight": weight + 1}
related_user, __ = self.assignment_related_users.update_or_create( related_user, __ = self.assignment_related_users.update_or_create(
user=user, user=user, defaults=defaults
defaults=defaults) )
def set_elected(self, user): def set_elected(self, user):
""" """
Makes user an elected user for this assignment. Makes user an elected user for this assignment.
""" """
related_user, __ = self.assignment_related_users.update_or_create( related_user, __ = self.assignment_related_users.update_or_create(
user=user, user=user, defaults={"elected": True}
defaults={'elected': True}) )
def delete_related_user(self, user): def delete_related_user(self, user):
""" """
@ -258,39 +248,43 @@ class Assignment(RESTModelMixin, models.Model):
candidates = self.candidates.all() candidates = self.candidates.all()
# Find out the method of the election # Find out the method of the election
if config['assignments_poll_vote_values'] == 'votes': if config["assignments_poll_vote_values"] == "votes":
pollmethod = 'votes' pollmethod = "votes"
elif config['assignments_poll_vote_values'] == 'yesnoabstain': elif config["assignments_poll_vote_values"] == "yesnoabstain":
pollmethod = 'yna' pollmethod = "yna"
elif config['assignments_poll_vote_values'] == 'yesno': elif config["assignments_poll_vote_values"] == "yesno":
pollmethod = 'yn' pollmethod = "yn"
else: else:
# config['assignments_poll_vote_values'] == 'auto' # config['assignments_poll_vote_values'] == 'auto'
# candidates <= available posts -> yes/no/abstain # candidates <= available posts -> yes/no/abstain
if len(candidates) <= (self.open_posts - self.elected.count()): if len(candidates) <= (self.open_posts - self.elected.count()):
pollmethod = 'yna' pollmethod = "yna"
else: else:
pollmethod = 'votes' pollmethod = "votes"
# Create the poll with the candidates. # Create the poll with the candidates.
poll = self.polls.create( poll = self.polls.create(
description=self.poll_description_default, description=self.poll_description_default, pollmethod=pollmethod
pollmethod=pollmethod) )
options = [] 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: for related_user in related_users:
options.append({ options.append(
'candidate': related_user.user, {"candidate": related_user.user, "weight": related_user.weight}
'weight': related_user.weight}) )
poll.set_options(options, skip_autoupdate=True) poll.set_options(options, skip_autoupdate=True)
inform_changed_data(self) inform_changed_data(self)
# Add all candidates to list of speakers of related agenda item # Add all candidates to list of speakers of related agenda item
# TODO: Try to do this in a bulk create # 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: for candidate in self.candidates:
try: try:
Speaker.objects.add(candidate, self.agenda_item, skip_autoupdate=True) Speaker.objects.add(
candidate, self.agenda_item, skip_autoupdate=True
)
except OpenSlidesError: except OpenSlidesError:
# The Speaker is already on the list. Do nothing. # The Speaker is already on the list. Do nothing.
# TODO: Find a smart way not to catch the error concerning AnonymousUser. # 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. Return a title for the agenda with the appended assignment verbose name.
Note: It has to be the same return value like in JavaScript. 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 @property
def agenda_item(self): def agenda_item(self):
@ -370,9 +364,8 @@ class Assignment(RESTModelMixin, models.Model):
class AssignmentVote(RESTModelMixin, BaseVote): class AssignmentVote(RESTModelMixin, BaseVote):
option = models.ForeignKey( option = models.ForeignKey(
'AssignmentOption', "AssignmentOption", on_delete=models.CASCADE, related_name="votes"
on_delete=models.CASCADE, )
related_name='votes')
class Meta: class Meta:
default_permissions = () default_permissions = ()
@ -386,12 +379,9 @@ class AssignmentVote(RESTModelMixin, BaseVote):
class AssignmentOption(RESTModelMixin, BaseOption): class AssignmentOption(RESTModelMixin, BaseOption):
poll = models.ForeignKey( poll = models.ForeignKey(
'AssignmentPoll', "AssignmentPoll", on_delete=models.CASCADE, related_name="options"
on_delete=models.CASCADE, )
related_name='options') candidate = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
candidate = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
weight = models.IntegerField(default=0) weight = models.IntegerField(default=0)
vote_class = AssignmentVote vote_class = AssignmentVote
@ -411,26 +401,32 @@ class AssignmentOption(RESTModelMixin, BaseOption):
# TODO: remove the type-ignoring in the next line, after this is solved: # TODO: remove the type-ignoring in the next line, after this is solved:
# https://github.com/python/mypy/issues/3855 # https://github.com/python/mypy/issues/3855
class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore class AssignmentPoll( # type: ignore
PublishPollMixin, BasePoll): RESTModelMixin, CollectDefaultVotesMixin, PublishPollMixin, BasePoll
):
option_class = AssignmentOption option_class = AssignmentOption
assignment = models.ForeignKey( assignment = models.ForeignKey(
Assignment, Assignment, on_delete=models.CASCADE, related_name="polls"
on_delete=models.CASCADE, )
related_name='polls') pollmethod = models.CharField(max_length=5, default="yna")
pollmethod = models.CharField( description = models.CharField(max_length=79, blank=True)
max_length=5,
default='yna')
description = models.CharField(
max_length=79,
blank=True)
votesabstain = models.DecimalField(null=True, blank=True, validators=[ votesabstain = models.DecimalField(
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) null=True,
blank=True,
validators=[MinValueValidator(Decimal("-2"))],
max_digits=15,
decimal_places=6,
)
""" General abstain votes, used for pollmethod 'votes' """ """ General abstain votes, used for pollmethod 'votes' """
votesno = models.DecimalField(null=True, blank=True, validators=[ votesno = models.DecimalField(
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) null=True,
blank=True,
validators=[MinValueValidator(Decimal("-2"))],
max_digits=15,
decimal_places=6,
)
""" General no votes, used for pollmethod 'votes' """ """ General no votes, used for pollmethod 'votes' """
class Meta: class Meta:
@ -443,27 +439,30 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore
""" """
Projector.remove_any( Projector.remove_any(
skip_autoupdate=skip_autoupdate, skip_autoupdate=skip_autoupdate,
name='assignments/assignment', name="assignments/assignment",
id=self.assignment.pk, id=self.assignment.pk,
poll=self.pk) poll=self.pk,
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore # TODO: fix typing )
return super().delete( # type: ignore
skip_autoupdate=skip_autoupdate, *args, **kwargs
)
def get_assignment(self): def get_assignment(self):
return self.assignment return self.assignment
def get_vote_values(self): def get_vote_values(self):
if self.pollmethod == 'yna': if self.pollmethod == "yna":
return ['Yes', 'No', 'Abstain'] return ["Yes", "No", "Abstain"]
elif self.pollmethod == 'yn': elif self.pollmethod == "yn":
return ['Yes', 'No'] return ["Yes", "No"]
else: else:
return ['Votes'] return ["Votes"]
def get_ballot(self): def get_ballot(self):
return self.assignment.polls.filter(id__lte=self.pk).count() return self.assignment.polls.filter(id__lte=self.pk).count()
def get_percent_base_choice(self): 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): def get_root_rest_element(self):
""" """

View File

@ -11,30 +11,33 @@ class AssignmentSlide(ProjectorElement):
You can send a poll id to get a poll slide. You can send a poll id to get a poll slide.
""" """
name = 'assignments/assignment'
name = "assignments/assignment"
def check_data(self): def check_data(self):
if not Assignment.objects.filter(pk=self.config_entry.get('id')).exists(): if not Assignment.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException('Election does not exist.') raise ProjectorException("Election does not exist.")
poll_id = self.config_entry.get('poll') poll_id = self.config_entry.get("poll")
if poll_id: if poll_id:
# Poll slide. # Poll slide.
try: try:
poll = AssignmentPoll.objects.get(pk=poll_id) poll = AssignmentPoll.objects.get(pk=poll_id)
except AssignmentPoll.DoesNotExist: except AssignmentPoll.DoesNotExist:
raise ProjectorException('Poll does not exist.') raise ProjectorException("Poll does not exist.")
if poll.assignment_id != self.config_entry.get('id'): if poll.assignment_id != self.config_entry.get("id"):
raise ProjectorException('Assignment id and poll do not belong together.') raise ProjectorException(
"Assignment id and poll do not belong together."
)
def update_data(self): def update_data(self):
data = None data = None
try: try:
assignment = Assignment.objects.get(pk=self.config_entry.get('id')) assignment = Assignment.objects.get(pk=self.config_entry.get("id"))
except Assignment.DoesNotExist: except Assignment.DoesNotExist:
# Assignment does not exist, so just do nothing. # Assignment does not exist, so just do nothing.
pass pass
else: else:
data = {'agenda_item_id': assignment.agenda_item_id} data = {"agenda_item_id": assignment.agenda_item_id}
return data return data

View File

@ -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. 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): 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')}) raise ValidationError(
{"detail": _("Value for {} must be greater than 0").format("open_posts")}
)
return data return data
@ -37,35 +39,39 @@ class AssignmentRelatedUserSerializer(ModelSerializer):
""" """
Serializer for assignment.models.AssignmentRelatedUser objects. Serializer for assignment.models.AssignmentRelatedUser objects.
""" """
class Meta: class Meta:
model = AssignmentRelatedUser model = AssignmentRelatedUser
fields = ( fields = (
'id', "id",
'user', "user",
'elected', "elected",
'assignment', "assignment",
'weight') # js-data needs the assignment-id in the nested object to define relations. "weight",
) # js-data needs the assignment-id in the nested object to define relations.
class AssignmentVoteSerializer(ModelSerializer): class AssignmentVoteSerializer(ModelSerializer):
""" """
Serializer for assignment.models.AssignmentVote objects. Serializer for assignment.models.AssignmentVote objects.
""" """
class Meta: class Meta:
model = AssignmentVote model = AssignmentVote
fields = ('weight', 'value',) fields = ("weight", "value")
class AssignmentOptionSerializer(ModelSerializer): class AssignmentOptionSerializer(ModelSerializer):
""" """
Serializer for assignment.models.AssignmentOption objects. Serializer for assignment.models.AssignmentOption objects.
""" """
votes = AssignmentVoteSerializer(many=True, read_only=True) votes = AssignmentVoteSerializer(many=True, read_only=True)
is_elected = SerializerMethodField() is_elected = SerializerMethodField()
class Meta: class Meta:
model = AssignmentOption model = AssignmentOption
fields = ('id', 'candidate', 'is_elected', 'votes', 'poll', 'weight') fields = ("id", "candidate", "is_elected", "votes", "poll", "weight")
def get_is_elected(self, obj): def get_is_elected(self, obj):
""" """
@ -78,6 +84,7 @@ class FilterPollListSerializer(ListSerializer):
""" """
Customized serializer to filter polls (exclude unpublished). Customized serializer to filter polls (exclude unpublished).
""" """
def to_representation(self, data): def to_representation(self, data):
""" """
List of object instances -> List of dicts of primitive datatypes. 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, # Dealing with nested relationships, data can be a Manager,
# so, first get a queryset from the Manager if needed # 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] return [self.child.to_representation(item) for item in iterable]
@ -96,31 +105,35 @@ class AssignmentAllPollSerializer(ModelSerializer):
Serializes all polls. Serializes all polls.
""" """
options = AssignmentOptionSerializer(many=True, read_only=True) options = AssignmentOptionSerializer(many=True, read_only=True)
votes = ListField( votes = ListField(
child=DictField( 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, write_only=True,
required=False) required=False,
)
has_votes = SerializerMethodField() has_votes = SerializerMethodField()
class Meta: class Meta:
model = AssignmentPoll model = AssignmentPoll
fields = ( fields = (
'id', "id",
'pollmethod', "pollmethod",
'description', "description",
'published', "published",
'options', "options",
'votesabstain', "votesabstain",
'votesno', "votesno",
'votesvalid', "votesvalid",
'votesinvalid', "votesinvalid",
'votescast', "votescast",
'votes', "votes",
'has_votes', "has_votes",
'assignment') # js-data needs the assignment-id in the nested object to define relations. "assignment",
read_only_fields = ('pollmethod',) ) # js-data needs the assignment-id in the nested object to define relations.
read_only_fields = ("pollmethod",)
validators = (default_votes_validator,) validators = (default_votes_validator,)
def get_has_votes(self, obj): def get_has_votes(self, obj):
@ -144,30 +157,45 @@ class AssignmentAllPollSerializer(ModelSerializer):
"votes": [{"Votes": 10}, {"Votes": 0}] "votes": [{"Votes": 10}, {"Votes": 0}]
""" """
# Update votes. # Update votes.
votes = validated_data.get('votes') votes = validated_data.get("votes")
if votes: if votes:
options = list(instance.get_options()) options = list(instance.get_options())
if len(votes) != len(options): if len(votes) != len(options):
raise ValidationError({ raise ValidationError(
'detail': _('You have to submit data for %d candidates.') % len(options)}) {
"detail": _("You have to submit data for %d candidates.")
% len(options)
}
)
for index, option in enumerate(options): for index, option in enumerate(options):
if len(votes[index]) != len(instance.get_vote_values()): if len(votes[index]) != len(instance.get_vote_values()):
raise ValidationError({ raise ValidationError(
'detail': _('You have to submit data for %d vote values.') % len(instance.get_vote_values())}) {
"detail": _("You have to submit data for %d vote values.")
% len(instance.get_vote_values())
}
)
for vote_value, vote_weight in votes[index].items(): for vote_value, vote_weight in votes[index].items():
if vote_value not in instance.get_vote_values(): if vote_value not in instance.get_vote_values():
raise ValidationError({ raise ValidationError(
'detail': _('Vote value %s is invalid.') % vote_value}) {"detail": _("Vote value %s is invalid.") % vote_value}
instance.set_vote_objects_with_values(option, votes[index], skip_autoupdate=True) )
instance.set_vote_objects_with_values(
option, votes[index], skip_autoupdate=True
)
# Update remaining writeable fields. # Update remaining writeable fields.
instance.description = validated_data.get('description', instance.description) instance.description = validated_data.get("description", instance.description)
instance.published = validated_data.get('published', instance.published) instance.published = validated_data.get("published", instance.published)
instance.votesabstain = validated_data.get('votesabstain', instance.votesabstain) instance.votesabstain = validated_data.get(
instance.votesno = validated_data.get('votesno', instance.votesno) "votesabstain", instance.votesabstain
instance.votesvalid = validated_data.get('votesvalid', instance.votesvalid) )
instance.votesinvalid = validated_data.get('votesinvalid', instance.votesinvalid) instance.votesno = validated_data.get("votesno", instance.votesno)
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() instance.save()
return instance return instance
@ -178,52 +206,60 @@ class AssignmentShortPollSerializer(AssignmentAllPollSerializer):
Serializes only short polls (excluded unpublished polls). Serializes only short polls (excluded unpublished polls).
""" """
class Meta: class Meta:
list_serializer_class = FilterPollListSerializer list_serializer_class = FilterPollListSerializer
model = AssignmentPoll model = AssignmentPoll
fields = ( fields = (
'id', "id",
'pollmethod', "pollmethod",
'description', "description",
'published', "published",
'options', "options",
'votesabstain', "votesabstain",
'votesno', "votesno",
'votesvalid', "votesvalid",
'votesinvalid', "votesinvalid",
'votescast', "votescast",
'has_votes',) "has_votes",
)
class AssignmentFullSerializer(ModelSerializer): class AssignmentFullSerializer(ModelSerializer):
""" """
Serializer for assignment.models.Assignment objects. With all polls. 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) 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) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
class Meta: class Meta:
model = Assignment model = Assignment
fields = ( fields = (
'id', "id",
'title', "title",
'description', "description",
'open_posts', "open_posts",
'phase', "phase",
'assignment_related_users', "assignment_related_users",
'poll_description_default', "poll_description_default",
'polls', "polls",
'agenda_item_id', "agenda_item_id",
'agenda_type', "agenda_type",
'agenda_parent_id', "agenda_parent_id",
'tags',) "tags",
)
validators = (posts_validator,) validators = (posts_validator,)
def validate(self, data): def validate(self, data):
if 'description' in data: if "description" in data:
data['description'] = validate_html(data['description']) data["description"] = validate_html(data["description"])
return data return data
def create(self, validated_data): def create(self, validated_data):
@ -231,10 +267,10 @@ class AssignmentFullSerializer(ModelSerializer):
Customized create method. Set information about related agenda item Customized create method. Set information about related agenda item
into agenda_item_update_information container. into agenda_item_update_information container.
""" """
agenda_type = validated_data.pop('agenda_type', None) agenda_type = validated_data.pop("agenda_type", None)
agenda_parent_id = validated_data.pop('agenda_parent_id', None) agenda_parent_id = validated_data.pop("agenda_parent_id", None)
assignment = Assignment(**validated_data) assignment = Assignment(**validated_data)
assignment.agenda_item_update_information['type'] = agenda_type assignment.agenda_item_update_information["type"] = agenda_type
assignment.agenda_item_update_information['parent_id'] = agenda_parent_id assignment.agenda_item_update_information["parent_id"] = agenda_parent_id
assignment.save() assignment.save()
return assignment return assignment

View File

@ -5,8 +5,11 @@ def get_permission_change_data(sender, permissions=None, **kwargs):
""" """
Yields all necessary collections if 'assignments.can_see' permission changes. 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: for permission in permissions:
# There could be only one 'assignment.can_see' and then we want to return data. # 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() yield from assignments_app.get_startup_elements()

View File

@ -21,6 +21,7 @@ from .serializers import AssignmentAllPollSerializer
# Viewsets for the REST API # Viewsets for the REST API
class AssignmentViewSet(ModelViewSet): class AssignmentViewSet(ModelViewSet):
""" """
API endpoint for assignments. API endpoint for assignments.
@ -29,6 +30,7 @@ class AssignmentViewSet(ModelViewSet):
partial_update, update, destroy, candidature_self, candidature_other, partial_update, update, destroy, candidature_self, candidature_other,
mark_elected and create_poll. mark_elected and create_poll.
""" """
access_permissions = AssignmentAccessPermissions() access_permissions = AssignmentAccessPermissions()
queryset = Assignment.objects.all() queryset = Assignment.objects.all()
@ -36,26 +38,36 @@ class AssignmentViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. 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) 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. # Everybody is allowed to see the metadata.
result = True result = True
elif self.action in ('create', 'partial_update', 'update', 'destroy', elif self.action in (
'mark_elected', 'create_poll', 'sort_related_users'): "create",
result = (has_perm(self.request.user, 'assignments.can_see') and "partial_update",
has_perm(self.request.user, 'assignments.can_manage')) "update",
elif self.action == 'candidature_self': "destroy",
result = (has_perm(self.request.user, 'assignments.can_see') and "mark_elected",
has_perm(self.request.user, 'assignments.can_nominate_self')) "create_poll",
elif self.action == 'candidature_other': "sort_related_users",
result = (has_perm(self.request.user, 'assignments.can_see') and ):
has_perm(self.request.user, 'assignments.can_nominate_other')) 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: else:
result = False result = False
return result return result
@detail_route(methods=['post', 'delete']) @detail_route(methods=["post", "delete"])
def candidature_self(self, request, pk=None): def candidature_self(self, request, pk=None):
""" """
View to nominate self as candidate (POST) or withdraw own View to nominate self as candidate (POST) or withdraw own
@ -63,18 +75,26 @@ class AssignmentViewSet(ModelViewSet):
""" """
assignment = self.get_object() assignment = self.get_object()
if assignment.is_elected(request.user): if assignment.is_elected(request.user):
raise ValidationError({'detail': _('You are already elected.')}) raise ValidationError({"detail": _("You are already elected.")})
if request.method == 'POST': if request.method == "POST":
message = self.nominate_self(request, assignment) message = self.nominate_self(request, assignment)
else: else:
# request.method == 'DELETE' # request.method == 'DELETE'
message = self.withdraw_self(request, assignment) message = self.withdraw_self(request, assignment)
return Response({'detail': message}) return Response({"detail": message})
def nominate_self(self, request, assignment): def nominate_self(self, request, assignment):
if assignment.phase == assignment.PHASE_FINISHED: if assignment.phase == assignment.PHASE_FINISHED:
raise ValidationError({'detail': _('You can not candidate to this election because it is finished.')}) raise ValidationError(
if assignment.phase == assignment.PHASE_VOTING and not has_perm(request.user, 'assignments.can_manage'): {
"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. # To nominate self during voting you have to be a manager.
self.permission_denied(request) self.permission_denied(request)
# If the request.user is already a candidate he can nominate himself nevertheless. # 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 # Send new candidate via autoupdate because users without permission
# to see users may not have it but can get it now. # to see users may not have it but can get it now.
inform_changed_data([request.user]) inform_changed_data([request.user])
return _('You were nominated successfully.') return _("You were nominated successfully.")
def withdraw_self(self, request, assignment): def withdraw_self(self, request, assignment):
# Withdraw candidature. # Withdraw candidature.
if assignment.phase == assignment.PHASE_FINISHED: if assignment.phase == assignment.PHASE_FINISHED:
raise ValidationError({'detail': _('You can not withdraw your candidature to this election because it is finished.')}) raise ValidationError(
if assignment.phase == assignment.PHASE_VOTING and not has_perm(request.user, 'assignments.can_manage'): {
"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. # To withdraw self during voting you have to be a manager.
self.permission_denied(request) self.permission_denied(request)
if not assignment.is_candidate(request.user): 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) 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): def get_user_from_request_data(self, request):
""" """
@ -103,20 +133,26 @@ class AssignmentViewSet(ModelViewSet):
self.mark_elected can play with it. self.mark_elected can play with it.
""" """
if not isinstance(request.data, dict): if not isinstance(request.data, dict):
detail = _('Invalid data. Expected dictionary, got %s.') % type(request.data) detail = _("Invalid data. Expected dictionary, got %s.") % type(
raise ValidationError({'detail': detail}) request.data
user_str = request.data.get('user', '') )
raise ValidationError({"detail": detail})
user_str = request.data.get("user", "")
try: try:
user_pk = int(user_str) user_pk = int(user_str)
except ValueError: except ValueError:
raise ValidationError({'detail': _('Invalid data. Expected something like {"user": <id>}.')}) raise ValidationError(
{"detail": _('Invalid data. Expected something like {"user": <id>}.')}
)
try: try:
user = get_user_model().objects.get(pk=user_pk) user = get_user_model().objects.get(pk=user_pk)
except get_user_model().DoesNotExist: 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 return user
@detail_route(methods=['post', 'delete']) @detail_route(methods=["post", "delete"])
def candidature_other(self, request, pk=None): def candidature_other(self, request, pk=None):
""" """
View to nominate other users (POST) or delete their candidature 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) user = self.get_user_from_request_data(request)
assignment = self.get_object() assignment = self.get_object()
if request.method == 'POST': if request.method == "POST":
message = self.nominate_other(request, user, assignment) message = self.nominate_other(request, user, assignment)
else: else:
# request.method == 'DELETE' # request.method == 'DELETE'
message = self.delete_other(request, user, assignment) message = self.delete_other(request, user, assignment)
return Response({'detail': message}) return Response({"detail": message})
def nominate_other(self, request, user, assignment): def nominate_other(self, request, user, assignment):
if assignment.is_elected(user): 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: if assignment.phase == assignment.PHASE_FINISHED:
detail = _('You can not nominate someone to this election because it is finished.') detail = _(
raise ValidationError({'detail': detail}) "You can not nominate someone 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": 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. # To nominate another user during voting you have to be a manager.
self.permission_denied(request) self.permission_denied(request)
if assignment.is_candidate(user): 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) assignment.set_candidate(user)
# Send new candidate via autoupdate because users without permission # Send new candidate via autoupdate because users without permission
# to see users may not have it but can get it now. # to see users may not have it but can get it now.
inform_changed_data(user) 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): def delete_other(self, request, user, assignment):
# To delete candidature status you have to be a manager. # 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) self.permission_denied(request)
if assignment.phase == assignment.PHASE_FINISHED: if assignment.phase == assignment.PHASE_FINISHED:
detail = _("You can not delete someone's candidature to this election because it is finished.") detail = _(
raise ValidationError({'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): 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) 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): def mark_elected(self, request, pk=None):
""" """
View to mark other users as elected (POST) or undo this (DELETE). 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) user = self.get_user_from_request_data(request)
assignment = self.get_object() assignment = self.get_object()
if request.method == 'POST': if request.method == "POST":
if not assignment.is_candidate(user): 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) assignment.set_elected(user)
message = _('User %s was successfully elected.') % user message = _("User %s was successfully elected.") % user
else: else:
# request.method == 'DELETE' # request.method == 'DELETE'
if not assignment.is_elected(user): if not assignment.is_elected(user):
detail = _('User %s is not an elected candidate of this election.') % user detail = (
raise ValidationError({'detail': detail}) _("User %s is not an elected candidate of this election.") % user
)
raise ValidationError({"detail": detail})
assignment.set_candidate(user) assignment.set_candidate(user)
message = _('User %s was successfully unelected.') % user message = _("User %s was successfully unelected.") % user
return Response({'detail': message}) return Response({"detail": message})
@detail_route(methods=['post']) @detail_route(methods=["post"])
def create_poll(self, request, pk=None): def create_poll(self, request, pk=None):
""" """
View to create a poll. It is a POST request without any data. View to create a poll. It is a POST request without any data.
""" """
assignment = self.get_object() assignment = self.get_object()
if not assignment.candidates.exists(): 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(): with transaction.atomic():
poll = assignment.create_poll() poll = assignment.create_poll()
return Response({ return Response(
'detail': _('Ballot created successfully.'), {"detail": _("Ballot created successfully."), "createdPollId": poll.pk}
'createdPollId': poll.pk}) )
@detail_route(methods=['post']) @detail_route(methods=["post"])
def sort_related_users(self, request, pk=None): def sort_related_users(self, request, pk=None):
""" """
Special view endpoint to sort the assignment related users. Special view endpoint to sort the assignment related users.
@ -206,22 +256,25 @@ class AssignmentViewSet(ModelViewSet):
assignment = self.get_object() assignment = self.get_object()
# Check data # 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): if not isinstance(related_user_ids, list):
raise ValidationError( raise ValidationError({"detail": _("users has to be a list of IDs.")})
{'detail': _('users has to be a list of IDs.')})
# Get all related users from AssignmentRelatedUser. # Get all related users from AssignmentRelatedUser.
related_users = {} 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 related_users[related_user.pk] = related_user
# Check all given candidates from the request # Check all given candidates from the request
valid_related_users = [] valid_related_users = []
for related_user_id in related_user_ids: for related_user_id in related_user_ids:
if not isinstance(related_user_id, int) or related_users.get(related_user_id) is None: if (
raise ValidationError( not isinstance(related_user_id, int)
{'detail': _('Invalid data.')}) or related_users.get(related_user_id) is None
):
raise ValidationError({"detail": _("Invalid data.")})
valid_related_users.append(related_users[related_user_id]) valid_related_users.append(related_users[related_user_id])
# Sort the related users # Sort the related users
@ -236,7 +289,7 @@ class AssignmentViewSet(ModelViewSet):
inform_changed_data(assignment) inform_changed_data(assignment)
# Initiate response. # Initiate response.
return Response({'detail': _('Assignment related users successfully sorted.')}) return Response({"detail": _("Assignment related users successfully sorted.")})
class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
@ -245,6 +298,7 @@ class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet)
There are the following views: update, partial_update and destroy. There are the following views: update, partial_update and destroy.
""" """
queryset = AssignmentPoll.objects.all() queryset = AssignmentPoll.objects.all()
serializer_class = AssignmentAllPollSerializer serializer_class = AssignmentAllPollSerializer
@ -252,5 +306,6 @@ class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet)
""" """
Returns True if the user has required permissions. Returns True if the user has required permissions.
""" """
return (has_perm(self.request.user, 'assignments.can_see') and return has_perm(self.request.user, "assignments.can_see") and has_perm(
has_perm(self.request.user, 'assignments.can_manage')) self.request.user, "assignments.can_manage"
)

View File

@ -1 +1 @@
default_app_config = 'openslides.core.apps.CoreAppConfig' default_app_config = "openslides.core.apps.CoreAppConfig"

View File

@ -6,7 +6,8 @@ class ProjectorAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Projector and ProjectorViewSet. Access permissions container for Projector and ProjectorViewSet.
""" """
base_permission = 'core.can_see_projector'
base_permission = "core.can_see_projector"
class TagAccessPermissions(BaseAccessPermissions): class TagAccessPermissions(BaseAccessPermissions):
@ -19,21 +20,24 @@ class ChatMessageAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for ChatMessage and ChatMessageViewSet. Access permissions container for ChatMessage and ChatMessageViewSet.
""" """
base_permission = 'core.can_use_chat'
base_permission = "core.can_use_chat"
class ProjectorMessageAccessPermissions(BaseAccessPermissions): class ProjectorMessageAccessPermissions(BaseAccessPermissions):
""" """
Access permissions for ProjectorMessage. Access permissions for ProjectorMessage.
""" """
base_permission = 'core.can_see_projector'
base_permission = "core.can_see_projector"
class CountdownAccessPermissions(BaseAccessPermissions): class CountdownAccessPermissions(BaseAccessPermissions):
""" """
Access permissions for Countdown. Access permissions for Countdown.
""" """
base_permission = 'core.can_see_projector'
base_permission = "core.can_see_projector"
class ConfigAccessPermissions(BaseAccessPermissions): class ConfigAccessPermissions(BaseAccessPermissions):

View File

@ -11,8 +11,8 @@ from ..utils.projector import register_projector_elements
class CoreAppConfig(AppConfig): class CoreAppConfig(AppConfig):
name = 'openslides.core' name = "openslides.core"
verbose_name = 'OpenSlides Core' verbose_name = "OpenSlides Core"
angular_site_module = True angular_site_module = True
angular_projector_module = True angular_projector_module = True
@ -54,7 +54,7 @@ class CoreAppConfig(AppConfig):
# Skip all database related accesses during migrations. # Skip all database related accesses during migrations.
is_normal_server_start = False is_normal_server_start = False
for sys_part in sys.argv: 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): if sys_part.endswith(entry):
is_normal_server_start = True is_normal_server_start = True
break break
@ -68,27 +68,46 @@ class CoreAppConfig(AppConfig):
# Connect signals. # Connect signals.
post_permission_creation.connect( post_permission_creation.connect(
delete_django_app_permissions, delete_django_app_permissions, dispatch_uid="delete_django_app_permissions"
dispatch_uid='delete_django_app_permissions') )
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data, dispatch_uid="core_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. # Register viewsets.
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet) router.register(
router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet) self.get_model("Projector").get_collection_string(), ProjectorViewSet
router.register(self.get_model('Tag').get_collection_string(), TagViewSet) )
router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config') router.register(
router.register(self.get_model('ProjectorMessage').get_collection_string(), ProjectorMessageViewSet) self.get_model("ChatMessage").get_collection_string(), ChatMessageViewSet
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("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 # Sets the cache and builds the startup history
if is_normal_server_start: if is_normal_server_start:
element_cache.ensure_cache() element_cache.ensure_cache()
self.get_model('History').objects.build_history() self.get_model("History").objects.build_history()
# Register client messages # Register client messages
register_client_message(NotifyWebsocketClientMessage()) register_client_message(NotifyWebsocketClientMessage())
@ -97,10 +116,13 @@ class CoreAppConfig(AppConfig):
register_client_message(AutoupdateWebsocketClientMessage()) register_client_message(AutoupdateWebsocketClientMessage())
# register required_users # 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): def get_config_variables(self):
from .config_variables import get_config_variables from .config_variables import get_config_variables
return get_config_variables() return get_config_variables()
def get_startup_elements(self): def get_startup_elements(self):
@ -108,7 +130,15 @@ class CoreAppConfig(AppConfig):
Yields all Cachables required on startup i. e. opening the websocket Yields all Cachables required on startup i. e. opening the websocket
connection. 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) yield self.get_model(model_name)
def get_angular_constants(self): def get_angular_constants(self):
@ -118,9 +148,9 @@ class CoreAppConfig(AppConfig):
# Client settings # Client settings
client_settings_keys = [ client_settings_keys = [
'MOTION_IDENTIFIER_MIN_DIGITS', "MOTION_IDENTIFIER_MIN_DIGITS",
'MOTION_IDENTIFIER_WITHOUT_BLANKS', "MOTION_IDENTIFIER_WITHOUT_BLANKS",
'MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS' "MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS",
] ]
client_settings_dict = {} client_settings_dict = {}
for key in client_settings_keys: for key in client_settings_keys:
@ -130,33 +160,40 @@ class CoreAppConfig(AppConfig):
# Settings key does not exist. Do nothing. The client will # Settings key does not exist. Do nothing. The client will
# treat this as undefined. # treat this as undefined.
pass pass
constants['OpenSlidesSettings'] = client_settings_dict constants["OpenSlidesSettings"] = client_settings_dict
# Config variables # Config variables
config_groups: List[Any] = [] 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(): if config_variable.is_hidden():
# Skip hidden config variables. Do not even check groups and subgroups. # Skip hidden config variables. Do not even check groups and subgroups.
continue 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. # Add new group.
config_groups.append(OrderedDict( config_groups.append(
name=config_variable.group, OrderedDict(name=config_variable.group, subgroups=[])
subgroups=[])) )
if not config_groups[-1]['subgroups'] or config_groups[-1]['subgroups'][-1]['name'] != config_variable.subgroup: if (
not config_groups[-1]["subgroups"]
or config_groups[-1]["subgroups"][-1]["name"]
!= config_variable.subgroup
):
# Add new subgroup. # Add new subgroup.
config_groups[-1]['subgroups'].append(OrderedDict( config_groups[-1]["subgroups"].append(
name=config_variable.subgroup, OrderedDict(name=config_variable.subgroup, items=[])
items=[])) )
# Add the config variable to the current group and subgroup. # Add the config variable to the current group and subgroup.
config_groups[-1]['subgroups'][-1]['items'].append(config_variable.data) config_groups[-1]["subgroups"][-1]["items"].append(config_variable.data)
constants['OpenSlidesConfigVariables'] = config_groups constants["OpenSlidesConfigVariables"] = config_groups
return constants return constants
def call_save_default_values(**kwargs): def call_save_default_values(**kwargs):
from .config import config from .config import config
config.save_default_values() 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. Returns all user ids that are displayed as chatters.
""" """
return set(element['user_id']) return set(element["user_id"])

View File

@ -1,13 +1,4 @@
from typing import ( from typing import Any, Callable, Dict, Iterable, Optional, TypeVar, Union, cast
Any,
Callable,
Dict,
Iterable,
Optional,
TypeVar,
Union,
cast,
)
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from django.apps import apps from django.apps import apps
@ -21,16 +12,16 @@ from .models import ConfigStore
INPUT_TYPE_MAPPING = { INPUT_TYPE_MAPPING = {
'string': str, "string": str,
'text': str, "text": str,
'markupText': str, "markupText": str,
'integer': int, "integer": int,
'boolean': bool, "boolean": bool,
'choice': str, "choice": str,
'colorpicker': str, "colorpicker": str,
'datetimepicker': int, "datetimepicker": int,
'static': dict, "static": dict,
'translations': list, "translations": list,
} }
@ -54,9 +45,11 @@ class ConfigHandler:
Returns the value of the config variable. Returns the value of the config variable.
""" """
if not self.exists(key): 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]: def get_key_to_id(self) -> Dict[str, int]:
""" """
@ -80,7 +73,7 @@ class ConfigHandler:
all_data = await element_cache.get_all_full_data() all_data = await element_cache.get_all_full_data()
elements = all_data[self.get_collection_string()] elements = all_data[self.get_collection_string()]
for element in elements: 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: def exists(self, key: str) -> bool:
""" """
@ -102,7 +95,7 @@ class ConfigHandler:
try: try:
config_variable = self.config_variables[key] config_variable = self.config_variables[key]
except KeyError: 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. # Validate datatype and run validators.
expected_type = INPUT_TYPE_MAPPING[config_variable.input_type] expected_type = INPUT_TYPE_MAPPING[config_variable.input_type]
@ -111,17 +104,21 @@ class ConfigHandler:
try: try:
value = expected_type(value) value = expected_type(value)
except ValueError: except ValueError:
raise ConfigError(_('Wrong datatype. Expected %(expected_type)s, got %(got_type)s.') % { raise ConfigError(
'expected_type': expected_type, 'got_type': type(value)}) _("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 # Choices can be a callable. In this case call it at this place
if callable(config_variable.choices): if callable(config_variable.choices):
choices = config_variable.choices() choices = config_variable.choices()
else: else:
choices = config_variable.choices choices = config_variable.choices
if choices is None or value not in map(lambda choice: choice['value'], choices): if choices is None or value not in map(
raise ConfigError(_('Invalid input. Choice does not match.')) lambda choice: choice["value"], choices
):
raise ConfigError(_("Invalid input. Choice does not match."))
for validator in config_variable.validators: for validator in config_variable.validators:
try: try:
@ -129,34 +126,36 @@ class ConfigHandler:
except DjangoValidationError as e: except DjangoValidationError as e:
raise ConfigError(e.messages[0]) raise ConfigError(e.messages[0])
if config_variable.input_type == 'static': if config_variable.input_type == "static":
if not isinstance(value, dict): if not isinstance(value, dict):
raise ConfigError(_('This has to be a dict.')) raise ConfigError(_("This has to be a dict."))
whitelist = ( whitelist = ("path", "display_name")
'path',
'display_name',
)
for required_entry in whitelist: for required_entry in whitelist:
if required_entry not in value: 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): 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): 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: for entry in value:
if not isinstance(entry, dict): if not isinstance(entry, dict):
raise ConfigError(_('Every value has to be a dict, not {}.'.format(type(entry)))) raise ConfigError(
whitelist = ( _("Every value has to be a dict, not {}.".format(type(entry)))
'original', )
'translation', whitelist = ("original", "translation")
)
for required_entry in whitelist: for required_entry in whitelist:
if required_entry not in entry: 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): 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. # Save the new value to the database.
db_value = ConfigStore.objects.get(key=key) db_value = ConfigStore.objects.get(key=key)
@ -178,7 +177,7 @@ class ConfigHandler:
continue continue
self.update_config_variables(get_config_variables()) 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. Updates the config_variables dict.
""" """
@ -189,7 +188,9 @@ class ConfigHandler:
# be in already in self.config_variables # be in already in self.config_variables
intersection = set(item_index.keys()).intersection(self.config_variables.keys()) intersection = set(item_index.keys()).intersection(self.config_variables.keys())
if intersection: 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) 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]]] ChoiceType = Optional[Iterable[Dict[str, str]]]
ChoiceCallableType = Union[ChoiceType, Callable[[], ChoiceType]] ChoiceCallableType = Union[ChoiceType, Callable[[], ChoiceType]]
ValidatorsType = Iterable[Callable[[T], None]] ValidatorsType = Iterable[Callable[[T], None]]
OnChangeType = Callable[[], None] OnChangeType = Callable[[], None]
ConfigVariableDict = TypedDict('ConfigVariableDict', { ConfigVariableDict = TypedDict(
'key': str, "ConfigVariableDict",
'default_value': Any, {
'input_type': str, "key": str,
'label': str, "default_value": Any,
'help_text': str, "input_type": str,
'choices': ChoiceType, "label": str,
}) "help_text": str,
"choices": ChoiceType,
},
)
class ConfigVariable: class ConfigVariable:
@ -265,27 +269,47 @@ class ConfigVariable:
the value during setup of the database if the admin uses the respective the value during setup of the database if the admin uses the respective
command line option. 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, def __init__(
hidden: bool = False, weight: int = 0, group: str = None, subgroup: str = None, self,
validators: ValidatorsType = None, on_change: OnChangeType = None) -> None: 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: if input_type not in INPUT_TYPE_MAPPING:
raise ValueError(_('Invalid value for config attribute input_type.')) raise ValueError(_("Invalid value for config attribute input_type."))
if input_type == 'choice' and choices is None: if input_type == "choice" and choices is None:
raise ConfigError(_("Either config attribute 'choices' must not be None or " raise ConfigError(
"'input_type' must not be 'choice'.")) _(
elif input_type != 'choice' and choices is not None: "Either config attribute 'choices' must not be None or "
raise ConfigError(_("Either config attribute 'choices' must be None or " "'input_type' must not be 'choice'."
"'input_type' must 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.name = name
self.default_value = default_value self.default_value = default_value
self.input_type = input_type self.input_type = input_type
self.label = label or name self.label = label or name
self.help_text = help_text or '' self.help_text = help_text or ""
self.choices = choices self.choices = choices
self.hidden = hidden self.hidden = hidden
self.weight = weight self.weight = weight
self.group = group or _('General') self.group = group or _("General")
self.subgroup = subgroup self.subgroup = subgroup
self.validators = validators or () self.validators = validators or ()
self.on_change = on_change self.on_change = on_change
@ -301,7 +325,7 @@ class ConfigVariable:
input_type=self.input_type, input_type=self.input_type,
label=self.label, label=self.label,
help_text=self.help_text, 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: def is_hidden(self) -> bool:

View File

@ -12,401 +12,429 @@ def get_config_variables():
(see apps.py). (see apps.py).
""" """
yield ConfigVariable( yield ConfigVariable(
name='general_event_name', name="general_event_name",
default_value='OpenSlides', default_value="OpenSlides",
label='Event name', label="Event name",
weight=110, weight=110,
group='General', group="General",
subgroup='Event', subgroup="Event",
validators=(MaxLengthValidator(100),)) validators=(MaxLengthValidator(100),),
)
yield ConfigVariable( yield ConfigVariable(
name='general_event_description', name="general_event_description",
default_value='Presentation and assembly system', default_value="Presentation and assembly system",
label='Short description of event', label="Short description of event",
weight=115, weight=115,
group='General', group="General",
subgroup='Event', subgroup="Event",
validators=(MaxLengthValidator(100),)) validators=(MaxLengthValidator(100),),
)
yield ConfigVariable( yield ConfigVariable(
name='general_event_date', name="general_event_date",
default_value='', default_value="",
label='Event date', label="Event date",
weight=120, weight=120,
group='General', group="General",
subgroup='Event') subgroup="Event",
)
yield ConfigVariable( yield ConfigVariable(
name='general_event_location', name="general_event_location",
default_value='', default_value="",
label='Event location', label="Event location",
weight=125, weight=125,
group='General', group="General",
subgroup='Event') subgroup="Event",
)
yield ConfigVariable( yield ConfigVariable(
name='general_event_legal_notice', name="general_event_legal_notice",
default_value='<a href="http://www.openslides.org">OpenSlides</a> is a ' default_value='<a href="http://www.openslides.org">OpenSlides</a> is a '
'free web based presentation and assembly system for ' "free web based presentation and assembly system for "
'visualizing and controlling agenda, motions and ' "visualizing and controlling agenda, motions and "
'elections of an assembly.', "elections of an assembly.",
input_type='markupText', input_type="markupText",
label='Legal notice', label="Legal notice",
weight=132, weight=132,
group='General', group="General",
subgroup='Event') subgroup="Event",
)
yield ConfigVariable( yield ConfigVariable(
name='general_event_privacy_policy', name="general_event_privacy_policy",
default_value='', default_value="",
input_type='markupText', input_type="markupText",
label='Privacy policy', label="Privacy policy",
weight=132, weight=132,
group='General', group="General",
subgroup='Event') subgroup="Event",
)
yield ConfigVariable( yield ConfigVariable(
name='general_event_welcome_title', name="general_event_welcome_title",
default_value='Welcome to OpenSlides', default_value="Welcome to OpenSlides",
label='Front page title', label="Front page title",
weight=134, weight=134,
group='General', group="General",
subgroup='Event') subgroup="Event",
)
yield ConfigVariable( yield ConfigVariable(
name='general_event_welcome_text', name="general_event_welcome_text",
default_value='[Space for your welcome text.]', default_value="[Space for your welcome text.]",
input_type='markupText', input_type="markupText",
label='Front page text', label="Front page text",
weight=136, weight=136,
group='General', group="General",
subgroup='Event') subgroup="Event",
)
# General System # General System
yield ConfigVariable( yield ConfigVariable(
name='general_system_enable_anonymous', name="general_system_enable_anonymous",
default_value=False, default_value=False,
input_type='boolean', input_type="boolean",
label='Allow access for anonymous guest users', label="Allow access for anonymous guest users",
weight=138, weight=138,
group='General', group="General",
subgroup='System') subgroup="System",
)
yield ConfigVariable( yield ConfigVariable(
name='general_login_info_text', name="general_login_info_text",
default_value='', default_value="",
label='Show this text on the login page', label="Show this text on the login page",
weight=140, weight=140,
group='General', group="General",
subgroup='System') subgroup="System",
)
# General export settings # General export settings
yield ConfigVariable( yield ConfigVariable(
name='general_csv_separator', name="general_csv_separator",
default_value=',', default_value=",",
label='Separator used for all csv exports and examples', label="Separator used for all csv exports and examples",
weight=142, weight=142,
group='General', group="General",
subgroup='Export') subgroup="Export",
)
yield ConfigVariable( yield ConfigVariable(
name='general_export_pdf_pagenumber_alignment', name="general_export_pdf_pagenumber_alignment",
default_value='center', default_value="center",
input_type='choice', input_type="choice",
label='Page number alignment in PDF', label="Page number alignment in PDF",
choices=( choices=(
{'value': 'left', 'display_name': 'Left'}, {"value": "left", "display_name": "Left"},
{'value': 'center', 'display_name': 'Center'}, {"value": "center", "display_name": "Center"},
{'value': 'right', 'display_name': 'Right'}), {"value": "right", "display_name": "Right"},
),
weight=144, weight=144,
group='General', group="General",
subgroup='Export') subgroup="Export",
)
yield ConfigVariable( yield ConfigVariable(
name='general_export_pdf_fontsize', name="general_export_pdf_fontsize",
default_value='10', default_value="10",
input_type='choice', input_type="choice",
label='Standard font size in PDF', label="Standard font size in PDF",
choices=( choices=(
{'value': '10', 'display_name': '10'}, {"value": "10", "display_name": "10"},
{'value': '11', 'display_name': '11'}, {"value": "11", "display_name": "11"},
{'value': '12', 'display_name': '12'}), {"value": "12", "display_name": "12"},
),
weight=146, weight=146,
group='General', group="General",
subgroup='Export') subgroup="Export",
)
# Projector # Projector
yield ConfigVariable( yield ConfigVariable(
name='projector_language', name="projector_language",
default_value='browser', default_value="browser",
input_type='choice', input_type="choice",
label='Projector language', label="Projector language",
choices=( choices=(
{'value': 'browser', 'display_name': 'Current browser language'}, {"value": "browser", "display_name": "Current browser language"},
{'value': 'en', 'display_name': 'English'}, {"value": "en", "display_name": "English"},
{'value': 'de', 'display_name': 'Deutsch'}, {"value": "de", "display_name": "Deutsch"},
{'value': 'fr', 'display_name': 'Français'}, {"value": "fr", "display_name": "Français"},
{'value': 'es', 'display_name': 'Español'}, {"value": "es", "display_name": "Español"},
{'value': 'pt', 'display_name': 'Português'}, {"value": "pt", "display_name": "Português"},
{'value': 'cs', 'display_name': 'Čeština'}, {"value": "cs", "display_name": "Čeština"},
{'value': 'ru', 'display_name': 'русский'}), {"value": "ru", "display_name": "русский"},
),
weight=150, weight=150,
group='Projector') group="Projector",
)
yield ConfigVariable( yield ConfigVariable(
name='projector_enable_logo', name="projector_enable_logo",
default_value=True, default_value=True,
input_type='boolean', input_type="boolean",
label='Show logo on projector', label="Show logo on projector",
help_text='You can replace the logo by uploading an image and set it as ' help_text="You can replace the logo by uploading an image and set it as "
'the "Projector logo" in "files".', 'the "Projector logo" in "files".',
weight=152, weight=152,
group='Projector') group="Projector",
)
yield ConfigVariable( yield ConfigVariable(
name='projector_enable_clock', name="projector_enable_clock",
default_value=True, default_value=True,
input_type='boolean', input_type="boolean",
label='Show the clock on projector', label="Show the clock on projector",
weight=154, weight=154,
group='Projector') group="Projector",
)
yield ConfigVariable( yield ConfigVariable(
name='projector_enable_title', name="projector_enable_title",
default_value=True, default_value=True,
input_type='boolean', input_type="boolean",
label='Show title and description of event on projector', label="Show title and description of event on projector",
weight=155, weight=155,
group='Projector') group="Projector",
)
yield ConfigVariable( yield ConfigVariable(
name='projector_enable_header_footer', name="projector_enable_header_footer",
default_value=True, default_value=True,
input_type='boolean', input_type="boolean",
label='Display header and footer', label="Display header and footer",
weight=157, weight=157,
group='Projector') group="Projector",
)
yield ConfigVariable( yield ConfigVariable(
name='projector_header_backgroundcolor', name="projector_header_backgroundcolor",
default_value='#317796', default_value="#317796",
input_type='colorpicker', input_type="colorpicker",
label='Background color of projector header and footer', label="Background color of projector header and footer",
weight=160, weight=160,
group='Projector') group="Projector",
)
yield ConfigVariable( yield ConfigVariable(
name='projector_header_fontcolor', name="projector_header_fontcolor",
default_value='#F5F5F5', default_value="#F5F5F5",
input_type='colorpicker', input_type="colorpicker",
label='Font color of projector header and footer', label="Font color of projector header and footer",
weight=165, weight=165,
group='Projector') group="Projector",
)
yield ConfigVariable( yield ConfigVariable(
name='projector_h1_fontcolor', name="projector_h1_fontcolor",
default_value='#317796', default_value="#317796",
input_type='colorpicker', input_type="colorpicker",
label='Font color of projector headline', label="Font color of projector headline",
weight=170, weight=170,
group='Projector') group="Projector",
)
yield ConfigVariable( yield ConfigVariable(
name='projector_default_countdown', name="projector_default_countdown",
default_value=60, default_value=60,
input_type='integer', input_type="integer",
label='Predefined seconds of new countdowns', label="Predefined seconds of new countdowns",
weight=185, weight=185,
group='Projector') group="Projector",
)
yield ConfigVariable( yield ConfigVariable(
name='projector_blank_color', name="projector_blank_color",
default_value='#FFFFFF', default_value="#FFFFFF",
input_type='colorpicker', input_type="colorpicker",
label='Color for blanked projector', label="Color for blanked projector",
weight=190, weight=190,
group='Projector') group="Projector",
)
yield ConfigVariable( yield ConfigVariable(
name='projector_broadcast', name="projector_broadcast",
default_value=0, default_value=0,
input_type='integer', input_type="integer",
label='Projector which is broadcasted', label="Projector which is broadcasted",
weight=200, weight=200,
group='Projector', group="Projector",
hidden=True) hidden=True,
)
yield ConfigVariable( yield ConfigVariable(
name='projector_currentListOfSpeakers_reference', name="projector_currentListOfSpeakers_reference",
default_value=1, default_value=1,
input_type='integer', input_type="integer",
label='Projector reference for list of speakers', label="Projector reference for list of speakers",
weight=201, weight=201,
group='Projector', group="Projector",
hidden=True) hidden=True,
)
# Logos. # Logos.
yield ConfigVariable( yield ConfigVariable(
name='logos_available', name="logos_available",
default_value=[ default_value=[
'logo_projector_main', "logo_projector_main",
'logo_projector_header', "logo_projector_header",
'logo_web_header', "logo_web_header",
'logo_pdf_header_L', "logo_pdf_header_L",
'logo_pdf_header_R', "logo_pdf_header_R",
'logo_pdf_footer_L', "logo_pdf_footer_L",
'logo_pdf_footer_R', "logo_pdf_footer_R",
'logo_pdf_ballot_paper'], "logo_pdf_ballot_paper",
],
weight=300, weight=300,
group='Logo', group="Logo",
hidden=True) hidden=True,
)
yield ConfigVariable( yield ConfigVariable(
name='logo_projector_main', name="logo_projector_main",
default_value={ default_value={"display_name": "Projector logo", "path": ""},
'display_name': 'Projector logo', input_type="static",
'path': ''},
input_type='static',
weight=301, weight=301,
group='Logo', group="Logo",
hidden=True) hidden=True,
)
yield ConfigVariable( yield ConfigVariable(
name='logo_projector_header', name="logo_projector_header",
default_value={ default_value={"display_name": "Projector header image", "path": ""},
'display_name': 'Projector header image', input_type="static",
'path': ''},
input_type='static',
weight=302, weight=302,
group='Logo', group="Logo",
hidden=True) hidden=True,
)
yield ConfigVariable( yield ConfigVariable(
name='logo_web_header', name="logo_web_header",
default_value={ default_value={"display_name": "Web interface header logo", "path": ""},
'display_name': 'Web interface header logo', input_type="static",
'path': ''},
input_type='static',
weight=303, weight=303,
group='Logo', group="Logo",
hidden=True) hidden=True,
)
# PDF logos # PDF logos
yield ConfigVariable( yield ConfigVariable(
name='logo_pdf_header_L', name="logo_pdf_header_L",
default_value={ default_value={"display_name": "PDF header logo (left)", "path": ""},
'display_name': 'PDF header logo (left)', input_type="static",
'path': ''},
input_type='static',
weight=310, weight=310,
group='Logo', group="Logo",
hidden=True) hidden=True,
)
yield ConfigVariable( yield ConfigVariable(
name='logo_pdf_header_R', name="logo_pdf_header_R",
default_value={ default_value={"display_name": "PDF header logo (right)", "path": ""},
'display_name': 'PDF header logo (right)', input_type="static",
'path': ''},
input_type='static',
weight=311, weight=311,
group='Logo', group="Logo",
hidden=True) hidden=True,
)
yield ConfigVariable( yield ConfigVariable(
name='logo_pdf_footer_L', name="logo_pdf_footer_L",
default_value={ default_value={"display_name": "PDF footer logo (left)", "path": ""},
'display_name': 'PDF footer logo (left)', input_type="static",
'path': ''},
input_type='static',
weight=312, weight=312,
group='Logo', group="Logo",
hidden=True) hidden=True,
)
yield ConfigVariable( yield ConfigVariable(
name='logo_pdf_footer_R', name="logo_pdf_footer_R",
default_value={ default_value={"display_name": "PDF footer logo (right)", "path": ""},
'display_name': 'PDF footer logo (right)', input_type="static",
'path': ''},
input_type='static',
weight=313, weight=313,
group='Logo', group="Logo",
hidden=True) hidden=True,
)
yield ConfigVariable( yield ConfigVariable(
name='logo_pdf_ballot_paper', name="logo_pdf_ballot_paper",
default_value={ default_value={"display_name": "PDF ballot paper logo", "path": ""},
'display_name': 'PDF ballot paper logo', input_type="static",
'path': ''},
input_type='static',
weight=314, weight=314,
group='Logo', group="Logo",
hidden=True) hidden=True,
)
# Fonts # Fonts
yield ConfigVariable( yield ConfigVariable(
name='fonts_available', name="fonts_available",
default_value=[ default_value=["font_regular", "font_italic", "font_bold", "font_bold_italic"],
'font_regular',
'font_italic',
'font_bold',
'font_bold_italic'],
weight=320, weight=320,
group='Font', group="Font",
hidden=True) hidden=True,
)
yield ConfigVariable( yield ConfigVariable(
name='font_regular', name="font_regular",
default_value={ default_value={
'display_name': 'Font regular', "display_name": "Font regular",
'default': 'static/fonts/Roboto-Regular.woff', "default": "static/fonts/Roboto-Regular.woff",
'path': ''}, "path": "",
input_type='static', },
input_type="static",
weight=321, weight=321,
group='Font', group="Font",
hidden=True) hidden=True,
)
yield ConfigVariable( yield ConfigVariable(
name='font_italic', name="font_italic",
default_value={ default_value={
'display_name': 'Font italic', "display_name": "Font italic",
'default': 'static/fonts/Roboto-Medium.woff', "default": "static/fonts/Roboto-Medium.woff",
'path': ''}, "path": "",
input_type='static', },
input_type="static",
weight=321, weight=321,
group='Font', group="Font",
hidden=True) hidden=True,
)
yield ConfigVariable( yield ConfigVariable(
name='font_bold', name="font_bold",
default_value={ default_value={
'display_name': 'Font bold', "display_name": "Font bold",
'default': 'static/fonts/Roboto-Condensed-Regular.woff', "default": "static/fonts/Roboto-Condensed-Regular.woff",
'path': ''}, "path": "",
input_type='static', },
input_type="static",
weight=321, weight=321,
group='Font', group="Font",
hidden=True) hidden=True,
)
yield ConfigVariable( yield ConfigVariable(
name='font_bold_italic', name="font_bold_italic",
default_value={ default_value={
'display_name': 'Font bold italic', "display_name": "Font bold italic",
'default': 'static/fonts/Roboto-Condensed-Light.woff', "default": "static/fonts/Roboto-Condensed-Light.woff",
'path': ''}, "path": "",
input_type='static', },
input_type="static",
weight=321, weight=321,
group='Font', group="Font",
hidden=True) hidden=True,
)
# Custom translations # Custom translations
yield ConfigVariable( yield ConfigVariable(
name='translations', name="translations",
label='Custom translations', label="Custom translations",
default_value=[], default_value=[],
input_type='translations', input_type="translations",
weight=1000, weight=1000,
group='Custom translations') group="Custom translations",
)

View File

@ -10,17 +10,18 @@ class Command(BaseCommand):
""" """
Command to backup the SQLite3 database. Command to backup the SQLite3 database.
""" """
help = 'Backups the SQLite3 database.'
help = "Backups the SQLite3 database."
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'--path', "--path",
default='database_backup.sqlite', default="database_backup.sqlite",
help='Path for the backup file (Default: database_backup.sqlite).' help="Path for the backup file (Default: database_backup.sqlite).",
) )
def handle(self, *args, **options): def handle(self, *args, **options):
path = options.get('path') path = options.get("path")
@transaction.atomic @transaction.atomic
def do_backup(src_path, dest_path): def do_backup(src_path, dest_path):
@ -39,8 +40,11 @@ class Command(BaseCommand):
database_path = get_database_path_from_settings() database_path = get_database_path_from_settings()
if database_path: if database_path:
do_backup(database_path, 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: else:
raise CommandError( raise CommandError(
'Default database is not SQLite3. Only SQLite3 databases' "Default database is not SQLite3. Only SQLite3 databases"
'can currently be backuped.') "can currently be backuped."
)

View File

@ -8,25 +8,28 @@ class Command(BaseCommand):
""" """
Command to change OpenSlides config values. Command to change OpenSlides config values.
""" """
help = 'Changes OpenSlides config values.'
help = "Changes OpenSlides config values."
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'key', "key", help="Config key. See config_variables.py in every app."
help='Config key. See config_variables.py in every app.'
) )
parser.add_argument( parser.add_argument(
'value', "value", help='New config value. For a falsy boolean use "False".'
help='New config value. For a falsy boolean use "False".'
) )
def handle(self, *args, **options): def handle(self, *args, **options):
if options['value'].lower() == 'false': if options["value"].lower() == "false":
options['value'] = False options["value"] = False
try: try:
config[options['key']] = options['value'] config[options["key"]] = options["value"]
except (ConfigError, ConfigNotFound) as e: except (ConfigError, ConfigNotFound) as e:
raise CommandError(str(e)) raise CommandError(str(e))
self.stdout.write( self.stdout.write(
self.style.SUCCESS('Config {key} successfully changed to {value}.'.format( self.style.SUCCESS(
key=options['key'], value=config[options['key']]))) "Config {key} successfully changed to {value}.".format(
key=options["key"], value=config[options["key"]]
)
)
)

View File

@ -7,19 +7,16 @@ class Command(BaseCommand):
""" """
Command to change a user's password. Command to change a user's password.
""" """
help = 'Changes user password.'
help = "Changes user password."
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'username', "username", help="The name of the user to set the password for"
help='The name of the user to set the password for'
)
parser.add_argument(
'password',
help='The new password of the user'
) )
parser.add_argument("password", help="The new password of the user")
def handle(self, *args, **options): def handle(self, *args, **options):
user = User.objects.get(username=options['username']) user = User.objects.get(username=options["username"])
user.set_password(options['password']) user.set_password(options["password"])
user.save() user.save()

View File

@ -10,10 +10,12 @@ class Command(_Command):
Migration command that does nearly the same as Django's migration command Migration command that does nearly the same as Django's migration command
but also calls the post_permission_creation signal. but also calls the post_permission_creation signal.
""" """
def handle(self, *args, **options): def handle(self, *args, **options):
from django.conf import settings from django.conf import settings
# Creates the folder for a SQLite3 database if necessary. # 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: try:
os.makedirs(settings.OPENSLIDES_USER_DATA_PATH) os.makedirs(settings.OPENSLIDES_USER_DATA_PATH)
except (FileExistsError, AttributeError): except (FileExistsError, AttributeError):

View File

@ -18,11 +18,9 @@ def add_default_projector(apps, schema_editor):
""" """
# We get the model from the versioned app registry; # We get the model from the versioned app registry;
# if we directly import it, it will be the wrong version. # 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 = {}
projector_config[uuid.uuid4().hex] = { projector_config[uuid.uuid4().hex] = {"name": "core/clock", "stable": True}
'name': 'core/clock',
'stable': True}
# We use bulk_create here because we do not want model's save() method # 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 # to be called because we do not want our autoupdate signals to be
# triggered. # triggered.
@ -34,79 +32,126 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('mediafiles', '0001_initial'), ("mediafiles", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='ChatMessage', name="ChatMessage",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('message', models.TextField()), "id",
('timestamp', models.DateTimeField(auto_now_add=True)), models.AutoField(
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 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={ options={
'default_permissions': (), "default_permissions": (),
'permissions': (('can_use_chat', 'Can use the chat'),), "permissions": (("can_use_chat", "Can use the chat"),),
}, },
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='ConfigStore', name="ConfigStore",
fields=[ 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)), "id",
('value', jsonfield.fields.JSONField()), 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={ options={
'default_permissions': (), "default_permissions": (),
'permissions': (('can_manage_config', 'Can manage configuration'),), "permissions": (("can_manage_config", "Can manage configuration"),),
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='CustomSlide', name="CustomSlide",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('title', models.CharField(max_length=256)), "id",
('text', models.TextField(blank=True)), models.AutoField(
('weight', models.IntegerField(default=0)), auto_created=True,
('attachments', models.ManyToManyField(blank=True, to='mediafiles.Mediafile')), 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={ options={
'default_permissions': (), "default_permissions": (),
'ordering': ('weight', 'title'), "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), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='Projector', name="Tag",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('config', jsonfield.fields.JSONField()), "id",
('scale', models.IntegerField(default=0)), models.AutoField(
('scroll', models.IntegerField(default=0)), auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
], ],
options={ options={
'default_permissions': (), "default_permissions": (),
'permissions': ( "permissions": (("can_manage_tags", "Can manage tags"),),
('can_see_projector', 'Can see the projector'), "ordering": ("name",),
('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',),
}, },
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),

View File

@ -15,10 +15,10 @@ def move_custom_slides_to_topics(apps, schema_editor):
""" """
# We get the model from the versioned app registry; # We get the model from the versioned app registry;
# if we directly import it, it will be the wrong version. # if we directly import it, it will be the wrong version.
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model("contenttypes", "ContentType")
CustomSlide = apps.get_model('core', 'CustomSlide') CustomSlide = apps.get_model("core", "CustomSlide")
Item = apps.get_model('agenda', 'Item') Item = apps.get_model("agenda", "Item")
Topic = apps.get_model('topics', 'Topic') Topic = apps.get_model("topics", "Topic")
# Copy data. # Copy data.
content_type_custom_slide = ContentType.objects.get_for_model(CustomSlide) 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. # no method 'get_agenda_title()'. See agenda/signals.py.
topic = Topic.objects.create(title=custom_slide.title, text=custom_slide.text) topic = Topic.objects.create(title=custom_slide.title, text=custom_slide.text)
topic.attachments.add(*custom_slide.attachments.all()) 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.object_id = topic.pk
item.content_type = content_type_topic item.content_type = content_type_topic
item.save(skip_autoupdate=True) 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' Set the name of the default projector to 'Defaultprojector'
""" """
Projector = apps.get_model('core', 'Projector') Projector = apps.get_model("core", "Projector")
Projector.objects.filter(pk=1).update(name='Default projector') Projector.objects.filter(pk=1).update(name="Default projector")
def remove_old_countdowns_messages(apps, schema_editor): 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. 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 = Projector.objects.get(pk=1)
projector_config = projector.config projector_config = projector.config
for key, value in list(projector.config.items()): 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] del projector_config[key]
projector.config = projector_config projector.config = projector_config
projector.save(skip_autoupdate=True) projector.save(skip_autoupdate=True)
@ -65,57 +67,74 @@ def add_projection_defaults(apps, schema_editor):
""" """
Adds projectiondefaults for messages and countdowns. Adds projectiondefaults for messages and countdowns.
""" """
Projector = apps.get_model('core', 'Projector') Projector = apps.get_model("core", "Projector")
ProjectionDefault = apps.get_model('core', 'ProjectionDefault') ProjectionDefault = apps.get_model("core", "ProjectionDefault")
# The default projector (pk=1) is always available. # The default projector (pk=1) is always available.
default_projector = Projector.objects.get(pk=1) default_projector = Projector.objects.get(pk=1)
projectiondefaults = [] projectiondefaults = []
projectiondefaults.append(ProjectionDefault( projectiondefaults.append(
name='agenda_all_items', ProjectionDefault(
display_name='Agenda', name="agenda_all_items", display_name="Agenda", projector=default_projector
projector=default_projector)) )
projectiondefaults.append(ProjectionDefault( )
name='topics', projectiondefaults.append(
display_name='Topics', ProjectionDefault(
projector=default_projector)) name="topics", display_name="Topics", projector=default_projector
projectiondefaults.append(ProjectionDefault( )
name='agenda_list_of_speakers', )
display_name='List of speakers', projectiondefaults.append(
projector=default_projector)) ProjectionDefault(
projectiondefaults.append(ProjectionDefault( name="agenda_list_of_speakers",
name='agenda_current_list_of_speakers', display_name="List of speakers",
display_name='Current list of speakers', projector=default_projector,
projector=default_projector)) )
projectiondefaults.append(ProjectionDefault( )
name='motions', projectiondefaults.append(
display_name='Motions', ProjectionDefault(
projector=default_projector)) name="agenda_current_list_of_speakers",
projectiondefaults.append(ProjectionDefault( display_name="Current list of speakers",
name='motionBlocks', projector=default_projector,
display_name='Motion blocks', )
projector=default_projector)) )
projectiondefaults.append(ProjectionDefault( projectiondefaults.append(
name='assignments', ProjectionDefault(
display_name='Elections', name="motions", display_name="Motions", projector=default_projector
projector=default_projector)) )
projectiondefaults.append(ProjectionDefault( )
name='users', projectiondefaults.append(
display_name='Participants', ProjectionDefault(
projector=default_projector)) name="motionBlocks",
projectiondefaults.append(ProjectionDefault( display_name="Motion blocks",
name='mediafiles', projector=default_projector,
display_name='Files', )
projector=default_projector)) )
projectiondefaults.append(ProjectionDefault( projectiondefaults.append(
name='messages', ProjectionDefault(
display_name='Messages', name="assignments", display_name="Elections", projector=default_projector
projector=default_projector)) )
projectiondefaults.append(ProjectionDefault( )
name='countdowns', projectiondefaults.append(
display_name='Countdowns', ProjectionDefault(
projector=default_projector)) 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 # Create all new projectiondefaults
ProjectionDefault.objects.bulk_create(projectiondefaults) ProjectionDefault.objects.bulk_create(projectiondefaults)
@ -125,114 +144,141 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('sessions', '0001_initial'), ("sessions", "0001_initial"),
('contenttypes', '0002_remove_content_type_name'), ("contenttypes", "0002_remove_content_type_name"),
('core', '0001_initial'), ("core", "0001_initial"),
('agenda', '0001_initial'), # ('agenda', '0002_item_duration') is not required but would be also ok. (
('topics', '0001_initial'), "agenda",
"0001_initial",
), # ('agenda', '0002_item_duration') is not required but would be also ok.
("topics", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Countdown', name="Countdown",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('description', models.CharField(blank=True, max_length=256)), "id",
('running', models.BooleanField(default=False)), models.AutoField(
('default_time', models.PositiveIntegerField(default=60)), auto_created=True,
('countdown_time', models.FloatField(default=60)), 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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='ProjectionDefault', name="ProjectionDefault",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=256)), "id",
('display_name', models.CharField(max_length=256)), 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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='ProjectorMessage', name="ProjectorMessage",
fields=[ 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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='Session', name="Session",
fields=[ fields=[
('session_ptr', models.OneToOneField( (
auto_created=True, "session_ptr",
on_delete=django.db.models.deletion.CASCADE, models.OneToOneField(
parent_link=True, auto_created=True,
primary_key=True, on_delete=django.db.models.deletion.CASCADE,
serialize=False, parent_link=True,
to='sessions.Session')), primary_key=True,
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), serialize=False,
to="sessions.Session",
),
),
(
"user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
], ],
options={ options={"default_permissions": ()},
'default_permissions': (), bases=("sessions.session",),
},
bases=('sessions.session',),
),
migrations.RunPython(
move_custom_slides_to_topics
),
migrations.RemoveField(
model_name='customslide',
name='attachments',
),
migrations.DeleteModel(
name='CustomSlide',
), ),
migrations.RunPython(move_custom_slides_to_topics),
migrations.RemoveField(model_name="customslide", name="attachments"),
migrations.DeleteModel(name="CustomSlide"),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='chatmessage', name="chatmessage",
options={'default_permissions': (), 'permissions': (('can_use_chat', 'Can use the chat'), ('can_manage_chat', 'Can manage the chat'))}, options={
"default_permissions": (),
"permissions": (
("can_use_chat", "Can use the chat"),
("can_manage_chat", "Can manage the chat"),
),
},
), ),
migrations.AddField( migrations.AddField(
model_name='projector', model_name="projector",
name='blank', name="blank",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
migrations.AddField( migrations.AddField(
model_name='projector', model_name="projector",
name='height', name="height",
field=models.PositiveIntegerField(default=915), field=models.PositiveIntegerField(default=915),
), ),
migrations.AddField( migrations.AddField(
model_name='projector', model_name="projector",
name='name', name="name",
field=models.CharField(blank=True, max_length=255, unique=True), field=models.CharField(blank=True, max_length=255, unique=True),
), ),
migrations.AddField( migrations.AddField(
model_name='projector', model_name="projector",
name='width', name="width",
field=models.PositiveIntegerField(default=1220), field=models.PositiveIntegerField(default=1220),
), ),
migrations.AddField( migrations.AddField(
model_name='projectiondefault', model_name="projectiondefault",
name='projector', name="projector",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projectiondefaults', to='core.Projector'), field=models.ForeignKey(
), on_delete=django.db.models.deletion.CASCADE,
migrations.RunPython( related_name="projectiondefaults",
name_default_projector to="core.Projector",
), ),
migrations.RunPython(
remove_old_countdowns_messages
),
migrations.RunPython(
add_projection_defaults
), ),
migrations.RunPython(name_default_projector),
migrations.RunPython(remove_old_countdowns_messages),
migrations.RunPython(add_projection_defaults),
] ]

View File

@ -11,22 +11,16 @@ def remove_session_content_type(apps, schema_editor):
""" """
# We get the model from the versioned app registry; # We get the model from the versioned app registry;
# if we directly import it, it will be the wrong version. # if we directly import it, it will be the wrong version.
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model("contenttypes", "ContentType")
Session = apps.get_model('core', 'Session') Session = apps.get_model("core", "Session")
ContentType.objects.get_for_model(Session).delete() ContentType.objects.get_for_model(Session).delete()
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("core", "0002_misc_features")]
('core', '0002_misc_features'),
]
operations = [ operations = [
migrations.RunPython( migrations.RunPython(remove_session_content_type),
remove_session_content_type migrations.DeleteModel(name="Session"),
),
migrations.DeleteModel(
name='Session',
),
] ]

View File

@ -13,25 +13,19 @@ def rename_projector_message_slides(apps, schema_editor):
""" """
# We get the model from the versioned app registry; # We get the model from the versioned app registry;
# if we directly import it, it will be the wrong version. # 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(): for projector in Projector.objects.all():
new_config = {} new_config = {}
for key, value in projector.config.items(): for key, value in projector.config.items():
new_config[key] = value new_config[key] = value
if value['name'] == 'core/projectormessage': if value["name"] == "core/projectormessage":
new_config[key]['name'] = 'core/projector-message' new_config[key]["name"] = "core/projector-message"
projector.config = new_config projector.config = new_config
projector.save(skip_autoupdate=True) projector.save(skip_autoupdate=True)
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("core", "0003_auto_20161217_1158")]
('core', '0003_auto_20161217_1158'),
]
operations = [ operations = [migrations.RunPython(rename_projector_message_slides)]
migrations.RunPython(
rename_projector_message_slides
),
]

View File

@ -11,22 +11,26 @@ from openslides.utils.migrations import (
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("core", "0004_auto_20170215_1624")]
('core', '0004_auto_20170215_1624'),
]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='configstore', name="configstore",
options={ options={
'default_permissions': (), "default_permissions": (),
'permissions': ( "permissions": (
('can_manage_config', 'Can manage configuration'), ("can_manage_config", "Can manage configuration"),
('can_manage_logos', 'Can manage logos') ("can_manage_logos", "Can manage logos"),
) ),
}, },
), ),
migrations.RunPython(add_permission_to_groups_based_on_existing_permission( migrations.RunPython(
'can_manage_config', 'configstore', 'core', 'can_manage_logos', 'Can manage logos' add_permission_to_groups_based_on_existing_permission(
)), "can_manage_config",
"configstore",
"core",
"can_manage_logos",
"Can manage logos",
)
),
] ]

View File

@ -7,19 +7,17 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("core", "0005_auto_20170412_1258")]
('core', '0005_auto_20170412_1258'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='projector', model_name="projector",
name='height', name="height",
field=models.PositiveIntegerField(default=768), field=models.PositiveIntegerField(default=768),
), ),
migrations.AlterField( migrations.AlterField(
model_name='projector', model_name="projector",
name='width', name="width",
field=models.PositiveIntegerField(default=1024), field=models.PositiveIntegerField(default=1024),
), ),
] ]

View File

@ -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 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. 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): if len(perm):
perm = perm.get() perm = perm.get()
@ -30,9 +30,10 @@ def delete_old_logo_permission(apps, schema_editor):
# Create new permission # Create new permission
perm = Permission.objects.create( perm = Permission.objects.create(
codename='can_manage_logos_and_fonts', codename="can_manage_logos_and_fonts",
name='Can manage logos and fonts', name="Can manage logos and fonts",
content_type=content_type) content_type=content_type,
)
for group in groups: for group in groups:
group.permissions.add(perm) group.permissions.add(perm)
@ -41,22 +42,18 @@ def delete_old_logo_permission(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("core", "0006_auto_20180123_0903")]
('core', '0006_auto_20180123_0903'),
]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='configstore', name="configstore",
options={ options={
'default_permissions': (), "default_permissions": (),
'permissions': ( "permissions": (
('can_manage_config', 'Can manage configuration'), ("can_manage_config", "Can manage configuration"),
('can_manage_logos_and_fonts', 'Can manage logos and fonts') ("can_manage_logos_and_fonts", "Can manage logos and fonts"),
) ),
}, },
), ),
migrations.RunPython( migrations.RunPython(delete_old_logo_permission),
delete_old_logo_permission
),
] ]

View File

@ -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. 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: try:
logos_available = ConfigStore.objects.get(key='logos_available') logos_available = ConfigStore.objects.get(key="logos_available")
except ConfigStore.DoesNotExist: except ConfigStore.DoesNotExist:
return # The key is not in the database, nothing to change here 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.value = default_value
logos_available.save() 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 moves the value of 'logo_pdf_header' to 'logo_pdf_header_L' and the same
for the footer. The old ones are deleted. 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: try:
logo_pdf = ConfigStore.objects.get(key='logo_pdf_{}'.format(place)) logo_pdf = ConfigStore.objects.get(key="logo_pdf_{}".format(place))
except ConfigStore.DoesNotExist: except ConfigStore.DoesNotExist:
continue # The old entry is not in the database, nothing to change here continue # The old entry is not in the database, nothing to change here
# The key of the new entry # The key of the new entry
new_value_key = 'logo_pdf_{}_L'.format(place) new_value_key = "logo_pdf_{}_L".format(place)
try: try:
logo_pdf_L = ConfigStore.objects.get(key=new_value_key) logo_pdf_L = ConfigStore.objects.get(key=new_value_key)
except ConfigStore.DoesNotExist: except ConfigStore.DoesNotExist:
@ -45,7 +45,7 @@ def move_old_logo_settings(apps, schema_editor):
logo_pdf_L.value = {} logo_pdf_L.value = {}
# Move the path to the new configentry # 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 # Save the new one, delete the old
logo_pdf_L.save() logo_pdf_L.save()
logo_pdf.delete() logo_pdf.delete()
@ -53,15 +53,9 @@ def move_old_logo_settings(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("core", "0007_auto_20180130_1400")]
('core', '0007_auto_20180130_1400'),
]
operations = [ operations = [
migrations.RunPython( migrations.RunPython(logos_available_default_to_database),
logos_available_default_to_database migrations.RunPython(move_old_logo_settings),
),
migrations.RunPython(
move_old_logo_settings
),
] ]

View File

@ -13,42 +13,68 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('core', '0008_changed_logo_fields'), ("core", "0008_changed_logo_fields"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='History', name="History",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('element_id', models.CharField(max_length=255)), "id",
('now', models.DateTimeField(auto_now_add=True)), models.AutoField(
('information', models.CharField(max_length=255)), 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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='HistoryData', name="HistoryData",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('full_data', jsonfield.fields.JSONField( "id",
dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={})), 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={ options={"default_permissions": ()},
'default_permissions': (),
},
), ),
migrations.AddField( migrations.AddField(
model_name='history', model_name="history",
name='full_data', name="full_data",
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='core.HistoryData'), field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE, to="core.HistoryData"
),
), ),
migrations.AddField( migrations.AddField(
model_name='history', model_name="history",
name='user', name="user",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
), ),
] ]

View File

@ -24,13 +24,13 @@ class ProjectorManager(models.Manager):
""" """
Customized model manager to support our get_full_queryset method. Customized model manager to support our get_full_queryset method.
""" """
def get_full_queryset(self): def get_full_queryset(self):
""" """
Returns the normal queryset with all projectors. In the background Returns the normal queryset with all projectors. In the background
projector defaults are prefetched from the database. projector defaults are prefetched from the database.
""" """
return self.get_queryset().prefetch_related( return self.get_queryset().prefetch_related("projectiondefaults")
'projectiondefaults')
class Projector(RESTModelMixin, models.Model): 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 The projector can be controlled using the REST API with POST requests
on e. g. the URL /rest/core/projector/1/activate_elements/. on e. g. the URL /rest/core/projector/1/activate_elements/.
""" """
access_permissions = ProjectorAccessPermissions() access_permissions = ProjectorAccessPermissions()
objects = ProjectorManager() objects = ProjectorManager()
@ -86,24 +87,21 @@ class Projector(RESTModelMixin, models.Model):
height = models.PositiveIntegerField(default=768) height = models.PositiveIntegerField(default=768)
name = models.CharField( name = models.CharField(max_length=255, unique=True, blank=True)
max_length=255,
unique=True,
blank=True)
blank = models.BooleanField( blank = models.BooleanField(blank=False, default=False)
blank=False,
default=False)
class Meta: class Meta:
""" """
Contains general permissions that can not be placed in a specific app. Contains general permissions that can not be placed in a specific app.
""" """
default_permissions = () default_permissions = ()
permissions = ( permissions = (
('can_see_projector', 'Can see the projector'), ("can_see_projector", "Can see the projector"),
('can_manage_projector', 'Can manage the projector'), ("can_manage_projector", "Can manage the projector"),
('can_see_frontpage', 'Can see the front page'),) ("can_see_frontpage", "Can see the front page"),
)
@property @property
def elements(self): def elements(self):
@ -120,17 +118,19 @@ class Projector(RESTModelMixin, models.Model):
for key, value in self.config.items(): for key, value in self.config.items():
# Use a copy here not to change the origin value in the config field. # Use a copy here not to change the origin value in the config field.
result[key] = value.copy() result[key] = value.copy()
result[key]['uuid'] = key result[key]["uuid"] = key
element = elements.get(value['name']) element = elements.get(value["name"])
if element is None: if element is None:
result[key]['error'] = 'Projector element does not exist.' result[key]["error"] = "Projector element does not exist."
else: else:
try: try:
result[key].update(element.check_and_update_data( result[key].update(
projector_object=self, element.check_and_update_data(
config_entry=value)) projector_object=self, config_entry=value
)
)
except ProjectorException as e: except ProjectorException as e:
result[key]['error'] = str(e) result[key]["error"] = str(e)
return result return result
@classmethod @classmethod
@ -173,14 +173,14 @@ class ProjectionDefault(RESTModelMixin, models.Model):
special name like 'list_of_speakers'. The display_name is the shown special name like 'list_of_speakers'. The display_name is the shown
name on the front end for the user. name on the front end for the user.
""" """
name = models.CharField(max_length=256) name = models.CharField(max_length=256)
display_name = models.CharField(max_length=256) display_name = models.CharField(max_length=256)
projector = models.ForeignKey( projector = models.ForeignKey(
Projector, Projector, on_delete=models.CASCADE, related_name="projectiondefaults"
on_delete=models.CASCADE, )
related_name='projectiondefaults')
def get_root_rest_element(self): def get_root_rest_element(self):
return self.projector 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, Model for tags. This tags can be used for other models like agenda items,
motions or assignments. motions or assignments.
""" """
access_permissions = TagAccessPermissions() access_permissions = TagAccessPermissions()
name = models.CharField( name = models.CharField(max_length=255, unique=True)
max_length=255,
unique=True)
class Meta: class Meta:
ordering = ('name',) ordering = ("name",)
default_permissions = () default_permissions = ()
permissions = ( permissions = (("can_manage_tags", "Can manage tags"),)
('can_manage_tags', 'Can manage tags'),)
def __str__(self): def __str__(self):
return self.name return self.name
@ -217,6 +215,7 @@ class ConfigStore(RESTModelMixin, models.Model):
""" """
A model class to store all config variables in the database. A model class to store all config variables in the database.
""" """
access_permissions = ConfigAccessPermissions() access_permissions = ConfigAccessPermissions()
key = models.CharField(max_length=255, unique=True, db_index=True) key = models.CharField(max_length=255, unique=True, db_index=True)
@ -228,12 +227,13 @@ class ConfigStore(RESTModelMixin, models.Model):
class Meta: class Meta:
default_permissions = () default_permissions = ()
permissions = ( permissions = (
('can_manage_config', 'Can manage configuration'), ("can_manage_config", "Can manage configuration"),
('can_manage_logos_and_fonts', 'Can manage logos and fonts')) ("can_manage_logos_and_fonts", "Can manage logos and fonts"),
)
@classmethod @classmethod
def get_collection_string(cls): def get_collection_string(cls):
return 'core/config' return "core/config"
class ChatMessage(RESTModelMixin, models.Model): 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. At the moment we only have one global chat room for managers.
""" """
access_permissions = ChatMessageAccessPermissions() access_permissions = ChatMessageAccessPermissions()
can_see_permission = 'core.can_use_chat' can_see_permission = "core.can_use_chat"
message = models.TextField() message = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey( user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
class Meta: class Meta:
default_permissions = () default_permissions = ()
permissions = ( permissions = (
('can_use_chat', 'Can use the chat'), ("can_use_chat", "Can use the chat"),
('can_manage_chat', 'Can manage the chat'),) ("can_manage_chat", "Can manage the chat"),
)
def __str__(self): def __str__(self):
return 'Message {}'.format(self.timestamp) return "Message {}".format(self.timestamp)
class ProjectorMessage(RESTModelMixin, models.Model): class ProjectorMessage(RESTModelMixin, models.Model):
""" """
Model for ProjectorMessages. Model for ProjectorMessages.
""" """
access_permissions = ProjectorMessageAccessPermissions() access_permissions = ProjectorMessageAccessPermissions()
message = models.TextField(blank=True) message = models.TextField(blank=True)
@ -280,16 +281,18 @@ class ProjectorMessage(RESTModelMixin, models.Model):
projector message projector element is disabled. projector message projector element is disabled.
""" """
Projector.remove_any( Projector.remove_any(
skip_autoupdate=skip_autoupdate, skip_autoupdate=skip_autoupdate, name="core/projector-message", id=self.pk
name='core/projector-message', )
id=self.pk) return super().delete( # type: ignore
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore skip_autoupdate=skip_autoupdate, *args, **kwargs
)
class Countdown(RESTModelMixin, models.Model): class Countdown(RESTModelMixin, models.Model):
""" """
Model for countdowns. Model for countdowns.
""" """
access_permissions = CountdownAccessPermissions() access_permissions = CountdownAccessPermissions()
description = models.CharField(max_length=256, blank=True) description = models.CharField(max_length=256, blank=True)
@ -309,19 +312,22 @@ class Countdown(RESTModelMixin, models.Model):
countdown projector element is disabled. countdown projector element is disabled.
""" """
Projector.remove_any( Projector.remove_any(
skip_autoupdate=skip_autoupdate, skip_autoupdate=skip_autoupdate, name="core/countdown", id=self.pk
name='core/countdown', )
id=self.pk) return super().delete( # type: ignore
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore skip_autoupdate=skip_autoupdate, *args, **kwargs
)
def control(self, action, skip_autoupdate=False): def control(self, action, skip_autoupdate=False):
if action not in ('start', 'stop', 'reset'): if action not in ("start", "stop", "reset"):
raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action)) raise ValueError(
"Action must be 'start', 'stop' or 'reset', not {}.".format(action)
)
if action == 'start': if action == "start":
self.running = True self.running = True
self.countdown_time = now().timestamp() + self.default_time self.countdown_time = now().timestamp() + self.default_time
elif action == 'stop' and self.running: elif action == "stop" and self.running:
self.running = False self.running = False
self.countdown_time = self.countdown_time - now().timestamp() self.countdown_time = self.countdown_time - now().timestamp()
else: # reset 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 This is not a RESTModel. It is not cachable and can only be reached by a
special viewset. special viewset.
""" """
full_data = JSONField() full_data = JSONField()
class Meta: class Meta:
@ -347,6 +354,7 @@ class HistoryManager(models.Manager):
""" """
Customized model manager for the history model. Customized model manager for the history model.
""" """
def add_elements(self, elements): def add_elements(self, elements):
""" """
Method to add elements to the history. This does not trigger autoupdate. Method to add elements to the history. This does not trigger autoupdate.
@ -354,18 +362,26 @@ class HistoryManager(models.Manager):
with transaction.atomic(): with transaction.atomic():
instances = [] instances = []
for element in elements: 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. # Do not update history for history elements itself or if history is disabled.
continue continue
# HistoryData is not a root rest element so there is no autoupdate and not history saving here. # 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( instance = self.model(
element_id=get_element_id(element['collection_string'], element['id']), element_id=get_element_id(
information=element['information'], element["collection_string"], element["id"]
user_id=element['user_id'], ),
information=element["information"],
user_id=element["user_id"],
full_data=data, 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) instances.append(instance)
return instances return instances
@ -380,14 +396,16 @@ class HistoryManager(models.Manager):
all_full_data = async_to_sync(element_cache.get_all_full_data)() all_full_data = async_to_sync(element_cache.get_all_full_data)()
for collection_string, data in all_full_data.items(): for collection_string, data in all_full_data.items():
for full_data in data: for full_data in data:
elements.append(Element( elements.append(
id=full_data['id'], Element(
collection_string=collection_string, id=full_data["id"],
full_data=full_data, collection_string=collection_string,
information='', full_data=full_data,
user_id=None, information="",
disable_history=False, user_id=None,
)) disable_history=False,
)
)
instances = self.add_elements(elements) instances = self.add_elements(elements)
return instances 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 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. delete a user you may lose the information of the user field here.
""" """
access_permissions = HistoryAccessPermissions() access_permissions = HistoryAccessPermissions()
objects = HistoryManager() objects = HistoryManager()
element_id = models.CharField( element_id = models.CharField(max_length=255)
max_length=255,
)
now = models.DateTimeField(auto_now_add=True) now = models.DateTimeField(auto_now_add=True)
information = models.CharField( information = models.CharField(max_length=255)
max_length=255,
)
user = models.ForeignKey( user = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
null=True,
on_delete=models.SET_NULL)
full_data = models.OneToOneField(
HistoryData,
on_delete=models.CASCADE,
) )
full_data = models.OneToOneField(HistoryData, on_delete=models.CASCADE)
class Meta: class Meta:
default_permissions = () default_permissions = ()

View File

@ -9,29 +9,32 @@ class Clock(ProjectorElement):
""" """
Clock on the projector. Clock on the projector.
""" """
name = 'core/clock'
name = "core/clock"
class CountdownElement(ProjectorElement): class CountdownElement(ProjectorElement):
""" """
Countdown slide for the projector. Countdown slide for the projector.
""" """
name = 'core/countdown'
name = "core/countdown"
def check_data(self): def check_data(self):
if not Countdown.objects.filter(pk=self.config_entry.get('id')).exists(): if not Countdown.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException('Countdown does not exists.') raise ProjectorException("Countdown does not exists.")
class ProjectorMessageElement(ProjectorElement): class ProjectorMessageElement(ProjectorElement):
""" """
Short message on the projector. Rendered as overlay. Short message on the projector. Rendered as overlay.
""" """
name = 'core/projector-message'
name = "core/projector-message"
def check_data(self): def check_data(self):
if not ProjectorMessage.objects.filter(pk=self.config_entry.get('id')).exists(): if not ProjectorMessage.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException('Message does not exists.') raise ProjectorException("Message does not exists.")
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:

View File

@ -17,18 +17,21 @@ class JSONSerializerField(Field):
""" """
Serializer for projector's and config JSONField. Serializer for projector's and config JSONField.
""" """
def to_internal_value(self, data): def to_internal_value(self, data):
""" """
Checks that data is a dictionary. The key is a hex UUID and the Checks that data is a dictionary. The key is a hex UUID and the
value is a dictionary with must have a key 'name'. value is a dictionary with must have a key 'name'.
""" """
if type(data) is not dict: 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(): for element in data.values():
if type(element) is not dict: if type(element) is not dict:
raise ValidationError({'detail': 'Data must be a dictionary.'}) raise ValidationError({"detail": "Data must be a dictionary."})
elif element.get('name') is None: elif element.get("name") is None:
raise ValidationError({'detail': "Every dictionary must have a key 'name'."}) raise ValidationError(
{"detail": "Every dictionary must have a key 'name'."}
)
return data return data
def to_representation(self, value): def to_representation(self, value):
@ -42,65 +45,82 @@ class ProjectionDefaultSerializer(ModelSerializer):
""" """
Serializer for core.models.ProjectionDefault objects. Serializer for core.models.ProjectionDefault objects.
""" """
class Meta: class Meta:
model = ProjectionDefault model = ProjectionDefault
fields = ('id', 'name', 'display_name', 'projector', ) fields = ("id", "name", "display_name", "projector")
class ProjectorSerializer(ModelSerializer): class ProjectorSerializer(ModelSerializer):
""" """
Serializer for core.models.Projector objects. Serializer for core.models.Projector objects.
""" """
config = JSONSerializerField(write_only=True) config = JSONSerializerField(write_only=True)
projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True) projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True)
class Meta: class Meta:
model = Projector model = Projector
fields = ('id', 'config', 'elements', 'scale', 'scroll', 'name', 'blank', 'width', 'height', 'projectiondefaults', ) fields = (
read_only_fields = ('scale', 'scroll', 'blank', 'width', 'height', ) "id",
"config",
"elements",
"scale",
"scroll",
"name",
"blank",
"width",
"height",
"projectiondefaults",
)
read_only_fields = ("scale", "scroll", "blank", "width", "height")
class TagSerializer(ModelSerializer): class TagSerializer(ModelSerializer):
""" """
Serializer for core.models.Tag objects. Serializer for core.models.Tag objects.
""" """
class Meta: class Meta:
model = Tag model = Tag
fields = ('id', 'name', ) fields = ("id", "name")
class ConfigSerializer(ModelSerializer): class ConfigSerializer(ModelSerializer):
""" """
Serializer for core.models.Tag objects. Serializer for core.models.Tag objects.
""" """
value = JSONSerializerField() value = JSONSerializerField()
class Meta: class Meta:
model = ConfigStore model = ConfigStore
fields = ('id', 'key', 'value') fields = ("id", "key", "value")
class ChatMessageSerializer(ModelSerializer): class ChatMessageSerializer(ModelSerializer):
""" """
Serializer for core.models.ChatMessage objects. Serializer for core.models.ChatMessage objects.
""" """
class Meta: class Meta:
model = ChatMessage model = ChatMessage
fields = ('id', 'message', 'timestamp', 'user', ) fields = ("id", "message", "timestamp", "user")
read_only_fields = ('user', ) read_only_fields = ("user",)
class ProjectorMessageSerializer(ModelSerializer): class ProjectorMessageSerializer(ModelSerializer):
""" """
Serializer for core.models.ProjectorMessage objects. Serializer for core.models.ProjectorMessage objects.
""" """
class Meta: class Meta:
model = ProjectorMessage model = ProjectorMessage
fields = ('id', 'message', ) fields = ("id", "message")
def validate(self, data): def validate(self, data):
if 'message' in data: if "message" in data:
data['message'] = validate_html(data['message']) data["message"] = validate_html(data["message"])
return data return data
@ -108,9 +128,10 @@ class CountdownSerializer(ModelSerializer):
""" """
Serializer for core.models.Countdown objects. Serializer for core.models.Countdown objects.
""" """
class Meta: class Meta:
model = Countdown model = Countdown
fields = ('id', 'description', 'default_time', 'countdown_time', 'running', ) fields = ("id", "description", "default_time", "countdown_time", "running")
class HistorySerializer(ModelSerializer): class HistorySerializer(ModelSerializer):
@ -119,6 +140,7 @@ class HistorySerializer(ModelSerializer):
Does not contain full data of history object. Does not contain full data of history object.
""" """
class Meta: class Meta:
model = History model = History
fields = ('id', 'element_id', 'now', 'information', 'user', ) fields = ("id", "element_id", "now", "information", "user")

View File

@ -21,9 +21,8 @@ def delete_django_app_permissions(sender, **kwargs):
for auth, contenttypes and sessions. for auth, contenttypes and sessions.
""" """
contenttypes = ContentType.objects.filter( contenttypes = ContentType.objects.filter(
Q(app_label='auth') | Q(app_label="auth") | Q(app_label="contenttypes") | Q(app_label="sessions")
Q(app_label='contenttypes') | )
Q(app_label='sessions'))
Permission.objects.filter(content_type__in=contenttypes).delete() 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. 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: for permission in permissions:
if permission.content_type.app_label == core_app.label: if permission.content_type.app_label == core_app.label:
if permission.codename == 'can_see_projector': if permission.codename == "can_see_projector":
yield core_app.get_model('Projector') yield core_app.get_model("Projector")
elif permission.codename == 'can_manage_projector': elif permission.codename == "can_manage_projector":
yield core_app.get_model('ProjectorMessage') yield core_app.get_model("ProjectorMessage")
yield core_app.get_model('Countdown') yield core_app.get_model("Countdown")
elif permission.codename == 'can_use_chat': elif permission.codename == "can_use_chat":
yield core_app.get_model('ChatMessage') yield core_app.get_model("ChatMessage")

View File

@ -4,15 +4,7 @@ from . import views
urlpatterns = [ urlpatterns = [
url(r'^servertime/$', url(r"^servertime/$", views.ServerTime.as_view(), name="core_servertime"),
views.ServerTime.as_view(), url(r"^version/$", views.VersionView.as_view(), name="core_version"),
name='core_servertime'), url(r"^history/$", views.HistoryView.as_view(), name="core_history"),
url(r'^version/$',
views.VersionView.as_view(),
name='core_version'),
url(r'^history/$',
views.HistoryView.as_view(),
name='core_history'),
] ]

View File

@ -17,12 +17,7 @@ from mypy_extensions import TypedDict
from .. import __license__ as license, __url__ as url, __version__ as version from .. import __license__ as license, __url__ as url, __version__ as version
from ..utils import views as utils_views from ..utils import views as utils_views
from ..utils.arguments import arguments from ..utils.arguments import arguments
from ..utils.auth import ( from ..utils.auth import GROUP_ADMIN_PK, anonymous_is_enabled, has_perm, in_some_groups
GROUP_ADMIN_PK,
anonymous_is_enabled,
has_perm,
in_some_groups,
)
from ..utils.autoupdate import inform_changed_data, inform_deleted_data from ..utils.autoupdate import inform_changed_data, inform_deleted_data
from ..utils.plugins import ( from ..utils.plugins import (
get_plugin_description, get_plugin_description,
@ -67,6 +62,7 @@ from .models import (
# Special Django views # Special Django views
class IndexView(View): class IndexView(View):
""" """
The primary view for the OpenSlides client. Serves static files. If a file 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: def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
no_caching = arguments.get('no_template_caching', False) no_caching = arguments.get("no_template_caching", False)
if 'index' not in self.cache or no_caching: if "index" not in self.cache or no_caching:
self.cache['index'] = finders.find('index.html') 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: def get(self, request, path, **kwargs) -> HttpResponse:
""" """
@ -97,18 +93,25 @@ class IndexView(View):
try: try:
response = serve(request, path, **kwargs) response = serve(request, path, **kwargs)
except Http404: 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 return response
# Viewsets for the REST API # Viewsets for the REST API
class ProjectorViewSet(ModelViewSet): class ProjectorViewSet(ModelViewSet):
""" """
API endpoint for the projector slide info. API endpoint for the projector slide info.
There are the following views: See strings in check_view_permissions(). There are the following views: See strings in check_view_permissions().
""" """
access_permissions = ProjectorAccessPermissions() access_permissions = ProjectorAccessPermissions()
queryset = Projector.objects.all() queryset = Projector.objects.all()
@ -116,18 +119,31 @@ class ProjectorViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. 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) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action == 'metadata': elif self.action == "metadata":
result = has_perm(self.request.user, 'core.can_see_projector') result = has_perm(self.request.user, "core.can_see_projector")
elif self.action in ( elif self.action in (
'create', 'update', 'partial_update', 'destroy', "create",
'activate_elements', 'prune_elements', 'update_elements', 'deactivate_elements', 'clear_elements', "update",
'project', 'control_view', 'set_resolution', 'set_scroll', 'control_blank', "partial_update",
'broadcast', 'set_projectiondefault', "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 result = has_perm(self.request.user, "core.can_see_projector") and has_perm(
has_perm(self.request.user, 'core.can_manage_projector')) self.request.user, "core.can_manage_projector"
)
else: else:
result = False result = False
return result return result
@ -144,11 +160,11 @@ class ProjectorViewSet(ModelViewSet):
if projection_default.projector.id == projector_instance.id: if projection_default.projector.id == projector_instance.id:
projection_default.projector_id = 1 projection_default.projector_id = 1
projection_default.save() projection_default.save()
if config['projector_broadcast'] == projector_instance.pk: if config["projector_broadcast"] == projector_instance.pk:
config['projector_broadcast'] = 0 config["projector_broadcast"] = 0
return super(ProjectorViewSet, self).destroy(*args, **kwargs) return super(ProjectorViewSet, self).destroy(*args, **kwargs)
@detail_route(methods=['post']) @detail_route(methods=["post"])
def activate_elements(self, request, pk): def activate_elements(self, request, pk):
""" """
REST API operation to activate projector elements. It expects a POST 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. of dictionaries to be appended to the projector config entry.
""" """
if not isinstance(request.data, list): 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_instance = self.get_object()
projector_config = projector_instance.config projector_config = projector_instance.config
for element in request.data: for element in request.data:
if element.get('name') is None: if element.get("name") is None:
raise ValidationError({'detail': 'Invalid projector element. Name is missing.'}) raise ValidationError(
{"detail": "Invalid projector element. Name is missing."}
)
projector_config[uuid.uuid4().hex] = element 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.is_valid(raise_exception=True)
serializer.save() serializer.save()
return Response(serializer.data) return Response(serializer.data)
@detail_route(methods=['post']) @detail_route(methods=["post"])
def prune_elements(self, request, pk): def prune_elements(self, request, pk):
""" """
REST API operation to activate projector elements. It expects a POST 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. entries are deleted but not entries with stable == True.
""" """
if not isinstance(request.data, list): 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() projector = self.get_object()
elements = request.data elements = request.data
if not isinstance(elements, list): 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: for element in elements:
if not isinstance(element, dict): if not isinstance(element, dict):
raise ValidationError({'detail': _('All elements have to be dicts.')}) raise ValidationError({"detail": _("All elements have to be dicts.")})
if element.get('name') is None: if element.get("name") is None:
raise ValidationError({'detail': 'Invalid projector element. Name is missing.'}) raise ValidationError(
{"detail": "Invalid projector element. Name is missing."}
)
return Response(self.prune(projector, elements)) return Response(self.prune(projector, elements))
def prune(self, projector, elements): def prune(self, projector, elements):
@ -201,21 +223,23 @@ class ProjectorViewSet(ModelViewSet):
""" """
projector_config = {} projector_config = {}
for key, value in projector.config.items(): for key, value in projector.config.items():
if value.get('stable'): if value.get("stable"):
projector_config[key] = value projector_config[key] = value
for element in elements: for element in elements:
projector_config[uuid.uuid4().hex] = element 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.is_valid(raise_exception=True)
serializer.save() serializer.save()
# reset scroll level # reset scroll level
if (projector.scroll != 0): if projector.scroll != 0:
projector.scroll = 0 projector.scroll = 0
projector.save() projector.save()
return serializer.data return serializer.data
@detail_route(methods=['post']) @detail_route(methods=["post"])
def update_elements(self, request, pk): def update_elements(self, request, pk):
""" """
REST API operation to update projector elements. It expects a POST REST API operation to update projector elements. It expects a POST
@ -237,8 +261,10 @@ class ProjectorViewSet(ModelViewSet):
} }
""" """
if not isinstance(request.data, dict): if not isinstance(request.data, dict):
raise ValidationError({'detail': 'Data must be a dictionary.'}) raise ValidationError({"detail": "Data must be a dictionary."})
error = {'detail': 'Data must be a dictionary with UUIDs as keys and dictionaries as values.'} error = {
"detail": "Data must be a dictionary with UUIDs as keys and dictionaries as values."
}
for key, value in request.data.items(): for key, value in request.data.items():
try: try:
uuid.UUID(hex=str(key)) uuid.UUID(hex=str(key))
@ -251,15 +277,19 @@ class ProjectorViewSet(ModelViewSet):
projector_config = projector_instance.config projector_config = projector_instance.config
for key, value in request.data.items(): for key, value in request.data.items():
if key not in projector_config: 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]) 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.is_valid(raise_exception=True)
serializer.save() serializer.save()
return Response(serializer.data) return Response(serializer.data)
@detail_route(methods=['post']) @detail_route(methods=["post"])
def deactivate_elements(self, request, pk): def deactivate_elements(self, request, pk):
""" """
REST API operation to deactivate projector elements. It expects a REST API operation to deactivate projector elements. It expects a
@ -268,12 +298,12 @@ class ProjectorViewSet(ModelViewSet):
that should be deleted. that should be deleted.
""" """
if not isinstance(request.data, list): 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: for item in request.data:
try: try:
uuid.UUID(hex=str(item)) uuid.UUID(hex=str(item))
except ValueError: 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_instance = self.get_object()
projector_config = projector_instance.config projector_config = projector_instance.config
@ -281,14 +311,16 @@ class ProjectorViewSet(ModelViewSet):
try: try:
del projector_config[key] del projector_config[key]
except KeyError: 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.is_valid(raise_exception=True)
serializer.save() serializer.save()
return Response(serializer.data) return Response(serializer.data)
@detail_route(methods=['post']) @detail_route(methods=["post"])
def clear_elements(self, request, pk): def clear_elements(self, request, pk):
""" """
REST API operation to deactivate all projector elements but not REST API operation to deactivate all projector elements but not
@ -301,15 +333,17 @@ class ProjectorViewSet(ModelViewSet):
def clear(self, projector): def clear(self, projector):
projector_config = {} projector_config = {}
for key, value in projector.config.items(): for key, value in projector.config.items():
if value.get('stable'): if value.get("stable"):
projector_config[key] = value 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.is_valid(raise_exception=True)
serializer.save() serializer.save()
return serializer.data return serializer.data
@list_route(methods=['post']) @list_route(methods=["post"])
def project(self, request, *args, **kwargs): def project(self, request, *args, **kwargs):
""" """
REST API operation. Does a combination of clear_elements and prune_elements: 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. # The data has to be a dict.
if not isinstance(request.data, 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 # 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: for id in clear_projector_ids:
if not isinstance(id, int): 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. # 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 prune is not None:
if not isinstance(prune, dict): if not isinstance(prune, dict):
raise ValidationError({'detail': _('Prune has to be an object.')}) raise ValidationError({"detail": _("Prune has to be an object.")})
prune_projector_id = prune.get('id') prune_projector_id = prune.get("id")
if not isinstance(prune_projector_id, int): 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. # Get the projector after all clear operations, but check, if it exist.
if not Projector.objects.filter(pk=prune_projector_id).exists(): if not Projector.objects.filter(pk=prune_projector_id).exists():
raise ValidationError({ raise ValidationError(
'detail': _('The projector with id "{}" does not exist').format(prune_projector_id)}) {
"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): if not isinstance(prune_element, dict):
raise ValidationError({'detail': _('Prune element has to be a dict or not given.')}) raise ValidationError(
if prune_element.get('name') is None: {"detail": _("Prune element has to be a dict or not given.")}
raise ValidationError({'detail': 'Invalid projector element. Name is missing.'}) )
if prune_element.get("name") is None:
raise ValidationError(
{"detail": "Invalid projector element. Name is missing."}
)
# First step: Clear all given projectors # First step: Clear all given projectors
for projector in Projector.objects.filter(pk__in=clear_projector_ids): for projector in Projector.objects.filter(pk__in=clear_projector_ids):
@ -367,7 +414,7 @@ class ProjectorViewSet(ModelViewSet):
return Response() return Response()
@detail_route(methods=['post']) @detail_route(methods=["post"])
def set_resolution(self, request, pk): def set_resolution(self, request, pk):
""" """
REST API operation to set the resolution. REST API operation to set the resolution.
@ -388,26 +435,34 @@ class ProjectorViewSet(ModelViewSet):
} }
""" """
if not isinstance(request.data, dict): if not isinstance(request.data, dict):
raise ValidationError({'detail': 'Data must be a dictionary.'}) raise ValidationError({"detail": "Data must be a dictionary."})
if request.data.get('width') is None or request.data.get('height') is None: 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.'}) 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): if not isinstance(request.data["width"], int) or not isinstance(
raise ValidationError({'detail': 'Data has to be integers.'}) request.data["height"], int
if (request.data['width'] < 800 or request.data['width'] > 3840 or ):
request.data['height'] < 340 or request.data['height'] > 2880): raise ValidationError({"detail": "Data has to be integers."})
raise ValidationError({'detail': 'The Resolution have to be between 800x340 and 3840x2880.'}) 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 = self.get_object()
projector_instance.width = request.data['width'] projector_instance.width = request.data["width"]
projector_instance.height = request.data['height'] projector_instance.height = request.data["height"]
projector_instance.save() projector_instance.save()
message = 'Changing resolution to {width}x{height} was successful.'.format( message = "Changing resolution to {width}x{height} was successful.".format(
width=request.data['width'], width=request.data["width"], height=request.data["height"]
height=request.data['height']) )
return Response({'detail': message}) return Response({"detail": message})
@detail_route(methods=['post']) @detail_route(methods=["post"])
def control_view(self, request, pk): def control_view(self, request, pk):
""" """
REST API operation to control the projector view, i. e. scale and 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): if not isinstance(request.data, dict):
raise ValidationError({'detail': 'Data must be a dictionary.'}) raise ValidationError({"detail": "Data must be a dictionary."})
if (request.data.get('action') not in ('scale', 'scroll') or if request.data.get("action") not in ("scale", "scroll") or request.data.get(
request.data.get('direction') not in ('up', 'down', 'reset')): "direction"
raise ValidationError({'detail': "Data must be a dictionary with an action ('scale' or 'scroll') " ) not in ("up", "down", "reset"):
"and a direction ('up', 'down' or '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() projector_instance = self.get_object()
if request.data['action'] == 'scale': if request.data["action"] == "scale":
if request.data['direction'] == 'up': if request.data["direction"] == "up":
projector_instance.scale = F('scale') + 1 projector_instance.scale = F("scale") + 1
elif request.data['direction'] == 'down': elif request.data["direction"] == "down":
projector_instance.scale = F('scale') - 1 projector_instance.scale = F("scale") - 1
else: else:
# request.data['direction'] == 'reset' # request.data['direction'] == 'reset'
projector_instance.scale = 0 projector_instance.scale = 0
else: else:
# request.data['action'] == 'scroll' # request.data['action'] == 'scroll'
if request.data['direction'] == 'up': if request.data["direction"] == "up":
projector_instance.scroll = F('scroll') + 1 projector_instance.scroll = F("scroll") + 1
elif request.data['direction'] == 'down': elif request.data["direction"] == "down":
projector_instance.scroll = F('scroll') - 1 projector_instance.scroll = F("scroll") - 1
else: else:
# request.data['direction'] == 'reset' # request.data['direction'] == 'reset'
projector_instance.scroll = 0 projector_instance.scroll = 0
@ -454,12 +514,13 @@ class ProjectorViewSet(ModelViewSet):
projector_instance.save(skip_autoupdate=True) projector_instance.save(skip_autoupdate=True)
projector_instance.refresh_from_db() projector_instance.refresh_from_db()
inform_changed_data(projector_instance) inform_changed_data(projector_instance)
message = '{action} {direction} was successful.'.format( message = "{action} {direction} was successful.".format(
action=request.data['action'].capitalize(), action=request.data["action"].capitalize(),
direction=request.data['direction']) direction=request.data["direction"],
return Response({'detail': message}) )
return Response({"detail": message})
@detail_route(methods=['post']) @detail_route(methods=["post"])
def set_scroll(self, request, pk): def set_scroll(self, request, pk):
""" """
REST API operation to scroll the projector. REST API operation to scroll the projector.
@ -468,17 +529,18 @@ class ProjectorViewSet(ModelViewSet):
/rest/core/projector/<pk>/set_scroll/ with a new value for scroll. /rest/core/projector/<pk>/set_scroll/ with a new value for scroll.
""" """
if not isinstance(request.data, int): 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 = self.get_object()
projector_instance.scroll = request.data projector_instance.scroll = request.data
projector_instance.save() projector_instance.save()
message = 'Setting scroll to {scroll} was successful.'.format( message = "Setting scroll to {scroll} was successful.".format(
scroll=request.data) scroll=request.data
return Response({'detail': message}) )
return Response({"detail": message})
@detail_route(methods=['post']) @detail_route(methods=["post"])
def control_blank(self, request, pk): def control_blank(self, request, pk):
""" """
REST API operation to blank the projector. REST API operation to blank the projector.
@ -487,16 +549,17 @@ class ProjectorViewSet(ModelViewSet):
/rest/core/projector/<pk>/control_blank/ with a value for blank. /rest/core/projector/<pk>/control_blank/ with a value for blank.
""" """
if not isinstance(request.data, bool): 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 = self.get_object()
projector_instance.blank = request.data projector_instance.blank = request.data
projector_instance.save() projector_instance.save()
message = "Setting 'blank' to {blank} was successful.".format( message = "Setting 'blank' to {blank} was successful.".format(
blank=request.data) blank=request.data
return Response({'detail': message}) )
return Response({"detail": message})
@detail_route(methods=['post']) @detail_route(methods=["post"])
def broadcast(self, request, pk): def broadcast(self, request, pk):
""" """
REST API operation to (un-)broadcast the given projector. REST API operation to (un-)broadcast the given projector.
@ -505,16 +568,17 @@ class ProjectorViewSet(ModelViewSet):
It expects a POST request to It expects a POST request to
/rest/core/projector/<pk>/broadcast/ without an argument /rest/core/projector/<pk>/broadcast/ without an argument
""" """
if config['projector_broadcast'] == 0: if config["projector_broadcast"] == 0:
config['projector_broadcast'] = pk config["projector_broadcast"] = pk
message = "Setting projector {id} as broadcast projector was successful.".format( message = "Setting projector {id} as broadcast projector was successful.".format(
id=pk) id=pk
)
else: else:
config['projector_broadcast'] = 0 config["projector_broadcast"] = 0
message = "Disabling broadcast was successful." 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): def set_projectiondefault(self, request, pk):
""" """
REST API operation to set a projectiondefault to the requested projector. The argument REST API operation to set a projectiondefault to the requested projector. The argument
@ -524,21 +588,28 @@ class ProjectorViewSet(ModelViewSet):
/rest/core/projector/<pk>/set_projectiondefault/ with the projectiondefault id as the argument /rest/core/projector/<pk>/set_projectiondefault/ with the projectiondefault id as the argument
""" """
if not isinstance(request.data, int): if not isinstance(request.data, int):
raise ValidationError({'detail': 'Data must be an int.'}) raise ValidationError({"detail": "Data must be an int."})
try: try:
projectiondefault = ProjectionDefault.objects.get(pk=request.data) projectiondefault = ProjectionDefault.objects.get(pk=request.data)
except ProjectionDefault.DoesNotExist: except ProjectionDefault.DoesNotExist:
raise ValidationError({'detail': 'The projectiondefault with pk={pk} was not found.'.format( raise ValidationError(
pk=request.data)}) {
"detail": "The projectiondefault with pk={pk} was not found.".format(
pk=request.data
)
}
)
else: else:
projector_instance = self.get_object() projector_instance = self.get_object()
projectiondefault.projector = projector_instance projectiondefault.projector = projector_instance
projectiondefault.save() projectiondefault.save()
return Response('Setting projectiondefault "{name}" to projector {projector_id} was successful.'.format( return Response(
name=projectiondefault.display_name, 'Setting projectiondefault "{name}" to projector {projector_id} was successful.'.format(
projector_id=projector_instance.pk)) name=projectiondefault.display_name, projector_id=projector_instance.pk
)
)
class TagViewSet(ModelViewSet): class TagViewSet(ModelViewSet):
@ -548,6 +619,7 @@ class TagViewSet(ModelViewSet):
There are the following views: metadata, list, retrieve, create, There are the following views: metadata, list, retrieve, create,
partial_update, update and destroy. partial_update, update and destroy.
""" """
access_permissions = TagAccessPermissions() access_permissions = TagAccessPermissions()
queryset = Tag.objects.all() queryset = Tag.objects.all()
@ -555,14 +627,14 @@ class TagViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. 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) 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. # Every authenticated user can see the metadata.
# Anonymous users can do so if they are enabled. # Anonymous users can do so if they are enabled.
result = self.request.user.is_authenticated or anonymous_is_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"):
result = has_perm(self.request.user, 'core.can_manage_tags') result = has_perm(self.request.user, "core.can_manage_tags")
else: else:
result = False result = False
return result return result
@ -575,6 +647,7 @@ class ConfigViewSet(ModelViewSet):
There are the following views: metadata, list, retrieve, update and There are the following views: metadata, list, retrieve, update and
partial_update. partial_update.
""" """
access_permissions = ConfigAccessPermissions() access_permissions = ConfigAccessPermissions()
queryset = ConfigStore.objects.all() queryset = ConfigStore.objects.all()
@ -582,22 +655,22 @@ class ConfigViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. 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) 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 # Every authenticated user can see the metadata and list or
# retrieve the config. Anonymous users can do so if they are # retrieve the config. Anonymous users can do so if they are
# enabled. # enabled.
result = self.request.user.is_authenticated or anonymous_is_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 # 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 # starting with 'logo' and 'font'. For all other config values th euser needs
# the default permissions 'core.can_manage_config'. # the default permissions 'core.can_manage_config'.
pk = self.kwargs['pk'] pk = self.kwargs["pk"]
if pk.startswith('logo') or pk.startswith('font'): if pk.startswith("logo") or pk.startswith("font"):
result = has_perm(self.request.user, 'core.can_manage_logos_and_fonts') result = has_perm(self.request.user, "core.can_manage_logos_and_fonts")
else: else:
result = has_perm(self.request.user, 'core.can_manage_config') result = has_perm(self.request.user, "core.can_manage_config")
else: else:
result = False result = False
return result return result
@ -608,10 +681,10 @@ class ConfigViewSet(ModelViewSet):
Example: {"value": 42} Example: {"value": 42}
""" """
key = kwargs['pk'] key = kwargs["pk"]
value = request.data.get('value') value = request.data.get("value")
if value is None: 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. # Validate and change value.
try: try:
@ -619,10 +692,10 @@ class ConfigViewSet(ModelViewSet):
except ConfigNotFound: except ConfigNotFound:
raise Http404 raise Http404
except ConfigError as e: except ConfigError as e:
raise ValidationError({'detail': str(e)}) raise ValidationError({"detail": str(e)})
# Return response. # Return response.
return Response({'key': key, 'value': value}) return Response({"key": key, "value": value})
class ChatMessageViewSet(ModelViewSet): class ChatMessageViewSet(ModelViewSet):
@ -632,6 +705,7 @@ class ChatMessageViewSet(ModelViewSet):
There are the following views: metadata, list, retrieve and create. There are the following views: metadata, list, retrieve and create.
The views partial_update, update and destroy are disabled. The views partial_update, update and destroy are disabled.
""" """
access_permissions = ChatMessageAccessPermissions() access_permissions = ChatMessageAccessPermissions()
queryset = ChatMessage.objects.all() queryset = ChatMessage.objects.all()
@ -639,18 +713,18 @@ class ChatMessageViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. 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) 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 # We do not want anonymous users to use the chat even the anonymous
# group has the permission core.can_use_chat. # group has the permission core.can_use_chat.
result = ( result = self.request.user.is_authenticated and has_perm(
self.request.user.is_authenticated and self.request.user, "core.can_use_chat"
has_perm(self.request.user, 'core.can_use_chat')) )
elif self.action == 'clear': elif self.action == "clear":
result = ( result = has_perm(self.request.user, "core.can_use_chat") and has_perm(
has_perm(self.request.user, 'core.can_use_chat') and self.request.user, "core.can_manage_chat"
has_perm(self.request.user, 'core.can_manage_chat')) )
else: else:
result = False result = False
return result return result
@ -665,7 +739,7 @@ class ChatMessageViewSet(ModelViewSet):
# to see users may not have it but can get it now. # to see users may not have it but can get it now.
inform_changed_data([self.request.user]) inform_changed_data([self.request.user])
@list_route(methods=['post']) @list_route(methods=["post"])
def clear(self, request): def clear(self, request):
""" """
Deletes all chat messages. Deletes all chat messages.
@ -679,7 +753,7 @@ class ChatMessageViewSet(ModelViewSet):
# Trigger autoupdate and setup response. # Trigger autoupdate and setup response.
if len(args) > 0: if len(args) > 0:
inform_deleted_data(args) inform_deleted_data(args)
return Response({'detail': _('All chat messages deleted successfully.')}) return Response({"detail": _("All chat messages deleted successfully.")})
class ProjectorMessageViewSet(ModelViewSet): class ProjectorMessageViewSet(ModelViewSet):
@ -689,6 +763,7 @@ class ProjectorMessageViewSet(ModelViewSet):
There are the following views: list, retrieve, create, update, There are the following views: list, retrieve, create, update,
partial_update and destroy. partial_update and destroy.
""" """
access_permissions = ProjectorMessageAccessPermissions() access_permissions = ProjectorMessageAccessPermissions()
queryset = ProjectorMessage.objects.all() queryset = ProjectorMessage.objects.all()
@ -696,10 +771,10 @@ class ProjectorMessageViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. 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) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('create', 'partial_update', 'update', 'destroy'): elif self.action in ("create", "partial_update", "update", "destroy"):
result = has_perm(self.request.user, 'core.can_manage_projector') result = has_perm(self.request.user, "core.can_manage_projector")
else: else:
result = False result = False
return result return result
@ -712,6 +787,7 @@ class CountdownViewSet(ModelViewSet):
There are the following views: list, retrieve, create, update, There are the following views: list, retrieve, create, update,
partial_update and destroy. partial_update and destroy.
""" """
access_permissions = CountdownAccessPermissions() access_permissions = CountdownAccessPermissions()
queryset = Countdown.objects.all() queryset = Countdown.objects.all()
@ -719,10 +795,10 @@ class CountdownViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. 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) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('create', 'partial_update', 'update', 'destroy'): elif self.action in ("create", "partial_update", "update", "destroy"):
result = has_perm(self.request.user, 'core.can_manage_projector') result = has_perm(self.request.user, "core.can_manage_projector")
else: else:
result = False result = False
return result return result
@ -734,6 +810,7 @@ class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
There are the following views: list, retrieve, clear_history. There are the following views: list, retrieve, clear_history.
""" """
access_permissions = HistoryAccessPermissions() access_permissions = HistoryAccessPermissions()
queryset = History.objects.all() queryset = History.objects.all()
@ -741,13 +818,13 @@ class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
""" """
Returns True if the user has required permissions. 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) result = self.get_access_permissions().check_permissions(self.request.user)
else: else:
result = False result = False
return result return result
@list_route(methods=['post']) @list_route(methods=["post"])
def clear_history(self, request): def clear_history(self, request):
""" """
Deletes and rebuilds the history. Deletes and rebuilds the history.
@ -769,16 +846,18 @@ class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
inform_changed_data(history_instances) inform_changed_data(history_instances)
# Setup response. # Setup response.
return Response({'detail': _('History was deleted successfully.')}) return Response({"detail": _("History was deleted successfully.")})
# Special API views # Special API views
class ServerTime(utils_views.APIView): class ServerTime(utils_views.APIView):
""" """
Returns the server time as UNIX timestamp. Returns the server time as UNIX timestamp.
""" """
http_method_names = ['get']
http_method_names = ["get"]
def get_context_data(self, **context): def get_context_data(self, **context):
return now().timestamp() return now().timestamp()
@ -789,27 +868,36 @@ class VersionView(utils_views.APIView):
Returns a dictionary with the OpenSlides version and the version of all Returns a dictionary with the OpenSlides version and the version of all
plugins. plugins.
""" """
http_method_names = ['get']
http_method_names = ["get"]
def get_context_data(self, **context): def get_context_data(self, **context):
Result = TypedDict('Result', { Result = TypedDict(
'openslides_version': str, "Result",
'openslides_license': str, {
'openslides_url': str, "openslides_version": str,
'plugins': List[Dict[str, str]]}) "openslides_license": str,
"openslides_url": str,
"plugins": List[Dict[str, str]],
},
)
result: Result = dict( result: Result = dict(
openslides_version=version, openslides_version=version,
openslides_license=license, openslides_license=license,
openslides_url=url, openslides_url=url,
plugins=[]) plugins=[],
)
# Versions of plugins. # Versions of plugins.
for plugin in settings.INSTALLED_PLUGINS: for plugin in settings.INSTALLED_PLUGINS:
result['plugins'].append({ result["plugins"].append(
'verbose_name': get_plugin_verbose_name(plugin), {
'description': get_plugin_description(plugin), "verbose_name": get_plugin_verbose_name(plugin),
'version': get_plugin_version(plugin), "description": get_plugin_description(plugin),
'license': get_plugin_license(plugin), "version": get_plugin_version(plugin),
'url': get_plugin_url(plugin)}) "license": get_plugin_license(plugin),
"url": get_plugin_url(plugin),
}
)
return result return result
@ -820,7 +908,8 @@ class HistoryView(utils_views.APIView):
Use query paramter timestamp (UNIX timestamp) to get all elements from begin Use query paramter timestamp (UNIX timestamp) to get all elements from begin
until (including) this timestamp. until (including) this timestamp.
""" """
http_method_names = ['get']
http_method_names = ["get"]
def get_context_data(self, **context): 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]): if not in_some_groups(self.request.user.pk or 0, [GROUP_ADMIN_PK]):
self.permission_denied(self.request) self.permission_denied(self.request)
try: try:
timestamp = int(self.request.query_params.get('timestamp', 0)) timestamp = int(self.request.query_params.get("timestamp", 0))
except (ValueError): except (ValueError):
raise ValidationError({'detail': 'Invalid input. Timestamp should be an integer.'}) raise ValidationError(
{"detail": "Invalid input. Timestamp should be an integer."}
)
data = [] data = []
queryset = History.objects.select_related('full_data') queryset = History.objects.select_related("full_data")
if timestamp: if timestamp:
queryset = queryset.filter(now__lte=datetime.datetime.fromtimestamp(timestamp)) queryset = queryset.filter(
now__lte=datetime.datetime.fromtimestamp(timestamp)
)
for instance in queryset: for instance in queryset:
data.append({ data.append(
'full_data': instance.full_data.full_data, {
'element_id': instance.element_id, "full_data": instance.full_data.full_data,
'timestamp': instance.now.timestamp(), "element_id": instance.element_id,
'information': instance.information, "timestamp": instance.now.timestamp(),
'user_id': instance.user.pk if instance.user else None, "information": instance.information,
}) "user_id": instance.user.pk if instance.user else None,
}
)
return data return data

View File

@ -12,7 +12,8 @@ class NotifyWebsocketClientMessage(BaseWebsocketClientMessage):
""" """
Websocket message from a client to send a message to other clients. Websocket message from a client to send a message to other clients.
""" """
identifier = 'notify'
identifier = "notify"
schema = { schema = {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "Notify elements.", "title": "Notify elements.",
@ -21,31 +22,24 @@ class NotifyWebsocketClientMessage(BaseWebsocketClientMessage):
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
"projectors": { "projectors": {"type": "array", "items": {"type": "integer"}},
"type": "array", "reply_channels": {"type": "array", "items": {"type": "string"}},
"items": {"type": "integer"}, "users": {"type": "array", "items": {"type": "integer"}},
}, },
"reply_channels": {
"type": "array",
"items": {"type": "string"},
},
"users": {
"type": "array",
"items": {"type": "integer"},
}
}
}, },
"minItems": 1, "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( await consumer.channel_layer.group_send(
"site", "site",
{ {
"type": "send_notify", "type": "send_notify",
"incomming": content, "incomming": content,
"senderReplyChannelName": consumer.channel_name, "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. The Client requests the constants.
""" """
identifier = 'constants'
identifier = "constants"
content_required = False 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. # 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): class GetElementsWebsocketClientMessage(BaseWebsocketClientMessage):
""" """
The Client request database elements. The Client request database elements.
""" """
identifier = 'getElements'
identifier = "getElements"
schema = { schema = {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"titel": "getElement request", "titel": "getElement request",
@ -74,31 +74,40 @@ class GetElementsWebsocketClientMessage(BaseWebsocketClientMessage):
"type": "object", "type": "object",
"properties": { "properties": {
# change_id is not required # change_id is not required
"change_id": { "change_id": {"type": "integer"}
"type": "integer",
}
}, },
} }
async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str) -> None: async def receive_content(
requested_change_id = content.get('change_id', 0) self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str
) -> None:
requested_change_id = content.get("change_id", 0)
try: 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: 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: 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): class AutoupdateWebsocketClientMessage(BaseWebsocketClientMessage):
""" """
The Client turns autoupdate on or off. 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 # Turn on or off the autoupdate for the client
if content: # accept any value, that can be interpreted as bool 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: else:
await consumer.channel_layer.group_discard('autoupdate', consumer.channel_name) await consumer.channel_layer.group_discard(
"autoupdate", consumer.channel_name
)

View File

@ -9,68 +9,70 @@ MODULE_DIR = os.path.realpath(os.path.dirname(os.path.abspath(__file__)))
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'openslides.core', "openslides.core",
'openslides.users', "openslides.users",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.staticfiles', "django.contrib.staticfiles",
'rest_framework', "rest_framework",
'channels', "channels",
'openslides.agenda', "openslides.agenda",
'openslides.topics', "openslides.topics",
'openslides.motions', "openslides.motions",
'openslides.assignments', "openslides.assignments",
'openslides.mediafiles', "openslides.mediafiles",
] ]
INSTALLED_PLUGINS = collect_plugins() # Adds all automaticly collected plugins INSTALLED_PLUGINS = collect_plugins() # Adds all automaticly collected plugins
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.locale.LocaleMiddleware', "django.middleware.locale.LocaleMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
'openslides.utils.autoupdate.AutoupdateBundleMiddleware', "openslides.utils.autoupdate.AutoupdateBundleMiddleware",
] ]
ROOT_URLCONF = 'openslides.urls' ROOT_URLCONF = "openslides.urls"
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ["*"]
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [], "DIRS": [],
'APP_DIRS': True, "APP_DIRS": True,
}, }
] ]
# Email # Email
# https://docs.djangoproject.com/en/1.10/topics/email/ # https://docs.djangoproject.com/en/1.10/topics/email/
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_TIMEOUT = 5 # Timeout in seconds for blocking operations like the connection attempt EMAIL_TIMEOUT = (
5
) # Timeout in seconds for blocking operations like the connection attempt
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/ # https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'en' LANGUAGE_CODE = "en"
LANGUAGES = ( LANGUAGES = (
('en', 'English'), ("en", "English"),
('de', 'Deutsch'), ("de", "Deutsch"),
('fr', 'Français'), ("fr", "Français"),
('es', 'Español'), ("es", "Español"),
('pt', 'Português'), ("pt", "Português"),
('cs', 'Český'), ("cs", "Český"),
('ru', 'русский'), ("ru", "русский"),
) )
TIME_ZONE = 'UTC' TIME_ZONE = "UTC"
USE_I18N = True USE_I18N = True
@ -78,62 +80,54 @@ USE_L10N = True
USE_TZ = True USE_TZ = True
LOCALE_PATHS = [ LOCALE_PATHS = [os.path.join(MODULE_DIR, "locale")]
os.path.join(MODULE_DIR, 'locale'),
]
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/ # https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = "/static/"
STATICFILES_DIRS = [ STATICFILES_DIRS = [os.path.join(MODULE_DIR, "static")]
os.path.join(MODULE_DIR, 'static'),
]
# Sessions and user authentication # Sessions and user authentication
# https://docs.djangoproject.com/en/1.10/topics/http/sessions/ # https://docs.djangoproject.com/en/1.10/topics/http/sessions/
# https://docs.djangoproject.com/en/1.10/topics/auth/ # 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 SESSION_EXPIRE_AT_BROWSER_CLOSE = True
CSRF_COOKIE_NAME = 'OpenSlidesCsrfToken' CSRF_COOKIE_NAME = "OpenSlidesCsrfToken"
CSRF_COOKIE_AGE = None CSRF_COOKIE_AGE = None
PASSWORD_HASHERS = [ PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher', "django.contrib.auth.hashers.PBKDF2PasswordHasher",
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
'django.contrib.auth.hashers.Argon2PasswordHasher', "django.contrib.auth.hashers.Argon2PasswordHasher",
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
'django.contrib.auth.hashers.BCryptPasswordHasher', "django.contrib.auth.hashers.BCryptPasswordHasher",
] ]
# Files # Files
# https://docs.djangoproject.com/en/1.10/topics/files/ # https://docs.djangoproject.com/en/1.10/topics/files/
MEDIA_URL = '/media/' MEDIA_URL = "/media/"
# Django Channels # Django Channels
# http://channels.readthedocs.io/en/latest/ # http://channels.readthedocs.io/en/latest/
ASGI_APPLICATION = 'openslides.routing.application' ASGI_APPLICATION = "openslides.routing.application"
CHANNEL_LAYERS = { CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer',
},
}
# Enable updating the last_login field for users on every login. # Enable updating the last_login field for users on every login.

View File

@ -1 +1 @@
default_app_config = 'openslides.mediafiles.apps.MediafilesAppConfig' default_app_config = "openslides.mediafiles.apps.MediafilesAppConfig"

View File

@ -8,22 +8,24 @@ class MediafileAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Mediafile and MediafileViewSet. Access permissions container for Mediafile and MediafileViewSet.
""" """
base_permission = 'mediafiles.can_see'
base_permission = "mediafiles.can_see"
async def get_restricted_data( async def get_restricted_data(
self, self, full_data: List[Dict[str, Any]], user_id: int
full_data: List[Dict[str, Any]], ) -> List[Dict[str, Any]]:
user_id: int) -> List[Dict[str, Any]]:
""" """
Returns the restricted serialized data for the instance prepared Returns the restricted serialized data for the instance prepared
for the user. Removes hidden mediafiles for some users. for the user. Removes hidden mediafiles for some users.
""" """
# Parse data. # 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 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. # 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: else:
data = [] data = []

View File

@ -6,8 +6,8 @@ from ..utils.projector import register_projector_elements
class MediafilesAppConfig(AppConfig): class MediafilesAppConfig(AppConfig):
name = 'openslides.mediafiles' name = "openslides.mediafiles"
verbose_name = 'OpenSlides Mediafiles' verbose_name = "OpenSlides Mediafiles"
angular_site_module = True angular_site_module = True
angular_projector_module = True angular_projector_module = True
@ -27,20 +27,25 @@ class MediafilesAppConfig(AppConfig):
# Connect signals. # Connect signals.
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data,
dispatch_uid='mediafiles_get_permission_change_data') dispatch_uid="mediafiles_get_permission_change_data",
)
# Register viewsets. # 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 # 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): def get_startup_elements(self):
""" """
Yields all Cachables required on startup i. e. opening the websocket Yields all Cachables required on startup i. e. opening the websocket
connection. connection.
""" """
yield self.get_model('Mediafile') yield self.get_model("Mediafile")
def required_users(element: Dict[str, Any]) -> Set[int]: 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 if request_user can see mediafiles. This function may return an empty
set. set.
""" """
return set(element['uploader_id']) return set(element["uploader_id"])

View File

@ -13,25 +13,43 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Mediafile', name="Mediafile",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('mediafile', models.FileField(upload_to='file')), "id",
('title', models.CharField(max_length=255, unique=True)), models.AutoField(
('timestamp', models.DateTimeField(auto_now_add=True)), auto_created=True,
('uploader', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), 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={ options={
'permissions': (('can_see', 'Can see the list of files'), ('can_upload', 'Can upload files'), ('can_manage', 'Can manage files')), "permissions": (
'default_permissions': (), ("can_see", "Can see the list of files"),
'ordering': ['title'], ("can_upload", "Can upload files"),
("can_manage", "Can manage files"),
),
"default_permissions": (),
"ordering": ["title"],
}, },
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), )
] ]

View File

@ -7,22 +7,25 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("mediafiles", "0001_initial")]
('mediafiles', '0001_initial'),
]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='mediafile', name="mediafile",
options={'default_permissions': (), 'ordering': ['title'], 'permissions': ( options={
('can_see', 'Can see the list of files'), "default_permissions": (),
('can_see_hidden', 'Can see hidden files'), "ordering": ["title"],
('can_upload', 'Can upload files'), "permissions": (
('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"),
),
},
), ),
migrations.AddField( migrations.AddField(
model_name='mediafile', model_name="mediafile",
name='hidden', name="hidden",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
] ]

View File

@ -13,10 +13,11 @@ class Mediafile(RESTModelMixin, models.Model):
""" """
Class for uploaded files which can be delivered under a certain url. 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 See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield
for more information. for more information.
@ -26,10 +27,8 @@ class Mediafile(RESTModelMixin, models.Model):
"""A string representing the title of the file.""" """A string representing the title of the file."""
uploader = models.ForeignKey( uploader = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True
on_delete=models.SET_NULL, )
null=True,
blank=True)
"""A user the uploader of a file.""" """A user the uploader of a file."""
hidden = models.BooleanField(default=False) hidden = models.BooleanField(default=False)
@ -42,13 +41,15 @@ class Mediafile(RESTModelMixin, models.Model):
""" """
Meta class for the mediafile model. Meta class for the mediafile model.
""" """
ordering = ['title']
ordering = ["title"]
default_permissions = () default_permissions = ()
permissions = ( permissions = (
('can_see', 'Can see the list of files'), ("can_see", "Can see the list of files"),
('can_see_hidden', 'Can see hidden files'), ("can_see_hidden", "Can see hidden files"),
('can_upload', 'Can upload files'), ("can_upload", "Can upload files"),
('can_manage', 'Can manage files')) ("can_manage", "Can manage files"),
)
def __str__(self): def __str__(self):
""" """
@ -72,10 +73,11 @@ class Mediafile(RESTModelMixin, models.Model):
mediafile projector element is disabled. mediafile projector element is disabled.
""" """
Projector.remove_any( Projector.remove_any(
skip_autoupdate=skip_autoupdate, skip_autoupdate=skip_autoupdate, name="mediafiles/mediafile", id=self.pk
name='mediafiles/mediafile', )
id=self.pk) return super().delete( # type: ignore
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore skip_autoupdate=skip_autoupdate, *args, **kwargs
)
def get_filesize(self): def get_filesize(self):
""" """
@ -85,26 +87,26 @@ class Mediafile(RESTModelMixin, models.Model):
try: try:
size = self.mediafile.size size = self.mediafile.size
except OSError: except OSError:
size_string = _('unknown') size_string = _("unknown")
else: else:
if size < 1024: if size < 1024:
size_string = '< 1 kB' size_string = "< 1 kB"
elif size >= 1024 * 1024: elif size >= 1024 * 1024:
mB = size / 1024 / 1024 mB = size / 1024 / 1024
size_string = '%d MB' % mB size_string = "%d MB" % mB
else: else:
kB = size / 1024 kB = size / 1024
size_string = '%d kB' % kB size_string = "%d kB" % kB
return size_string return size_string
def is_logo(self): def is_logo(self):
for key in config['logos_available']: for key in config["logos_available"]:
if config[key]['path'] == self.mediafile.url: if config[key]["path"] == self.mediafile.url:
return True return True
return False return False
def is_font(self): def is_font(self):
for key in config['fonts_available']: for key in config["fonts_available"]:
if config[key]['path'] == self.mediafile.url: if config[key]["path"] == self.mediafile.url:
return True return True
return False return False

View File

@ -9,11 +9,12 @@ class MediafileSlide(ProjectorElement):
""" """
Slide definitions for Mediafile model. Slide definitions for Mediafile model.
""" """
name = 'mediafiles/mediafile'
name = "mediafiles/mediafile"
def check_data(self): def check_data(self):
if not Mediafile.objects.filter(pk=self.config_entry.get('id')).exists(): if not Mediafile.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException('File does not exist.') raise ProjectorException("File does not exist.")
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]: def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:

View File

@ -10,9 +10,8 @@ from .models import Mediafile
class AngularCompatibleFileField(FileField): class AngularCompatibleFileField(FileField):
def to_internal_value(self, data): def to_internal_value(self, data):
if data == '': if data == "":
return None return None
return super(AngularCompatibleFileField, self).to_internal_value(data) return super(AngularCompatibleFileField, self).to_internal_value(data)
@ -20,20 +19,17 @@ class AngularCompatibleFileField(FileField):
if value is None: if value is None:
return None return None
filetype = mimetypes.guess_type(value.path)[0] filetype = mimetypes.guess_type(value.path)[0]
result = { result = {"name": value.name, "type": filetype}
'name': value.name, if filetype == "application/pdf":
'type': filetype
}
if filetype == 'application/pdf':
try: try:
result['pages'] = PdfFileReader(open(value.path, 'rb')).getNumPages() result["pages"] = PdfFileReader(open(value.path, "rb")).getNumPages()
except FileNotFoundError: except FileNotFoundError:
# File was deleted from server. Set 'pages' to 0. # File was deleted from server. Set 'pages' to 0.
result['pages'] = 0 result["pages"] = 0
except PdfReadError: except PdfReadError:
# File could be encrypted but not be detected by PyPDF. # File could be encrypted but not be detected by PyPDF.
result['pages'] = 0 result["pages"] = 0
result['encrypted'] = True result["encrypted"] = True
return result return result
@ -41,6 +37,7 @@ class MediafileSerializer(ModelSerializer):
""" """
Serializer for mediafile.models.Mediafile objects. Serializer for mediafile.models.Mediafile objects.
""" """
media_url_prefix = SerializerMethodField() media_url_prefix = SerializerMethodField()
filesize = SerializerMethodField() filesize = SerializerMethodField()
@ -52,19 +49,20 @@ class MediafileSerializer(ModelSerializer):
super(MediafileSerializer, self).__init__(*args, **kwargs) super(MediafileSerializer, self).__init__(*args, **kwargs)
self.serializer_field_mapping[dbmodels.FileField] = AngularCompatibleFileField self.serializer_field_mapping[dbmodels.FileField] = AngularCompatibleFileField
if self.instance is not None: if self.instance is not None:
self.fields['mediafile'].read_only = True self.fields["mediafile"].read_only = True
class Meta: class Meta:
model = Mediafile model = Mediafile
fields = ( fields = (
'id', "id",
'title', "title",
'mediafile', "mediafile",
'media_url_prefix', "media_url_prefix",
'uploader', "uploader",
'filesize', "filesize",
'hidden', "hidden",
'timestamp',) "timestamp",
)
def get_filesize(self, mediafile): def get_filesize(self, mediafile):
return mediafile.get_filesize() return mediafile.get_filesize()

View File

@ -5,8 +5,11 @@ def get_permission_change_data(sender, permissions=None, **kwargs):
""" """
Yields all necessary collections if 'mediafiles.can_see' permission changes. 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: for permission in permissions:
# There could be only one 'mediafiles.can_see' and then we want to return data. # 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() yield from mediafiles_app.get_startup_elements()

View File

@ -9,6 +9,7 @@ from .models import Mediafile
# Viewsets for the REST API # Viewsets for the REST API
class MediafileViewSet(ModelViewSet): class MediafileViewSet(ModelViewSet):
""" """
API endpoint for mediafile objects. API endpoint for mediafile objects.
@ -16,6 +17,7 @@ class MediafileViewSet(ModelViewSet):
There are the following views: metadata, list, retrieve, create, There are the following views: metadata, list, retrieve, create,
partial_update, update and destroy. partial_update, update and destroy.
""" """
access_permissions = MediafileAccessPermissions() access_permissions = MediafileAccessPermissions()
queryset = Mediafile.objects.all() queryset = Mediafile.objects.all()
@ -23,20 +25,24 @@ class MediafileViewSet(ModelViewSet):
""" """
Returns True if the user has required permissions. 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) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action == 'metadata': elif self.action == "metadata":
result = has_perm(self.request.user, 'mediafiles.can_see') result = has_perm(self.request.user, "mediafiles.can_see")
elif self.action == 'create': elif self.action == "create":
result = (has_perm(self.request.user, 'mediafiles.can_see') and result = has_perm(self.request.user, "mediafiles.can_see") and has_perm(
has_perm(self.request.user, 'mediafiles.can_upload')) self.request.user, "mediafiles.can_upload"
elif self.action in ('partial_update', 'update'): )
result = (has_perm(self.request.user, 'mediafiles.can_see') and elif self.action in ("partial_update", "update"):
has_perm(self.request.user, 'mediafiles.can_upload') and result = (
has_perm(self.request.user, 'mediafiles.can_manage')) has_perm(self.request.user, "mediafiles.can_see")
elif self.action == 'destroy': and has_perm(self.request.user, "mediafiles.can_upload")
result = (has_perm(self.request.user, 'mediafiles.can_see') and and has_perm(self.request.user, "mediafiles.can_manage")
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: else:
result = False result = False
return result return result
@ -46,13 +52,15 @@ class MediafileViewSet(ModelViewSet):
Customized view endpoint to upload a new file. Customized view endpoint to upload a new file.
""" """
# Check permission to check if the uploader has to be changed. # Check permission to check if the uploader has to be changed.
uploader_id = self.request.data.get('uploader_id') uploader_id = self.request.data.get("uploader_id")
if (uploader_id and if (
not has_perm(request.user, 'mediafiles.can_manage') and uploader_id
str(self.request.user.pk) != str(uploader_id)): and not has_perm(request.user, "mediafiles.can_manage")
and str(self.request.user.pk) != str(uploader_id)
):
self.permission_denied(request) self.permission_denied(request)
if not self.request.data.get('mediafile'): if not self.request.data.get("mediafile"):
raise ValidationError({'detail': 'You forgot to provide a file.'}) raise ValidationError({"detail": "You forgot to provide a file."})
return super().create(request, *args, **kwargs) return super().create(request, *args, **kwargs)
def destroy(self, 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: except Mediafile.DoesNotExist:
return HttpResponseNotFound(content="Not found.") 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_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): if not is_special_file and (not can_see or is_hidden_but_no_perms):
return HttpResponseForbidden(content="Forbidden.") return HttpResponseForbidden(content="Forbidden.")

View File

@ -1 +1 @@
default_app_config = 'openslides.motions.apps.MotionsAppConfig' default_app_config = "openslides.motions.apps.MotionsAppConfig"

View File

@ -9,12 +9,12 @@ class MotionAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Motion and MotionViewSet. Access permissions container for Motion and MotionViewSet.
""" """
base_permission = 'motions.can_see'
base_permission = "motions.can_see"
async def get_restricted_data( async def get_restricted_data(
self, self, full_data: List[Dict[str, Any]], user_id: int
full_data: List[Dict[str, Any]], ) -> List[Dict[str, Any]]:
user_id: int) -> List[Dict[str, Any]]:
""" """
Returns the restricted serialized data for the instance prepared for Returns the restricted serialized data for the instance prepared for
the user. Removes motion if the user has not the permission to see the user. Removes motion if the user has not the permission to see
@ -23,33 +23,37 @@ class MotionAccessPermissions(BaseAccessPermissions):
personal notes. personal notes.
""" """
# Parse data. # 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. # TODO: Refactor this after personal_notes system is refactored.
data = [] data = []
for full in full_data: for full in full_data:
# Check if user is submitter of this motion. # Check if user is submitter of this motion.
if user_id: if user_id:
is_submitter = user_id in [ is_submitter = user_id in [
submitter['user_id'] for submitter in full.get('submitters', [])] submitter["user_id"] for submitter in full.get("submitters", [])
]
else: else:
# Anonymous users can not be submitters. # Anonymous users can not be submitters.
is_submitter = False is_submitter = False
# Check see permission for this motion. # 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 = ( permission = (
not required_permission_to_see or not required_permission_to_see
await async_has_perm(user_id, required_permission_to_see) or or await async_has_perm(user_id, required_permission_to_see)
await async_has_perm(user_id, 'motions.can_manage') or or await async_has_perm(user_id, "motions.can_manage")
is_submitter) or is_submitter
)
# Parse single motion. # Parse single motion.
if permission: if permission:
full_copy = deepcopy(full) full_copy = deepcopy(full)
full_copy['comments'] = [] full_copy["comments"] = []
for comment in full['comments']: for comment in full["comments"]:
if await async_in_some_groups(user_id, comment['read_groups_id']): if await async_in_some_groups(
full_copy['comments'].append(comment) user_id, comment["read_groups_id"]
):
full_copy["comments"].append(comment)
data.append(full_copy) data.append(full_copy)
else: else:
data = [] data = []
@ -61,23 +65,23 @@ class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for MotionChangeRecommendation and MotionChangeRecommendationViewSet. Access permissions container for MotionChangeRecommendation and MotionChangeRecommendationViewSet.
""" """
base_permission = 'motions.can_see'
base_permission = "motions.can_see"
async def get_restricted_data( async def get_restricted_data(
self, self, full_data: List[Dict[str, Any]], user_id: int
full_data: List[Dict[str, Any]], ) -> List[Dict[str, Any]]:
user_id: int) -> List[Dict[str, Any]]:
""" """
Removes change recommendations if they are internal and the user has Removes change recommendations if they are internal and the user has
not the can_manage permission. To see change recommendation the user needs not the can_manage permission. To see change recommendation the user needs
the can_see permission. the can_see permission.
""" """
# Parse data. # Parse data.
if await async_has_perm(user_id, 'motions.can_see'): if await async_has_perm(user_id, "motions.can_see"):
has_manage_perms = await async_has_perm(user_id, 'motion.can_manage') has_manage_perms = await async_has_perm(user_id, "motion.can_manage")
data = [] data = []
for full in full_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) data.append(full)
else: else:
data = [] data = []
@ -89,22 +93,22 @@ class MotionCommentSectionAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for MotionCommentSection and MotionCommentSectionViewSet. Access permissions container for MotionCommentSection and MotionCommentSectionViewSet.
""" """
base_permission = 'motions.can_see'
base_permission = "motions.can_see"
async def get_restricted_data( async def get_restricted_data(
self, self, full_data: List[Dict[str, Any]], user_id: int
full_data: List[Dict[str, Any]], ) -> 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 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. will be removed, when the user is not in at least one of the read_groups.
""" """
data: List[Dict[str, Any]] = [] 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 data = full_data
else: else:
for full in full_data: 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): if await async_in_some_groups(user_id, read_groups):
data.append(full) data.append(full)
return data return data
@ -114,25 +118,29 @@ class StatuteParagraphAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for StatuteParagraph and StatuteParagraphViewSet. Access permissions container for StatuteParagraph and StatuteParagraphViewSet.
""" """
base_permission = 'motions.can_see'
base_permission = "motions.can_see"
class CategoryAccessPermissions(BaseAccessPermissions): class CategoryAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Category and CategoryViewSet. Access permissions container for Category and CategoryViewSet.
""" """
base_permission = 'motions.can_see'
base_permission = "motions.can_see"
class MotionBlockAccessPermissions(BaseAccessPermissions): class MotionBlockAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Category and CategoryViewSet. Access permissions container for Category and CategoryViewSet.
""" """
base_permission = 'motions.can_see'
base_permission = "motions.can_see"
class WorkflowAccessPermissions(BaseAccessPermissions): class WorkflowAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for Workflow and WorkflowViewSet. Access permissions container for Workflow and WorkflowViewSet.
""" """
base_permission = 'motions.can_see'
base_permission = "motions.can_see"

View File

@ -7,8 +7,8 @@ from ..utils.projector import register_projector_elements
class MotionsAppConfig(AppConfig): class MotionsAppConfig(AppConfig):
name = 'openslides.motions' name = "openslides.motions"
verbose_name = 'OpenSlides Motion' verbose_name = "OpenSlides Motion"
angular_site_module = True angular_site_module = True
angular_projector_module = True angular_projector_module = True
@ -17,10 +17,7 @@ class MotionsAppConfig(AppConfig):
from openslides.core.signals import permission_change from openslides.core.signals import permission_change
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
from .projector import get_projector_elements from .projector import get_projector_elements
from .signals import ( from .signals import create_builtin_workflows, get_permission_change_data
create_builtin_workflows,
get_permission_change_data,
)
from . import serializers # noqa from . import serializers # noqa
from .views import ( from .views import (
CategoryViewSet, CategoryViewSet,
@ -40,29 +37,49 @@ class MotionsAppConfig(AppConfig):
# Connect signals. # Connect signals.
post_migrate.connect( post_migrate.connect(
create_builtin_workflows, create_builtin_workflows, dispatch_uid="motion_create_builtin_workflows"
dispatch_uid='motion_create_builtin_workflows') )
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data,
dispatch_uid='motions_get_permission_change_data') dispatch_uid="motions_get_permission_change_data",
)
# Register viewsets. # Register viewsets.
router.register(self.get_model('Category').get_collection_string(), CategoryViewSet) router.register(
router.register(self.get_model('StatuteParagraph').get_collection_string(), StatuteParagraphViewSet) self.get_model("Category").get_collection_string(), CategoryViewSet
router.register(self.get_model('Motion').get_collection_string(), MotionViewSet) )
router.register(self.get_model('MotionBlock').get_collection_string(), MotionBlockViewSet) router.register(
router.register(self.get_model('MotionCommentSection').get_collection_string(), MotionCommentSectionViewSet) self.get_model("StatuteParagraph").get_collection_string(),
router.register(self.get_model('Workflow').get_collection_string(), WorkflowViewSet) StatuteParagraphViewSet,
router.register(self.get_model('MotionChangeRecommendation').get_collection_string(), )
MotionChangeRecommendationViewSet) router.register(self.get_model("Motion").get_collection_string(), MotionViewSet)
router.register(self.get_model('MotionPoll').get_collection_string(), MotionPollViewSet) router.register(
router.register(self.get_model('State').get_collection_string(), StateViewSet) 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 # 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): def get_config_variables(self):
from .config_variables import get_config_variables from .config_variables import get_config_variables
return get_config_variables() return get_config_variables()
def get_startup_elements(self): def get_startup_elements(self):
@ -70,8 +87,15 @@ class MotionsAppConfig(AppConfig):
Yields all Cachables required on startup i. e. opening the websocket Yields all Cachables required on startup i. e. opening the websocket
connection. connection.
""" """
for model_name in ('Category', 'StatuteParagraph', 'Motion', 'MotionBlock', for model_name in (
'Workflow', 'MotionChangeRecommendation', 'MotionCommentSection'): "Category",
"StatuteParagraph",
"Motion",
"MotionBlock",
"Workflow",
"MotionChangeRecommendation",
"MotionCommentSection",
):
yield self.get_model(model_name) 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 any motion if request_user can see motions. This function may return an
empty set. empty set.
""" """
submitters_supporters = set([submitter['user_id'] for submitter in element['submitters']]) submitters_supporters = set(
submitters_supporters.update(element['supporters_id']) [submitter["user_id"] for submitter in element["submitters"]]
)
submitters_supporters.update(element["supporters_id"])
return submitters_supporters return submitters_supporters

View File

@ -11,8 +11,10 @@ def get_workflow_choices():
Returns a list of all workflows to be used as choices for the config variable 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. 'motions_workflow'. Each list item contains the pk and the display name.
""" """
return [{'value': str(workflow.pk), 'display_name': workflow.name} return [
for workflow in Workflow.objects.all()] {"value": str(workflow.pk), "display_name": workflow.name}
for workflow in Workflow.objects.all()
]
def get_config_variables(): def get_config_variables():
@ -26,300 +28,339 @@ def get_config_variables():
# General # General
yield ConfigVariable( yield ConfigVariable(
name='motions_workflow', name="motions_workflow",
default_value='1', default_value="1",
input_type='choice', input_type="choice",
label='Workflow of new motions', label="Workflow of new motions",
choices=get_workflow_choices, choices=get_workflow_choices,
weight=310, weight=310,
group='Motions', group="Motions",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_statute_amendments_workflow', name="motions_statute_amendments_workflow",
default_value='1', default_value="1",
input_type='choice', input_type="choice",
label='Workflow of new statute amendments', label="Workflow of new statute amendments",
choices=get_workflow_choices, choices=get_workflow_choices,
weight=312, weight=312,
group='Motions', group="Motions",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_identifier', name="motions_identifier",
default_value='per_category', default_value="per_category",
input_type='choice', input_type="choice",
label='Identifier', label="Identifier",
choices=( choices=(
{'value': 'per_category', 'display_name': 'Numbered per category'}, {"value": "per_category", "display_name": "Numbered per category"},
{'value': 'serially_numbered', 'display_name': 'Serially numbered'}, {"value": "serially_numbered", "display_name": "Serially numbered"},
{'value': 'manually', 'display_name': 'Set it manually'}), {"value": "manually", "display_name": "Set it manually"},
),
weight=315, weight=315,
group='Motions', group="Motions",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_preamble', name="motions_preamble",
default_value='The assembly may decide:', default_value="The assembly may decide:",
label='Motion preamble', label="Motion preamble",
weight=320, weight=320,
group='Motions', group="Motions",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_default_line_numbering', name="motions_default_line_numbering",
default_value='none', default_value="none",
input_type='choice', input_type="choice",
label='Default line numbering', label="Default line numbering",
choices=( choices=(
{'value': 'outside', 'display_name': 'outside'}, {"value": "outside", "display_name": "outside"},
{'value': 'inline', 'display_name': 'inline'}, {"value": "inline", "display_name": "inline"},
{'value': 'none', 'display_name': 'Disabled'}), {"value": "none", "display_name": "Disabled"},
),
weight=322, weight=322,
group='Motions', group="Motions",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_line_length', name="motions_line_length",
default_value=90, default_value=90,
input_type='integer', input_type="integer",
label='Line length', label="Line length",
help_text='The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40', help_text="The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40",
weight=323, weight=323,
group='Motions', group="Motions",
subgroup='General', subgroup="General",
validators=(MinValueValidator(40),)) validators=(MinValueValidator(40),),
)
yield ConfigVariable( yield ConfigVariable(
name='motions_disable_reason_on_projector', name="motions_disable_reason_on_projector",
default_value=False, default_value=False,
input_type='boolean', input_type="boolean",
label='Hide reason on projector', label="Hide reason on projector",
weight=325, weight=325,
group='Motions', group="Motions",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_disable_sidebox_on_projector', name="motions_disable_sidebox_on_projector",
default_value=False, default_value=False,
input_type='boolean', input_type="boolean",
label='Hide meta information box on projector', label="Hide meta information box on projector",
weight=326, weight=326,
group='Motions', group="Motions",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_disable_recommendation_on_projector', name="motions_disable_recommendation_on_projector",
default_value=False, default_value=False,
input_type='boolean', input_type="boolean",
label='Hide recommendation on projector', label="Hide recommendation on projector",
weight=327, weight=327,
group='Motions', group="Motions",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_stop_submitting', name="motions_stop_submitting",
default_value=False, default_value=False,
input_type='boolean', input_type="boolean",
label='Stop submitting new motions by non-staff users', label="Stop submitting new motions by non-staff users",
weight=331, weight=331,
group='Motions', group="Motions",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_recommendations_by', name="motions_recommendations_by",
default_value='', default_value="",
label='Name of recommender', label="Name of recommender",
help_text='Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.', help_text="Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.",
weight=332, weight=332,
group='Motions', group="Motions",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_statute_recommendations_by', name="motions_statute_recommendations_by",
default_value='', default_value="",
label='Name of recommender for statute amendments', label="Name of recommender for statute amendments",
help_text='Will be displayed as label before selected recommendation in 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.', + "Use an empty value to disable the recommendation system for statute amendments.",
weight=333, weight=333,
group='Motions', group="Motions",
subgroup='General') subgroup="General",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_recommendation_text_mode', name="motions_recommendation_text_mode",
default_value='original', default_value="original",
input_type='choice', input_type="choice",
label='Default text version for change recommendations', label="Default text version for change recommendations",
choices=( choices=(
{'value': 'original', 'display_name': 'Original version'}, {"value": "original", "display_name": "Original version"},
{'value': 'changed', 'display_name': 'Changed version'}, {"value": "changed", "display_name": "Changed version"},
{'value': 'diff', 'display_name': 'Diff version'}, {"value": "diff", "display_name": "Diff version"},
{'value': 'agreed', 'display_name': 'Final version'}), {"value": "agreed", "display_name": "Final version"},
),
weight=334, weight=334,
group='Motions', group="Motions",
subgroup='General') subgroup="General",
)
# Amendments # Amendments
yield ConfigVariable( yield ConfigVariable(
name='motions_statutes_enabled', name="motions_statutes_enabled",
default_value=False, default_value=False,
input_type='boolean', input_type="boolean",
label='Activate statute amendments', label="Activate statute amendments",
weight=335, weight=335,
group='Motions', group="Motions",
subgroup='Amendments') subgroup="Amendments",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_amendments_enabled', name="motions_amendments_enabled",
default_value=False, default_value=False,
input_type='boolean', input_type="boolean",
label='Activate amendments', label="Activate amendments",
weight=336, weight=336,
group='Motions', group="Motions",
subgroup='Amendments') subgroup="Amendments",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_amendments_main_table', name="motions_amendments_main_table",
default_value=False, default_value=False,
input_type='boolean', input_type="boolean",
label='Show amendments together with motions', label="Show amendments together with motions",
weight=337, weight=337,
group='Motions', group="Motions",
subgroup='Amendments') subgroup="Amendments",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_amendments_prefix', name="motions_amendments_prefix",
default_value='-', default_value="-",
label='Prefix for the identifier for amendments', label="Prefix for the identifier for amendments",
weight=340, weight=340,
group='Motions', group="Motions",
subgroup='Amendments') subgroup="Amendments",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_amendments_text_mode', name="motions_amendments_text_mode",
default_value='freestyle', default_value="freestyle",
input_type='choice', input_type="choice",
label='How to create new amendments', label="How to create new amendments",
choices=( choices=(
{'value': 'freestyle', 'display_name': 'Empty text field'}, {"value": "freestyle", "display_name": "Empty text field"},
{'value': 'fulltext', 'display_name': 'Edit the whole motion text'}, {"value": "fulltext", "display_name": "Edit the whole motion text"},
{'value': 'paragraph', 'display_name': 'Paragraph-based, Diff-enabled'}, {"value": "paragraph", "display_name": "Paragraph-based, Diff-enabled"},
), ),
weight=342, weight=342,
group='Motions', group="Motions",
subgroup='Amendments') subgroup="Amendments",
)
# Supporters # Supporters
yield ConfigVariable( yield ConfigVariable(
name='motions_min_supporters', name="motions_min_supporters",
default_value=0, default_value=0,
input_type='integer', input_type="integer",
label='Number of (minimum) required supporters for a motion', label="Number of (minimum) required supporters for a motion",
help_text='Choose 0 to disable the supporting system.', help_text="Choose 0 to disable the supporting system.",
weight=345, weight=345,
group='Motions', group="Motions",
subgroup='Supporters', subgroup="Supporters",
validators=(MinValueValidator(0),)) validators=(MinValueValidator(0),),
)
yield ConfigVariable( yield ConfigVariable(
name='motions_remove_supporters', name="motions_remove_supporters",
default_value=False, default_value=False,
input_type='boolean', input_type="boolean",
label='Remove all supporters of a motion if a submitter edits his motion in early state', label="Remove all supporters of a motion if a submitter edits his motion in early state",
weight=350, weight=350,
group='Motions', group="Motions",
subgroup='Supporters') subgroup="Supporters",
)
# Voting and ballot papers # Voting and ballot papers
yield ConfigVariable( yield ConfigVariable(
name='motions_poll_100_percent_base', name="motions_poll_100_percent_base",
default_value='YES_NO_ABSTAIN', default_value="YES_NO_ABSTAIN",
input_type='choice', input_type="choice",
label='The 100 % base of a voting result consists of', label="The 100 % base of a voting result consists of",
choices=( choices=(
{'value': 'YES_NO_ABSTAIN', 'display_name': 'Yes/No/Abstain'}, {"value": "YES_NO_ABSTAIN", "display_name": "Yes/No/Abstain"},
{'value': 'YES_NO', 'display_name': 'Yes/No'}, {"value": "YES_NO", "display_name": "Yes/No"},
{'value': 'VALID', 'display_name': 'All valid ballots'}, {"value": "VALID", "display_name": "All valid ballots"},
{'value': 'CAST', 'display_name': 'All casted ballots'}, {"value": "CAST", "display_name": "All casted ballots"},
{'value': 'DISABLED', 'display_name': 'Disabled (no percents)'} {"value": "DISABLED", "display_name": "Disabled (no percents)"},
), ),
weight=355, weight=355,
group='Motions', group="Motions",
subgroup='Voting and ballot papers') subgroup="Voting and ballot papers",
)
# TODO: Add server side validation of the choices. # TODO: Add server side validation of the choices.
yield ConfigVariable( yield ConfigVariable(
name='motions_poll_default_majority_method', name="motions_poll_default_majority_method",
default_value=majorityMethods[0]['value'], default_value=majorityMethods[0]["value"],
input_type='choice', input_type="choice",
choices=majorityMethods, choices=majorityMethods,
label='Required majority', label="Required majority",
help_text='Default method to check whether a motion has reached the required majority.', help_text="Default method to check whether a motion has reached the required majority.",
weight=357, weight=357,
group='Motions', group="Motions",
subgroup='Voting and ballot papers') subgroup="Voting and ballot papers",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_pdf_ballot_papers_selection', name="motions_pdf_ballot_papers_selection",
default_value='CUSTOM_NUMBER', default_value="CUSTOM_NUMBER",
input_type='choice', input_type="choice",
label='Number of ballot papers (selection)', label="Number of ballot papers (selection)",
choices=( choices=(
{'value': 'NUMBER_OF_DELEGATES', 'display_name': 'Number of all delegates'}, {"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_ALL_PARTICIPANTS",
"display_name": "Number of all participants",
},
{
"value": "CUSTOM_NUMBER",
"display_name": "Use the following custom number",
},
),
weight=360, weight=360,
group='Motions', group="Motions",
subgroup='Voting and ballot papers') subgroup="Voting and ballot papers",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_pdf_ballot_papers_number', name="motions_pdf_ballot_papers_number",
default_value=8, default_value=8,
input_type='integer', input_type="integer",
label='Custom number of ballot papers', label="Custom number of ballot papers",
weight=365, weight=365,
group='Motions', group="Motions",
subgroup='Voting and ballot papers', subgroup="Voting and ballot papers",
validators=(MinValueValidator(1),)) validators=(MinValueValidator(1),),
)
# PDF and DOCX export # PDF and DOCX export
yield ConfigVariable( yield ConfigVariable(
name='motions_export_title', name="motions_export_title",
default_value='Motions', default_value="Motions",
label='Title for PDF and DOCX documents (all motions)', label="Title for PDF and DOCX documents (all motions)",
weight=370, weight=370,
group='Motions', group="Motions",
subgroup='Export') subgroup="Export",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_export_preamble', name="motions_export_preamble",
default_value='', default_value="",
label='Preamble text for PDF and DOCX documents (all motions)', label="Preamble text for PDF and DOCX documents (all motions)",
weight=375, weight=375,
group='Motions', group="Motions",
subgroup='Export') subgroup="Export",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_export_category_sorting', name="motions_export_category_sorting",
default_value='prefix', default_value="prefix",
input_type='choice', input_type="choice",
label='Sort categories by', label="Sort categories by",
choices=( choices=(
{'value': 'prefix', 'display_name': 'Prefix'}, {"value": "prefix", "display_name": "Prefix"},
{'value': 'name', 'display_name': 'Name'}), {"value": "name", "display_name": "Name"},
),
weight=380, weight=380,
group='Motions', group="Motions",
subgroup='Export') subgroup="Export",
)
yield ConfigVariable( yield ConfigVariable(
name='motions_export_sequential_number', name="motions_export_sequential_number",
default_value=True, default_value=True,
input_type='boolean', input_type="boolean",
label='Include the sequential number in PDF and DOCX', label="Include the sequential number in PDF and DOCX",
weight=385, weight=385,
group='Motions', group="Motions",
subgroup='Export') subgroup="Export",
)

View File

@ -3,4 +3,5 @@ from openslides.utils.exceptions import OpenSlidesError
class WorkflowError(OpenSlidesError): class WorkflowError(OpenSlidesError):
"""Exception raised when errors in a workflow or state accure.""" """Exception raised when errors in a workflow or state accure."""
pass pass

View File

@ -15,200 +15,344 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('mediafiles', '0001_initial'), ("mediafiles", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('core', '0001_initial'), ("core", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Category', name="Category",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=255)), "id",
('prefix', models.CharField(blank=True, max_length=32)), 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={ options={
'ordering': ['prefix'], "verbose_name": "Motion",
'default_permissions': (), "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), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='Motion', name="MotionLog",
fields=[ 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)), "id",
('identifier_number', models.IntegerField(null=True)), 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={ options={"ordering": ["-time"], "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), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='MotionLog', name="MotionOption",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('message_list', jsonfield.fields.JSONField()), "id",
('time', models.DateTimeField(auto_now=True)), models.AutoField(
('motion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='log_messages', to='motions.Motion')), auto_created=True,
('person', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), primary_key=True,
serialize=False,
verbose_name="ID",
),
)
], ],
options={ options={"default_permissions": ()},
'ordering': ['-time'],
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='MotionOption', name="MotionPoll",
fields=[ 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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='MotionPoll', name="MotionVersion",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('votesvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)), "id",
('votesinvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)), models.AutoField(
('votescast', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)), auto_created=True,
('motion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polls', to='motions.Motion')), 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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='MotionVersion', name="MotionVote",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('version_number', models.PositiveIntegerField(default=1)), "id",
('title', models.CharField(max_length=255)), models.AutoField(
('text', models.TextField()), auto_created=True,
('reason', models.TextField(blank=True, null=True)), primary_key=True,
('creation_time', models.DateTimeField(auto_now=True)), serialize=False,
('motion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='motions.Motion')), 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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='MotionVote', name="State",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('weight', models.IntegerField(default=1, null=True)), "id",
('value', models.CharField(max_length=255, null=True)), models.AutoField(
('option', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='motions.MotionOption')), 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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='State', name="Workflow",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=255)), "id",
('action_word', models.CharField(max_length=255)), models.AutoField(
('css_class', models.CharField(default='primary', max_length=255)), auto_created=True,
('required_permission_to_see', models.CharField(blank=True, max_length=255)), primary_key=True,
('allow_support', models.BooleanField(default=False)), serialize=False,
('allow_create_poll', models.BooleanField(default=False)), verbose_name="ID",
('allow_submitter_edit', models.BooleanField(default=False)), ),
('versioning', models.BooleanField(default=False)), ),
('leave_old_version_active', models.BooleanField(default=False)), ("name", models.CharField(max_length=255)),
('dont_set_identifier', models.BooleanField(default=False)), (
('next_states', models.ManyToManyField(to='motions.State')), "first_state",
models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="motions.State",
),
),
], ],
options={ options={"default_permissions": ()},
'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': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.AddField( migrations.AddField(
model_name='state', model_name="state",
name='workflow', 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',
field=models.ForeignKey( 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( migrations.AddField(
model_name='motion', model_name="motionoption",
name='attachments', name="poll",
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',
field=models.ForeignKey( 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( migrations.AddField(
model_name='motion', model_name="motion",
name='state', name="active_version",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='motions.State'), field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="active_version",
to="motions.MotionVersion",
),
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='submitters', name="attachments",
field=models.ManyToManyField(blank=True, related_name='motion_submitters', to=settings.AUTH_USER_MODEL), field=models.ManyToManyField(blank=True, to="mediafiles.Mediafile"),
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='supporters', name="category",
field=models.ManyToManyField(blank=True, related_name='motion_supporters', to=settings.AUTH_USER_MODEL), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="motions.Category",
),
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='tags', name="parent",
field=models.ManyToManyField(blank=True, to='core.Tag'), 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( migrations.AlterUniqueTogether(
name='motionversion', name="motionversion", unique_together=set([("motion", "version_number")])
unique_together=set([('motion', 'version_number')]),
), ),
] ]

View File

@ -16,16 +16,16 @@ def change_label_of_state(apps, schema_editor):
""" """
# We get the model from the versioned app registry; # We get the model from the versioned app registry;
# if we directly import it, it will be the wrong version. # if we directly import it, it will be the wrong version.
State = apps.get_model('motions', 'State') State = apps.get_model("motions", "State")
try: try:
state = State.objects.get(name='commited a bill') state = State.objects.get(name="commited a bill")
except State.DoesNotExist: except State.DoesNotExist:
# State does not exists, there is nothing to change. # State does not exists, there is nothing to change.
pass pass
else: else:
state.name = 'refered to committee' state.name = "refered to committee"
state.action_word = 'Refer to committee' state.action_word = "Refer to committee"
state.save(skip_autoupdate=True) 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; # We get the model from the versioned app registry;
# if we directly import it, it will be the wrong version. # 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 = { name_label_map = {
'accepted': 'Acceptance', "accepted": "Acceptance",
'rejected': 'Rejection', "rejected": "Rejection",
'not decided': 'No decision', "not decided": "No decision",
'permitted': 'Permission', "permitted": "Permission",
'adjourned': 'Adjournment', "adjourned": "Adjournment",
'not concerned': 'No concernment', "not concerned": "No concernment",
'refered to committee': 'Referral to committee', "refered to committee": "Referral to committee",
'rejected (not authorized)': 'Rejection (not authorized)', "rejected (not authorized)": "Rejection (not authorized)",
} }
for state in State.objects.all(): for state in State.objects.all():
if name_label_map.get(state.name): if name_label_map.get(state.name):
@ -57,101 +57,135 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('motions', '0001_initial'), ("motions", "0001_initial"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='MotionBlock', name="MotionBlock",
fields=[ 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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='MotionChangeRecommendation', name="MotionChangeRecommendation",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('rejected', models.BooleanField(default=False)), "id",
('type', models.PositiveIntegerField(default=0)), models.AutoField(
('line_from', models.PositiveIntegerField()), auto_created=True,
('line_to', models.PositiveIntegerField()), primary_key=True,
('text', models.TextField(blank=True)), serialize=False,
('creation_time', models.DateTimeField(auto_now=True)), verbose_name="ID",
('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')), ("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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='motion', name="motion",
options={ options={
'default_permissions': (), "default_permissions": (),
'ordering': ( "ordering": ("identifier",),
'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': ( "verbose_name": "Motion",
('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',
}, },
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='comments', name="comments",
field=jsonfield.fields.JSONField(null=True), field=jsonfield.fields.JSONField(null=True),
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='origin', name="origin",
field=models.CharField(blank=True, max_length=255), field=models.CharField(blank=True, max_length=255),
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='recommendation', name="recommendation",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='motions.State'), field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="motions.State",
),
), ),
migrations.AddField( migrations.AddField(
model_name='state', model_name="state",
name='recommendation_label', name="recommendation_label",
field=models.CharField(max_length=255, null=True), field=models.CharField(max_length=255, null=True),
), ),
migrations.AddField( migrations.AddField(
model_name='state', model_name="state",
name='show_recommendation_extension_field', name="show_recommendation_extension_field",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
migrations.AddField( migrations.AddField(
model_name='state', model_name="state",
name='show_state_extension_field', name="show_state_extension_field",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), ),
migrations.AlterField( migrations.AlterField(
model_name='motion', model_name="motion",
name='state', name="state",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='motions.State'), field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="motions.State",
),
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='motion_block', name="motion_block",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='motions.MotionBlock'), field=models.ForeignKey(
), blank=True,
migrations.RunPython( null=True,
change_label_of_state on_delete=django.db.models.deletion.SET_NULL,
), to="motions.MotionBlock",
migrations.RunPython( ),
add_recommendation_labels
), ),
migrations.RunPython(change_label_of_state),
migrations.RunPython(add_recommendation_labels),
] ]

View File

@ -12,24 +12,24 @@ def change_motions_comments(apps, schema_editor):
""" """
# We get the model from the versioned app registry; # We get the model from the versioned app registry;
# if we directly import it, it will be the wrong version. # if we directly import it, it will be the wrong version.
ConfigStore = apps.get_model('core', 'ConfigStore') ConfigStore = apps.get_model("core", "ConfigStore")
Motion = apps.get_model('motions', 'Motion') Motion = apps.get_model("motions", "Motion")
try: try:
config_comments_fields = ConfigStore.objects.get(key='motions_comments').value config_comments_fields = ConfigStore.objects.get(key="motions_comments").value
except ConfigStore.DoesNotExist: except ConfigStore.DoesNotExist:
config_comments_fields = [] # The old default: An empty list. config_comments_fields = [] # The old default: An empty list.
comments_fields = {} comments_fields = {}
for index, field in enumerate(config_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: try:
db_value = ConfigStore.objects.get(key='motions_comments') db_value = ConfigStore.objects.get(key="motions_comments")
except ConfigStore.DoesNotExist: except ConfigStore.DoesNotExist:
db_value = ConfigStore(key='motions_comments') db_value = ConfigStore(key="motions_comments")
db_value.value = comments_fields db_value.value = comments_fields
# We cannot provide skip_autoupdate=True here, becuase this object is a fake object. It does *not* # 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) # 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 []): for index, comment in enumerate(motion.comments or []):
if index > max_index: if index > max_index:
break break
comments[index+1] = comment comments[index + 1] = comment
motion.comments = comments motion.comments = comments
motion.save(skip_autoupdate=True) motion.save(skip_autoupdate=True)
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("motions", "0002_misc_features")]
('motions', '0002_misc_features'),
]
operations = [ operations = [migrations.RunPython(change_motions_comments)]
migrations.RunPython(
change_motions_comments
),
]

View File

@ -7,14 +7,12 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("motions", "0003_motion_comments")]
('motions', '0003_motion_comments'),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='motionchangerecommendation', model_name="motionchangerecommendation",
name='other_description', name="other_description",
field=models.TextField(blank=True), field=models.TextField(blank=True),
), )
] ]

View File

@ -11,7 +11,7 @@ def delete_old_comment_permission(apps, schema_editor):
Deletes the old 'can_see_and_manage_comments' permission which is Deletes the old 'can_see_and_manage_comments' permission which is
split up into two seperate permissions. 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): if len(perm):
perm = perm.get() perm = perm.get()
@ -26,13 +26,15 @@ def delete_old_comment_permission(apps, schema_editor):
# Create new permission # Create new permission
perm_see = Permission.objects.create( perm_see = Permission.objects.create(
codename='can_see_comments', codename="can_see_comments",
name='Can see comments', name="Can see comments",
content_type=content_type) content_type=content_type,
)
perm_manage = Permission.objects.create( perm_manage = Permission.objects.create(
codename='can_manage_comments', codename="can_manage_comments",
name='Can manage comments', name="Can manage comments",
content_type=content_type) content_type=content_type,
)
for group in groups: for group in groups:
group.permissions.add(perm_see) group.permissions.add(perm_see)
@ -42,28 +44,24 @@ def delete_old_comment_permission(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("motions", "0004_motionchangerecommendation_other_description")]
('motions', '0004_motionchangerecommendation_other_description'),
]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='motion', name="motion",
options={ options={
'default_permissions': (), "default_permissions": (),
'ordering': ('identifier',), "ordering": ("identifier",),
'permissions': ( "permissions": (
('can_see', 'Can see motions'), ("can_see", "Can see motions"),
('can_create', 'Can create motions'), ("can_create", "Can create motions"),
('can_support', 'Can support motions'), ("can_support", "Can support motions"),
('can_see_comments', 'Can see comments'), ("can_see_comments", "Can see comments"),
('can_manage_comments', 'Can manage comments'), ("can_manage_comments", "Can manage comments"),
('can_manage', 'Can manage motions') ("can_manage", "Can manage motions"),
), ),
'verbose_name': 'Motion' "verbose_name": "Motion",
}, },
), ),
migrations.RunPython( migrations.RunPython(delete_old_comment_permission),
delete_old_comment_permission
),
] ]

View File

@ -11,8 +11,8 @@ import openslides.utils.models
def move_submitters_to_own_model(apps, schema_editor): def move_submitters_to_own_model(apps, schema_editor):
Motion = apps.get_model('motions', 'Motion') Motion = apps.get_model("motions", "Motion")
Submitter = apps.get_model('motions', 'Submitter') Submitter = apps.get_model("motions", "Submitter")
for motion in Motion.objects.all(): for motion in Motion.objects.all():
weight = 0 weight = 0
@ -32,41 +32,46 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('motions', '0005_auto_20180202_1318'), ("motions", "0005_auto_20180202_1318"),
] ]
operations = [ operations = [
migrations.RenameField( migrations.RenameField(
model_name='motion', model_name="motion", old_name="submitters", new_name="submittersOld"
old_name='submitters',
new_name='submittersOld',
), ),
migrations.CreateModel( migrations.CreateModel(
name='Submitter', name="Submitter",
fields=[ 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={ options={"default_permissions": ()},
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.AddField( migrations.AddField(
model_name='submitter', model_name="submitter",
name='motion', name="motion",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submitters', to='motions.Motion'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="submitters",
to="motions.Motion",
),
), ),
migrations.AddField( migrations.AddField(
model_name='submitter', model_name="submitter",
name='user', name="user",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 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',
), ),
migrations.RunPython(move_submitters_to_own_model),
migrations.RemoveField(model_name="motion", name="submittersOld"),
] ]

View File

@ -8,14 +8,12 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("motions", "0006_submitter_model")]
('motions', '0006_submitter_model'),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='motionversion', model_name="motionversion",
name='amendment_paragraphs', name="amendment_paragraphs",
field=jsonfield.fields.JSONField(null=True), field=jsonfield.fields.JSONField(null=True),
), )
] ]

View File

@ -8,38 +8,38 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("motions", "0007_motionversion_amendment_data")]
('motions', '0007_motionversion_amendment_data'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='workflow', model_name="workflow",
name='first_state', name="first_state",
field=models.OneToOneField( field=models.OneToOneField(
blank=True, blank=True,
null=True, null=True,
on_delete=django.db.models.deletion.SET_NULL, on_delete=django.db.models.deletion.SET_NULL,
related_name='+', related_name="+",
to='motions.State'), to="motions.State",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='motion', model_name="motion",
name='state', name="state",
field=models.ForeignKey( field=models.ForeignKey(
null=True, null=True,
on_delete=django.db.models.deletion.PROTECT, on_delete=django.db.models.deletion.PROTECT,
related_name='+', related_name="+",
to='motions.State'), to="motions.State",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='state', model_name="state",
name='next_states', name="next_states",
field=models.ManyToManyField(blank=True, to='motions.State'), field=models.ManyToManyField(blank=True, to="motions.State"),
), ),
migrations.AlterField( migrations.AlterField(
model_name='state', model_name="state",
name='action_word', name="action_word",
field=models.CharField(blank=True, max_length=255), field=models.CharField(blank=True, max_length=255),
), ),
] ]

View File

@ -7,14 +7,12 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("motions", "0008_auto_20180702_1128")]
('motions', '0008_auto_20180702_1128'),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='motionversion', model_name="motionversion",
name='modified_final_version', name="modified_final_version",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), )
] ]

View File

@ -8,49 +8,51 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("motions", "0009_motionversion_modified_final_version")]
('motions', '0009_motionversion_modified_final_version'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='motionpoll', model_name="motionpoll",
name='votescast', name="votescast",
field=models.DecimalField( field=models.DecimalField(
blank=True, blank=True,
decimal_places=6, decimal_places=6,
max_digits=15, max_digits=15,
null=True, null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='motionpoll', model_name="motionpoll",
name='votesinvalid', name="votesinvalid",
field=models.DecimalField( field=models.DecimalField(
blank=True, blank=True,
decimal_places=6, decimal_places=6,
max_digits=15, max_digits=15,
null=True, null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='motionpoll', model_name="motionpoll",
name='votesvalid', name="votesvalid",
field=models.DecimalField( field=models.DecimalField(
blank=True, blank=True,
decimal_places=6, decimal_places=6,
max_digits=15, max_digits=15,
null=True, null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='motionvote', model_name="motionvote",
name='weight', name="weight",
field=models.DecimalField( field=models.DecimalField(
decimal_places=6, decimal_places=6,
default=Decimal('1'), default=Decimal("1"),
max_digits=15, max_digits=15,
null=True, null=True,
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]), validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
),
), ),
] ]

View File

@ -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. 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(): for motion in Motion.objects.all():
motion.title = motion.active_version.title 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 Delete all change recommendation of motion versions, that are not active. For active
change recommendations the motion id will be set. change recommendations the motion id will be set.
""" """
MotionChangeRecommendation = apps.get_model('motions', 'MotionChangeRecommendation') MotionChangeRecommendation = apps.get_model("motions", "MotionChangeRecommendation")
to_delete = [] to_delete = []
for cr in MotionChangeRecommendation.objects.all(): for cr in MotionChangeRecommendation.objects.all():
# chack if version id matches the active version of the motion # 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): class Migration(migrations.Migration):
dependencies = [ dependencies = [("motions", "0010_auto_20180822_1042")]
('motions', '0010_auto_20180822_1042'),
]
operations = [ operations = [
# Create new fields. Title and Text have empty defaults, but the values # 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 # should be overwritten by copy_motion_version_content_to_motion. In the next
# migration file these defaults are removed. # migration file these defaults are removed.
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='title', name="title",
field=models.CharField(max_length=255, default=''), field=models.CharField(max_length=255, default=""),
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion", name="text", field=models.TextField(default="")
name='text',
field=models.TextField(default=''),
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='reason', name="reason",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='modified_final_version', name="modified_final_version",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='amendment_paragraphs', name="amendment_paragraphs",
field=jsonfield.fields.JSONField( field=jsonfield.fields.JSONField(
dump_kwargs={ dump_kwargs={
'cls': jsonfield.encoder.JSONEncoder, "cls": jsonfield.encoder.JSONEncoder,
'separators': (',', ':') "separators": (",", ":"),
}, },
load_kwargs={}, load_kwargs={},
null=True), null=True,
),
), ),
# Copy old motion version data # Copy old motion version data
migrations.RunPython(copy_motion_version_content_to_motion), migrations.RunPython(copy_motion_version_content_to_motion),
# Change recommendations # Change recommendations
migrations.AddField( migrations.AddField(
model_name='motionchangerecommendation', model_name="motionchangerecommendation",
name='motion', name="motion",
field=models.ForeignKey( field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
null=True, # This is reverted in the next migration null=True, # This is reverted in the next migration
related_name='change_recommendations', related_name="change_recommendations",
to='motions.Motion'), to="motions.Motion",
),
), ),
migrations.RunPython(migrate_active_change_recommendations), migrations.RunPython(migrate_active_change_recommendations),
migrations.RemoveField( migrations.RemoveField(
model_name='motionchangerecommendation', model_name="motionchangerecommendation", name="motion_version"
name='motion_version',
), ),
# remove motion version references from motion and state. # remove motion version references from motion and state.
migrations.RemoveField( migrations.RemoveField(model_name="motion", name="active_version"),
model_name='motion', migrations.AlterUniqueTogether(name="motionversion", unique_together=set()),
name='active_version', migrations.RemoveField(model_name="motionversion", name="motion"),
), migrations.RemoveField(model_name="state", name="leave_old_version_active"),
migrations.AlterUniqueTogether( migrations.RemoveField(model_name="state", name="versioning"),
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. # Delete motion version.
migrations.DeleteModel( migrations.DeleteModel(name="MotionVersion"),
name='MotionVersion',
),
] ]

View File

@ -8,16 +8,18 @@ from django.db import migrations, models
import openslides import openslides
def create_comment_sections_from_config_and_move_comments_to_own_model(apps, schema_editor): def create_comment_sections_from_config_and_move_comments_to_own_model(
ConfigStore = apps.get_model('core', 'ConfigStore') apps, schema_editor
Motion = apps.get_model('motions', 'Motion') ):
MotionComment = apps.get_model('motions', 'MotionComment') ConfigStore = apps.get_model("core", "ConfigStore")
MotionCommentSection = apps.get_model('motions', 'MotionCommentSection') 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) Group = apps.get_model(settings.AUTH_GROUP_MODEL)
# try to get old motions_comments config variable, where all comment fields are saved # try to get old motions_comments config variable, where all comment fields are saved
try: try:
motions_comments = ConfigStore.objects.get(key='motions_comments') motions_comments = ConfigStore.objects.get(key="motions_comments")
except ConfigStore.DoesNotExist: except ConfigStore.DoesNotExist:
return return
comments_sections = motions_comments.value 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() motions_comments.delete()
# Get can_see_comments and can_manage_comments permissions and the associated groups # 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: if len(can_see_comments) == 1:
# Save groups. list() is necessary to evaluate the database query right now. # Save groups. list() is necessary to evaluate the database query right now.
can_see_groups = list(can_see_comments.get().group_set.all()) can_see_groups = list(can_see_comments.get().group_set.all())
else: else:
can_see_groups = Group.objects.all() 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: if len(can_manage_comments) == 1:
# Save groups. list() is necessary to evaluate the database query right now. # Save groups. list() is necessary to evaluate the database query right now.
can_manage_groups = list(can_manage_comments.get().group_set.all()) 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(): for id, section in comments_sections.items():
if section is None: if section is None:
continue continue
if section.get('forState', False): if section.get("forState", False):
forStateId = id forStateId = id
elif section.get('forRecommendation', False): elif section.get("forRecommendation", False):
forRecommendationId = id forRecommendationId = id
else: else:
comment_section = MotionCommentSection(name=section['name']) comment_section = MotionCommentSection(name=section["name"])
comment_section.save(skip_autoupdate=True) comment_section.save(skip_autoupdate=True)
comment_section.read_groups.add(*[group.id for group in can_see_groups]) 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]) 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(): for section_id, comment_value in motion.comments.items():
# Skip empty sections. # Skip empty sections.
comment_value = comment_value.strip() comment_value = comment_value.strip()
if comment_value == '': if comment_value == "":
continue continue
# Special comments will be moved to separate fields. # Special comments will be moved to separate fields.
if section_id == forStateId: 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 = MotionComment(
comment=comment_value, comment=comment_value,
motion=motion, motion=motion,
section=old_id_mapping[section_id]) section=old_id_mapping[section_id],
)
comments.append(comment) comments.append(comment)
MotionComment.objects.bulk_create(comments) MotionComment.objects.bulk_create(comments)
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("users", "0006_user_email"), ("motions", "0011_motion_version")]
('users', '0006_user_email'),
('motions', '0011_motion_version'),
]
operations = [ operations = [
# Cleanup from last migration. Somehow cannot be done there. # Cleanup from last migration. Somehow cannot be done there.
migrations.AlterField( # remove default='' migrations.AlterField( # remove default=''
model_name='motion', model_name="motion", name="text", field=models.TextField()
name='text',
field=models.TextField(),
), ),
migrations.AlterField( # remove default='' migrations.AlterField( # remove default=''
model_name='motion', model_name="motion", name="title", field=models.CharField(max_length=255)
name='title',
field=models.CharField(max_length=255),
), ),
migrations.AlterField( # remove null=True migrations.AlterField( # remove null=True
model_name='motionchangerecommendation', model_name="motionchangerecommendation",
name='motion', name="motion",
field=models.ForeignKey( field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
related_name='change_recommendations', related_name="change_recommendations",
to='motions.Motion'), to="motions.Motion",
),
), ),
# Add extension fields for former "special comments". No hack anymore.. # Add extension fields for former "special comments". No hack anymore..
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='recommendation_extension', name="recommendation_extension",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='state_extension', name="state_extension",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='motion', name="motion",
options={ options={
'default_permissions': (), "default_permissions": (),
'ordering': ('identifier',), "ordering": ("identifier",),
'permissions': ( "permissions": (
('can_see', 'Can see motions'), ("can_see", "Can see motions"),
('can_create', 'Can create motions'), ("can_create", "Can create motions"),
('can_support', 'Can support motions'), ("can_support", "Can support motions"),
('can_manage', 'Can manage motions')), ("can_manage", "Can manage motions"),
'verbose_name': 'Motion'}, ),
"verbose_name": "Motion",
},
), ),
# Comments and CommentsSection models # Comments and CommentsSection models
migrations.CreateModel( migrations.CreateModel(
name='MotionComment', name="MotionComment",
fields=[ fields=[
('id', models.AutoField( (
"id",
models.AutoField(
auto_created=True, auto_created=True,
primary_key=True, primary_key=True,
serialize=False, serialize=False,
verbose_name='ID')), verbose_name="ID",
('comment', models.TextField()), ),
),
("comment", models.TextField()),
], ],
options={ options={"default_permissions": ()},
'default_permissions': (), bases=(
}, openslides.utils.models.RESTModelMixin, # type: ignore
bases=(openslides.utils.models.RESTModelMixin, models.Model), # type: ignore models.Model,
),
), ),
migrations.CreateModel( migrations.CreateModel(
name='MotionCommentSection', name="MotionCommentSection",
fields=[ fields=[
('id', models.AutoField( (
auto_created=True, "id",
primary_key=True, models.AutoField(
serialize=False, auto_created=True,
verbose_name='ID')), primary_key=True,
('name', models.CharField(max_length=255)), serialize=False,
('read_groups', models.ManyToManyField( verbose_name="ID",
blank=True, ),
related_name='read_comments', ),
to=settings.AUTH_GROUP_MODEL)), ("name", models.CharField(max_length=255)),
('write_groups', models.ManyToManyField( (
blank=True, "read_groups",
related_name='write_comments', models.ManyToManyField(
to=settings.AUTH_GROUP_MODEL)), 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={ options={"default_permissions": ()},
'default_permissions': (), bases=(
}, openslides.utils.models.RESTModelMixin, # type: ignore
bases=(openslides.utils.models.RESTModelMixin, models.Model), # type: ignore models.Model,
),
), ),
migrations.AddField( migrations.AddField(
model_name='motioncomment', model_name="motioncomment",
name='section', name="section",
field=models.ForeignKey( field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, on_delete=django.db.models.deletion.PROTECT,
related_name='comments', related_name="comments",
to='motions.MotionCommentSection'), to="motions.MotionCommentSection",
),
), ),
migrations.AddField( migrations.AddField(
model_name='motioncomment', model_name="motioncomment",
name='motion', name="motion",
field=models.ForeignKey( field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE, to="motions.Motion"
to='motions.Motion'), ),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='motioncomment', name="motioncomment", unique_together={("motion", "section")}
unique_together={('motion', 'section')},
), ),
# Move the comments and sections # Move the comments and sections
migrations.RunPython(create_comment_sections_from_config_and_move_comments_to_own_model), 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',
), ),
# Remove old comment field from motion, use the new model instead
migrations.RemoveField(model_name="motion", name="comments"),
migrations.AlterField( migrations.AlterField(
model_name='motioncomment', model_name="motioncomment",
name='motion', name="motion",
field=models.ForeignKey( field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
related_name='comments', related_name="comments",
to='motions.Motion'), to="motions.Motion",
),
), ),
] ]

View File

@ -8,58 +8,55 @@ import openslides.utils.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("motions", "0012_motion_comments")]
('motions', '0012_motion_comments'),
]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='motionblock', name="motionblock",
options={ options={"default_permissions": (), "verbose_name": "Motion block"},
'default_permissions': (),
'verbose_name': 'Motion block'},
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='sort_parent', name="sort_parent",
field=models.ForeignKey( field=models.ForeignKey(
blank=True, blank=True,
null=True, null=True,
on_delete=django.db.models.deletion.SET_NULL, on_delete=django.db.models.deletion.SET_NULL,
related_name='children', related_name="children",
to='motions.Motion'), to="motions.Motion",
),
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion", name="weight", field=models.IntegerField(default=10000)
name='weight',
field=models.IntegerField(default=10000),
), ),
migrations.CreateModel( migrations.CreateModel(
name='StatuteParagraph', name="StatuteParagraph",
fields=[ fields=[
('id', models.AutoField( (
auto_created=True, "id",
primary_key=True, models.AutoField(
serialize=False, auto_created=True,
verbose_name='ID')), primary_key=True,
('title', models.CharField(max_length=255)), serialize=False,
('text', models.TextField()), verbose_name="ID",
('weight', models.IntegerField(default=10000)), ),
),
("title", models.CharField(max_length=255)),
("text", models.TextField()),
("weight", models.IntegerField(default=10000)),
], ],
options={ options={"ordering": ["weight", "title"], "default_permissions": ()},
'ordering': ['weight', 'title'],
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model), bases=(openslides.utils.models.RESTModelMixin, models.Model),
), ),
migrations.AddField( migrations.AddField(
model_name='motion', model_name="motion",
name='statute_paragraph', name="statute_paragraph",
field=models.ForeignKey( field=models.ForeignKey(
blank=True, blank=True,
null=True, null=True,
on_delete=django.db.models.deletion.SET_NULL, on_delete=django.db.models.deletion.SET_NULL,
related_name='motions', related_name="motions",
to='motions.StatuteParagraph'), to="motions.StatuteParagraph",
),
), ),
] ]

View File

@ -5,14 +5,12 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("motions", "0013_motion_sorting_and_statute")]
('motions', '0013_motion_sorting_and_statute'),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='motionchangerecommendation', model_name="motionchangerecommendation",
name='internal', name="internal",
field=models.BooleanField(default=False), field=models.BooleanField(default=False),
), )
] ]

View File

@ -5,24 +5,22 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("motions", "0014_motionchangerecommendation_internal")]
('motions', '0014_motionchangerecommendation_internal'),
]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='motion', name="motion",
options={ options={
'default_permissions': (), "default_permissions": (),
'ordering': ('identifier',), "ordering": ("identifier",),
'permissions': ( "permissions": (
('can_see', 'Can see motions'), ("can_see", "Can see motions"),
('can_create', 'Can create motions'), ("can_create", "Can create motions"),
('can_support', 'Can support motions'), ("can_support", "Can support motions"),
('can_manage_metadata', 'Can manage motion metadata'), ("can_manage_metadata", "Can manage motion metadata"),
('can_manage', 'Can manage motions') ("can_manage", "Can manage motions"),
), ),
'verbose_name': 'Motion' "verbose_name": "Motion",
}, },
), )
] ]

View File

@ -5,14 +5,12 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("motions", "0015_metadata_permission")]
('motions', '0015_metadata_permission'),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='state', model_name="state",
name='merge_amendment_into_final', name="merge_amendment_into_final",
field=models.SmallIntegerField(default=0), field=models.SmallIntegerField(default=0),
), )
] ]

View File

@ -5,13 +5,6 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("motions", "0016_merge_amendment_into_final")]
('motions', '0016_merge_amendment_into_final'),
]
operations = [ operations = [migrations.RemoveField(model_name="state", name="action_word")]
migrations.RemoveField(
model_name='state',
name='action_word',
),
]

View File

@ -40,6 +40,7 @@ class StatuteParagraph(RESTModelMixin, models.Model):
""" """
Model for parts of the statute Model for parts of the statute
""" """
access_permissions = StatuteParagraphAccessPermissions() access_permissions = StatuteParagraphAccessPermissions()
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
@ -55,7 +56,7 @@ class StatuteParagraph(RESTModelMixin, models.Model):
class Meta: class Meta:
default_permissions = () default_permissions = ()
ordering = ['weight', 'title'] ordering = ["weight", "title"]
def __str__(self): def __str__(self):
return self.title return self.title
@ -65,25 +66,29 @@ class MotionManager(models.Manager):
""" """
Customized model manager to support our get_full_queryset method. Customized model manager to support our get_full_queryset method.
""" """
def get_full_queryset(self): def get_full_queryset(self):
""" """
Returns the normal queryset with all motions. In the background we Returns the normal queryset with all motions. In the background we
join and prefetch all related models. join and prefetch all related models.
""" """
return (self.get_queryset() return (
.select_related('state') self.get_queryset()
.prefetch_related( .select_related("state")
'state__workflow', .prefetch_related(
'comments', "state__workflow",
'comments__section', "comments",
'comments__section__read_groups', "comments__section",
'agenda_items', "comments__section__read_groups",
'log_messages', "agenda_items",
'polls', "log_messages",
'attachments', "polls",
'tags', "attachments",
'submitters', "tags",
'supporters')) "submitters",
"supporters",
)
)
class Motion(RESTModelMixin, models.Model): 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. This class is the main entry point to all other classes related to a motion.
""" """
access_permissions = MotionAccessPermissions() access_permissions = MotionAccessPermissions()
can_see_permission = 'motions.can_see' can_see_permission = "motions.can_see"
objects = MotionManager() objects = MotionManager()
@ -119,10 +125,11 @@ class Motion(RESTModelMixin, models.Model):
"""The reason for a motion.""" """The reason for a motion."""
state = models.ForeignKey( state = models.ForeignKey(
'State', "State",
related_name='+', related_name="+",
on_delete=models.PROTECT, # Do not let the user delete states, that are used for motions 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. The related state object.
@ -135,10 +142,8 @@ class Motion(RESTModelMixin, models.Model):
""" """
recommendation = models.ForeignKey( recommendation = models.ForeignKey(
'State', "State", related_name="+", on_delete=models.SET_NULL, null=True
related_name='+', )
on_delete=models.SET_NULL,
null=True)
""" """
The recommendation of a person or committee for this motion. 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. A text field fo additional information about the recommendation.
""" """
identifier = models.CharField(max_length=255, null=True, blank=True, identifier = models.CharField(max_length=255, null=True, blank=True, unique=True)
unique=True)
""" """
A string as human readable identifier for the motion. A string as human readable identifier for the motion.
""" """
@ -167,29 +171,26 @@ class Motion(RESTModelMixin, models.Model):
""" """
sort_parent = models.ForeignKey( sort_parent = models.ForeignKey(
'self', "self",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
related_name='children') related_name="children",
)
""" """
A parent field for multi-depth sorting of motions. A parent field for multi-depth sorting of motions.
""" """
category = models.ForeignKey( category = models.ForeignKey(
'Category', "Category", on_delete=models.SET_NULL, null=True, blank=True
on_delete=models.SET_NULL, )
null=True,
blank=True)
""" """
ForeignKey to one category of motions. ForeignKey to one category of motions.
""" """
motion_block = models.ForeignKey( motion_block = models.ForeignKey(
'MotionBlock', "MotionBlock", on_delete=models.SET_NULL, null=True, blank=True
on_delete=models.SET_NULL, )
null=True,
blank=True)
""" """
ForeignKey to one block of motions. ForeignKey to one block of motions.
""" """
@ -206,11 +207,12 @@ class Motion(RESTModelMixin, models.Model):
""" """
parent = models.ForeignKey( parent = models.ForeignKey(
'self', "self",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
related_name='amendments') related_name="amendments",
)
""" """
Field for amendments to reference to the motion that should be altered. 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, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
related_name='motions') related_name="motions",
)
""" """
Field to reference to a statute paragraph if this motion is a Field to reference to a statute paragraph if this motion is a
statute-amendment. statute-amendment.
@ -235,26 +238,28 @@ class Motion(RESTModelMixin, models.Model):
Tags to categorise motions. 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. Users who support this motion.
""" """
# In theory there could be one then more agenda_item. But we support only # In theory there could be one then more agenda_item. But we support only
# one. See the property agenda_item. # one. See the property agenda_item.
agenda_items = GenericRelation(Item, related_name='motions') agenda_items = GenericRelation(Item, related_name="motions")
class Meta: class Meta:
default_permissions = () default_permissions = ()
permissions = ( permissions = (
('can_see', 'Can see motions'), ("can_see", "Can see motions"),
('can_create', 'Can create motions'), ("can_create", "Can create motions"),
('can_support', 'Can support motions'), ("can_support", "Can support motions"),
('can_manage_metadata', 'Can manage motion metadata'), ("can_manage_metadata", "Can manage motion metadata"),
('can_manage', 'Can manage motions'), ("can_manage", "Can manage motions"),
) )
ordering = ('identifier', ) ordering = ("identifier",)
verbose_name = ugettext_noop('Motion') verbose_name = ugettext_noop("Motion")
def __str__(self): def __str__(self):
""" """
@ -284,14 +289,15 @@ class Motion(RESTModelMixin, models.Model):
try: try:
# Always skip autoupdate. Maybe we run it later in this method. # Always skip autoupdate. Maybe we run it later in this method.
with transaction.atomic(): 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: except IntegrityError:
# Identifier is already used. # Identifier is already used.
if hasattr(self, '_identifier_prefix'): if hasattr(self, "_identifier_prefix"):
# Calculate a new one and try again. # Calculate a new one and try again.
self.identifier_number, self.identifier = self.increment_identifier_number( self.identifier_number, self.identifier = self.increment_identifier_number(
self.identifier_number, self.identifier_number, self._identifier_prefix
self._identifier_prefix,
) )
else: else:
# Do not calculate a new one but reraise the IntegrityError. # Do not calculate a new one but reraise the IntegrityError.
@ -310,10 +316,11 @@ class Motion(RESTModelMixin, models.Model):
motion projector element is disabled. motion projector element is disabled.
""" """
Projector.remove_any( Projector.remove_any(
skip_autoupdate=skip_autoupdate, skip_autoupdate=skip_autoupdate, name="motions/motion", id=self.pk
name='motions/motion', )
id=self.pk) return super().delete( # type: ignore
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore skip_autoupdate=skip_autoupdate, *args, **kwargs
)
def set_identifier(self): def set_identifier(self):
""" """
@ -321,27 +328,36 @@ class Motion(RESTModelMixin, models.Model):
it is not set yet. it is not set yet.
""" """
# The identifier is already set or should be set manually. # 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. # Do not set an identifier.
return return
# If MOTION_IDENTIFIER_WITHOUT_BLANKS is set, don't use blanks when building identifier. # 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. # Build prefix.
if self.is_amendment(): if self.is_amendment():
parent_identifier = self.parent.identifier or '' parent_identifier = self.parent.identifier or ""
if without_blank: if without_blank:
prefix = '%s%s' % (parent_identifier, config['motions_amendments_prefix']) prefix = "%s%s" % (
parent_identifier,
config["motions_amendments_prefix"],
)
else: 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: elif self.category is None or not self.category.prefix:
prefix = '' prefix = ""
else: else:
if without_blank: if without_blank:
prefix = '%s' % self.category.prefix prefix = "%s" % self.category.prefix
else: else:
prefix = '%s ' % self.category.prefix prefix = "%s " % self.category.prefix
self._identifier_prefix = prefix self._identifier_prefix = prefix
# Use the already assigned identifier_number, if the motion has one. # Use the already assigned identifier_number, if the motion has one.
@ -354,20 +370,22 @@ class Motion(RESTModelMixin, models.Model):
if self.is_amendment(): if self.is_amendment():
motions = self.parent.amendments.all() motions = self.parent.amendments.all()
# The motions should be counted per category. # 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) motions = Motion.objects.filter(category=self.category)
# The motions should be counted over all. # The motions should be counted over all.
else: else:
motions = Motion.objects.all() 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 initial_increment = True
# Calculate new identifier. # Calculate new identifier.
number, identifier = self.increment_identifier_number( number, identifier = self.increment_identifier_number(
number, number, prefix, initial_increment=initial_increment
prefix, )
initial_increment=initial_increment)
# Set identifier and identifier_number. # Set identifier and identifier_number.
self.identifier = identifier self.identifier = identifier
@ -380,10 +398,10 @@ class Motion(RESTModelMixin, models.Model):
""" """
if initial_increment: if initial_increment:
number += 1 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(): while Motion.objects.filter(identifier=identifier).exists():
number += 1 number += 1
identifier = '%s%s' % (prefix, self.extend_identifier_number(number)) identifier = "%s%s" % (prefix, self.extend_identifier_number(number))
return number, identifier return number, identifier
def extend_identifier_number(self, number): def extend_identifier_number(self, number):
@ -393,10 +411,18 @@ class Motion(RESTModelMixin, models.Model):
MOTION_IDENTIFIER_MIN_DIGITS. MOTION_IDENTIFIER_MIN_DIGITS.
""" """
result = str(number) 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): if not isinstance(settings.MOTION_IDENTIFIER_MIN_DIGITS, int):
raise ImproperlyConfigured('Settings value MOTION_IDENTIFIER_MIN_DIGITS must be an integer.') raise ImproperlyConfigured(
result = '0' * (settings.MOTION_IDENTIFIER_MIN_DIGITS - len(str(number))) + result "Settings value MOTION_IDENTIFIER_MIN_DIGITS must be an integer."
)
result = (
"0" * (settings.MOTION_IDENTIFIER_MIN_DIGITS - len(str(number)))
+ result
)
return result return result
def is_submitter(self, user): def is_submitter(self, user):
@ -423,7 +449,9 @@ class Motion(RESTModelMixin, models.Model):
poll.set_options(skip_autoupdate=skip_autoupdate) poll.set_options(skip_autoupdate=skip_autoupdate)
return poll return poll
else: 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 @property
def workflow_id(self): def workflow_id(self):
@ -464,8 +492,10 @@ class Motion(RESTModelMixin, models.Model):
elif self.state: elif self.state:
new_state = self.state.workflow.first_state new_state = self.state.workflow.first_state
else: else:
new_state = (Workflow.objects.get(pk=config['motions_workflow']).first_state or new_state = (
Workflow.objects.get(pk=config['motions_workflow']).states.all()[0]) 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) self.set_state(new_state)
def set_recommendation(self, recommendation): 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. Note: It has to be the same return value like in JavaScript.
""" """
if self.identifier: if self.identifier:
title = '%s %s' % (_(self._meta.verbose_name), self.identifier) title = "%s %s" % (_(self._meta.verbose_name), self.identifier)
else: else:
title = self.title title = self.title
return title return title
@ -512,9 +542,9 @@ class Motion(RESTModelMixin, models.Model):
Note: It has to be the same return value like in JavaScript. Note: It has to be the same return value like in JavaScript.
""" """
if self.identifier: if self.identifier:
title = '%s %s' % (_(self._meta.verbose_name), self.identifier) title = "%s %s" % (_(self._meta.verbose_name), self.identifier)
else: else:
title = '%s (%s)' % (self.title, _(self._meta.verbose_name)) title = "%s (%s)" % (self.title, _(self._meta.verbose_name))
return title return title
@property @property
@ -552,7 +582,7 @@ class Motion(RESTModelMixin, models.Model):
A motion is a amendment if amendments are activated in the config and A motion is a amendment if amendments are activated in the config and
the motion has a parent. 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): 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 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): 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 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. each motions has the ability to have comments from the same section.
""" """
access_permissions = MotionCommentSectionAccessPermissions() access_permissions = MotionCommentSectionAccessPermissions()
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
@ -590,17 +626,15 @@ class MotionCommentSection(RESTModelMixin, models.Model):
""" """
read_groups = models.ManyToManyField( read_groups = models.ManyToManyField(
settings.AUTH_GROUP_MODEL, settings.AUTH_GROUP_MODEL, blank=True, related_name="read_comments"
blank=True, )
related_name='read_comments')
""" """
These groups have read-access to the section. These groups have read-access to the section.
""" """
write_groups = models.ManyToManyField( write_groups = models.ManyToManyField(
settings.AUTH_GROUP_MODEL, settings.AUTH_GROUP_MODEL, blank=True, related_name="write_comments"
blank=True, )
related_name='write_comments')
""" """
These groups have write-access to the section. These groups have write-access to the section.
""" """
@ -621,24 +655,22 @@ class MotionComment(RESTModelMixin, models.Model):
""" """
motion = models.ForeignKey( motion = models.ForeignKey(
Motion, Motion, on_delete=models.CASCADE, related_name="comments"
on_delete=models.CASCADE, )
related_name='comments')
""" """
The motion where this comment belongs to. The motion where this comment belongs to.
""" """
section = models.ForeignKey( section = models.ForeignKey(
MotionCommentSection, MotionCommentSection, on_delete=models.PROTECT, related_name="comments"
on_delete=models.PROTECT, )
related_name='comments')
""" """
The section of the comment. The section of the comment.
""" """
class Meta: class Meta:
default_permissions = () default_permissions = ()
unique_together = ('motion', 'section') unique_together = ("motion", "section")
def get_root_rest_element(self): def get_root_rest_element(self):
""" """
@ -651,6 +683,7 @@ class SubmitterManager(models.Manager):
""" """
Manager for Submitter model. Provides a customized add method. Manager for Submitter model. Provides a customized add method.
""" """
def add(self, user, motion, skip_autoupdate=False): def add(self, user, motion, skip_autoupdate=False):
""" """
Customized manager method to prevent anonymous users to be a 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. for the initial sorting of the submitters.
""" """
if self.filter(user=user, motion=motion).exists(): if self.filter(user=user, motion=motion).exists():
raise OpenSlidesError( raise OpenSlidesError(_("{user} is already a submitter.").format(user=user))
_('{user} is already a submitter.').format(user=user))
if isinstance(user, AnonymousUser): if isinstance(user, AnonymousUser):
raise OpenSlidesError( raise OpenSlidesError(_("An anonymous user can not be a submitter."))
_('An anonymous user can not be a submitter.')) weight = (
weight = (self.filter(motion=motion).aggregate( self.filter(motion=motion).aggregate(models.Max("weight"))["weight__max"]
models.Max('weight'))['weight__max'] or 0) or 0
)
submitter = self.model(user=user, motion=motion, weight=weight + 1) submitter = self.model(user=user, motion=motion, weight=weight + 1)
submitter.save(force_insert=True, skip_autoupdate=skip_autoupdate) submitter.save(force_insert=True, skip_autoupdate=skip_autoupdate)
return submitter return submitter
@ -680,17 +713,14 @@ class Submitter(RESTModelMixin, models.Model):
Use custom Manager. Use custom Manager.
""" """
user = models.ForeignKey( user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
""" """
ForeignKey to the user who is the submitter. ForeignKey to the user who is the submitter.
""" """
motion = models.ForeignKey( motion = models.ForeignKey(
Motion, Motion, on_delete=models.CASCADE, related_name="submitters"
on_delete=models.CASCADE, )
related_name='submitters')
""" """
ForeignKey to the motion. ForeignKey to the motion.
""" """
@ -714,6 +744,7 @@ class MotionChangeRecommendationManager(models.Manager):
""" """
Customized model manager to support our get_full_queryset method. Customized model manager to support our get_full_queryset method.
""" """
def get_full_queryset(self): def get_full_queryset(self):
""" """
Returns the normal queryset with all change recommendations. In the background we Returns the normal queryset with all change recommendations. In the background we
@ -732,9 +763,8 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
objects = MotionChangeRecommendationManager() objects = MotionChangeRecommendationManager()
motion = models.ForeignKey( motion = models.ForeignKey(
Motion, Motion, on_delete=models.CASCADE, related_name="change_recommendations"
on_delete=models.CASCADE, )
related_name='change_recommendations')
"""The motion to which the change recommendation belongs.""" """The motion to which the change recommendation belongs."""
rejected = models.BooleanField(default=False) 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""" """The replacement for the section of the original text specified by motion, line_from and line_to"""
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True
on_delete=models.SET_NULL, )
null=True)
"""A user object, who created this change recommendation. Optional.""" """A user object, who created this change recommendation. Optional."""
creation_time = models.DateTimeField(auto_now=True) creation_time = models.DateTimeField(auto_now=True)
@ -769,20 +798,27 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
def collides_with_other_recommendation(self, recommendations): def collides_with_other_recommendation(self, recommendations):
for recommendation in recommendations: for recommendation in recommendations:
if (not (self.line_from < recommendation.line_from and self.line_to <= recommendation.line_from) and if not (
not (self.line_from >= recommendation.line_to and self.line_to > recommendation.line_to)): 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 True
return False return False
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
recommendations = (MotionChangeRecommendation.objects recommendations = MotionChangeRecommendation.objects.filter(
.filter(motion=self.motion) motion=self.motion
.exclude(pk=self.pk)) ).exclude(pk=self.pk)
if self.collides_with_other_recommendation(recommendations): if self.collides_with_other_recommendation(recommendations):
raise ValidationError('The recommendation collides with an existing one (line %s - %s).' % raise ValidationError(
(self.line_from, self.line_to)) "The recommendation collides with an existing one (line %s - %s)."
% (self.line_from, self.line_to)
)
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@ -791,13 +827,18 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
def __str__(self): def __str__(self):
"""Return a string, representing this object.""" """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): class Category(RESTModelMixin, models.Model):
""" """
Model for categories of motions. Model for categories of motions.
""" """
access_permissions = CategoryAccessPermissions() access_permissions = CategoryAccessPermissions()
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
@ -811,7 +852,7 @@ class Category(RESTModelMixin, models.Model):
class Meta: class Meta:
default_permissions = () default_permissions = ()
ordering = ['prefix'] ordering = ["prefix"]
def __str__(self): def __str__(self):
return self.name return self.name
@ -821,18 +862,20 @@ class MotionBlockManager(models.Manager):
""" """
Customized model manager to support our get_full_queryset method. Customized model manager to support our get_full_queryset method.
""" """
def get_full_queryset(self): def get_full_queryset(self):
""" """
Returns the normal queryset with all motion blocks. In the Returns the normal queryset with all motion blocks. In the
background the related agenda item is prefetched from the database. 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): class MotionBlock(RESTModelMixin, models.Model):
""" """
Model for blocks of motions. Model for blocks of motions.
""" """
access_permissions = MotionBlockAccessPermissions() access_permissions = MotionBlockAccessPermissions()
objects = MotionBlockManager() 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 # In theory there could be one then more agenda_item. But we support only
# one. See the property agenda_item. # one. See the property agenda_item.
agenda_items = GenericRelation(Item, related_name='topics') agenda_items = GenericRelation(Item, related_name="topics")
class Meta: class Meta:
verbose_name = ugettext_noop('Motion block') verbose_name = ugettext_noop("Motion block")
default_permissions = () default_permissions = ()
def __str__(self): def __str__(self):
@ -856,10 +899,11 @@ class MotionBlock(RESTModelMixin, models.Model):
motion block projector element is disabled. motion block projector element is disabled.
""" """
Projector.remove_any( Projector.remove_any(
skip_autoupdate=skip_autoupdate, skip_autoupdate=skip_autoupdate, name="motions/motion-block", id=self.pk
name='motions/motion-block', )
id=self.pk) return super().delete( # type: ignore
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore skip_autoupdate=skip_autoupdate, *args, **kwargs
)
""" """
Container for runtime information for agenda app (on create or update of this instance). 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 return self.title
def get_agenda_title_with_type(self): 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): class MotionLog(RESTModelMixin, models.Model):
"""Save a logmessage for a motion.""" """Save a logmessage for a motion."""
motion = models.ForeignKey( motion = models.ForeignKey(
Motion, Motion, on_delete=models.CASCADE, related_name="log_messages"
on_delete=models.CASCADE, )
related_name='log_messages')
"""The motion to witch the object belongs.""" """The motion to witch the object belongs."""
message_list = JSONField() message_list = JSONField()
@ -904,9 +947,8 @@ class MotionLog(RESTModelMixin, models.Model):
""" """
person = models.ForeignKey( person = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True
on_delete=models.SET_NULL, )
null=True)
"""A user object, who created the log message. Optional.""" """A user object, who created the log message. Optional."""
time = models.DateTimeField(auto_now=True) time = models.DateTimeField(auto_now=True)
@ -914,18 +956,20 @@ class MotionLog(RESTModelMixin, models.Model):
class Meta: class Meta:
default_permissions = () default_permissions = ()
ordering = ['-time'] ordering = ["-time"]
def __str__(self): def __str__(self):
""" """
Return a string, representing the log message. Return a string, representing the log message.
""" """
localtime = timezone.localtime(self.time) localtime = timezone.localtime(self.time)
time = formats.date_format(localtime, 'DATETIME_FORMAT') time = formats.date_format(localtime, "DATETIME_FORMAT")
time_and_messages = '%s ' % time + ''.join(map(_, self.message_list)) time_and_messages = "%s " % time + "".join(map(_, self.message_list))
if self.person is not None: if self.person is not None:
return _('%(time_and_messages)s by %(person)s') % {'time_and_messages': time_and_messages, return _("%(time_and_messages)s by %(person)s") % {
'person': self.person} "time_and_messages": time_and_messages,
"person": self.person,
}
return time_and_messages return time_and_messages
def get_root_rest_element(self): def get_root_rest_element(self):
@ -941,9 +985,7 @@ class MotionVote(RESTModelMixin, BaseVote):
There should allways be three MotionVote objects for each poll, There should allways be three MotionVote objects for each poll,
one for 'yes', 'no', and 'abstain'.""" one for 'yes', 'no', and 'abstain'."""
option = models.ForeignKey( option = models.ForeignKey("MotionOption", on_delete=models.CASCADE)
'MotionOption',
on_delete=models.CASCADE)
"""The option object, to witch the vote belongs.""" """The option object, to witch the vote belongs."""
class Meta: class Meta:
@ -961,9 +1003,7 @@ class MotionOption(RESTModelMixin, BaseOption):
There should be one MotionOption object for each poll.""" There should be one MotionOption object for each poll."""
poll = models.ForeignKey( poll = models.ForeignKey("MotionPoll", on_delete=models.CASCADE)
'MotionPoll',
on_delete=models.CASCADE)
"""The poll object, to witch the object belongs.""" """The poll object, to witch the object belongs."""
vote_class = MotionVote vote_class = MotionVote
@ -984,16 +1024,13 @@ class MotionOption(RESTModelMixin, BaseOption):
class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: ignore class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: ignore
"""The Class to saves the vote result for a motion poll.""" """The Class to saves the vote result for a motion poll."""
motion = models.ForeignKey( motion = models.ForeignKey(Motion, on_delete=models.CASCADE, related_name="polls")
Motion,
on_delete=models.CASCADE,
related_name='polls')
"""The motion to witch the object belongs.""" """The motion to witch the object belongs."""
option_class = MotionOption option_class = MotionOption
"""The option class, witch links between this object the the votes.""" """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'.""" """The possible anwers for the poll. 'Yes, 'No' and 'Abstain'."""
class Meta: class Meta:
@ -1003,7 +1040,7 @@ class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: i
""" """
Representation method only for debugging purposes. 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): def set_options(self, skip_autoupdate=False):
"""Create the option class for this poll.""" """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) self.get_option_class()(poll=self).save(skip_autoupdate=skip_autoupdate)
def get_percent_base_choice(self): 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): def get_slide_context(self, **context):
return super(MotionPoll, self).get_slide_context(poll=self) 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.""" """A string for a recommendation to set the motion to this state."""
workflow = models.ForeignKey( workflow = models.ForeignKey(
'Workflow', "Workflow", on_delete=models.CASCADE, related_name="states"
on_delete=models.CASCADE, )
related_name='states')
"""A many-to-one relation to a workflow.""" """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.""" """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, 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). 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. recommendation_label is not an empty string.
""" """
self.check_next_states() self.check_next_states()
if self.recommendation_label == '': if self.recommendation_label == "":
raise WorkflowError('The field recommendation_label of {} must not ' raise WorkflowError(
'be an empty string.'.format(self)) "The field recommendation_label of {} must not "
"be an empty string.".format(self)
)
super(State, self).save(**kwargs) super(State, self).save(**kwargs)
def check_next_states(self): def check_next_states(self):
@ -1143,7 +1181,10 @@ class State(RESTModelMixin, models.Model):
return return
for state in self.next_states.all(): for state in self.next_states.all():
if not state.workflow == self.workflow: 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): def get_root_rest_element(self):
""" """
@ -1156,21 +1197,25 @@ class WorkflowManager(models.Manager):
""" """
Customized model manager to support our get_full_queryset method. Customized model manager to support our get_full_queryset method.
""" """
def get_full_queryset(self): def get_full_queryset(self):
""" """
Returns the normal queryset with all workflows. In the background Returns the normal queryset with all workflows. In the background
the first state is joined and all states and next states are the first state is joined and all states and next states are
prefetched from the database. prefetched from the database.
""" """
return (self.get_queryset() return (
.select_related('first_state') self.get_queryset()
.prefetch_related('states', 'states__next_states')) .select_related("first_state")
.prefetch_related("states", "states__next_states")
)
class Workflow(RESTModelMixin, models.Model): class Workflow(RESTModelMixin, models.Model):
""" """
Defines a workflow for a motion. Defines a workflow for a motion.
""" """
access_permissions = WorkflowAccessPermissions() access_permissions = WorkflowAccessPermissions()
objects = WorkflowManager() objects = WorkflowManager()
@ -1179,11 +1224,8 @@ class Workflow(RESTModelMixin, models.Model):
"""A string representing the workflow.""" """A string representing the workflow."""
first_state = models.OneToOneField( first_state = models.OneToOneField(
State, State, on_delete=models.SET_NULL, related_name="+", null=True, blank=True
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.""" """A one-to-one relation to a state, the starting point for the workflow."""
class Meta: class Meta:
@ -1205,5 +1247,6 @@ class Workflow(RESTModelMixin, models.Model):
"""Checks whether the first_state itself belongs to the workflow.""" """Checks whether the first_state itself belongs to the workflow."""
if self.first_state and not self.first_state.workflow == self: if self.first_state and not self.first_state.workflow == self:
raise WorkflowError( raise WorkflowError(
'%s can not be first state of %s because it ' "%s can not be first state of %s because it "
'does not belong to it.' % (self.first_state, self)) "does not belong to it." % (self.first_state, self)
)

View File

@ -9,21 +9,22 @@ class MotionSlide(ProjectorElement):
""" """
Slide definitions for Motion model. Slide definitions for Motion model.
""" """
name = 'motions/motion'
name = "motions/motion"
def check_data(self): def check_data(self):
if not Motion.objects.filter(pk=self.config_entry.get('id')).exists(): if not Motion.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException('Motion does not exist.') raise ProjectorException("Motion does not exist.")
def update_data(self): def update_data(self):
data = None data = None
try: try:
motion = Motion.objects.get(pk=self.config_entry.get('id')) motion = Motion.objects.get(pk=self.config_entry.get("id"))
except Motion.DoesNotExist: except Motion.DoesNotExist:
# Motion does not exist, so just do nothing. # Motion does not exist, so just do nothing.
pass pass
else: else:
data = {'agenda_item_id': motion.agenda_item_id} data = {"agenda_item_id": motion.agenda_item_id}
return data return data
@ -31,21 +32,22 @@ class MotionBlockSlide(ProjectorElement):
""" """
Slide definitions for a block of motions (MotionBlock model). Slide definitions for a block of motions (MotionBlock model).
""" """
name = 'motions/motion-block'
name = "motions/motion-block"
def check_data(self): def check_data(self):
if not MotionBlock.objects.filter(pk=self.config_entry.get('id')).exists(): if not MotionBlock.objects.filter(pk=self.config_entry.get("id")).exists():
raise ProjectorException('MotionBlock does not exist.') raise ProjectorException("MotionBlock does not exist.")
def update_data(self): def update_data(self):
data = None data = None
try: 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: except MotionBlock.DoesNotExist:
# MotionBlock does not exist, so just do nothing. # MotionBlock does not exist, so just do nothing.
pass pass
else: else:
data = {'agenda_item_id': motion_block.agenda_item_id} data = {"agenda_item_id": motion_block.agenda_item_id}
return data return data

View File

@ -39,48 +39,55 @@ def validate_workflow_field(value):
Validator to ensure that the workflow with the given id exists. Validator to ensure that the workflow with the given id exists.
""" """
if not Workflow.objects.filter(pk=value).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): class StatuteParagraphSerializer(ModelSerializer):
""" """
Serializer for motion.models.StatuteParagraph objects. Serializer for motion.models.StatuteParagraph objects.
""" """
class Meta: class Meta:
model = StatuteParagraph model = StatuteParagraph
fields = ('id', 'title', 'text', 'weight') fields = ("id", "title", "text", "weight")
class CategorySerializer(ModelSerializer): class CategorySerializer(ModelSerializer):
""" """
Serializer for motion.models.Category objects. Serializer for motion.models.Category objects.
""" """
class Meta: class Meta:
model = Category model = Category
fields = ('id', 'name', 'prefix',) fields = ("id", "name", "prefix")
class MotionBlockSerializer(ModelSerializer): class MotionBlockSerializer(ModelSerializer):
""" """
Serializer for motion.models.Category objects. 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) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
class Meta: class Meta:
model = MotionBlock 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): def create(self, validated_data):
""" """
Customized create method. Set information about related agenda item Customized create method. Set information about related agenda item
into agenda_item_update_information container. into agenda_item_update_information container.
""" """
agenda_type = validated_data.pop('agenda_type', None) agenda_type = validated_data.pop("agenda_type", None)
agenda_parent_id = validated_data.pop('agenda_parent_id', None) agenda_parent_id = validated_data.pop("agenda_parent_id", None)
motion_block = MotionBlock(**validated_data) motion_block = MotionBlock(**validated_data)
motion_block.agenda_item_update_information['type'] = agenda_type 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["parent_id"] = agenda_parent_id
motion_block.save() motion_block.save()
return motion_block return motion_block
@ -89,35 +96,38 @@ class StateSerializer(ModelSerializer):
""" """
Serializer for motion.models.State objects. Serializer for motion.models.State objects.
""" """
class Meta: class Meta:
model = State model = State
fields = ( fields = (
'id', "id",
'name', "name",
'recommendation_label', "recommendation_label",
'css_class', "css_class",
'required_permission_to_see', "required_permission_to_see",
'allow_support', "allow_support",
'allow_create_poll', "allow_create_poll",
'allow_submitter_edit', "allow_submitter_edit",
'dont_set_identifier', "dont_set_identifier",
'show_state_extension_field', "show_state_extension_field",
'merge_amendment_into_final', "merge_amendment_into_final",
'show_recommendation_extension_field', "show_recommendation_extension_field",
'next_states', "next_states",
'workflow') "workflow",
)
class WorkflowSerializer(ModelSerializer): class WorkflowSerializer(ModelSerializer):
""" """
Serializer for motion.models.Workflow objects. Serializer for motion.models.Workflow objects.
""" """
states = StateSerializer(many=True, read_only=True) states = StateSerializer(many=True, read_only=True)
class Meta: class Meta:
model = Workflow model = Workflow
fields = ('id', 'name', 'states', 'first_state',) fields = ("id", "name", "states", "first_state")
read_only_fields = ('first_state',) read_only_fields = ("first_state",)
@transaction.atomic @transaction.atomic
def create(self, validated_data): def create(self, validated_data):
@ -127,11 +137,11 @@ class WorkflowSerializer(ModelSerializer):
""" """
workflow = super().create(validated_data) workflow = super().create(validated_data)
first_state = State.objects.create( first_state = State.objects.create(
name='new', name="new",
workflow=workflow, workflow=workflow,
allow_create_poll=True, allow_create_poll=True,
allow_support=True, allow_support=True,
allow_submitter_edit=True allow_submitter_edit=True,
) )
workflow.first_state = first_state workflow.first_state = first_state
workflow.save() workflow.save()
@ -142,6 +152,7 @@ class AmendmentParagraphsJSONSerializerField(Field):
""" """
Serializer for motions's amendment_paragraphs JSONField. Serializer for motions's amendment_paragraphs JSONField.
""" """
def to_representation(self, obj): def to_representation(self, obj):
""" """
Returns the value of the field. Returns the value of the field.
@ -153,10 +164,12 @@ class AmendmentParagraphsJSONSerializerField(Field):
Checks that data is a list of strings. Checks that data is a list of strings.
""" """
if type(data) is not list: 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: for paragraph in data:
if type(paragraph) is not str and paragraph is not None: 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 return data
@ -164,11 +177,12 @@ class MotionLogSerializer(ModelSerializer):
""" """
Serializer for motion.models.MotionLog objects. Serializer for motion.models.MotionLog objects.
""" """
message = SerializerMethodField() message = SerializerMethodField()
class Meta: class Meta:
model = MotionLog model = MotionLog
fields = ('message_list', 'person', 'time', 'message',) fields = ("message_list", "person", "time", "message")
def get_message(self, obj): def get_message(self, obj):
""" """
@ -181,27 +195,32 @@ class MotionPollSerializer(ModelSerializer):
""" """
Serializer for motion.models.MotionPoll objects. Serializer for motion.models.MotionPoll objects.
""" """
yes = SerializerMethodField() yes = SerializerMethodField()
no = SerializerMethodField() no = SerializerMethodField()
abstain = SerializerMethodField() abstain = SerializerMethodField()
votes = DictField( votes = DictField(
child=DecimalField(max_digits=15, decimal_places=6, min_value=-2, allow_null=True), child=DecimalField(
write_only=True) max_digits=15, decimal_places=6, min_value=-2, allow_null=True
),
write_only=True,
)
has_votes = SerializerMethodField() has_votes = SerializerMethodField()
class Meta: class Meta:
model = MotionPoll model = MotionPoll
fields = ( fields = (
'id', "id",
'motion', "motion",
'yes', "yes",
'no', "no",
'abstain', "abstain",
'votesvalid', "votesvalid",
'votesinvalid', "votesinvalid",
'votescast', "votescast",
'votes', "votes",
'has_votes') "has_votes",
)
validators = (default_votes_validator,) validators = (default_votes_validator,)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -211,21 +230,21 @@ class MotionPollSerializer(ModelSerializer):
def get_yes(self, obj): def get_yes(self, obj):
try: try:
result: Optional[str] = str(self.get_votes_dict(obj)['Yes']) result: Optional[str] = str(self.get_votes_dict(obj)["Yes"])
except KeyError: except KeyError:
result = None result = None
return result return result
def get_no(self, obj): def get_no(self, obj):
try: try:
result: Optional[str] = str(self.get_votes_dict(obj)['No']) result: Optional[str] = str(self.get_votes_dict(obj)["No"])
except KeyError: except KeyError:
result = None result = None
return result return result
def get_abstain(self, obj): def get_abstain(self, obj):
try: try:
result: Optional[str] = str(self.get_votes_dict(obj)['Abstain']) result: Optional[str] = str(self.get_votes_dict(obj)["Abstain"])
except KeyError: except KeyError:
result = None result = None
return result return result
@ -256,21 +275,30 @@ class MotionPollSerializer(ModelSerializer):
"votes": {"Yes": 10, "No": 4, "Abstain": -2} "votes": {"Yes": 10, "No": 4, "Abstain": -2}
""" """
# Update votes. # Update votes.
votes = validated_data.get('votes') votes = validated_data.get("votes")
if votes: if votes:
if len(votes) != len(instance.get_vote_values()): if len(votes) != len(instance.get_vote_values()):
raise ValidationError({ raise ValidationError(
'detail': _('You have to submit data for %d vote values.') % len(instance.get_vote_values())}) {
"detail": _("You have to submit data for %d vote values.")
% len(instance.get_vote_values())
}
)
for vote_value, vote_weight in votes.items(): for vote_value, vote_weight in votes.items():
if vote_value not in instance.get_vote_values(): if vote_value not in instance.get_vote_values():
raise ValidationError({ raise ValidationError(
'detail': _('Vote value %s is invalid.') % vote_value}) {"detail": _("Vote value %s is invalid.") % vote_value}
instance.set_vote_objects_with_values(instance.get_options().get(), votes, skip_autoupdate=True) )
instance.set_vote_objects_with_values(
instance.get_options().get(), votes, skip_autoupdate=True
)
# Update remaining writeable fields. # Update remaining writeable fields.
instance.votesvalid = validated_data.get('votesvalid', instance.votesvalid) instance.votesvalid = validated_data.get("votesvalid", instance.votesvalid)
instance.votesinvalid = validated_data.get('votesinvalid', instance.votesinvalid) instance.votesinvalid = validated_data.get(
instance.votescast = validated_data.get('votescast', instance.votescast) "votesinvalid", instance.votesinvalid
)
instance.votescast = validated_data.get("votescast", instance.votescast)
instance.save() instance.save()
return instance return instance
@ -279,27 +307,29 @@ class MotionChangeRecommendationSerializer(ModelSerializer):
""" """
Serializer for motion.models.MotionChangeRecommendation objects. Serializer for motion.models.MotionChangeRecommendation objects.
""" """
class Meta: class Meta:
model = MotionChangeRecommendation model = MotionChangeRecommendation
fields = ( fields = (
'id', "id",
'motion', "motion",
'rejected', "rejected",
'internal', "internal",
'type', "type",
'other_description', "other_description",
'line_from', "line_from",
'line_to', "line_to",
'text', "text",
'creation_time',) "creation_time",
)
def is_title_cr(self, data): 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): def validate(self, data):
# Change recommendations for titles are stored as plain-text, thus they don't need to be html-escaped # 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): if "text" in data and not self.is_title_cr(data):
data['text'] = validate_html(data['text']) data["text"] = validate_html(data["text"])
return data return data
@ -307,23 +337,18 @@ class MotionCommentSectionSerializer(ModelSerializer):
""" """
Serializer for motion.models.MotionCommentSection objects. Serializer for motion.models.MotionCommentSection objects.
""" """
read_groups = IdPrimaryKeyRelatedField( read_groups = IdPrimaryKeyRelatedField(
many=True, many=True, required=False, queryset=get_group_model().objects.all()
required=False, )
queryset=get_group_model().objects.all())
write_groups = IdPrimaryKeyRelatedField( write_groups = IdPrimaryKeyRelatedField(
many=True, many=True, required=False, queryset=get_group_model().objects.all()
required=False, )
queryset=get_group_model().objects.all())
class Meta: class Meta:
model = MotionCommentSection model = MotionCommentSection
fields = ( fields = ("id", "name", "read_groups", "write_groups")
'id',
'name',
'read_groups',
'write_groups',)
def create(self, validated_data): def create(self, validated_data):
""" Call inform_changed_data on creation, so the cache includes the groups. """ """ 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. Serializer for motion.models.MotionComment objects.
""" """
read_groups_id = SerializerMethodField() read_groups_id = SerializerMethodField()
class Meta: class Meta:
model = MotionComment model = MotionComment
fields = ( fields = ("id", "comment", "section", "read_groups_id")
'id',
'comment',
'section',
'read_groups_id',)
def get_read_groups_id(self, comment): def get_read_groups_id(self, comment):
return [group.id for group in comment.section.read_groups.all()] return [group.id for group in comment.section.read_groups.all()]
@ -354,20 +376,17 @@ class SubmitterSerializer(ModelSerializer):
""" """
Serializer for motion.models.Submitter objects. Serializer for motion.models.Submitter objects.
""" """
class Meta: class Meta:
model = Submitter model = Submitter
fields = ( fields = ("id", "user", "motion", "weight")
'id',
'user',
'motion',
'weight',
)
class MotionSerializer(ModelSerializer): class MotionSerializer(ModelSerializer):
""" """
Serializer for motion.models.Motion objects. Serializer for motion.models.Motion objects.
""" """
comments = MotionCommentSerializer(many=True, read_only=True) comments = MotionCommentSerializer(many=True, read_only=True)
log_messages = MotionLogSerializer(many=True, read_only=True) log_messages = MotionLogSerializer(many=True, read_only=True)
polls = MotionPollSerializer(many=True, read_only=True) polls = MotionPollSerializer(many=True, read_only=True)
@ -378,67 +397,76 @@ class MotionSerializer(ModelSerializer):
title = CharField(max_length=255) title = CharField(max_length=255)
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False) amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False)
workflow_id = IntegerField( workflow_id = IntegerField(
min_value=1, min_value=1, required=False, validators=[validate_workflow_field]
required=False, )
validators=[validate_workflow_field]) agenda_type = IntegerField(
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3) write_only=True, required=False, min_value=1, max_value=3
)
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
submitters = SubmitterSerializer(many=True, read_only=True) submitters = SubmitterSerializer(many=True, read_only=True)
class Meta: class Meta:
model = Motion model = Motion
fields = ( fields = (
'id', "id",
'identifier', "identifier",
'title', "title",
'text', "text",
'amendment_paragraphs', "amendment_paragraphs",
'modified_final_version', "modified_final_version",
'reason', "reason",
'parent', "parent",
'category', "category",
'comments', "comments",
'motion_block', "motion_block",
'origin', "origin",
'submitters', "submitters",
'supporters', "supporters",
'state', "state",
'state_extension', "state_extension",
'state_required_permission_to_see', "state_required_permission_to_see",
'statute_paragraph', "statute_paragraph",
'workflow_id', "workflow_id",
'recommendation', "recommendation",
'recommendation_extension', "recommendation_extension",
'tags', "tags",
'attachments', "attachments",
'polls', "polls",
'agenda_item_id', "agenda_item_id",
'agenda_type', "agenda_type",
'agenda_parent_id', "agenda_parent_id",
'log_messages', "log_messages",
'sort_parent', "sort_parent",
'weight',) "weight",
read_only_fields = ('state', 'recommendation',) # Some other fields are also read_only. See definitions above. )
read_only_fields = (
"state",
"recommendation",
) # Some other fields are also read_only. See definitions above.
def validate(self, data): def validate(self, data):
if 'text'in data: if "text" in data:
data['text'] = validate_html(data['text']) data["text"] = validate_html(data["text"])
if 'modified_final_version' in data: if "modified_final_version" in data:
data['modified_final_version'] = validate_html(data['modified_final_version']) data["modified_final_version"] = validate_html(
data["modified_final_version"]
)
if 'reason' in data: if "reason" in data:
data['reason'] = validate_html(data['reason']) data["reason"] = validate_html(data["reason"])
if 'amendment_paragraphs' in data: 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"] = list(
data['amendment_paragraphs'])) map(
data['text'] = '' lambda entry: validate_html(entry) if type(entry) is str else None,
data["amendment_paragraphs"],
)
)
data["text"] = ""
else: else:
if 'text' in data and len(data['text']) == 0: if "text" in data and len(data["text"]) == 0:
raise ValidationError({ raise ValidationError({"detail": _("This field may not be blank.")})
'detail': _('This field may not be blank.')
})
return data return data
@ -451,24 +479,28 @@ class MotionSerializer(ModelSerializer):
agenda_item_update_information container. agenda_item_update_information container.
""" """
motion = Motion() motion = Motion()
motion.title = validated_data['title'] motion.title = validated_data["title"]
motion.text = validated_data['text'] motion.text = validated_data["text"]
motion.amendment_paragraphs = validated_data.get('amendment_paragraphs') motion.amendment_paragraphs = validated_data.get("amendment_paragraphs")
motion.modified_final_version = validated_data.get('modified_final_version', '') motion.modified_final_version = validated_data.get("modified_final_version", "")
motion.reason = validated_data.get('reason', '') motion.reason = validated_data.get("reason", "")
motion.identifier = validated_data.get('identifier') motion.identifier = validated_data.get("identifier")
motion.category = validated_data.get('category') motion.category = validated_data.get("category")
motion.motion_block = validated_data.get('motion_block') motion.motion_block = validated_data.get("motion_block")
motion.origin = validated_data.get('origin', '') motion.origin = validated_data.get("origin", "")
motion.parent = validated_data.get('parent') motion.parent = validated_data.get("parent")
motion.statute_paragraph = validated_data.get('statute_paragraph') motion.statute_paragraph = validated_data.get("statute_paragraph")
motion.reset_state(validated_data.get('workflow_id')) motion.reset_state(validated_data.get("workflow_id"))
motion.agenda_item_update_information['type'] = validated_data.get('agenda_type') motion.agenda_item_update_information["type"] = validated_data.get(
motion.agenda_item_update_information['parent_id'] = validated_data.get('agenda_parent_id') "agenda_type"
)
motion.agenda_item_update_information["parent_id"] = validated_data.get(
"agenda_parent_id"
)
motion.save() motion.save()
motion.supporters.add(*validated_data.get('supporters', [])) motion.supporters.add(*validated_data.get("supporters", []))
motion.attachments.add(*validated_data.get('attachments', [])) motion.attachments.add(*validated_data.get("attachments", []))
motion.tags.add(*validated_data.get('tags', [])) motion.tags.add(*validated_data.get("tags", []))
return motion return motion
@transaction.atomic @transaction.atomic
@ -477,8 +509,8 @@ class MotionSerializer(ModelSerializer):
Customized method to update a motion. Customized method to update a motion.
""" """
workflow_id = None workflow_id = None
if 'workflow_id' in validated_data: if "workflow_id" in validated_data:
workflow_id = validated_data.pop('workflow_id') workflow_id = validated_data.pop("workflow_id")
result = super().update(motion, validated_data) result = super().update(motion, validated_data)

View File

@ -14,98 +14,128 @@ def create_builtin_workflows(sender, **kwargs):
# If there is at least one workflow, then do nothing. # If there is at least one workflow, then do nothing.
return return
workflow_1 = Workflow(name='Simple Workflow') workflow_1 = Workflow(name="Simple Workflow")
workflow_1.save(skip_autoupdate=True) workflow_1.save(skip_autoupdate=True)
state_1_1 = State(name=ugettext_noop('submitted'), state_1_1 = State(
workflow=workflow_1, name=ugettext_noop("submitted"),
allow_create_poll=True, workflow=workflow_1,
allow_support=True, allow_create_poll=True,
allow_submitter_edit=True) allow_support=True,
allow_submitter_edit=True,
)
state_1_1.save(skip_autoupdate=True) state_1_1.save(skip_autoupdate=True)
state_1_2 = State(name=ugettext_noop('accepted'), state_1_2 = State(
workflow=workflow_1, name=ugettext_noop("accepted"),
recommendation_label='Acceptance', workflow=workflow_1,
css_class='success', recommendation_label="Acceptance",
merge_amendment_into_final=1) css_class="success",
merge_amendment_into_final=1,
)
state_1_2.save(skip_autoupdate=True) state_1_2.save(skip_autoupdate=True)
state_1_3 = State(name=ugettext_noop('rejected'), state_1_3 = State(
workflow=workflow_1, name=ugettext_noop("rejected"),
recommendation_label='Rejection', workflow=workflow_1,
css_class='danger', recommendation_label="Rejection",
merge_amendment_into_final=-1) css_class="danger",
merge_amendment_into_final=-1,
)
state_1_3.save(skip_autoupdate=True) state_1_3.save(skip_autoupdate=True)
state_1_4 = State(name=ugettext_noop('not decided'), state_1_4 = State(
workflow=workflow_1, name=ugettext_noop("not decided"),
recommendation_label='No decision', workflow=workflow_1,
css_class='default', recommendation_label="No decision",
merge_amendment_into_final=-1) css_class="default",
merge_amendment_into_final=-1,
)
state_1_4.save(skip_autoupdate=True) state_1_4.save(skip_autoupdate=True)
state_1_1.next_states.add(state_1_2, state_1_3, state_1_4) state_1_1.next_states.add(state_1_2, state_1_3, state_1_4)
workflow_1.first_state = state_1_1 workflow_1.first_state = state_1_1
workflow_1.save(skip_autoupdate=True) workflow_1.save(skip_autoupdate=True)
workflow_2 = Workflow(name='Complex Workflow') workflow_2 = Workflow(name="Complex Workflow")
workflow_2.save(skip_autoupdate=True) workflow_2.save(skip_autoupdate=True)
state_2_1 = State(name=ugettext_noop('published'), state_2_1 = State(
workflow=workflow_2, name=ugettext_noop("published"),
allow_support=True, workflow=workflow_2,
allow_submitter_edit=True, allow_support=True,
dont_set_identifier=True) allow_submitter_edit=True,
dont_set_identifier=True,
)
state_2_1.save(skip_autoupdate=True) state_2_1.save(skip_autoupdate=True)
state_2_2 = State(name=ugettext_noop('permitted'), state_2_2 = State(
workflow=workflow_2, name=ugettext_noop("permitted"),
recommendation_label='Permission', workflow=workflow_2,
allow_create_poll=True, recommendation_label="Permission",
allow_submitter_edit=True) allow_create_poll=True,
allow_submitter_edit=True,
)
state_2_2.save(skip_autoupdate=True) state_2_2.save(skip_autoupdate=True)
state_2_3 = State(name=ugettext_noop('accepted'), state_2_3 = State(
workflow=workflow_2, name=ugettext_noop("accepted"),
recommendation_label='Acceptance', workflow=workflow_2,
css_class='success', recommendation_label="Acceptance",
merge_amendment_into_final=1) css_class="success",
merge_amendment_into_final=1,
)
state_2_3.save(skip_autoupdate=True) state_2_3.save(skip_autoupdate=True)
state_2_4 = State(name=ugettext_noop('rejected'), state_2_4 = State(
workflow=workflow_2, name=ugettext_noop("rejected"),
recommendation_label='Rejection', workflow=workflow_2,
css_class='danger', recommendation_label="Rejection",
merge_amendment_into_final=-1) css_class="danger",
merge_amendment_into_final=-1,
)
state_2_4.save(skip_autoupdate=True) state_2_4.save(skip_autoupdate=True)
state_2_5 = State(name=ugettext_noop('withdrawed'), state_2_5 = State(
workflow=workflow_2, name=ugettext_noop("withdrawed"),
css_class='default', workflow=workflow_2,
merge_amendment_into_final=-1) css_class="default",
merge_amendment_into_final=-1,
)
state_2_5.save(skip_autoupdate=True) state_2_5.save(skip_autoupdate=True)
state_2_6 = State(name=ugettext_noop('adjourned'), state_2_6 = State(
workflow=workflow_2, name=ugettext_noop("adjourned"),
recommendation_label='Adjournment', workflow=workflow_2,
css_class='default', recommendation_label="Adjournment",
merge_amendment_into_final=-1) css_class="default",
merge_amendment_into_final=-1,
)
state_2_6.save(skip_autoupdate=True) state_2_6.save(skip_autoupdate=True)
state_2_7 = State(name=ugettext_noop('not concerned'), state_2_7 = State(
workflow=workflow_2, name=ugettext_noop("not concerned"),
recommendation_label='No concernment', workflow=workflow_2,
css_class='default', recommendation_label="No concernment",
merge_amendment_into_final=-1) css_class="default",
merge_amendment_into_final=-1,
)
state_2_7.save(skip_autoupdate=True) state_2_7.save(skip_autoupdate=True)
state_2_8 = State(name=ugettext_noop('refered to committee'), state_2_8 = State(
workflow=workflow_2, name=ugettext_noop("refered to committee"),
recommendation_label='Referral to committee', workflow=workflow_2,
css_class='default', recommendation_label="Referral to committee",
merge_amendment_into_final=-1) css_class="default",
merge_amendment_into_final=-1,
)
state_2_8.save(skip_autoupdate=True) state_2_8.save(skip_autoupdate=True)
state_2_9 = State(name=ugettext_noop('needs review'), state_2_9 = State(
workflow=workflow_2, name=ugettext_noop("needs review"),
css_class='default', workflow=workflow_2,
merge_amendment_into_final=-1) css_class="default",
merge_amendment_into_final=-1,
)
state_2_9.save(skip_autoupdate=True) state_2_9.save(skip_autoupdate=True)
state_2_10 = State(name=ugettext_noop('rejected (not authorized)'), state_2_10 = State(
workflow=workflow_2, name=ugettext_noop("rejected (not authorized)"),
recommendation_label='Rejection (not authorized)', workflow=workflow_2,
css_class='default', recommendation_label="Rejection (not authorized)",
merge_amendment_into_final=-1) css_class="default",
merge_amendment_into_final=-1,
)
state_2_10.save(skip_autoupdate=True) state_2_10.save(skip_autoupdate=True)
state_2_1.next_states.add(state_2_2, state_2_5, state_2_10) 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.first_state = state_2_1
workflow_2.save(skip_autoupdate=True) 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. 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: for permission in permissions:
# There could be only one 'motions.can_see' and then we want to return data. # 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() yield from motions_app.get_startup_elements()

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
# Common majority methods for all apps using polls. The first one should be the default. # Common majority methods for all apps using polls. The first one should be the default.
majorityMethods = ( majorityMethods = (
{'value': 'simple_majority', 'display_name': 'Simple majority'}, {"value": "simple_majority", "display_name": "Simple majority"},
{'value': 'two-thirds_majority', 'display_name': 'Two-thirds majority'}, {"value": "two-thirds_majority", "display_name": "Two-thirds majority"},
{'value': 'three-quarters_majority', 'display_name': 'Three-quarters majority'}, {"value": "three-quarters_majority", "display_name": "Three-quarters majority"},
{'value': 'disabled', 'display_name': 'Disabled'}, {"value": "disabled", "display_name": "Disabled"},
) )

View File

@ -17,7 +17,8 @@ class BaseOption(models.Model):
which has to be a subclass of BaseVote. Otherwise you have to override the which has to be a subclass of BaseVote. Otherwise you have to override the
get_vote_class method. get_vote_class method.
""" """
vote_class: Optional[Type['BaseVote']] = None
vote_class: Optional[Type["BaseVote"]] = None
class Meta: class Meta:
abstract = True abstract = True
@ -27,7 +28,9 @@ class BaseOption(models.Model):
def get_vote_class(self): def get_vote_class(self):
if self.vote_class is None: 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 return self.vote_class
def __getitem__(self, name): def __getitem__(self, name):
@ -44,8 +47,14 @@ class BaseVote(models.Model):
Subclasses have to define an option field. This must be a ForeignKeyField Subclasses have to define an option field. This must be a ForeignKeyField
to a subclass of BasePoll. 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) value = models.CharField(max_length=255, null=True)
class Meta: class Meta:
@ -73,12 +82,28 @@ class CollectDefaultVotesMixin(models.Model):
Mixin for a poll to collect the default vote values for valid votes, Mixin for a poll to collect the default vote values for valid votes,
invalid votes and votes cast. invalid votes and votes cast.
""" """
votesvalid = models.DecimalField(null=True, blank=True, validators=[
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) votesvalid = models.DecimalField(
votesinvalid = models.DecimalField(null=True, blank=True, validators=[ null=True,
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) blank=True,
votescast = models.DecimalField(null=True, blank=True, validators=[ validators=[MinValueValidator(Decimal("-2"))],
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6) 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: class Meta:
abstract = True abstract = True
@ -87,13 +112,16 @@ class CollectDefaultVotesMixin(models.Model):
""" """
Returns one of the strings of the percent base. 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): class PublishPollMixin(models.Model):
""" """
Mixin for a poll to add a flag whether the poll is published or not. Mixin for a poll to add a flag whether the poll is published or not.
""" """
published = models.BooleanField(default=False) published = models.BooleanField(default=False)
class Meta: class Meta:
@ -108,7 +136,8 @@ class BasePoll(models.Model):
""" """
Base poll class. Base poll class.
""" """
vote_values = ['Votes']
vote_values = ["Votes"]
class Meta: class Meta:
abstract = True abstract = True
@ -183,7 +212,7 @@ class BasePoll(models.Model):
try: try:
vote = self.get_votes().filter(option=option_id).get(value=value) vote = self.get_votes().filter(option=option_id).get(value=value)
except ObjectDoesNotExist: except ObjectDoesNotExist:
values.append(self.get_vote_class()(value=value, weight='')) values.append(self.get_vote_class()(value=value, weight=""))
else: else:
values.append(vote) values.append(vote)
return values return values
@ -195,15 +224,18 @@ def print_value(value, percent_base=0):
'undocumented' or the vote value with percent value if so. 'undocumented' or the vote value with percent value if so.
""" """
if value == -1: if value == -1:
verbose_value = _('majority') verbose_value = _("majority")
elif value == -2: elif value == -2:
verbose_value = _('undocumented') verbose_value = _("undocumented")
elif value is None: elif value is None:
verbose_value = _('undocumented') verbose_value = _("undocumented")
else: else:
if percent_base: if percent_base:
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, "")
verbose_value = u'%d (%s %%)' % (value, locale.format('%.1f', value * percent_base)) verbose_value = "%d (%s %%)" % (
value,
locale.format("%.1f", value * percent_base),
)
else: else:
verbose_value = u'%s' % value verbose_value = "%s" % value
return verbose_value return verbose_value

View File

@ -10,8 +10,12 @@ def default_votes_validator(data):
than or equal to -2. than or equal to -2.
""" """
for key in data: for key in data:
if (key in ('votesvalid', 'votesinvalid', 'votescast') and if (
data[key] is not None and key in ("votesvalid", "votesinvalid", "votescast")
data[key] < -2): and data[key] is not None
raise ValidationError({'detail': _('Value for {} must not be less than -2').format(key)}) and data[key] < -2
):
raise ValidationError(
{"detail": _("Value for {} must not be less than -2").format(key)}
)
return data return data

View File

@ -5,11 +5,9 @@ from openslides.utils.consumers import SiteConsumer
from openslides.utils.middleware import AuthMiddlewareStack from openslides.utils.middleware import AuthMiddlewareStack
application = ProtocolTypeRouter({ application = ProtocolTypeRouter(
# WebSocket chat handler {
"websocket": AuthMiddlewareStack( # WebSocket chat handler
URLRouter([ "websocket": AuthMiddlewareStack(URLRouter([url(r"^ws/$", SiteConsumer)]))
url(r"^ws/$", SiteConsumer), }
]) )
)
})

Some files were not shown because too many files have changed in this diff Show More