commit
ebf51507f8
@ -16,8 +16,6 @@ matrix:
|
||||
- pip install --upgrade .[big_mode]
|
||||
- pip freeze
|
||||
script:
|
||||
- flake8 openslides tests
|
||||
- isort --check-only --diff --recursive openslides tests
|
||||
- python -m mypy openslides/ tests/
|
||||
- python -W ignore -m pytest --cov --cov-fail-under=70
|
||||
|
||||
@ -36,6 +34,7 @@ matrix:
|
||||
script:
|
||||
- flake8 openslides tests
|
||||
- isort --check-only --diff --recursive openslides tests
|
||||
- black --check --diff --py36 openslides tests
|
||||
- python -m mypy openslides/ tests/
|
||||
- python -W ignore -m pytest --cov --cov-fail-under=70
|
||||
|
||||
|
@ -5,7 +5,7 @@ from parser import parser
|
||||
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
args = parser.parse_args(['--help'])
|
||||
args = parser.parse_args(["--help"])
|
||||
else:
|
||||
args = parser.parse_args()
|
||||
|
||||
|
@ -2,47 +2,48 @@ from parser import command, argument, call
|
||||
import yaml
|
||||
import requirements
|
||||
|
||||
FAIL = '\033[91m'
|
||||
SUCCESS = '\033[92m'
|
||||
RESET = '\033[0m'
|
||||
FAIL = "\033[91m"
|
||||
SUCCESS = "\033[92m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
@command('check', help='Checks for pep8 errors in openslides and tests')
|
||||
@command("check", help="Checks for pep8 errors in openslides and tests")
|
||||
def check(args=None):
|
||||
"""
|
||||
Checks for pep8 and other code styling conventions.
|
||||
"""
|
||||
value = call('flake8 --max-line-length=150 --statistics openslides tests')
|
||||
value += call('python -m mypy openslides/ tests/')
|
||||
value = call("flake8 --max-line-length=150 --statistics openslides tests")
|
||||
value += call("python -m mypy openslides/ tests/")
|
||||
return value
|
||||
|
||||
|
||||
@command('travis', help='Runs the code that travis does')
|
||||
@command("travis", help="Runs the code that travis does")
|
||||
def travis(args=None):
|
||||
"""
|
||||
Runs all commands that travis tests.
|
||||
"""
|
||||
return_codes = []
|
||||
with open('.travis.yml') as f:
|
||||
with open(".travis.yml") as f:
|
||||
travis = yaml.load(f)
|
||||
for line in travis['script']:
|
||||
print('Run: {}'.format(line))
|
||||
for line in travis["script"]:
|
||||
print("Run: {}".format(line))
|
||||
return_code = call(line)
|
||||
return_codes.append(return_code)
|
||||
if return_code:
|
||||
print(FAIL + 'fail!\n' + RESET)
|
||||
print(FAIL + "fail!\n" + RESET)
|
||||
else:
|
||||
print(SUCCESS + 'success!\n' + RESET)
|
||||
print(SUCCESS + "success!\n" + RESET)
|
||||
|
||||
# Retuns True if one command exited with a different statuscode then 1
|
||||
return bool(list(filter(bool, return_codes)))
|
||||
|
||||
|
||||
@argument('-r', '--requirements', nargs='?',
|
||||
default='requirements.txt')
|
||||
@command('min_requirements',
|
||||
help='Prints a pip line to install the minimum supported versions of '
|
||||
'the requirements.')
|
||||
@argument("-r", "--requirements", nargs="?", default="requirements.txt")
|
||||
@command(
|
||||
"min_requirements",
|
||||
help="Prints a pip line to install the minimum supported versions of "
|
||||
"the requirements.",
|
||||
)
|
||||
def min_requirements(args=None):
|
||||
"""
|
||||
Prints a pip install command to install the minimal supported versions of a
|
||||
@ -54,6 +55,7 @@ def min_requirements(args=None):
|
||||
|
||||
pip install $(python make min_requirements)
|
||||
"""
|
||||
|
||||
def get_lowest_versions(requirements_file):
|
||||
with open(requirements_file) as f:
|
||||
for req in requirements.parse(f):
|
||||
@ -62,21 +64,20 @@ def min_requirements(args=None):
|
||||
if spec == ">=":
|
||||
yield "{}=={}".format(req.name, version)
|
||||
|
||||
print(' '.join(get_lowest_versions(args.requirements)))
|
||||
print(" ".join(get_lowest_versions(args.requirements)))
|
||||
|
||||
|
||||
@command('clear',
|
||||
help='Deletes unneeded files and folders')
|
||||
@command("clean", help="Deletes unneeded files and folders")
|
||||
def clean(args=None):
|
||||
"""
|
||||
Deletes all .pyc and .orig files and empty folders.
|
||||
"""
|
||||
call('find -name "*.pyc" -delete')
|
||||
call('find -name "*.orig" -delete')
|
||||
call('find -type d -empty -delete')
|
||||
call("find -type d -empty -delete")
|
||||
|
||||
|
||||
@command('isort',
|
||||
help='Sorts all imports in all python files.')
|
||||
@command("format", help="Format code with isort and black")
|
||||
def isort(args=None):
|
||||
return call('isort --recursive openslides tests')
|
||||
call("isort --recursive openslides tests")
|
||||
call("black --py36 openslides tests")
|
||||
|
@ -1,7 +1,7 @@
|
||||
from argparse import ArgumentParser
|
||||
from subprocess import call as _call
|
||||
|
||||
parser = ArgumentParser(description='Development scripts for OpenSlides')
|
||||
parser = ArgumentParser(description="Development scripts for OpenSlides")
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ def command(*args, **kwargs):
|
||||
The arguments to this decorator are used as arguments for the argparse
|
||||
command.
|
||||
"""
|
||||
|
||||
class decorator:
|
||||
def __init__(self, func):
|
||||
self.parser = subparsers.add_parser(*args, **kwargs)
|
||||
@ -34,11 +35,15 @@ def argument(*args, **kwargs):
|
||||
Does only work if the decorated function was decorated with the
|
||||
command-decorator before.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
func.parser.add_argument(*args, **kwargs)
|
||||
|
||||
def wrapper(*func_args, **func_kwargs):
|
||||
return func(*func_args, **func_kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
__author__ = 'OpenSlides Team <support@openslides.org>'
|
||||
__description__ = 'Presentation and assembly system'
|
||||
__version__ = '3.0-dev'
|
||||
__license__ = 'MIT'
|
||||
__url__ = 'https://openslides.org'
|
||||
__author__ = "OpenSlides Team <support@openslides.org>"
|
||||
__description__ = "Presentation and assembly system"
|
||||
__version__ = "3.0-dev"
|
||||
__license__ = "MIT"
|
||||
__url__ = "https://openslides.org"
|
||||
|
||||
args = None
|
||||
|
@ -42,7 +42,7 @@ def main():
|
||||
else:
|
||||
# Check for unknown_args.
|
||||
if unknown_args:
|
||||
parser.error('Unknown arguments {}'.format(' '.join(unknown_args)))
|
||||
parser.error("Unknown arguments {}".format(" ".join(unknown_args)))
|
||||
|
||||
# Save arguments, if one wants to access them later.
|
||||
arguments.set_arguments(known_args)
|
||||
@ -59,11 +59,11 @@ def get_parser():
|
||||
if len(sys.argv) == 1:
|
||||
# Use start subcommand if called by openslides console script without
|
||||
# any other arguments.
|
||||
sys.argv.append('start')
|
||||
sys.argv.append("start")
|
||||
|
||||
# Init parser
|
||||
description = 'Start script for OpenSlides.'
|
||||
if 'manage.py' not in sys.argv[0]:
|
||||
description = "Start script for OpenSlides."
|
||||
if "manage.py" not in sys.argv[0]:
|
||||
description += """
|
||||
If it is called without any argument, this will be treated as
|
||||
if it is called with the 'start' subcommand. That means
|
||||
@ -77,109 +77,116 @@ def get_parser():
|
||||
(without the two hyphen-minus characters) to list them all. Type
|
||||
'%(prog)s help <subcommand>' for help on a specific subcommand.
|
||||
"""
|
||||
parser = ExceptionArgumentParser(
|
||||
description=description,
|
||||
epilog=epilog)
|
||||
parser = ExceptionArgumentParser(description=description, epilog=epilog)
|
||||
|
||||
# Add version argument
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
action='version',
|
||||
"--version",
|
||||
action="version",
|
||||
version=openslides.__version__,
|
||||
help='Show version number and exit.')
|
||||
help="Show version number and exit.",
|
||||
)
|
||||
|
||||
# Init subparsers
|
||||
subparsers = parser.add_subparsers(
|
||||
dest='subcommand',
|
||||
title='Available subcommands',
|
||||
dest="subcommand",
|
||||
title="Available subcommands",
|
||||
description="Type '%s <subcommand> --help' for help on a "
|
||||
"specific subcommand." % parser.prog, # type: ignore
|
||||
help='You can choose only one subcommand at once.',
|
||||
metavar='')
|
||||
"specific subcommand." % parser.prog, # type: ignore
|
||||
help="You can choose only one subcommand at once.",
|
||||
metavar="",
|
||||
)
|
||||
|
||||
# Subcommand start
|
||||
start_help = (
|
||||
'Setup settings and database, start webserver, launch the '
|
||||
'default web browser and open the webinterface. The environment '
|
||||
'variable DJANGO_SETTINGS_MODULE is ignored.')
|
||||
"Setup settings and database, start webserver, launch the "
|
||||
"default web browser and open the webinterface. The environment "
|
||||
"variable DJANGO_SETTINGS_MODULE is ignored."
|
||||
)
|
||||
subcommand_start = subparsers.add_parser(
|
||||
'start',
|
||||
description=start_help,
|
||||
help=start_help)
|
||||
"start", description=start_help, help=start_help
|
||||
)
|
||||
subcommand_start.set_defaults(callback=start)
|
||||
subcommand_start.add_argument(
|
||||
'--no-browser',
|
||||
action='store_true',
|
||||
help='Do not launch the default web browser.')
|
||||
"--no-browser",
|
||||
action="store_true",
|
||||
help="Do not launch the default web browser.",
|
||||
)
|
||||
subcommand_start.add_argument(
|
||||
'--debug-email',
|
||||
action='store_true',
|
||||
help='Change the email backend to console output.')
|
||||
"--debug-email",
|
||||
action="store_true",
|
||||
help="Change the email backend to console output.",
|
||||
)
|
||||
subcommand_start.add_argument(
|
||||
'--no-template-caching',
|
||||
action='store_true',
|
||||
"--no-template-caching",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help='Disables caching of templates.')
|
||||
help="Disables caching of templates.",
|
||||
)
|
||||
subcommand_start.add_argument(
|
||||
'--host',
|
||||
action='store',
|
||||
default='0.0.0.0',
|
||||
help='IP address to listen on. Default is 0.0.0.0.')
|
||||
"--host",
|
||||
action="store",
|
||||
default="0.0.0.0",
|
||||
help="IP address to listen on. Default is 0.0.0.0.",
|
||||
)
|
||||
subcommand_start.add_argument(
|
||||
'--port',
|
||||
action='store',
|
||||
default='8000',
|
||||
help='Port to listen on. Default is 8000.')
|
||||
"--port",
|
||||
action="store",
|
||||
default="8000",
|
||||
help="Port to listen on. Default is 8000.",
|
||||
)
|
||||
subcommand_start.add_argument(
|
||||
'--settings_dir',
|
||||
action='store',
|
||||
default=None,
|
||||
help='The settings directory.')
|
||||
"--settings_dir", action="store", default=None, help="The settings directory."
|
||||
)
|
||||
subcommand_start.add_argument(
|
||||
'--settings_filename',
|
||||
action='store',
|
||||
default='settings.py',
|
||||
help='The used settings file name. The file is created, if it does not exist.')
|
||||
"--settings_filename",
|
||||
action="store",
|
||||
default="settings.py",
|
||||
help="The used settings file name. The file is created, if it does not exist.",
|
||||
)
|
||||
subcommand_start.add_argument(
|
||||
'--local-installation',
|
||||
action='store_true',
|
||||
help='Store settings and user files in a local directory.')
|
||||
"--local-installation",
|
||||
action="store_true",
|
||||
help="Store settings and user files in a local directory.",
|
||||
)
|
||||
|
||||
# Subcommand createsettings
|
||||
createsettings_help = 'Creates the settings file.'
|
||||
createsettings_help = "Creates the settings file."
|
||||
subcommand_createsettings = subparsers.add_parser(
|
||||
'createsettings',
|
||||
description=createsettings_help,
|
||||
help=createsettings_help)
|
||||
"createsettings", description=createsettings_help, help=createsettings_help
|
||||
)
|
||||
subcommand_createsettings.set_defaults(callback=createsettings)
|
||||
subcommand_createsettings.add_argument(
|
||||
'--settings_dir',
|
||||
action='store',
|
||||
"--settings_dir",
|
||||
action="store",
|
||||
default=None,
|
||||
help='The used settings file directory. All settings files are created, even if they exist.')
|
||||
help="The used settings file directory. All settings files are created, even if they exist.",
|
||||
)
|
||||
subcommand_createsettings.add_argument(
|
||||
'--settings_filename',
|
||||
action='store',
|
||||
default='settings.py',
|
||||
help='The used settings file name. The file is created, if it does not exist.')
|
||||
"--settings_filename",
|
||||
action="store",
|
||||
default="settings.py",
|
||||
help="The used settings file name. The file is created, if it does not exist.",
|
||||
)
|
||||
subcommand_createsettings.add_argument(
|
||||
'--local-installation',
|
||||
action='store_true',
|
||||
help='Store settings and user files in a local directory.')
|
||||
"--local-installation",
|
||||
action="store_true",
|
||||
help="Store settings and user files in a local directory.",
|
||||
)
|
||||
|
||||
# Help text for several Django subcommands
|
||||
django_subcommands = (
|
||||
('backupdb', 'Backups the SQLite3 database.'),
|
||||
('createsuperuser', 'Creates or resets the admin user.'),
|
||||
('migrate', 'Updates database schema.'),
|
||||
('runserver', 'Starts the Tornado webserver.'),
|
||||
("backupdb", "Backups the SQLite3 database."),
|
||||
("createsuperuser", "Creates or resets the admin user."),
|
||||
("migrate", "Updates database schema."),
|
||||
("runserver", "Starts the Tornado webserver."),
|
||||
)
|
||||
for django_subcommand, help_text in django_subcommands:
|
||||
subparsers._choices_actions.append( # type: ignore
|
||||
subparsers._ChoicesPseudoAction( # type: ignore
|
||||
django_subcommand,
|
||||
(),
|
||||
help_text))
|
||||
django_subcommand, (), help_text
|
||||
)
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
@ -188,8 +195,10 @@ def start(args):
|
||||
"""
|
||||
Starts OpenSlides: Runs migrations and runs runserver.
|
||||
"""
|
||||
raise OpenSlidesError('The start command does not work anymore. ' +
|
||||
'Please use `createsettings`, `migrate` and `runserver`.')
|
||||
raise OpenSlidesError(
|
||||
"The start command does not work anymore. "
|
||||
+ "Please use `createsettings`, `migrate` and `runserver`."
|
||||
)
|
||||
settings_dir = args.settings_dir
|
||||
settings_filename = args.settings_filename
|
||||
local_installation = is_local_installation()
|
||||
@ -212,10 +221,10 @@ def start(args):
|
||||
from django.conf import settings
|
||||
|
||||
if args.debug_email:
|
||||
settings.EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
settings.EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
# Migrate database
|
||||
call_command('migrate')
|
||||
call_command("migrate")
|
||||
|
||||
# Open the browser
|
||||
if not args.no_browser:
|
||||
@ -229,8 +238,8 @@ def start(args):
|
||||
#
|
||||
# Use flag --insecure to serve static files even if DEBUG is False.
|
||||
call_command(
|
||||
'runserver',
|
||||
'{}:{}'.format(args.host, args.port),
|
||||
"runserver",
|
||||
"{}:{}".format(args.host, args.port),
|
||||
noreload=False, # Means True, see above.
|
||||
insecure=True,
|
||||
)
|
||||
@ -248,11 +257,14 @@ def createsettings(args):
|
||||
if settings_dir is None:
|
||||
settings_dir = get_local_settings_dir()
|
||||
context = {
|
||||
'openslides_user_data_dir': repr(os.path.join(os.getcwd(), 'personal_data', 'var')),
|
||||
'debug': 'True'}
|
||||
"openslides_user_data_dir": repr(
|
||||
os.path.join(os.getcwd(), "personal_data", "var")
|
||||
),
|
||||
"debug": "True",
|
||||
}
|
||||
|
||||
settings_path = write_settings(settings_dir, args.settings_filename, **context)
|
||||
print('Settings created at %s' % settings_path)
|
||||
print("Settings created at %s" % settings_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -1 +1 @@
|
||||
default_app_config = 'openslides.agenda.apps.AgendaAppConfig'
|
||||
default_app_config = "openslides.agenda.apps.AgendaAppConfig"
|
||||
|
@ -8,14 +8,14 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for Item and ItemViewSet.
|
||||
"""
|
||||
base_permission = 'agenda.can_see'
|
||||
|
||||
base_permission = "agenda.can_see"
|
||||
|
||||
# TODO: In the following method we use full_data['is_hidden'] and
|
||||
# full_data['is_internal'] but this can be out of date.
|
||||
async def get_restricted_data(
|
||||
self,
|
||||
full_data: List[Dict[str, Any]],
|
||||
user_id: int) -> List[Dict[str, Any]]:
|
||||
self, full_data: List[Dict[str, Any]], user_id: int
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns the restricted serialized data for the instance prepared
|
||||
for the user.
|
||||
@ -25,6 +25,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
||||
We remove comments for non admins/managers and a lot of fields of
|
||||
internal items for users without permission to see internal items.
|
||||
"""
|
||||
|
||||
def filtered_data(full_data, blocked_keys):
|
||||
"""
|
||||
Returns a new dict like full_data but with all blocked_keys removed.
|
||||
@ -33,47 +34,56 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
||||
return {key: full_data[key] for key in whitelist}
|
||||
|
||||
# Parse data.
|
||||
if full_data and await async_has_perm(user_id, 'agenda.can_see'):
|
||||
if await async_has_perm(user_id, 'agenda.can_manage') and await async_has_perm(user_id, 'agenda.can_see_internal_items'):
|
||||
if full_data and await async_has_perm(user_id, "agenda.can_see"):
|
||||
if await async_has_perm(
|
||||
user_id, "agenda.can_manage"
|
||||
) and await async_has_perm(user_id, "agenda.can_see_internal_items"):
|
||||
# Managers with special permission can see everything.
|
||||
data = full_data
|
||||
elif await async_has_perm(user_id, 'agenda.can_see_internal_items'):
|
||||
elif await async_has_perm(user_id, "agenda.can_see_internal_items"):
|
||||
# Non managers with special permission can see everything but
|
||||
# comments and hidden items.
|
||||
data = [full for full in full_data if not full['is_hidden']] # filter hidden items
|
||||
blocked_keys = ('comment',)
|
||||
data = [filtered_data(full, blocked_keys) for full in data] # remove blocked_keys
|
||||
data = [
|
||||
full for full in full_data if not full["is_hidden"]
|
||||
] # filter hidden items
|
||||
blocked_keys = ("comment",)
|
||||
data = [
|
||||
filtered_data(full, blocked_keys) for full in data
|
||||
] # remove blocked_keys
|
||||
else:
|
||||
# Users without special permission for internal items.
|
||||
|
||||
# In internal and hidden case managers and non managers see only some fields
|
||||
# so that list of speakers is provided regardless. Hidden items can only be seen by managers.
|
||||
# We know that full_data has at least one entry which can be used to parse the keys.
|
||||
blocked_keys_internal_hidden_case = set(full_data[0].keys()) - set((
|
||||
'id',
|
||||
'title',
|
||||
'speakers',
|
||||
'speaker_list_closed',
|
||||
'content_object'))
|
||||
blocked_keys_internal_hidden_case = set(full_data[0].keys()) - set(
|
||||
("id", "title", "speakers", "speaker_list_closed", "content_object")
|
||||
)
|
||||
|
||||
# In non internal case managers see everything and non managers see
|
||||
# everything but comments.
|
||||
if await async_has_perm(user_id, 'agenda.can_manage'):
|
||||
if await async_has_perm(user_id, "agenda.can_manage"):
|
||||
blocked_keys_non_internal_hidden_case: Iterable[str] = []
|
||||
can_see_hidden = True
|
||||
else:
|
||||
blocked_keys_non_internal_hidden_case = ('comment',)
|
||||
blocked_keys_non_internal_hidden_case = ("comment",)
|
||||
can_see_hidden = False
|
||||
|
||||
data = []
|
||||
for full in full_data:
|
||||
if full['is_hidden'] and can_see_hidden:
|
||||
if full["is_hidden"] and can_see_hidden:
|
||||
# Same filtering for internal and hidden items
|
||||
data.append(filtered_data(full, blocked_keys_internal_hidden_case))
|
||||
elif full['is_internal']:
|
||||
data.append(filtered_data(full, blocked_keys_internal_hidden_case))
|
||||
data.append(
|
||||
filtered_data(full, blocked_keys_internal_hidden_case)
|
||||
)
|
||||
elif full["is_internal"]:
|
||||
data.append(
|
||||
filtered_data(full, blocked_keys_internal_hidden_case)
|
||||
)
|
||||
else: # agenda item
|
||||
data.append(filtered_data(full, blocked_keys_non_internal_hidden_case))
|
||||
data.append(
|
||||
filtered_data(full, blocked_keys_non_internal_hidden_case)
|
||||
)
|
||||
else:
|
||||
data = []
|
||||
|
||||
|
@ -6,8 +6,8 @@ from ..utils.projector import register_projector_elements
|
||||
|
||||
|
||||
class AgendaAppConfig(AppConfig):
|
||||
name = 'openslides.agenda'
|
||||
verbose_name = 'OpenSlides Agenda'
|
||||
name = "openslides.agenda"
|
||||
verbose_name = "OpenSlides Agenda"
|
||||
angular_site_module = True
|
||||
angular_projector_module = True
|
||||
|
||||
@ -20,7 +20,8 @@ class AgendaAppConfig(AppConfig):
|
||||
from .signals import (
|
||||
get_permission_change_data,
|
||||
listen_to_related_object_post_delete,
|
||||
listen_to_related_object_post_save)
|
||||
listen_to_related_object_post_save,
|
||||
)
|
||||
from .views import ItemViewSet
|
||||
from . import serializers # noqa
|
||||
from ..utils.access_permissions import required_user
|
||||
@ -31,22 +32,27 @@ class AgendaAppConfig(AppConfig):
|
||||
# Connect signals.
|
||||
post_save.connect(
|
||||
listen_to_related_object_post_save,
|
||||
dispatch_uid='listen_to_related_object_post_save')
|
||||
dispatch_uid="listen_to_related_object_post_save",
|
||||
)
|
||||
pre_delete.connect(
|
||||
listen_to_related_object_post_delete,
|
||||
dispatch_uid='listen_to_related_object_post_delete')
|
||||
dispatch_uid="listen_to_related_object_post_delete",
|
||||
)
|
||||
permission_change.connect(
|
||||
get_permission_change_data,
|
||||
dispatch_uid='agenda_get_permission_change_data')
|
||||
get_permission_change_data, dispatch_uid="agenda_get_permission_change_data"
|
||||
)
|
||||
|
||||
# Register viewsets.
|
||||
router.register(self.get_model('Item').get_collection_string(), ItemViewSet)
|
||||
router.register(self.get_model("Item").get_collection_string(), ItemViewSet)
|
||||
|
||||
# register required_users
|
||||
required_user.add_collection_string(self.get_model('Item').get_collection_string(), required_users)
|
||||
required_user.add_collection_string(
|
||||
self.get_model("Item").get_collection_string(), required_users
|
||||
)
|
||||
|
||||
def get_config_variables(self):
|
||||
from .config_variables import get_config_variables
|
||||
|
||||
return get_config_variables()
|
||||
|
||||
def get_startup_elements(self):
|
||||
@ -54,11 +60,11 @@ class AgendaAppConfig(AppConfig):
|
||||
Yields all Cachables required on startup i. e. opening the websocket
|
||||
connection.
|
||||
"""
|
||||
yield self.get_model('Item')
|
||||
yield self.get_model("Item")
|
||||
|
||||
|
||||
def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||
"""
|
||||
Returns all user ids that are displayed as speaker in the given element.
|
||||
"""
|
||||
return set(speaker['user_id'] for speaker in element['speakers'])
|
||||
return set(speaker["user_id"] for speaker in element["speakers"])
|
||||
|
@ -10,97 +10,108 @@ def get_config_variables():
|
||||
It has to be evaluated during app loading (see apps.py).
|
||||
"""
|
||||
yield ConfigVariable(
|
||||
name='agenda_enable_numbering',
|
||||
label='Enable numbering for agenda items',
|
||||
input_type='boolean',
|
||||
name="agenda_enable_numbering",
|
||||
label="Enable numbering for agenda items",
|
||||
input_type="boolean",
|
||||
default_value=True,
|
||||
weight=200,
|
||||
group='Agenda',
|
||||
subgroup='General')
|
||||
group="Agenda",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='agenda_number_prefix',
|
||||
default_value='',
|
||||
label='Numbering prefix for agenda items',
|
||||
help_text='This prefix will be set if you run the automatic agenda numbering.',
|
||||
name="agenda_number_prefix",
|
||||
default_value="",
|
||||
label="Numbering prefix for agenda items",
|
||||
help_text="This prefix will be set if you run the automatic agenda numbering.",
|
||||
weight=210,
|
||||
group='Agenda',
|
||||
subgroup='General',
|
||||
validators=(MaxLengthValidator(20),))
|
||||
group="Agenda",
|
||||
subgroup="General",
|
||||
validators=(MaxLengthValidator(20),),
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='agenda_numeral_system',
|
||||
default_value='arabic',
|
||||
input_type='choice',
|
||||
label='Numeral system for agenda items',
|
||||
name="agenda_numeral_system",
|
||||
default_value="arabic",
|
||||
input_type="choice",
|
||||
label="Numeral system for agenda items",
|
||||
choices=(
|
||||
{'value': 'arabic', 'display_name': 'Arabic'},
|
||||
{'value': 'roman', 'display_name': 'Roman'}),
|
||||
{"value": "arabic", "display_name": "Arabic"},
|
||||
{"value": "roman", "display_name": "Roman"},
|
||||
),
|
||||
weight=215,
|
||||
group='Agenda',
|
||||
subgroup='General')
|
||||
group="Agenda",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='agenda_start_event_date_time',
|
||||
name="agenda_start_event_date_time",
|
||||
default_value=None,
|
||||
input_type='datetimepicker',
|
||||
label='Begin of event',
|
||||
help_text='Input format: DD.MM.YYYY HH:MM',
|
||||
input_type="datetimepicker",
|
||||
label="Begin of event",
|
||||
help_text="Input format: DD.MM.YYYY HH:MM",
|
||||
weight=220,
|
||||
group='Agenda',
|
||||
subgroup='General')
|
||||
group="Agenda",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='agenda_hide_internal_items_on_projector',
|
||||
name="agenda_hide_internal_items_on_projector",
|
||||
default_value=True,
|
||||
input_type='boolean',
|
||||
label='Hide internal items when projecting subitems',
|
||||
input_type="boolean",
|
||||
label="Hide internal items when projecting subitems",
|
||||
weight=225,
|
||||
group='Agenda',
|
||||
subgroup='General')
|
||||
group="Agenda",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='agenda_new_items_default_visibility',
|
||||
default_value='2',
|
||||
input_type='choice',
|
||||
name="agenda_new_items_default_visibility",
|
||||
default_value="2",
|
||||
input_type="choice",
|
||||
choices=(
|
||||
{'value': '1', 'display_name': 'Public item'},
|
||||
{'value': '2', 'display_name': 'Internal item'},
|
||||
{'value': '3', 'display_name': 'Hidden item'}),
|
||||
label='Default visibility for new agenda items (except topics)',
|
||||
{"value": "1", "display_name": "Public item"},
|
||||
{"value": "2", "display_name": "Internal item"},
|
||||
{"value": "3", "display_name": "Hidden item"},
|
||||
),
|
||||
label="Default visibility for new agenda items (except topics)",
|
||||
weight=227,
|
||||
group='Agenda',
|
||||
subgroup='General')
|
||||
group="Agenda",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
# List of speakers
|
||||
|
||||
yield ConfigVariable(
|
||||
name='agenda_show_last_speakers',
|
||||
name="agenda_show_last_speakers",
|
||||
default_value=1,
|
||||
input_type='integer',
|
||||
label='Number of last speakers to be shown on the projector',
|
||||
input_type="integer",
|
||||
label="Number of last speakers to be shown on the projector",
|
||||
weight=230,
|
||||
group='Agenda',
|
||||
subgroup='List of speakers',
|
||||
validators=(MinValueValidator(0),))
|
||||
group="Agenda",
|
||||
subgroup="List of speakers",
|
||||
validators=(MinValueValidator(0),),
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='agenda_countdown_warning_time',
|
||||
name="agenda_countdown_warning_time",
|
||||
default_value=0,
|
||||
input_type='integer',
|
||||
label='Show orange countdown in the last x seconds of speaking time',
|
||||
help_text='Enter duration in seconds. Choose 0 to disable warning color.',
|
||||
input_type="integer",
|
||||
label="Show orange countdown in the last x seconds of speaking time",
|
||||
help_text="Enter duration in seconds. Choose 0 to disable warning color.",
|
||||
weight=235,
|
||||
group='Agenda',
|
||||
subgroup='List of speakers',
|
||||
validators=(MinValueValidator(0),))
|
||||
group="Agenda",
|
||||
subgroup="List of speakers",
|
||||
validators=(MinValueValidator(0),),
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='agenda_couple_countdown_and_speakers',
|
||||
name="agenda_couple_countdown_and_speakers",
|
||||
default_value=False,
|
||||
input_type='boolean',
|
||||
label='Couple countdown with the list of speakers',
|
||||
help_text='[Begin speech] starts the countdown, [End speech] stops the countdown.',
|
||||
input_type="boolean",
|
||||
label="Couple countdown with the list of speakers",
|
||||
help_text="[Begin speech] starts the countdown, [End speech] stops the countdown.",
|
||||
weight=240,
|
||||
group='Agenda',
|
||||
subgroup='List of speakers')
|
||||
group="Agenda",
|
||||
subgroup="List of speakers",
|
||||
)
|
||||
|
@ -14,55 +14,109 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Item',
|
||||
name="Item",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('item_number', models.CharField(blank=True, max_length=255)),
|
||||
('comment', models.TextField(blank=True, null=True)),
|
||||
('closed', models.BooleanField(default=False)),
|
||||
('type', models.IntegerField(choices=[(1, 'Agenda item'), (2, 'Hidden item')], default=2)),
|
||||
('duration', models.CharField(blank=True, max_length=5, null=True)),
|
||||
('weight', models.IntegerField(default=10000)),
|
||||
('object_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('speaker_list_closed', models.BooleanField(default=False)),
|
||||
('content_type', models.ForeignKey(
|
||||
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contenttypes.ContentType')),
|
||||
('parent', models.ForeignKey(
|
||||
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='agenda.Item')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("item_number", models.CharField(blank=True, max_length=255)),
|
||||
("comment", models.TextField(blank=True, null=True)),
|
||||
("closed", models.BooleanField(default=False)),
|
||||
(
|
||||
"type",
|
||||
models.IntegerField(
|
||||
choices=[(1, "Agenda item"), (2, "Hidden item")], default=2
|
||||
),
|
||||
),
|
||||
("duration", models.CharField(blank=True, max_length=5, null=True)),
|
||||
("weight", models.IntegerField(default=10000)),
|
||||
("object_id", models.PositiveIntegerField(blank=True, null=True)),
|
||||
("speaker_list_closed", models.BooleanField(default=False)),
|
||||
(
|
||||
"content_type",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="contenttypes.ContentType",
|
||||
),
|
||||
),
|
||||
(
|
||||
"parent",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="children",
|
||||
to="agenda.Item",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'permissions': (
|
||||
('can_see', 'Can see agenda'),
|
||||
('can_manage', 'Can manage agenda'),
|
||||
('can_see_hidden_items', 'Can see hidden items and time scheduling of agenda')),
|
||||
'default_permissions': (),
|
||||
"permissions": (
|
||||
("can_see", "Can see agenda"),
|
||||
("can_manage", "Can manage agenda"),
|
||||
(
|
||||
"can_see_hidden_items",
|
||||
"Can see hidden items and time scheduling of agenda",
|
||||
),
|
||||
),
|
||||
"default_permissions": (),
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Speaker',
|
||||
name="Speaker",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('begin_time', models.DateTimeField(null=True)),
|
||||
('end_time', models.DateTimeField(null=True)),
|
||||
('weight', models.IntegerField(null=True)),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='speakers', to='agenda.Item')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("begin_time", models.DateTimeField(null=True)),
|
||||
("end_time", models.DateTimeField(null=True)),
|
||||
("weight", models.IntegerField(null=True)),
|
||||
(
|
||||
"item",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="speakers",
|
||||
to="agenda.Item",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'permissions': (('can_be_speaker', 'Can put oneself on the list of speakers'),),
|
||||
'default_permissions': (),
|
||||
"permissions": (
|
||||
("can_be_speaker", "Can put oneself on the list of speakers"),
|
||||
),
|
||||
"default_permissions": (),
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='item',
|
||||
unique_together=set([('content_type', 'object_id')]),
|
||||
name="item", unique_together=set([("content_type", "object_id")])
|
||||
),
|
||||
]
|
||||
|
@ -11,7 +11,7 @@ def convert_duration(apps, schema_editor):
|
||||
IntegerField. It uses the temporary field for proper renaming the field
|
||||
in the end.
|
||||
"""
|
||||
Item = apps.get_model('agenda', 'Item')
|
||||
Item = apps.get_model("agenda", "Item")
|
||||
for item in Item.objects.all():
|
||||
duration = item.duration
|
||||
item.duration_tmp = None
|
||||
@ -20,7 +20,7 @@ def convert_duration(apps, schema_editor):
|
||||
item.duration_tmp = int(duration)
|
||||
elif isinstance(duration, str):
|
||||
# Assuming format (h)h:(m)m. If not, new value is None.
|
||||
split = duration.split(':')
|
||||
split = duration.split(":")
|
||||
if len(split) == 2 and is_int(split[0]) and is_int(split[1]):
|
||||
# Calculate new duration: hours * 60 + minutes.
|
||||
item.duration_tmp = int(split[0]) * 60 + int(split[1])
|
||||
@ -41,26 +41,17 @@ def is_int(s):
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agenda', '0001_initial'),
|
||||
]
|
||||
dependencies = [("agenda", "0001_initial")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='duration_tmp',
|
||||
model_name="item",
|
||||
name="duration_tmp",
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.RunPython(
|
||||
convert_duration
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='item',
|
||||
name='duration',
|
||||
),
|
||||
migrations.RunPython(convert_duration),
|
||||
migrations.RemoveField(model_name="item", name="duration"),
|
||||
migrations.RenameField(
|
||||
model_name='item',
|
||||
old_name='duration_tmp',
|
||||
new_name='duration',
|
||||
model_name="item", old_name="duration_tmp", new_name="duration"
|
||||
),
|
||||
]
|
||||
|
@ -11,24 +11,31 @@ from openslides.utils.migrations import (
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agenda', '0002_item_duration'),
|
||||
]
|
||||
dependencies = [("agenda", "0002_item_duration")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='item',
|
||||
name="item",
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'permissions': (
|
||||
('can_see', 'Can see agenda'),
|
||||
('can_manage', 'Can manage agenda'),
|
||||
('can_manage_list_of_speakers', 'Can manage list of speakers'),
|
||||
('can_see_hidden_items', 'Can see hidden items and time scheduling of agenda')
|
||||
)
|
||||
"default_permissions": (),
|
||||
"permissions": (
|
||||
("can_see", "Can see agenda"),
|
||||
("can_manage", "Can manage agenda"),
|
||||
("can_manage_list_of_speakers", "Can manage list of speakers"),
|
||||
(
|
||||
"can_see_hidden_items",
|
||||
"Can see hidden items and time scheduling of agenda",
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
migrations.RunPython(add_permission_to_groups_based_on_existing_permission(
|
||||
'can_manage', 'item', 'agenda', 'can_manage_list_of_speakers', 'Can manage list of speakers'
|
||||
)),
|
||||
migrations.RunPython(
|
||||
add_permission_to_groups_based_on_existing_permission(
|
||||
"can_manage",
|
||||
"item",
|
||||
"agenda",
|
||||
"can_manage_list_of_speakers",
|
||||
"Can manage list of speakers",
|
||||
)
|
||||
),
|
||||
]
|
||||
|
@ -7,14 +7,12 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agenda', '0003_auto_20170818_1202'),
|
||||
]
|
||||
dependencies = [("agenda", "0003_auto_20170818_1202")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='speaker',
|
||||
name='marked',
|
||||
model_name="speaker",
|
||||
name="marked",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
)
|
||||
]
|
||||
|
@ -11,44 +11,47 @@ from openslides.utils.migrations import (
|
||||
|
||||
|
||||
def delete_old_can_see_hidden_permission(apps, schema_editor):
|
||||
perm = Permission.objects.filter(codename='can_see_hidden_items')
|
||||
perm = Permission.objects.filter(codename="can_see_hidden_items")
|
||||
if len(perm):
|
||||
perm = perm.delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agenda', '0004_speaker_marked'),
|
||||
]
|
||||
dependencies = [("agenda", "0004_speaker_marked")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='item',
|
||||
name="item",
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'permissions': (
|
||||
('can_see', 'Can see agenda'),
|
||||
('can_manage', 'Can manage agenda'),
|
||||
('can_manage_list_of_speakers', 'Can manage list of speakers'),
|
||||
('can_see_internal_items', 'Can see internal items and time scheduling of agenda')
|
||||
)
|
||||
"default_permissions": (),
|
||||
"permissions": (
|
||||
("can_see", "Can see agenda"),
|
||||
("can_manage", "Can manage agenda"),
|
||||
("can_manage_list_of_speakers", "Can manage list of speakers"),
|
||||
(
|
||||
"can_see_internal_items",
|
||||
"Can see internal items and time scheduling of agenda",
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='item',
|
||||
name='type',
|
||||
model_name="item",
|
||||
name="type",
|
||||
field=models.IntegerField(
|
||||
choices=[
|
||||
(1, 'Agenda item'),
|
||||
(2, 'Internal item'),
|
||||
(3, 'Hidden item')
|
||||
],
|
||||
default=3
|
||||
choices=[(1, "Agenda item"), (2, "Internal item"), (3, "Hidden item")],
|
||||
default=3,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(add_permission_to_groups_based_on_existing_permission(
|
||||
'can_see_hidden_items', 'item', 'agenda', 'can_see_internal_items', 'Can see internal items and time scheduling of agenda'
|
||||
)),
|
||||
migrations.RunPython(
|
||||
add_permission_to_groups_based_on_existing_permission(
|
||||
"can_see_hidden_items",
|
||||
"item",
|
||||
"agenda",
|
||||
"can_see_internal_items",
|
||||
"Can see internal items and time scheduling of agenda",
|
||||
)
|
||||
),
|
||||
migrations.RunPython(delete_old_can_see_hidden_permission),
|
||||
]
|
||||
|
@ -24,13 +24,14 @@ class ItemManager(models.Manager):
|
||||
Customized model manager with special methods for agenda tree and
|
||||
numbering.
|
||||
"""
|
||||
|
||||
def get_full_queryset(self):
|
||||
"""
|
||||
Returns the normal queryset with all items. In the background all
|
||||
speakers and related items (topics, motions, assignments) are
|
||||
prefetched from the database.
|
||||
"""
|
||||
return self.get_queryset().prefetch_related('speakers', 'content_object')
|
||||
return self.get_queryset().prefetch_related("speakers", "content_object")
|
||||
|
||||
def get_only_non_public_items(self):
|
||||
"""
|
||||
@ -45,14 +46,17 @@ class ItemManager(models.Manager):
|
||||
Generator that yields a list of items and their children.
|
||||
"""
|
||||
for item in items:
|
||||
if parent_is_not_public or item.type in (item.INTERNAL_ITEM, item.HIDDEN_ITEM):
|
||||
if parent_is_not_public or item.type in (
|
||||
item.INTERNAL_ITEM,
|
||||
item.HIDDEN_ITEM,
|
||||
):
|
||||
item_is_not_public = True
|
||||
yield item
|
||||
else:
|
||||
item_is_not_public = False
|
||||
yield from yield_items(
|
||||
item_children[item.pk],
|
||||
parent_is_not_public=item_is_not_public)
|
||||
item_children[item.pk], parent_is_not_public=item_is_not_public
|
||||
)
|
||||
|
||||
yield from yield_items(root_items)
|
||||
|
||||
@ -64,7 +68,7 @@ class ItemManager(models.Manager):
|
||||
If only_item_type is given, the tree hides items with other types and
|
||||
all of their children.
|
||||
"""
|
||||
queryset = self.order_by('weight')
|
||||
queryset = self.order_by("weight")
|
||||
item_children: Dict[int, List[Item]] = defaultdict(list)
|
||||
root_items = []
|
||||
for item in queryset:
|
||||
@ -88,7 +92,9 @@ class ItemManager(models.Manager):
|
||||
If include_content is True, the yielded dictonaries have no key 'id'
|
||||
but a key 'item' with the entire object.
|
||||
"""
|
||||
root_items, item_children = self.get_root_and_children(only_item_type=only_item_type)
|
||||
root_items, item_children = self.get_root_and_children(
|
||||
only_item_type=only_item_type
|
||||
)
|
||||
|
||||
def get_children(items):
|
||||
"""
|
||||
@ -98,7 +104,9 @@ class ItemManager(models.Manager):
|
||||
if include_content:
|
||||
yield dict(item=item, children=get_children(item_children[item.pk]))
|
||||
else:
|
||||
yield dict(id=item.pk, children=get_children(item_children[item.pk]))
|
||||
yield dict(
|
||||
id=item.pk, children=get_children(item_children[item.pk])
|
||||
)
|
||||
|
||||
yield from get_children(root_items)
|
||||
|
||||
@ -110,6 +118,7 @@ class ItemManager(models.Manager):
|
||||
The tree has to be a nested object. For example:
|
||||
[{"id": 1}, {"id": 2, "children": [{"id": 3}]}]
|
||||
"""
|
||||
|
||||
def walk_items(tree, parent=None):
|
||||
"""
|
||||
Generator that returns each item in the tree as tuple.
|
||||
@ -118,15 +127,17 @@ class ItemManager(models.Manager):
|
||||
weight of the item.
|
||||
"""
|
||||
for weight, element in enumerate(tree):
|
||||
yield (element['id'], parent, weight)
|
||||
yield from walk_items(element.get('children', []), element['id'])
|
||||
yield (element["id"], parent, weight)
|
||||
yield from walk_items(element.get("children", []), element["id"])
|
||||
|
||||
touched_items: Set[int] = set()
|
||||
db_items = dict((item.pk, item) for item in Item.objects.all())
|
||||
for item_id, parent_id, weight in walk_items(tree):
|
||||
# Check that the item is only once in the tree to prevent invalid trees
|
||||
if item_id in touched_items:
|
||||
raise ValueError("Item {} is more then once in the tree.".format(item_id))
|
||||
raise ValueError(
|
||||
"Item {} is more then once in the tree.".format(item_id)
|
||||
)
|
||||
touched_items.add(item_id)
|
||||
|
||||
try:
|
||||
@ -143,36 +154,40 @@ class ItemManager(models.Manager):
|
||||
db_item.save()
|
||||
|
||||
@transaction.atomic
|
||||
def number_all(self, numeral_system='arabic'):
|
||||
def number_all(self, numeral_system="arabic"):
|
||||
"""
|
||||
Auto numbering of the agenda according to the numeral_system. Manually
|
||||
added item numbers will be overwritten.
|
||||
"""
|
||||
|
||||
def walk_tree(tree, number=None):
|
||||
for index, tree_element in enumerate(tree):
|
||||
# Calculate number of visable agenda items.
|
||||
if numeral_system == 'roman' and number is None:
|
||||
if numeral_system == "roman" and number is None:
|
||||
item_number = to_roman(index + 1)
|
||||
else:
|
||||
item_number = str(index + 1)
|
||||
if number is not None:
|
||||
item_number = '.'.join((number, item_number))
|
||||
item_number = ".".join((number, item_number))
|
||||
# Add prefix.
|
||||
if config['agenda_number_prefix']:
|
||||
item_number_tmp = "%s %s" % (config['agenda_number_prefix'], item_number)
|
||||
if config["agenda_number_prefix"]:
|
||||
item_number_tmp = "%s %s" % (
|
||||
config["agenda_number_prefix"],
|
||||
item_number,
|
||||
)
|
||||
else:
|
||||
item_number_tmp = item_number
|
||||
# Save the new value and go down the tree.
|
||||
tree_element['item'].item_number = item_number_tmp
|
||||
tree_element['item'].save()
|
||||
walk_tree(tree_element['children'], item_number)
|
||||
tree_element["item"].item_number = item_number_tmp
|
||||
tree_element["item"].save()
|
||||
walk_tree(tree_element["children"], item_number)
|
||||
|
||||
# Start numbering visable agenda items.
|
||||
walk_tree(self.get_tree(only_item_type=Item.AGENDA_ITEM, include_content=True))
|
||||
|
||||
# Reset number of hidden items.
|
||||
for item in self.get_only_non_public_items():
|
||||
item.item_number = ''
|
||||
item.item_number = ""
|
||||
item.save()
|
||||
|
||||
|
||||
@ -180,18 +195,20 @@ class Item(RESTModelMixin, models.Model):
|
||||
"""
|
||||
An Agenda Item
|
||||
"""
|
||||
|
||||
access_permissions = ItemAccessPermissions()
|
||||
objects = ItemManager()
|
||||
can_see_permission = 'agenda.can_see'
|
||||
can_see_permission = "agenda.can_see"
|
||||
|
||||
AGENDA_ITEM = 1
|
||||
INTERNAL_ITEM = 2
|
||||
HIDDEN_ITEM = 3
|
||||
|
||||
ITEM_TYPE = (
|
||||
(AGENDA_ITEM, ugettext_lazy('Agenda item')),
|
||||
(INTERNAL_ITEM, ugettext_lazy('Internal item')),
|
||||
(HIDDEN_ITEM, ugettext_lazy('Hidden item')))
|
||||
(AGENDA_ITEM, ugettext_lazy("Agenda item")),
|
||||
(INTERNAL_ITEM, ugettext_lazy("Internal item")),
|
||||
(HIDDEN_ITEM, ugettext_lazy("Hidden item")),
|
||||
)
|
||||
|
||||
item_number = models.CharField(blank=True, max_length=255)
|
||||
"""
|
||||
@ -208,9 +225,7 @@ class Item(RESTModelMixin, models.Model):
|
||||
Flag, if the item is finished.
|
||||
"""
|
||||
|
||||
type = models.IntegerField(
|
||||
choices=ITEM_TYPE,
|
||||
default=HIDDEN_ITEM)
|
||||
type = models.IntegerField(choices=ITEM_TYPE, default=HIDDEN_ITEM)
|
||||
"""
|
||||
Type of the agenda item.
|
||||
|
||||
@ -223,11 +238,12 @@ class Item(RESTModelMixin, models.Model):
|
||||
"""
|
||||
|
||||
parent = models.ForeignKey(
|
||||
'self',
|
||||
"self",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='children')
|
||||
related_name="children",
|
||||
)
|
||||
"""
|
||||
The parent item in the agenda tree.
|
||||
"""
|
||||
@ -238,10 +254,8 @@ class Item(RESTModelMixin, models.Model):
|
||||
"""
|
||||
|
||||
content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True)
|
||||
ContentType, on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
"""
|
||||
Field for generic relation to a related object. Type of the object.
|
||||
"""
|
||||
@ -256,8 +270,7 @@ class Item(RESTModelMixin, models.Model):
|
||||
Field for generic relation to a related object. General field to the related object.
|
||||
"""
|
||||
|
||||
speaker_list_closed = models.BooleanField(
|
||||
default=False)
|
||||
speaker_list_closed = models.BooleanField(default=False)
|
||||
"""
|
||||
True, if the list of speakers is closed.
|
||||
"""
|
||||
@ -265,11 +278,15 @@ class Item(RESTModelMixin, models.Model):
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
permissions = (
|
||||
('can_see', 'Can see agenda'),
|
||||
('can_manage', 'Can manage agenda'),
|
||||
('can_manage_list_of_speakers', 'Can manage list of speakers'),
|
||||
('can_see_internal_items', 'Can see internal items and time scheduling of agenda'))
|
||||
unique_together = ('content_type', 'object_id')
|
||||
("can_see", "Can see agenda"),
|
||||
("can_manage", "Can manage agenda"),
|
||||
("can_manage_list_of_speakers", "Can manage list of speakers"),
|
||||
(
|
||||
"can_see_internal_items",
|
||||
"Can see internal items and time scheduling of agenda",
|
||||
),
|
||||
)
|
||||
unique_together = ("content_type", "object_id")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@ -280,10 +297,11 @@ class Item(RESTModelMixin, models.Model):
|
||||
list of speakers projector element is disabled.
|
||||
"""
|
||||
Projector.remove_any(
|
||||
skip_autoupdate=skip_autoupdate,
|
||||
name='agenda/list-of-speakers',
|
||||
id=self.pk)
|
||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, name="agenda/list-of-speakers", id=self.pk
|
||||
)
|
||||
return super().delete( # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||
)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
@ -293,8 +311,10 @@ class Item(RESTModelMixin, models.Model):
|
||||
try:
|
||||
return self.content_object.get_agenda_title()
|
||||
except AttributeError:
|
||||
raise NotImplementedError('You have to provide a get_agenda_title '
|
||||
'method on your related model.')
|
||||
raise NotImplementedError(
|
||||
"You have to provide a get_agenda_title "
|
||||
"method on your related model."
|
||||
)
|
||||
|
||||
@property
|
||||
def title_with_type(self):
|
||||
@ -304,8 +324,10 @@ class Item(RESTModelMixin, models.Model):
|
||||
try:
|
||||
return self.content_object.get_agenda_title_with_type()
|
||||
except AttributeError:
|
||||
raise NotImplementedError('You have to provide a get_agenda_title_with_type '
|
||||
'method on your related model.')
|
||||
raise NotImplementedError(
|
||||
"You have to provide a get_agenda_title_with_type "
|
||||
"method on your related model."
|
||||
)
|
||||
|
||||
def is_internal(self):
|
||||
"""
|
||||
@ -314,8 +336,9 @@ class Item(RESTModelMixin, models.Model):
|
||||
|
||||
Attention! This executes one query for each ancestor of the item.
|
||||
"""
|
||||
return (self.type == self.INTERNAL_ITEM or
|
||||
(self.parent is not None and self.parent.is_internal()))
|
||||
return self.type == self.INTERNAL_ITEM or (
|
||||
self.parent is not None and self.parent.is_internal()
|
||||
)
|
||||
|
||||
def is_hidden(self):
|
||||
"""
|
||||
@ -324,15 +347,16 @@ class Item(RESTModelMixin, models.Model):
|
||||
|
||||
Attention! This executes one query for each ancestor of the item.
|
||||
"""
|
||||
return (self.type == self.HIDDEN_ITEM or
|
||||
(self.parent is not None and self.parent.is_hidden()))
|
||||
return self.type == self.HIDDEN_ITEM or (
|
||||
self.parent is not None and self.parent.is_hidden()
|
||||
)
|
||||
|
||||
def get_next_speaker(self):
|
||||
"""
|
||||
Returns the speaker object of the speaker who is next.
|
||||
"""
|
||||
try:
|
||||
return self.speakers.filter(begin_time=None).order_by('weight')[0]
|
||||
return self.speakers.filter(begin_time=None).order_by("weight")[0]
|
||||
except IndexError:
|
||||
# The list of speakers is empty.
|
||||
return None
|
||||
@ -342,6 +366,7 @@ class SpeakerManager(models.Manager):
|
||||
"""
|
||||
Manager for Speaker model. Provides a customized add method.
|
||||
"""
|
||||
|
||||
def add(self, user, item, skip_autoupdate=False):
|
||||
"""
|
||||
Customized manager method to prevent anonymous users to be on the
|
||||
@ -350,12 +375,15 @@ class SpeakerManager(models.Manager):
|
||||
"""
|
||||
if self.filter(user=user, item=item, begin_time=None).exists():
|
||||
raise OpenSlidesError(
|
||||
_('{user} is already on the list of speakers.').format(user=user))
|
||||
_("{user} is already on the list of speakers.").format(user=user)
|
||||
)
|
||||
if isinstance(user, AnonymousUser):
|
||||
raise OpenSlidesError(
|
||||
_('An anonymous user can not be on lists of speakers.'))
|
||||
weight = (self.filter(item=item).aggregate(
|
||||
models.Max('weight'))['weight__max'] or 0)
|
||||
_("An anonymous user can not be on lists of speakers.")
|
||||
)
|
||||
weight = (
|
||||
self.filter(item=item).aggregate(models.Max("weight"))["weight__max"] or 0
|
||||
)
|
||||
speaker = self.model(item=item, user=user, weight=weight + 1)
|
||||
speaker.save(force_insert=True, skip_autoupdate=skip_autoupdate)
|
||||
return speaker
|
||||
@ -368,17 +396,12 @@ class Speaker(RESTModelMixin, models.Model):
|
||||
|
||||
objects = SpeakerManager()
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
"""
|
||||
ForeinKey to the user who speaks.
|
||||
"""
|
||||
|
||||
item = models.ForeignKey(
|
||||
Item,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='speakers')
|
||||
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name="speakers")
|
||||
"""
|
||||
ForeinKey to the agenda item to which the user want to speak.
|
||||
"""
|
||||
@ -405,9 +428,7 @@ class Speaker(RESTModelMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
permissions = (
|
||||
('can_be_speaker', 'Can put oneself on the list of speakers'),
|
||||
)
|
||||
permissions = (("can_be_speaker", "Can put oneself on the list of speakers"),)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.user)
|
||||
@ -420,8 +441,11 @@ class Speaker(RESTModelMixin, models.Model):
|
||||
speaking, end his speech.
|
||||
"""
|
||||
try:
|
||||
current_speaker = (Speaker.objects.filter(item=self.item, end_time=None)
|
||||
.exclude(begin_time=None).get())
|
||||
current_speaker = (
|
||||
Speaker.objects.filter(item=self.item, end_time=None)
|
||||
.exclude(begin_time=None)
|
||||
.get()
|
||||
)
|
||||
except Speaker.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
@ -431,15 +455,21 @@ class Speaker(RESTModelMixin, models.Model):
|
||||
self.weight = None
|
||||
self.begin_time = timezone.now()
|
||||
self.save() # Here, the item is saved and causes an autoupdate.
|
||||
if config['agenda_couple_countdown_and_speakers']:
|
||||
countdown, created = Countdown.objects.get_or_create(pk=1, defaults={
|
||||
'default_time': config['projector_default_countdown'],
|
||||
'countdown_time': config['projector_default_countdown']})
|
||||
if config["agenda_couple_countdown_and_speakers"]:
|
||||
countdown, created = Countdown.objects.get_or_create(
|
||||
pk=1,
|
||||
defaults={
|
||||
"default_time": config["projector_default_countdown"],
|
||||
"countdown_time": config["projector_default_countdown"],
|
||||
},
|
||||
)
|
||||
if not created:
|
||||
countdown.control(action='reset', skip_autoupdate=True)
|
||||
countdown.control(action='start', skip_autoupdate=True)
|
||||
countdown.control(action="reset", skip_autoupdate=True)
|
||||
countdown.control(action="start", skip_autoupdate=True)
|
||||
|
||||
inform_changed_data(countdown) # Here, the autoupdate for the countdown is triggered.
|
||||
inform_changed_data(
|
||||
countdown
|
||||
) # Here, the autoupdate for the countdown is triggered.
|
||||
|
||||
def end_speech(self, skip_autoupdate=False):
|
||||
"""
|
||||
@ -447,13 +477,13 @@ class Speaker(RESTModelMixin, models.Model):
|
||||
"""
|
||||
self.end_time = timezone.now()
|
||||
self.save(skip_autoupdate=skip_autoupdate)
|
||||
if config['agenda_couple_countdown_and_speakers']:
|
||||
if config["agenda_couple_countdown_and_speakers"]:
|
||||
try:
|
||||
countdown = Countdown.objects.get(pk=1)
|
||||
except Countdown.DoesNotExist:
|
||||
pass # Do not create a new countdown on stop action
|
||||
else:
|
||||
countdown.control(action='reset', skip_autoupdate=skip_autoupdate)
|
||||
countdown.control(action="reset", skip_autoupdate=skip_autoupdate)
|
||||
|
||||
def get_root_rest_element(self):
|
||||
"""
|
||||
|
@ -16,14 +16,15 @@ class ItemListSlide(ProjectorElement):
|
||||
|
||||
Additionally set 'tree' to True to get also children of children.
|
||||
"""
|
||||
name = 'agenda/item-list'
|
||||
|
||||
name = "agenda/item-list"
|
||||
|
||||
def check_data(self):
|
||||
pk = self.config_entry.get('id')
|
||||
pk = self.config_entry.get("id")
|
||||
if pk is not None:
|
||||
# Children slide.
|
||||
if not Item.objects.filter(pk=pk).exists():
|
||||
raise ProjectorException('Item does not exist.')
|
||||
raise ProjectorException("Item does not exist.")
|
||||
|
||||
|
||||
class ListOfSpeakersSlide(ProjectorElement):
|
||||
@ -31,21 +32,23 @@ class ListOfSpeakersSlide(ProjectorElement):
|
||||
Slide definitions for Item model.
|
||||
This is only for list of speakers slide. You have to set 'id'.
|
||||
"""
|
||||
name = 'agenda/list-of-speakers'
|
||||
|
||||
name = "agenda/list-of-speakers"
|
||||
|
||||
def check_data(self):
|
||||
if not Item.objects.filter(pk=self.config_entry.get('id')).exists():
|
||||
raise ProjectorException('Item does not exist.')
|
||||
if not Item.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||
raise ProjectorException("Item does not exist.")
|
||||
|
||||
def update_data(self):
|
||||
return {'agenda_item_id': self.config_entry.get('id')}
|
||||
return {"agenda_item_id": self.config_entry.get("id")}
|
||||
|
||||
|
||||
class CurrentListOfSpeakersSlide(ProjectorElement):
|
||||
"""
|
||||
Slide for the current list of speakers.
|
||||
"""
|
||||
name = 'agenda/current-list-of-speakers'
|
||||
|
||||
name = "agenda/current-list-of-speakers"
|
||||
|
||||
|
||||
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
||||
|
@ -7,16 +7,17 @@ class SpeakerSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for agenda.models.Speaker objects.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Speaker
|
||||
fields = (
|
||||
'id',
|
||||
'user',
|
||||
'begin_time',
|
||||
'end_time',
|
||||
'weight',
|
||||
'marked',
|
||||
'item', # js-data needs the item-id in the nested object to define relations.
|
||||
"id",
|
||||
"user",
|
||||
"begin_time",
|
||||
"end_time",
|
||||
"weight",
|
||||
"marked",
|
||||
"item", # js-data needs the item-id in the nested object to define relations.
|
||||
)
|
||||
|
||||
|
||||
@ -24,36 +25,39 @@ class RelatedItemRelatedField(RelatedField):
|
||||
"""
|
||||
A custom field to use for the content_object generic relationship.
|
||||
"""
|
||||
|
||||
def to_representation(self, value):
|
||||
"""
|
||||
Returns info concerning the related object extracted from the api URL
|
||||
of this object.
|
||||
"""
|
||||
return {'collection': value.get_collection_string(), 'id': value.get_rest_pk()}
|
||||
return {"collection": value.get_collection_string(), "id": value.get_rest_pk()}
|
||||
|
||||
|
||||
class ItemSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for agenda.models.Item objects.
|
||||
"""
|
||||
|
||||
content_object = RelatedItemRelatedField(read_only=True)
|
||||
speakers = SpeakerSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = (
|
||||
'id',
|
||||
'item_number',
|
||||
'title',
|
||||
'title_with_type',
|
||||
'comment',
|
||||
'closed',
|
||||
'type',
|
||||
'is_internal',
|
||||
'is_hidden',
|
||||
'duration',
|
||||
'speakers',
|
||||
'speaker_list_closed',
|
||||
'content_object',
|
||||
'weight',
|
||||
'parent',)
|
||||
"id",
|
||||
"item_number",
|
||||
"title",
|
||||
"title_with_type",
|
||||
"comment",
|
||||
"closed",
|
||||
"type",
|
||||
"is_internal",
|
||||
"is_hidden",
|
||||
"duration",
|
||||
"speakers",
|
||||
"speaker_list_closed",
|
||||
"content_object",
|
||||
"weight",
|
||||
"parent",
|
||||
)
|
||||
|
@ -16,18 +16,18 @@ def listen_to_related_object_post_save(sender, instance, created, **kwargs):
|
||||
Do not run caching and autoupdate if the instance has a key
|
||||
skip_autoupdate in the agenda_item_update_information container.
|
||||
"""
|
||||
if hasattr(instance, 'get_agenda_title'):
|
||||
if hasattr(instance, "get_agenda_title"):
|
||||
if created:
|
||||
attrs = {}
|
||||
for attr in ('type', 'parent_id', 'comment', 'duration', 'weight'):
|
||||
for attr in ("type", "parent_id", "comment", "duration", "weight"):
|
||||
if instance.agenda_item_update_information.get(attr):
|
||||
attrs[attr] = instance.agenda_item_update_information.get(attr)
|
||||
Item.objects.create(content_object=instance, **attrs)
|
||||
|
||||
# If the object is created, the related_object has to be sent again.
|
||||
if not instance.agenda_item_update_information.get('skip_autoupdate'):
|
||||
if not instance.agenda_item_update_information.get("skip_autoupdate"):
|
||||
inform_changed_data(instance)
|
||||
elif not instance.agenda_item_update_information.get('skip_autoupdate'):
|
||||
elif not instance.agenda_item_update_information.get("skip_autoupdate"):
|
||||
# If the object has changed, then also the agenda item has to be sent.
|
||||
inform_changed_data(instance.agenda_item)
|
||||
|
||||
@ -37,7 +37,7 @@ def listen_to_related_object_post_delete(sender, instance, **kwargs):
|
||||
Receiver function to delete agenda items. It is connected to the signal
|
||||
django.db.models.signals.post_delete during app loading.
|
||||
"""
|
||||
if hasattr(instance, 'get_agenda_title'):
|
||||
if hasattr(instance, "get_agenda_title"):
|
||||
content_type = ContentType.objects.get_for_model(instance)
|
||||
try:
|
||||
# Attention: This delete() call is also necessary to remove
|
||||
@ -53,10 +53,12 @@ def get_permission_change_data(sender, permissions, **kwargs):
|
||||
Yields all necessary collections if 'agenda.can_see' or
|
||||
'agenda.can_see_internal_items' permissions changes.
|
||||
"""
|
||||
agenda_app = apps.get_app_config(app_label='agenda')
|
||||
agenda_app = apps.get_app_config(app_label="agenda")
|
||||
for permission in permissions:
|
||||
# There could be only one 'agenda.can_see' and then we want to return data.
|
||||
if (permission.content_type.app_label == agenda_app.label
|
||||
and permission.codename in ('can_see', 'can_see_internal_items')):
|
||||
if (
|
||||
permission.content_type.app_label == agenda_app.label
|
||||
and permission.codename in ("can_see", "can_see_internal_items")
|
||||
):
|
||||
yield from agenda_app.get_startup_elements()
|
||||
break
|
||||
|
@ -24,12 +24,14 @@ from .models import Item, Speaker
|
||||
|
||||
# Viewsets for the REST API
|
||||
|
||||
|
||||
class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
|
||||
"""
|
||||
API endpoint for agenda items.
|
||||
|
||||
There are some views, see check_view_permissions.
|
||||
"""
|
||||
|
||||
access_permissions = ItemAccessPermissions()
|
||||
queryset = Item.objects.all()
|
||||
|
||||
@ -37,22 +39,26 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
if self.action in ('list', 'retrieve'):
|
||||
if self.action in ("list", "retrieve"):
|
||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||
elif self.action in ('metadata', 'manage_speaker', 'tree'):
|
||||
result = has_perm(self.request.user, 'agenda.can_see')
|
||||
elif self.action in ("metadata", "manage_speaker", "tree"):
|
||||
result = has_perm(self.request.user, "agenda.can_see")
|
||||
# For manage_speaker and tree requests the rest of the check is
|
||||
# done in the specific method. See below.
|
||||
elif self.action in ('partial_update', 'update', 'sort', 'assign'):
|
||||
result = (has_perm(self.request.user, 'agenda.can_see') and
|
||||
has_perm(self.request.user, 'agenda.can_see_internal_items') and
|
||||
has_perm(self.request.user, 'agenda.can_manage'))
|
||||
elif self.action in ('speak', 'sort_speakers'):
|
||||
result = (has_perm(self.request.user, 'agenda.can_see') and
|
||||
has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'))
|
||||
elif self.action in ('numbering', ):
|
||||
result = (has_perm(self.request.user, 'agenda.can_see') and
|
||||
has_perm(self.request.user, 'agenda.can_manage'))
|
||||
elif self.action in ("partial_update", "update", "sort", "assign"):
|
||||
result = (
|
||||
has_perm(self.request.user, "agenda.can_see")
|
||||
and has_perm(self.request.user, "agenda.can_see_internal_items")
|
||||
and has_perm(self.request.user, "agenda.can_manage")
|
||||
)
|
||||
elif self.action in ("speak", "sort_speakers"):
|
||||
result = has_perm(self.request.user, "agenda.can_see") and has_perm(
|
||||
self.request.user, "agenda.can_manage_list_of_speakers"
|
||||
)
|
||||
elif self.action in ("numbering",):
|
||||
result = has_perm(self.request.user, "agenda.can_see") and has_perm(
|
||||
self.request.user, "agenda.can_manage"
|
||||
)
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
@ -82,7 +88,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
|
||||
return response
|
||||
|
||||
@detail_route(methods=['POST', 'PATCH', 'DELETE'])
|
||||
@detail_route(methods=["POST", "PATCH", "DELETE"])
|
||||
def manage_speaker(self, request, pk=None):
|
||||
"""
|
||||
Special view endpoint to add users to the list of speakers or remove
|
||||
@ -103,55 +109,61 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
# Retrieve item.
|
||||
item = self.get_object()
|
||||
|
||||
if request.method == 'POST':
|
||||
if request.method == "POST":
|
||||
# Retrieve user_id
|
||||
user_id = request.data.get('user')
|
||||
user_id = request.data.get("user")
|
||||
|
||||
# Check permissions and other conditions. Get user instance.
|
||||
if user_id is None:
|
||||
# Add oneself
|
||||
if not has_perm(self.request.user, 'agenda.can_be_speaker'):
|
||||
if not has_perm(self.request.user, "agenda.can_be_speaker"):
|
||||
self.permission_denied(request)
|
||||
if item.speaker_list_closed:
|
||||
raise ValidationError({'detail': _('The list of speakers is closed.')})
|
||||
raise ValidationError(
|
||||
{"detail": _("The list of speakers is closed.")}
|
||||
)
|
||||
user = self.request.user
|
||||
else:
|
||||
# Add someone else.
|
||||
if not has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'):
|
||||
if not has_perm(
|
||||
self.request.user, "agenda.can_manage_list_of_speakers"
|
||||
):
|
||||
self.permission_denied(request)
|
||||
try:
|
||||
user = get_user_model().objects.get(pk=int(user_id))
|
||||
except (ValueError, get_user_model().DoesNotExist):
|
||||
raise ValidationError({'detail': _('User does not exist.')})
|
||||
raise ValidationError({"detail": _("User does not exist.")})
|
||||
|
||||
# Try to add the user. This ensurse that a user is not twice in the
|
||||
# list of coming speakers.
|
||||
try:
|
||||
Speaker.objects.add(user, item)
|
||||
except OpenSlidesError as e:
|
||||
raise ValidationError({'detail': str(e)})
|
||||
message = _('User %s was successfully added to the list of speakers.') % user
|
||||
raise ValidationError({"detail": str(e)})
|
||||
message = (
|
||||
_("User %s was successfully added to the list of speakers.") % user
|
||||
)
|
||||
|
||||
# Send new speaker via autoupdate because users without permission
|
||||
# to see users may not have it but can get it now.
|
||||
inform_changed_data([user])
|
||||
|
||||
# Toggle 'marked' for the speaker
|
||||
elif request.method == 'PATCH':
|
||||
elif request.method == "PATCH":
|
||||
# Check permissions
|
||||
if not has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'):
|
||||
if not has_perm(self.request.user, "agenda.can_manage_list_of_speakers"):
|
||||
self.permission_denied(request)
|
||||
|
||||
# Retrieve user_id
|
||||
user_id = request.data.get('user')
|
||||
user_id = request.data.get("user")
|
||||
try:
|
||||
user = get_user_model().objects.get(pk=int(user_id))
|
||||
except (ValueError, get_user_model().DoesNotExist):
|
||||
raise ValidationError({'detail': _('User does not exist.')})
|
||||
raise ValidationError({"detail": _("User does not exist.")})
|
||||
|
||||
marked = request.data.get('marked')
|
||||
marked = request.data.get("marked")
|
||||
if not isinstance(marked, bool):
|
||||
raise ValidationError({'detail': _('Marked has to be a bool.')})
|
||||
raise ValidationError({"detail": _("Marked has to be a bool.")})
|
||||
|
||||
queryset = Speaker.objects.filter(item=item, user=user)
|
||||
try:
|
||||
@ -160,37 +172,46 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
# there is only one speaker instance or none.
|
||||
speaker = queryset.get()
|
||||
except Speaker.DoesNotExist:
|
||||
raise ValidationError({'detail': _('The user is not in the list of speakers.')})
|
||||
raise ValidationError(
|
||||
{"detail": _("The user is not in the list of speakers.")}
|
||||
)
|
||||
else:
|
||||
speaker.marked = marked
|
||||
speaker.save()
|
||||
if speaker.marked:
|
||||
message = _('You are successfully marked the speaker.')
|
||||
message = _("You are successfully marked the speaker.")
|
||||
else:
|
||||
message = _('You are successfully unmarked the speaker.')
|
||||
message = _("You are successfully unmarked the speaker.")
|
||||
|
||||
else:
|
||||
# request.method == 'DELETE'
|
||||
speaker_ids = request.data.get('speaker')
|
||||
speaker_ids = request.data.get("speaker")
|
||||
|
||||
# Check permissions and other conditions. Get speaker instance.
|
||||
if speaker_ids is None:
|
||||
# Remove oneself
|
||||
queryset = Speaker.objects.filter(
|
||||
item=item, user=self.request.user).exclude(weight=None)
|
||||
item=item, user=self.request.user
|
||||
).exclude(weight=None)
|
||||
try:
|
||||
# We assume that there aren't multiple entries because this
|
||||
# is forbidden by the Manager's add method. We assume that
|
||||
# there is only one speaker instance or none.
|
||||
speaker = queryset.get()
|
||||
except Speaker.DoesNotExist:
|
||||
raise ValidationError({'detail': _('You are not on the list of speakers.')})
|
||||
raise ValidationError(
|
||||
{"detail": _("You are not on the list of speakers.")}
|
||||
)
|
||||
else:
|
||||
speaker.delete()
|
||||
message = _('You are successfully removed from the list of speakers.')
|
||||
message = _(
|
||||
"You are successfully removed from the list of speakers."
|
||||
)
|
||||
else:
|
||||
# Remove someone else.
|
||||
if not has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'):
|
||||
if not has_perm(
|
||||
self.request.user, "agenda.can_manage_list_of_speakers"
|
||||
):
|
||||
self.permission_denied(request)
|
||||
if type(speaker_ids) is int:
|
||||
speaker_ids = [speaker_ids]
|
||||
@ -209,15 +230,24 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
inform_changed_data(item)
|
||||
|
||||
if deleted_speaker_count > 1:
|
||||
message = str(deleted_speaker_count) + ' ' + _('speakers have been removed from the list of speakers.')
|
||||
message = (
|
||||
str(deleted_speaker_count)
|
||||
+ " "
|
||||
+ _("speakers have been removed from the list of speakers.")
|
||||
)
|
||||
elif deleted_speaker_count == 1:
|
||||
message = _('User %s has been removed from the list of speakers.') % deleted_speaker_name
|
||||
message = (
|
||||
_("User %s has been removed from the list of speakers.")
|
||||
% deleted_speaker_name
|
||||
)
|
||||
else:
|
||||
message = _('No speakers have been removed from the list of speakers.')
|
||||
message = _(
|
||||
"No speakers have been removed from the list of speakers."
|
||||
)
|
||||
# Initiate response.
|
||||
return Response({'detail': message})
|
||||
return Response({"detail": message})
|
||||
|
||||
@detail_route(methods=['PUT', 'DELETE'])
|
||||
@detail_route(methods=["PUT", "DELETE"])
|
||||
def speak(self, request, pk=None):
|
||||
"""
|
||||
Special view endpoint to begin and end speech of speakers. Send PUT
|
||||
@ -227,20 +257,22 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
# Retrieve item.
|
||||
item = self.get_object()
|
||||
|
||||
if request.method == 'PUT':
|
||||
if request.method == "PUT":
|
||||
# Retrieve speaker_id
|
||||
speaker_id = request.data.get('speaker')
|
||||
speaker_id = request.data.get("speaker")
|
||||
if speaker_id is None:
|
||||
speaker = item.get_next_speaker()
|
||||
if speaker is None:
|
||||
raise ValidationError({'detail': _('The list of speakers is empty.')})
|
||||
raise ValidationError(
|
||||
{"detail": _("The list of speakers is empty.")}
|
||||
)
|
||||
else:
|
||||
try:
|
||||
speaker = Speaker.objects.get(pk=int(speaker_id))
|
||||
except (ValueError, Speaker.DoesNotExist):
|
||||
raise ValidationError({'detail': _('Speaker does not exist.')})
|
||||
raise ValidationError({"detail": _("Speaker does not exist.")})
|
||||
speaker.begin_speech()
|
||||
message = _('User is now speaking.')
|
||||
message = _("User is now speaking.")
|
||||
|
||||
else:
|
||||
# request.method == 'DELETE'
|
||||
@ -248,17 +280,27 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
# We assume that there aren't multiple entries because this
|
||||
# is forbidden by the Model's begin_speech method. We assume that
|
||||
# there is only one speaker instance or none.
|
||||
current_speaker = Speaker.objects.filter(item=item, end_time=None).exclude(begin_time=None).get()
|
||||
current_speaker = (
|
||||
Speaker.objects.filter(item=item, end_time=None)
|
||||
.exclude(begin_time=None)
|
||||
.get()
|
||||
)
|
||||
except Speaker.DoesNotExist:
|
||||
raise ValidationError(
|
||||
{'detail': _('There is no one speaking at the moment according to %(item)s.') % {'item': item}})
|
||||
{
|
||||
"detail": _(
|
||||
"There is no one speaking at the moment according to %(item)s."
|
||||
)
|
||||
% {"item": item}
|
||||
}
|
||||
)
|
||||
current_speaker.end_speech()
|
||||
message = _('The speech is finished now.')
|
||||
message = _("The speech is finished now.")
|
||||
|
||||
# Initiate response.
|
||||
return Response({'detail': message})
|
||||
return Response({"detail": message})
|
||||
|
||||
@detail_route(methods=['POST'])
|
||||
@detail_route(methods=["POST"])
|
||||
def sort_speakers(self, request, pk=None):
|
||||
"""
|
||||
Special view endpoint to sort the list of speakers.
|
||||
@ -269,10 +311,9 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
item = self.get_object()
|
||||
|
||||
# Check data
|
||||
speaker_ids = request.data.get('speakers')
|
||||
speaker_ids = request.data.get("speakers")
|
||||
if not isinstance(speaker_ids, list):
|
||||
raise ValidationError(
|
||||
{'detail': _('Invalid data.')})
|
||||
raise ValidationError({"detail": _("Invalid data.")})
|
||||
|
||||
# Get all speakers
|
||||
speakers = {}
|
||||
@ -283,8 +324,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
valid_speakers = []
|
||||
for speaker_id in speaker_ids:
|
||||
if not isinstance(speaker_id, int) or speakers.get(speaker_id) is None:
|
||||
raise ValidationError(
|
||||
{'detail': _('Invalid data.')})
|
||||
raise ValidationError({"detail": _("Invalid data.")})
|
||||
valid_speakers.append(speakers[speaker_id])
|
||||
weight = 0
|
||||
with transaction.atomic():
|
||||
@ -297,50 +337,57 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
inform_changed_data(item)
|
||||
|
||||
# Initiate response.
|
||||
return Response({'detail': _('List of speakers successfully sorted.')})
|
||||
return Response({"detail": _("List of speakers successfully sorted.")})
|
||||
|
||||
@list_route(methods=['post'])
|
||||
@list_route(methods=["post"])
|
||||
def numbering(self, request):
|
||||
"""
|
||||
Auto numbering of the agenda according to the config. Manually added
|
||||
item numbers will be overwritten.
|
||||
"""
|
||||
if not config['agenda_enable_numbering']:
|
||||
raise ValidationError({'detail': _('Numbering of agenda items is deactivated.')})
|
||||
if not config["agenda_enable_numbering"]:
|
||||
raise ValidationError(
|
||||
{"detail": _("Numbering of agenda items is deactivated.")}
|
||||
)
|
||||
|
||||
Item.objects.number_all(numeral_system=config['agenda_numeral_system'])
|
||||
return Response({'detail': _('The agenda has been numbered.')})
|
||||
Item.objects.number_all(numeral_system=config["agenda_numeral_system"])
|
||||
return Response({"detail": _("The agenda has been numbered.")})
|
||||
|
||||
@list_route(methods=['post'])
|
||||
@list_route(methods=["post"])
|
||||
def sort(self, request):
|
||||
"""
|
||||
Sort agenda items. Also checks parent field to prevent hierarchical
|
||||
loops.
|
||||
"""
|
||||
nodes = request.data.get('nodes', [])
|
||||
parent_id = request.data.get('parent_id')
|
||||
nodes = request.data.get("nodes", [])
|
||||
parent_id = request.data.get("parent_id")
|
||||
items = []
|
||||
with transaction.atomic():
|
||||
for index, node in enumerate(nodes):
|
||||
item = Item.objects.get(pk=node['id'])
|
||||
item = Item.objects.get(pk=node["id"])
|
||||
item.parent_id = parent_id
|
||||
item.weight = index
|
||||
item.save(skip_autoupdate=True)
|
||||
items.append(item)
|
||||
|
||||
# Now check consistency. TODO: Try to use less DB queries.
|
||||
item = Item.objects.get(pk=node['id'])
|
||||
item = Item.objects.get(pk=node["id"])
|
||||
ancestor = item.parent
|
||||
while ancestor is not None:
|
||||
if ancestor == item:
|
||||
raise ValidationError({'detail': _(
|
||||
'There must not be a hierarchical loop. Please reload the page.')})
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": _(
|
||||
"There must not be a hierarchical loop. Please reload the page."
|
||||
)
|
||||
}
|
||||
)
|
||||
ancestor = ancestor.parent
|
||||
|
||||
inform_changed_data(items)
|
||||
return Response({'detail': _('The agenda has been sorted.')})
|
||||
return Response({"detail": _("The agenda has been sorted.")})
|
||||
|
||||
@list_route(methods=['post'])
|
||||
@list_route(methods=["post"])
|
||||
@transaction.atomic
|
||||
def assign(self, request):
|
||||
"""
|
||||
@ -359,9 +406,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
"items": {
|
||||
"description": "An array of agenda item ids where the items should be assigned to the new parent id.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
},
|
||||
"items": {"type": "integer"},
|
||||
"minItems": 1,
|
||||
"uniqueItems": True,
|
||||
},
|
||||
@ -377,13 +422,19 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
try:
|
||||
jsonschema.validate(request.data, schema)
|
||||
except jsonschema.ValidationError as err:
|
||||
raise ValidationError({'detail': str(err)})
|
||||
raise ValidationError({"detail": str(err)})
|
||||
|
||||
# Check parent item
|
||||
try:
|
||||
parent = Item.objects.get(pk=request.data['parent_id'])
|
||||
parent = Item.objects.get(pk=request.data["parent_id"])
|
||||
except Item.DoesNotExist:
|
||||
raise ValidationError({'detail': 'Parent item {} does not exist'.format(request.data['parent_id'])})
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": "Parent item {} does not exist".format(
|
||||
request.data["parent_id"]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
# Collect ancestors
|
||||
ancestors = [parent.pk]
|
||||
@ -394,16 +445,24 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
|
||||
# First validate all items before changeing them.
|
||||
items = []
|
||||
for item_id in request.data['items']:
|
||||
for item_id in request.data["items"]:
|
||||
# Prevent hierarchical loops.
|
||||
if item_id in ancestors:
|
||||
raise ValidationError({'detail': 'Assigning item {} to one of its children is not possible.'.format(item_id)})
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": "Assigning item {} to one of its children is not possible.".format(
|
||||
item_id
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
# Check every item
|
||||
try:
|
||||
items.append(Item.objects.get(pk=item_id))
|
||||
except Item.DoesNotExist:
|
||||
raise ValidationError({'detail': 'Item {} does not exist'.format(item_id)})
|
||||
raise ValidationError(
|
||||
{"detail": "Item {} does not exist".format(item_id)}
|
||||
)
|
||||
|
||||
# OK, assign new parents.
|
||||
for item in items:
|
||||
@ -415,6 +474,10 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||
inform_changed_data(items)
|
||||
|
||||
# Send response.
|
||||
return Response({
|
||||
'detail': _('{number} items successfully assigned.').format(number=len(items)),
|
||||
})
|
||||
return Response(
|
||||
{
|
||||
"detail": _("{number} items successfully assigned.").format(
|
||||
number=len(items)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -1 +1 @@
|
||||
default_app_config = 'openslides.assignments.apps.AssignmentsAppConfig'
|
||||
default_app_config = "openslides.assignments.apps.AssignmentsAppConfig"
|
||||
|
@ -8,31 +8,35 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for Assignment and AssignmentViewSet.
|
||||
"""
|
||||
base_permission = 'assignments.can_see'
|
||||
|
||||
base_permission = "assignments.can_see"
|
||||
|
||||
async def get_restricted_data(
|
||||
self,
|
||||
full_data: List[Dict[str, Any]],
|
||||
user_id: int) -> List[Dict[str, Any]]:
|
||||
self, full_data: List[Dict[str, Any]], user_id: int
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns the restricted serialized data for the instance prepared
|
||||
for the user. Removes unpublished polls for non admins so that they
|
||||
only get a result like the AssignmentShortSerializer would give them.
|
||||
"""
|
||||
# Parse data.
|
||||
if await async_has_perm(user_id, 'assignments.can_see') and await async_has_perm(user_id, 'assignments.can_manage'):
|
||||
if await async_has_perm(
|
||||
user_id, "assignments.can_see"
|
||||
) and await async_has_perm(user_id, "assignments.can_manage"):
|
||||
data = full_data
|
||||
elif await async_has_perm(user_id, 'assignments.can_see'):
|
||||
elif await async_has_perm(user_id, "assignments.can_see"):
|
||||
# Exclude unpublished poll votes.
|
||||
data = []
|
||||
for full in full_data:
|
||||
full_copy = full.copy()
|
||||
polls = full_copy['polls']
|
||||
polls = full_copy["polls"]
|
||||
for poll in polls:
|
||||
if not poll['published']:
|
||||
for option in poll['options']:
|
||||
option['votes'] = [] # clear votes for not published polls
|
||||
poll['has_votes'] = False # A user should see, if there are votes.
|
||||
if not poll["published"]:
|
||||
for option in poll["options"]:
|
||||
option["votes"] = [] # clear votes for not published polls
|
||||
poll[
|
||||
"has_votes"
|
||||
] = False # A user should see, if there are votes.
|
||||
data.append(full_copy)
|
||||
else:
|
||||
data = []
|
||||
|
@ -7,8 +7,8 @@ from ..utils.projector import register_projector_elements
|
||||
|
||||
|
||||
class AssignmentsAppConfig(AppConfig):
|
||||
name = 'openslides.assignments'
|
||||
verbose_name = 'OpenSlides Assignments'
|
||||
name = "openslides.assignments"
|
||||
verbose_name = "OpenSlides Assignments"
|
||||
angular_site_module = True
|
||||
angular_projector_module = True
|
||||
|
||||
@ -28,17 +28,23 @@ class AssignmentsAppConfig(AppConfig):
|
||||
# Connect signals.
|
||||
permission_change.connect(
|
||||
get_permission_change_data,
|
||||
dispatch_uid='assignments_get_permission_change_data')
|
||||
dispatch_uid="assignments_get_permission_change_data",
|
||||
)
|
||||
|
||||
# Register viewsets.
|
||||
router.register(self.get_model('Assignment').get_collection_string(), AssignmentViewSet)
|
||||
router.register('assignments/poll', AssignmentPollViewSet)
|
||||
router.register(
|
||||
self.get_model("Assignment").get_collection_string(), AssignmentViewSet
|
||||
)
|
||||
router.register("assignments/poll", AssignmentPollViewSet)
|
||||
|
||||
# Register required_users
|
||||
required_user.add_collection_string(self.get_model('Assignment').get_collection_string(), required_users)
|
||||
required_user.add_collection_string(
|
||||
self.get_model("Assignment").get_collection_string(), required_users
|
||||
)
|
||||
|
||||
def get_config_variables(self):
|
||||
from .config_variables import get_config_variables
|
||||
|
||||
return get_config_variables()
|
||||
|
||||
def get_startup_elements(self):
|
||||
@ -46,18 +52,15 @@ class AssignmentsAppConfig(AppConfig):
|
||||
Yields all Cachables required on startup i. e. opening the websocket
|
||||
connection.
|
||||
"""
|
||||
yield self.get_model('Assignment')
|
||||
yield self.get_model("Assignment")
|
||||
|
||||
def get_angular_constants(self):
|
||||
assignment = self.get_model('Assignment')
|
||||
Item = TypedDict('Item', {'value': int, 'display_name': str})
|
||||
assignment = self.get_model("Assignment")
|
||||
Item = TypedDict("Item", {"value": int, "display_name": str})
|
||||
phases: List[Item] = []
|
||||
for phase in assignment.PHASES:
|
||||
phases.append({
|
||||
'value': phase[0],
|
||||
'display_name': phase[1],
|
||||
})
|
||||
return {'AssignmentPhases': phases}
|
||||
phases.append({"value": phase[0], "display_name": phase[1]})
|
||||
return {"AssignmentPhases": phases}
|
||||
|
||||
|
||||
def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||
@ -65,7 +68,9 @@ def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||
Returns all user ids that are displayed as candidates (including poll
|
||||
options) in the assignment element.
|
||||
"""
|
||||
candidates = set(related_user['user_id'] for related_user in element['assignment_related_users'])
|
||||
for poll in element['polls']:
|
||||
candidates.update(option['candidate_id'] for option in poll['options'])
|
||||
candidates = set(
|
||||
related_user["user_id"] for related_user in element["assignment_related_users"]
|
||||
)
|
||||
for poll in element["polls"]:
|
||||
candidates.update(option["candidate_id"] for option in poll["options"])
|
||||
return candidates
|
||||
|
@ -13,96 +13,118 @@ def get_config_variables():
|
||||
"""
|
||||
# Ballot and ballot papers
|
||||
yield ConfigVariable(
|
||||
name='assignments_poll_vote_values',
|
||||
default_value='auto',
|
||||
input_type='choice',
|
||||
label='Election method',
|
||||
name="assignments_poll_vote_values",
|
||||
default_value="auto",
|
||||
input_type="choice",
|
||||
label="Election method",
|
||||
choices=(
|
||||
{'value': 'auto', 'display_name': 'Automatic assign of method'},
|
||||
{'value': 'votes', 'display_name': 'Always one option per candidate'},
|
||||
{'value': 'yesnoabstain', 'display_name': 'Always Yes-No-Abstain per candidate'},
|
||||
{'value': 'yesno', 'display_name': 'Always Yes/No per candidate'}),
|
||||
{"value": "auto", "display_name": "Automatic assign of method"},
|
||||
{"value": "votes", "display_name": "Always one option per candidate"},
|
||||
{
|
||||
"value": "yesnoabstain",
|
||||
"display_name": "Always Yes-No-Abstain per candidate",
|
||||
},
|
||||
{"value": "yesno", "display_name": "Always Yes/No per candidate"},
|
||||
),
|
||||
weight=410,
|
||||
group='Elections',
|
||||
subgroup='Ballot and ballot papers')
|
||||
group="Elections",
|
||||
subgroup="Ballot and ballot papers",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='assignments_poll_100_percent_base',
|
||||
default_value='YES_NO_ABSTAIN',
|
||||
input_type='choice',
|
||||
label='The 100-%-base of an election result consists of',
|
||||
name="assignments_poll_100_percent_base",
|
||||
default_value="YES_NO_ABSTAIN",
|
||||
input_type="choice",
|
||||
label="The 100-%-base of an election result consists of",
|
||||
choices=(
|
||||
{'value': 'YES_NO_ABSTAIN', 'display_name': 'Yes/No/Abstain per candidate'},
|
||||
{'value': 'YES_NO', 'display_name': 'Yes/No per candidate'},
|
||||
{'value': 'VALID', 'display_name': 'All valid ballots'},
|
||||
{'value': 'CAST', 'display_name': 'All casted ballots'},
|
||||
{'value': 'DISABLED', 'display_name': 'Disabled (no percents)'}),
|
||||
help_text=('For Yes/No/Abstain per candidate and Yes/No per candidate the 100-%-base '
|
||||
'depends on the election method: If there is only one option per candidate, '
|
||||
'the sum of all votes of all candidates is 100 %. Otherwise for each '
|
||||
'candidate the sum of all votes is 100 %.'),
|
||||
{"value": "YES_NO_ABSTAIN", "display_name": "Yes/No/Abstain per candidate"},
|
||||
{"value": "YES_NO", "display_name": "Yes/No per candidate"},
|
||||
{"value": "VALID", "display_name": "All valid ballots"},
|
||||
{"value": "CAST", "display_name": "All casted ballots"},
|
||||
{"value": "DISABLED", "display_name": "Disabled (no percents)"},
|
||||
),
|
||||
help_text=(
|
||||
"For Yes/No/Abstain per candidate and Yes/No per candidate the 100-%-base "
|
||||
"depends on the election method: If there is only one option per candidate, "
|
||||
"the sum of all votes of all candidates is 100 %. Otherwise for each "
|
||||
"candidate the sum of all votes is 100 %."
|
||||
),
|
||||
weight=420,
|
||||
group='Elections',
|
||||
subgroup='Ballot and ballot papers')
|
||||
group="Elections",
|
||||
subgroup="Ballot and ballot papers",
|
||||
)
|
||||
|
||||
# TODO: Add server side validation of the choices.
|
||||
yield ConfigVariable(
|
||||
name='assignments_poll_default_majority_method',
|
||||
default_value=majorityMethods[0]['value'],
|
||||
input_type='choice',
|
||||
name="assignments_poll_default_majority_method",
|
||||
default_value=majorityMethods[0]["value"],
|
||||
input_type="choice",
|
||||
choices=majorityMethods,
|
||||
label='Required majority',
|
||||
help_text='Default method to check whether a candidate has reached the required majority.',
|
||||
label="Required majority",
|
||||
help_text="Default method to check whether a candidate has reached the required majority.",
|
||||
weight=425,
|
||||
group='Elections',
|
||||
subgroup='Ballot and ballot papers')
|
||||
group="Elections",
|
||||
subgroup="Ballot and ballot papers",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='assignments_add_candidates_to_list_of_speakers',
|
||||
name="assignments_add_candidates_to_list_of_speakers",
|
||||
default_value=True,
|
||||
input_type='boolean',
|
||||
label='Put all candidates on the list of speakers',
|
||||
input_type="boolean",
|
||||
label="Put all candidates on the list of speakers",
|
||||
weight=428,
|
||||
group='Elections',
|
||||
subgroup='Ballot and ballot papers')
|
||||
group="Elections",
|
||||
subgroup="Ballot and ballot papers",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='assignments_pdf_ballot_papers_selection',
|
||||
default_value='CUSTOM_NUMBER',
|
||||
input_type='choice',
|
||||
label='Number of ballot papers (selection)',
|
||||
name="assignments_pdf_ballot_papers_selection",
|
||||
default_value="CUSTOM_NUMBER",
|
||||
input_type="choice",
|
||||
label="Number of ballot papers (selection)",
|
||||
choices=(
|
||||
{'value': 'NUMBER_OF_DELEGATES', 'display_name': 'Number of all delegates'},
|
||||
{'value': 'NUMBER_OF_ALL_PARTICIPANTS', 'display_name': 'Number of all participants'},
|
||||
{'value': 'CUSTOM_NUMBER', 'display_name': 'Use the following custom number'}),
|
||||
{"value": "NUMBER_OF_DELEGATES", "display_name": "Number of all delegates"},
|
||||
{
|
||||
"value": "NUMBER_OF_ALL_PARTICIPANTS",
|
||||
"display_name": "Number of all participants",
|
||||
},
|
||||
{
|
||||
"value": "CUSTOM_NUMBER",
|
||||
"display_name": "Use the following custom number",
|
||||
},
|
||||
),
|
||||
weight=430,
|
||||
group='Elections',
|
||||
subgroup='Ballot and ballot papers')
|
||||
group="Elections",
|
||||
subgroup="Ballot and ballot papers",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='assignments_pdf_ballot_papers_number',
|
||||
name="assignments_pdf_ballot_papers_number",
|
||||
default_value=8,
|
||||
input_type='integer',
|
||||
label='Custom number of ballot papers',
|
||||
input_type="integer",
|
||||
label="Custom number of ballot papers",
|
||||
weight=440,
|
||||
group='Elections',
|
||||
subgroup='Ballot and ballot papers',
|
||||
validators=(MinValueValidator(1),))
|
||||
group="Elections",
|
||||
subgroup="Ballot and ballot papers",
|
||||
validators=(MinValueValidator(1),),
|
||||
)
|
||||
|
||||
# PDF
|
||||
|
||||
yield ConfigVariable(
|
||||
name='assignments_pdf_title',
|
||||
default_value='Elections',
|
||||
label='Title for PDF document (all elections)',
|
||||
name="assignments_pdf_title",
|
||||
default_value="Elections",
|
||||
label="Title for PDF document (all elections)",
|
||||
weight=460,
|
||||
group='Elections',
|
||||
subgroup='PDF')
|
||||
group="Elections",
|
||||
subgroup="PDF",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='assignments_pdf_preamble',
|
||||
default_value='',
|
||||
label='Preamble text for PDF document (all elections)',
|
||||
name="assignments_pdf_preamble",
|
||||
default_value="",
|
||||
label="Preamble text for PDF document (all elections)",
|
||||
weight=470,
|
||||
group='Elections',
|
||||
subgroup='PDF')
|
||||
group="Elections",
|
||||
subgroup="PDF",
|
||||
)
|
||||
|
@ -15,104 +15,196 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('core', '0001_initial'),
|
||||
("core", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Assignment',
|
||||
name="Assignment",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=100)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('open_posts', models.PositiveSmallIntegerField()),
|
||||
('poll_description_default', models.CharField(blank=True, max_length=79)),
|
||||
('phase', models.IntegerField(choices=[(0, 'Searching for candidates'), (1, 'Voting'), (2, 'Finished')], default=0)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=100)),
|
||||
("description", models.TextField(blank=True)),
|
||||
("open_posts", models.PositiveSmallIntegerField()),
|
||||
(
|
||||
"poll_description_default",
|
||||
models.CharField(blank=True, max_length=79),
|
||||
),
|
||||
(
|
||||
"phase",
|
||||
models.IntegerField(
|
||||
choices=[
|
||||
(0, "Searching for candidates"),
|
||||
(1, "Voting"),
|
||||
(2, "Finished"),
|
||||
],
|
||||
default=0,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Election',
|
||||
'default_permissions': (),
|
||||
'permissions': (
|
||||
('can_see', 'Can see elections'),
|
||||
('can_nominate_other', 'Can nominate another participant'),
|
||||
('can_nominate_self', 'Can nominate oneself'),
|
||||
('can_manage', 'Can manage elections')),
|
||||
'ordering': ('title',),
|
||||
"verbose_name": "Election",
|
||||
"default_permissions": (),
|
||||
"permissions": (
|
||||
("can_see", "Can see elections"),
|
||||
("can_nominate_other", "Can nominate another participant"),
|
||||
("can_nominate_self", "Can nominate oneself"),
|
||||
("can_manage", "Can manage elections"),
|
||||
),
|
||||
"ordering": ("title",),
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AssignmentOption',
|
||||
name="AssignmentOption",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"candidate",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AssignmentPoll',
|
||||
name="AssignmentPoll",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('votesvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)),
|
||||
('votesinvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)),
|
||||
('votescast', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)),
|
||||
('published', models.BooleanField(default=False)),
|
||||
('yesnoabstain', models.BooleanField(default=False)),
|
||||
('description', models.CharField(blank=True, max_length=79)),
|
||||
('assignment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polls', to='assignments.Assignment')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"votesvalid",
|
||||
openslides.utils.models.MinMaxIntegerField(blank=True, null=True),
|
||||
),
|
||||
(
|
||||
"votesinvalid",
|
||||
openslides.utils.models.MinMaxIntegerField(blank=True, null=True),
|
||||
),
|
||||
(
|
||||
"votescast",
|
||||
openslides.utils.models.MinMaxIntegerField(blank=True, null=True),
|
||||
),
|
||||
("published", models.BooleanField(default=False)),
|
||||
("yesnoabstain", models.BooleanField(default=False)),
|
||||
("description", models.CharField(blank=True, max_length=79)),
|
||||
(
|
||||
"assignment",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="polls",
|
||||
to="assignments.Assignment",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AssignmentRelatedUser',
|
||||
name="AssignmentRelatedUser",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('elected', models.BooleanField(default=False)),
|
||||
('assignment', models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name='assignment_related_users', to='assignments.Assignment')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("elected", models.BooleanField(default=False)),
|
||||
(
|
||||
"assignment",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="assignment_related_users",
|
||||
to="assignments.Assignment",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AssignmentVote',
|
||||
name="AssignmentVote",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('weight', models.IntegerField(default=1, null=True)),
|
||||
('value', models.CharField(max_length=255, null=True)),
|
||||
('option', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='assignments.AssignmentOption')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("weight", models.IntegerField(default=1, null=True)),
|
||||
("value", models.CharField(max_length=255, null=True)),
|
||||
(
|
||||
"option",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="votes",
|
||||
to="assignments.AssignmentOption",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='assignmentoption',
|
||||
name='poll',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='assignments.AssignmentPoll'),
|
||||
model_name="assignmentoption",
|
||||
name="poll",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="options",
|
||||
to="assignments.AssignmentPoll",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='assignment',
|
||||
name='related_users',
|
||||
field=models.ManyToManyField(through='assignments.AssignmentRelatedUser', to=settings.AUTH_USER_MODEL),
|
||||
model_name="assignment",
|
||||
name="related_users",
|
||||
field=models.ManyToManyField(
|
||||
through="assignments.AssignmentRelatedUser", to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='assignment',
|
||||
name='tags',
|
||||
field=models.ManyToManyField(blank=True, to='core.Tag'),
|
||||
model_name="assignment",
|
||||
name="tags",
|
||||
field=models.ManyToManyField(blank=True, to="core.Tag"),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='assignmentrelateduser',
|
||||
unique_together=set([('assignment', 'user')]),
|
||||
name="assignmentrelateduser", unique_together=set([("assignment", "user")])
|
||||
),
|
||||
]
|
||||
|
@ -7,18 +7,13 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assignments', '0001_initial'),
|
||||
]
|
||||
dependencies = [("assignments", "0001_initial")]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='assignmentpoll',
|
||||
name='yesnoabstain',
|
||||
),
|
||||
migrations.RemoveField(model_name="assignmentpoll", name="yesnoabstain"),
|
||||
migrations.AddField(
|
||||
model_name='assignmentpoll',
|
||||
name='pollmethod',
|
||||
field=models.CharField(default='yna', max_length=5),
|
||||
model_name="assignmentpoll",
|
||||
name="pollmethod",
|
||||
field=models.CharField(default="yna", max_length=5),
|
||||
),
|
||||
]
|
||||
|
@ -7,19 +7,17 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assignments', '0002_assignmentpoll_pollmethod'),
|
||||
]
|
||||
dependencies = [("assignments", "0002_assignmentpoll_pollmethod")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='assignmentrelateduser',
|
||||
name='weight',
|
||||
model_name="assignmentrelateduser",
|
||||
name="weight",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='assignmentoption',
|
||||
name='weight',
|
||||
model_name="assignmentoption",
|
||||
name="weight",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
||||
|
@ -9,19 +9,17 @@ from openslides.utils.models import MinMaxIntegerField
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assignments', '0003_candidate_weight'),
|
||||
]
|
||||
dependencies = [("assignments", "0003_candidate_weight")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='assignmentpoll',
|
||||
name='votesabstain',
|
||||
model_name="assignmentpoll",
|
||||
name="votesabstain",
|
||||
field=MinMaxIntegerField(null=True, blank=True, min_value=-2),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='assignmentpoll',
|
||||
name='votesno',
|
||||
model_name="assignmentpoll",
|
||||
name="votesno",
|
||||
field=MinMaxIntegerField(null=True, blank=True, min_value=-2),
|
||||
),
|
||||
]
|
||||
|
@ -8,69 +8,73 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assignments', '0004_auto_20180703_1523'),
|
||||
]
|
||||
dependencies = [("assignments", "0004_auto_20180703_1523")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='assignmentpoll',
|
||||
name='votescast',
|
||||
model_name="assignmentpoll",
|
||||
name="votescast",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=6,
|
||||
max_digits=15,
|
||||
null=True,
|
||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
||||
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='assignmentpoll',
|
||||
name='votesinvalid',
|
||||
model_name="assignmentpoll",
|
||||
name="votesinvalid",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=6,
|
||||
max_digits=15,
|
||||
null=True,
|
||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
||||
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='assignmentpoll',
|
||||
name='votesvalid',
|
||||
model_name="assignmentpoll",
|
||||
name="votesvalid",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=6,
|
||||
max_digits=15,
|
||||
null=True,
|
||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
||||
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='assignmentvote',
|
||||
name='weight',
|
||||
model_name="assignmentvote",
|
||||
name="weight",
|
||||
field=models.DecimalField(
|
||||
decimal_places=6,
|
||||
default=Decimal('1'),
|
||||
default=Decimal("1"),
|
||||
max_digits=15,
|
||||
null=True,
|
||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
||||
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='assignmentpoll',
|
||||
name='votesabstain',
|
||||
model_name="assignmentpoll",
|
||||
name="votesabstain",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=6,
|
||||
max_digits=15,
|
||||
null=True,
|
||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
||||
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='assignmentpoll',
|
||||
name='votesno',
|
||||
model_name="assignmentpoll",
|
||||
name="votesno",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=6,
|
||||
max_digits=15,
|
||||
null=True,
|
||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
||||
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -31,16 +31,13 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
||||
"""
|
||||
|
||||
assignment = models.ForeignKey(
|
||||
'Assignment',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='assignment_related_users')
|
||||
"Assignment", on_delete=models.CASCADE, related_name="assignment_related_users"
|
||||
)
|
||||
"""
|
||||
ForeinKey to the assignment.
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
"""
|
||||
ForeinKey to the user who is related to the assignment.
|
||||
"""
|
||||
@ -57,7 +54,7 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
unique_together = ('assignment', 'user')
|
||||
unique_together = ("assignment", "user")
|
||||
|
||||
def __str__(self):
|
||||
return "%s <-> %s" % (self.assignment, self.user)
|
||||
@ -73,6 +70,7 @@ class AssignmentManager(models.Manager):
|
||||
"""
|
||||
Customized model manager to support our get_full_queryset method.
|
||||
"""
|
||||
|
||||
def get_full_queryset(self):
|
||||
"""
|
||||
Returns the normal queryset with all assignments. In the background
|
||||
@ -80,18 +78,17 @@ class AssignmentManager(models.Manager):
|
||||
polls are prefetched from the database.
|
||||
"""
|
||||
return self.get_queryset().prefetch_related(
|
||||
'related_users',
|
||||
'agenda_items',
|
||||
'polls',
|
||||
'tags')
|
||||
"related_users", "agenda_items", "polls", "tags"
|
||||
)
|
||||
|
||||
|
||||
class Assignment(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Model for assignments.
|
||||
"""
|
||||
|
||||
access_permissions = AssignmentAccessPermissions()
|
||||
can_see_permission = 'assignments.can_see'
|
||||
can_see_permission = "assignments.can_see"
|
||||
|
||||
objects = AssignmentManager()
|
||||
|
||||
@ -100,19 +97,17 @@ class Assignment(RESTModelMixin, models.Model):
|
||||
PHASE_FINISHED = 2
|
||||
|
||||
PHASES = (
|
||||
(PHASE_SEARCH, 'Searching for candidates'),
|
||||
(PHASE_VOTING, 'Voting'),
|
||||
(PHASE_FINISHED, 'Finished'),
|
||||
(PHASE_SEARCH, "Searching for candidates"),
|
||||
(PHASE_VOTING, "Voting"),
|
||||
(PHASE_FINISHED, "Finished"),
|
||||
)
|
||||
|
||||
title = models.CharField(
|
||||
max_length=100)
|
||||
title = models.CharField(max_length=100)
|
||||
"""
|
||||
Title of the assignment.
|
||||
"""
|
||||
|
||||
description = models.TextField(
|
||||
blank=True)
|
||||
description = models.TextField(blank=True)
|
||||
"""
|
||||
Text to describe the assignment.
|
||||
"""
|
||||
@ -122,23 +117,19 @@ class Assignment(RESTModelMixin, models.Model):
|
||||
The number of members to be elected.
|
||||
"""
|
||||
|
||||
poll_description_default = models.CharField(
|
||||
max_length=79,
|
||||
blank=True)
|
||||
poll_description_default = models.CharField(max_length=79, blank=True)
|
||||
"""
|
||||
Default text for the poll description.
|
||||
"""
|
||||
|
||||
phase = models.IntegerField(
|
||||
choices=PHASES,
|
||||
default=PHASE_SEARCH)
|
||||
phase = models.IntegerField(choices=PHASES, default=PHASE_SEARCH)
|
||||
"""
|
||||
Phase in which the assignment is.
|
||||
"""
|
||||
|
||||
related_users = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
through='AssignmentRelatedUser')
|
||||
settings.AUTH_USER_MODEL, through="AssignmentRelatedUser"
|
||||
)
|
||||
"""
|
||||
Users that are candidates or elected.
|
||||
|
||||
@ -152,18 +143,18 @@ class Assignment(RESTModelMixin, models.Model):
|
||||
|
||||
# In theory there could be one then more agenda_item. But we support only
|
||||
# one. See the property agenda_item.
|
||||
agenda_items = GenericRelation(Item, related_name='assignments')
|
||||
agenda_items = GenericRelation(Item, related_name="assignments")
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
permissions = (
|
||||
('can_see', 'Can see elections'),
|
||||
('can_nominate_other', 'Can nominate another participant'),
|
||||
('can_nominate_self', 'Can nominate oneself'),
|
||||
('can_manage', 'Can manage elections'),
|
||||
("can_see", "Can see elections"),
|
||||
("can_nominate_other", "Can nominate another participant"),
|
||||
("can_nominate_self", "Can nominate oneself"),
|
||||
("can_manage", "Can manage elections"),
|
||||
)
|
||||
ordering = ('title', )
|
||||
verbose_name = ugettext_noop('Election')
|
||||
ordering = ("title",)
|
||||
verbose_name = ugettext_noop("Election")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@ -174,26 +165,25 @@ class Assignment(RESTModelMixin, models.Model):
|
||||
assignment projector element is disabled.
|
||||
"""
|
||||
Projector.remove_any(
|
||||
skip_autoupdate=skip_autoupdate,
|
||||
name='assignments/assignment',
|
||||
id=self.pk)
|
||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore # TODO fix typing
|
||||
skip_autoupdate=skip_autoupdate, name="assignments/assignment", id=self.pk
|
||||
)
|
||||
return super().delete( # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||
)
|
||||
|
||||
@property
|
||||
def candidates(self):
|
||||
"""
|
||||
Queryset that represents the candidates for the assignment.
|
||||
"""
|
||||
return self.related_users.filter(
|
||||
assignmentrelateduser__elected=False)
|
||||
return self.related_users.filter(assignmentrelateduser__elected=False)
|
||||
|
||||
@property
|
||||
def elected(self):
|
||||
"""
|
||||
Queryset that represents all elected users for the assignment.
|
||||
"""
|
||||
return self.related_users.filter(
|
||||
assignmentrelateduser__elected=True)
|
||||
return self.related_users.filter(assignmentrelateduser__elected=True)
|
||||
|
||||
def is_candidate(self, user):
|
||||
"""
|
||||
@ -215,22 +205,22 @@ class Assignment(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Adds the user as candidate.
|
||||
"""
|
||||
weight = self.assignment_related_users.aggregate(
|
||||
models.Max('weight'))['weight__max'] or 0
|
||||
defaults = {
|
||||
'elected': False,
|
||||
'weight': weight + 1}
|
||||
weight = (
|
||||
self.assignment_related_users.aggregate(models.Max("weight"))["weight__max"]
|
||||
or 0
|
||||
)
|
||||
defaults = {"elected": False, "weight": weight + 1}
|
||||
related_user, __ = self.assignment_related_users.update_or_create(
|
||||
user=user,
|
||||
defaults=defaults)
|
||||
user=user, defaults=defaults
|
||||
)
|
||||
|
||||
def set_elected(self, user):
|
||||
"""
|
||||
Makes user an elected user for this assignment.
|
||||
"""
|
||||
related_user, __ = self.assignment_related_users.update_or_create(
|
||||
user=user,
|
||||
defaults={'elected': True})
|
||||
user=user, defaults={"elected": True}
|
||||
)
|
||||
|
||||
def delete_related_user(self, user):
|
||||
"""
|
||||
@ -258,39 +248,43 @@ class Assignment(RESTModelMixin, models.Model):
|
||||
candidates = self.candidates.all()
|
||||
|
||||
# Find out the method of the election
|
||||
if config['assignments_poll_vote_values'] == 'votes':
|
||||
pollmethod = 'votes'
|
||||
elif config['assignments_poll_vote_values'] == 'yesnoabstain':
|
||||
pollmethod = 'yna'
|
||||
elif config['assignments_poll_vote_values'] == 'yesno':
|
||||
pollmethod = 'yn'
|
||||
if config["assignments_poll_vote_values"] == "votes":
|
||||
pollmethod = "votes"
|
||||
elif config["assignments_poll_vote_values"] == "yesnoabstain":
|
||||
pollmethod = "yna"
|
||||
elif config["assignments_poll_vote_values"] == "yesno":
|
||||
pollmethod = "yn"
|
||||
else:
|
||||
# config['assignments_poll_vote_values'] == 'auto'
|
||||
# candidates <= available posts -> yes/no/abstain
|
||||
if len(candidates) <= (self.open_posts - self.elected.count()):
|
||||
pollmethod = 'yna'
|
||||
pollmethod = "yna"
|
||||
else:
|
||||
pollmethod = 'votes'
|
||||
pollmethod = "votes"
|
||||
|
||||
# Create the poll with the candidates.
|
||||
poll = self.polls.create(
|
||||
description=self.poll_description_default,
|
||||
pollmethod=pollmethod)
|
||||
description=self.poll_description_default, pollmethod=pollmethod
|
||||
)
|
||||
options = []
|
||||
related_users = AssignmentRelatedUser.objects.filter(assignment__id=self.id).exclude(elected=True)
|
||||
related_users = AssignmentRelatedUser.objects.filter(
|
||||
assignment__id=self.id
|
||||
).exclude(elected=True)
|
||||
for related_user in related_users:
|
||||
options.append({
|
||||
'candidate': related_user.user,
|
||||
'weight': related_user.weight})
|
||||
options.append(
|
||||
{"candidate": related_user.user, "weight": related_user.weight}
|
||||
)
|
||||
poll.set_options(options, skip_autoupdate=True)
|
||||
inform_changed_data(self)
|
||||
|
||||
# Add all candidates to list of speakers of related agenda item
|
||||
# TODO: Try to do this in a bulk create
|
||||
if config['assignments_add_candidates_to_list_of_speakers']:
|
||||
if config["assignments_add_candidates_to_list_of_speakers"]:
|
||||
for candidate in self.candidates:
|
||||
try:
|
||||
Speaker.objects.add(candidate, self.agenda_item, skip_autoupdate=True)
|
||||
Speaker.objects.add(
|
||||
candidate, self.agenda_item, skip_autoupdate=True
|
||||
)
|
||||
except OpenSlidesError:
|
||||
# The Speaker is already on the list. Do nothing.
|
||||
# TODO: Find a smart way not to catch the error concerning AnonymousUser.
|
||||
@ -349,7 +343,7 @@ class Assignment(RESTModelMixin, models.Model):
|
||||
Return a title for the agenda with the appended assignment verbose name.
|
||||
Note: It has to be the same return value like in JavaScript.
|
||||
"""
|
||||
return '%s (%s)' % (self.get_agenda_title(), _(self._meta.verbose_name))
|
||||
return "%s (%s)" % (self.get_agenda_title(), _(self._meta.verbose_name))
|
||||
|
||||
@property
|
||||
def agenda_item(self):
|
||||
@ -370,9 +364,8 @@ class Assignment(RESTModelMixin, models.Model):
|
||||
|
||||
class AssignmentVote(RESTModelMixin, BaseVote):
|
||||
option = models.ForeignKey(
|
||||
'AssignmentOption',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='votes')
|
||||
"AssignmentOption", on_delete=models.CASCADE, related_name="votes"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
@ -386,12 +379,9 @@ class AssignmentVote(RESTModelMixin, BaseVote):
|
||||
|
||||
class AssignmentOption(RESTModelMixin, BaseOption):
|
||||
poll = models.ForeignKey(
|
||||
'AssignmentPoll',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='options')
|
||||
candidate = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)
|
||||
"AssignmentPoll", on_delete=models.CASCADE, related_name="options"
|
||||
)
|
||||
candidate = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
weight = models.IntegerField(default=0)
|
||||
|
||||
vote_class = AssignmentVote
|
||||
@ -411,26 +401,32 @@ class AssignmentOption(RESTModelMixin, BaseOption):
|
||||
|
||||
# TODO: remove the type-ignoring in the next line, after this is solved:
|
||||
# https://github.com/python/mypy/issues/3855
|
||||
class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore
|
||||
PublishPollMixin, BasePoll):
|
||||
class AssignmentPoll( # type: ignore
|
||||
RESTModelMixin, CollectDefaultVotesMixin, PublishPollMixin, BasePoll
|
||||
):
|
||||
option_class = AssignmentOption
|
||||
|
||||
assignment = models.ForeignKey(
|
||||
Assignment,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='polls')
|
||||
pollmethod = models.CharField(
|
||||
max_length=5,
|
||||
default='yna')
|
||||
description = models.CharField(
|
||||
max_length=79,
|
||||
blank=True)
|
||||
Assignment, on_delete=models.CASCADE, related_name="polls"
|
||||
)
|
||||
pollmethod = models.CharField(max_length=5, default="yna")
|
||||
description = models.CharField(max_length=79, blank=True)
|
||||
|
||||
votesabstain = models.DecimalField(null=True, blank=True, validators=[
|
||||
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
|
||||
votesabstain = models.DecimalField(
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(Decimal("-2"))],
|
||||
max_digits=15,
|
||||
decimal_places=6,
|
||||
)
|
||||
""" General abstain votes, used for pollmethod 'votes' """
|
||||
votesno = models.DecimalField(null=True, blank=True, validators=[
|
||||
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
|
||||
votesno = models.DecimalField(
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(Decimal("-2"))],
|
||||
max_digits=15,
|
||||
decimal_places=6,
|
||||
)
|
||||
""" General no votes, used for pollmethod 'votes' """
|
||||
|
||||
class Meta:
|
||||
@ -443,27 +439,30 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore
|
||||
"""
|
||||
Projector.remove_any(
|
||||
skip_autoupdate=skip_autoupdate,
|
||||
name='assignments/assignment',
|
||||
name="assignments/assignment",
|
||||
id=self.assignment.pk,
|
||||
poll=self.pk)
|
||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore # TODO: fix typing
|
||||
poll=self.pk,
|
||||
)
|
||||
return super().delete( # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||
)
|
||||
|
||||
def get_assignment(self):
|
||||
return self.assignment
|
||||
|
||||
def get_vote_values(self):
|
||||
if self.pollmethod == 'yna':
|
||||
return ['Yes', 'No', 'Abstain']
|
||||
elif self.pollmethod == 'yn':
|
||||
return ['Yes', 'No']
|
||||
if self.pollmethod == "yna":
|
||||
return ["Yes", "No", "Abstain"]
|
||||
elif self.pollmethod == "yn":
|
||||
return ["Yes", "No"]
|
||||
else:
|
||||
return ['Votes']
|
||||
return ["Votes"]
|
||||
|
||||
def get_ballot(self):
|
||||
return self.assignment.polls.filter(id__lte=self.pk).count()
|
||||
|
||||
def get_percent_base_choice(self):
|
||||
return config['assignments_poll_100_percent_base']
|
||||
return config["assignments_poll_100_percent_base"]
|
||||
|
||||
def get_root_rest_element(self):
|
||||
"""
|
||||
|
@ -11,30 +11,33 @@ class AssignmentSlide(ProjectorElement):
|
||||
|
||||
You can send a poll id to get a poll slide.
|
||||
"""
|
||||
name = 'assignments/assignment'
|
||||
|
||||
name = "assignments/assignment"
|
||||
|
||||
def check_data(self):
|
||||
if not Assignment.objects.filter(pk=self.config_entry.get('id')).exists():
|
||||
raise ProjectorException('Election does not exist.')
|
||||
poll_id = self.config_entry.get('poll')
|
||||
if not Assignment.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||
raise ProjectorException("Election does not exist.")
|
||||
poll_id = self.config_entry.get("poll")
|
||||
if poll_id:
|
||||
# Poll slide.
|
||||
try:
|
||||
poll = AssignmentPoll.objects.get(pk=poll_id)
|
||||
except AssignmentPoll.DoesNotExist:
|
||||
raise ProjectorException('Poll does not exist.')
|
||||
if poll.assignment_id != self.config_entry.get('id'):
|
||||
raise ProjectorException('Assignment id and poll do not belong together.')
|
||||
raise ProjectorException("Poll does not exist.")
|
||||
if poll.assignment_id != self.config_entry.get("id"):
|
||||
raise ProjectorException(
|
||||
"Assignment id and poll do not belong together."
|
||||
)
|
||||
|
||||
def update_data(self):
|
||||
data = None
|
||||
try:
|
||||
assignment = Assignment.objects.get(pk=self.config_entry.get('id'))
|
||||
assignment = Assignment.objects.get(pk=self.config_entry.get("id"))
|
||||
except Assignment.DoesNotExist:
|
||||
# Assignment does not exist, so just do nothing.
|
||||
pass
|
||||
else:
|
||||
data = {'agenda_item_id': assignment.agenda_item_id}
|
||||
data = {"agenda_item_id": assignment.agenda_item_id}
|
||||
return data
|
||||
|
||||
|
||||
|
@ -28,8 +28,10 @@ def posts_validator(data):
|
||||
"""
|
||||
Validator for open posts. It checks that the values for the open posts are greater than 0.
|
||||
"""
|
||||
if (data['open_posts'] and data['open_posts'] is not None and data['open_posts'] < 1):
|
||||
raise ValidationError({'detail': _('Value for {} must be greater than 0').format('open_posts')})
|
||||
if data["open_posts"] and data["open_posts"] is not None and data["open_posts"] < 1:
|
||||
raise ValidationError(
|
||||
{"detail": _("Value for {} must be greater than 0").format("open_posts")}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@ -37,35 +39,39 @@ class AssignmentRelatedUserSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for assignment.models.AssignmentRelatedUser objects.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = AssignmentRelatedUser
|
||||
fields = (
|
||||
'id',
|
||||
'user',
|
||||
'elected',
|
||||
'assignment',
|
||||
'weight') # js-data needs the assignment-id in the nested object to define relations.
|
||||
"id",
|
||||
"user",
|
||||
"elected",
|
||||
"assignment",
|
||||
"weight",
|
||||
) # js-data needs the assignment-id in the nested object to define relations.
|
||||
|
||||
|
||||
class AssignmentVoteSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for assignment.models.AssignmentVote objects.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = AssignmentVote
|
||||
fields = ('weight', 'value',)
|
||||
fields = ("weight", "value")
|
||||
|
||||
|
||||
class AssignmentOptionSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for assignment.models.AssignmentOption objects.
|
||||
"""
|
||||
|
||||
votes = AssignmentVoteSerializer(many=True, read_only=True)
|
||||
is_elected = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AssignmentOption
|
||||
fields = ('id', 'candidate', 'is_elected', 'votes', 'poll', 'weight')
|
||||
fields = ("id", "candidate", "is_elected", "votes", "poll", "weight")
|
||||
|
||||
def get_is_elected(self, obj):
|
||||
"""
|
||||
@ -78,6 +84,7 @@ class FilterPollListSerializer(ListSerializer):
|
||||
"""
|
||||
Customized serializer to filter polls (exclude unpublished).
|
||||
"""
|
||||
|
||||
def to_representation(self, data):
|
||||
"""
|
||||
List of object instances -> List of dicts of primitive datatypes.
|
||||
@ -86,7 +93,9 @@ class FilterPollListSerializer(ListSerializer):
|
||||
"""
|
||||
# Dealing with nested relationships, data can be a Manager,
|
||||
# so, first get a queryset from the Manager if needed
|
||||
iterable = data.filter(published=True) if isinstance(data, models.Manager) else data
|
||||
iterable = (
|
||||
data.filter(published=True) if isinstance(data, models.Manager) else data
|
||||
)
|
||||
return [self.child.to_representation(item) for item in iterable]
|
||||
|
||||
|
||||
@ -96,31 +105,35 @@ class AssignmentAllPollSerializer(ModelSerializer):
|
||||
|
||||
Serializes all polls.
|
||||
"""
|
||||
|
||||
options = AssignmentOptionSerializer(many=True, read_only=True)
|
||||
votes = ListField(
|
||||
child=DictField(
|
||||
child=DecimalField(max_digits=15, decimal_places=6, min_value=-2)),
|
||||
child=DecimalField(max_digits=15, decimal_places=6, min_value=-2)
|
||||
),
|
||||
write_only=True,
|
||||
required=False)
|
||||
required=False,
|
||||
)
|
||||
has_votes = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AssignmentPoll
|
||||
fields = (
|
||||
'id',
|
||||
'pollmethod',
|
||||
'description',
|
||||
'published',
|
||||
'options',
|
||||
'votesabstain',
|
||||
'votesno',
|
||||
'votesvalid',
|
||||
'votesinvalid',
|
||||
'votescast',
|
||||
'votes',
|
||||
'has_votes',
|
||||
'assignment') # js-data needs the assignment-id in the nested object to define relations.
|
||||
read_only_fields = ('pollmethod',)
|
||||
"id",
|
||||
"pollmethod",
|
||||
"description",
|
||||
"published",
|
||||
"options",
|
||||
"votesabstain",
|
||||
"votesno",
|
||||
"votesvalid",
|
||||
"votesinvalid",
|
||||
"votescast",
|
||||
"votes",
|
||||
"has_votes",
|
||||
"assignment",
|
||||
) # js-data needs the assignment-id in the nested object to define relations.
|
||||
read_only_fields = ("pollmethod",)
|
||||
validators = (default_votes_validator,)
|
||||
|
||||
def get_has_votes(self, obj):
|
||||
@ -144,30 +157,45 @@ class AssignmentAllPollSerializer(ModelSerializer):
|
||||
"votes": [{"Votes": 10}, {"Votes": 0}]
|
||||
"""
|
||||
# Update votes.
|
||||
votes = validated_data.get('votes')
|
||||
votes = validated_data.get("votes")
|
||||
if votes:
|
||||
options = list(instance.get_options())
|
||||
if len(votes) != len(options):
|
||||
raise ValidationError({
|
||||
'detail': _('You have to submit data for %d candidates.') % len(options)})
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": _("You have to submit data for %d candidates.")
|
||||
% len(options)
|
||||
}
|
||||
)
|
||||
for index, option in enumerate(options):
|
||||
if len(votes[index]) != len(instance.get_vote_values()):
|
||||
raise ValidationError({
|
||||
'detail': _('You have to submit data for %d vote values.') % len(instance.get_vote_values())})
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": _("You have to submit data for %d vote values.")
|
||||
% len(instance.get_vote_values())
|
||||
}
|
||||
)
|
||||
for vote_value, vote_weight in votes[index].items():
|
||||
if vote_value not in instance.get_vote_values():
|
||||
raise ValidationError({
|
||||
'detail': _('Vote value %s is invalid.') % vote_value})
|
||||
instance.set_vote_objects_with_values(option, votes[index], skip_autoupdate=True)
|
||||
raise ValidationError(
|
||||
{"detail": _("Vote value %s is invalid.") % vote_value}
|
||||
)
|
||||
instance.set_vote_objects_with_values(
|
||||
option, votes[index], skip_autoupdate=True
|
||||
)
|
||||
|
||||
# Update remaining writeable fields.
|
||||
instance.description = validated_data.get('description', instance.description)
|
||||
instance.published = validated_data.get('published', instance.published)
|
||||
instance.votesabstain = validated_data.get('votesabstain', instance.votesabstain)
|
||||
instance.votesno = validated_data.get('votesno', instance.votesno)
|
||||
instance.votesvalid = validated_data.get('votesvalid', instance.votesvalid)
|
||||
instance.votesinvalid = validated_data.get('votesinvalid', instance.votesinvalid)
|
||||
instance.votescast = validated_data.get('votescast', instance.votescast)
|
||||
instance.description = validated_data.get("description", instance.description)
|
||||
instance.published = validated_data.get("published", instance.published)
|
||||
instance.votesabstain = validated_data.get(
|
||||
"votesabstain", instance.votesabstain
|
||||
)
|
||||
instance.votesno = validated_data.get("votesno", instance.votesno)
|
||||
instance.votesvalid = validated_data.get("votesvalid", instance.votesvalid)
|
||||
instance.votesinvalid = validated_data.get(
|
||||
"votesinvalid", instance.votesinvalid
|
||||
)
|
||||
instance.votescast = validated_data.get("votescast", instance.votescast)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
@ -178,52 +206,60 @@ class AssignmentShortPollSerializer(AssignmentAllPollSerializer):
|
||||
|
||||
Serializes only short polls (excluded unpublished polls).
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
list_serializer_class = FilterPollListSerializer
|
||||
model = AssignmentPoll
|
||||
fields = (
|
||||
'id',
|
||||
'pollmethod',
|
||||
'description',
|
||||
'published',
|
||||
'options',
|
||||
'votesabstain',
|
||||
'votesno',
|
||||
'votesvalid',
|
||||
'votesinvalid',
|
||||
'votescast',
|
||||
'has_votes',)
|
||||
"id",
|
||||
"pollmethod",
|
||||
"description",
|
||||
"published",
|
||||
"options",
|
||||
"votesabstain",
|
||||
"votesno",
|
||||
"votesvalid",
|
||||
"votesinvalid",
|
||||
"votescast",
|
||||
"has_votes",
|
||||
)
|
||||
|
||||
|
||||
class AssignmentFullSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for assignment.models.Assignment objects. With all polls.
|
||||
"""
|
||||
assignment_related_users = AssignmentRelatedUserSerializer(many=True, read_only=True)
|
||||
|
||||
assignment_related_users = AssignmentRelatedUserSerializer(
|
||||
many=True, read_only=True
|
||||
)
|
||||
polls = AssignmentAllPollSerializer(many=True, read_only=True)
|
||||
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3)
|
||||
agenda_type = IntegerField(
|
||||
write_only=True, required=False, min_value=1, max_value=3
|
||||
)
|
||||
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
|
||||
|
||||
class Meta:
|
||||
model = Assignment
|
||||
fields = (
|
||||
'id',
|
||||
'title',
|
||||
'description',
|
||||
'open_posts',
|
||||
'phase',
|
||||
'assignment_related_users',
|
||||
'poll_description_default',
|
||||
'polls',
|
||||
'agenda_item_id',
|
||||
'agenda_type',
|
||||
'agenda_parent_id',
|
||||
'tags',)
|
||||
"id",
|
||||
"title",
|
||||
"description",
|
||||
"open_posts",
|
||||
"phase",
|
||||
"assignment_related_users",
|
||||
"poll_description_default",
|
||||
"polls",
|
||||
"agenda_item_id",
|
||||
"agenda_type",
|
||||
"agenda_parent_id",
|
||||
"tags",
|
||||
)
|
||||
validators = (posts_validator,)
|
||||
|
||||
def validate(self, data):
|
||||
if 'description' in data:
|
||||
data['description'] = validate_html(data['description'])
|
||||
if "description" in data:
|
||||
data["description"] = validate_html(data["description"])
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
@ -231,10 +267,10 @@ class AssignmentFullSerializer(ModelSerializer):
|
||||
Customized create method. Set information about related agenda item
|
||||
into agenda_item_update_information container.
|
||||
"""
|
||||
agenda_type = validated_data.pop('agenda_type', None)
|
||||
agenda_parent_id = validated_data.pop('agenda_parent_id', None)
|
||||
agenda_type = validated_data.pop("agenda_type", None)
|
||||
agenda_parent_id = validated_data.pop("agenda_parent_id", None)
|
||||
assignment = Assignment(**validated_data)
|
||||
assignment.agenda_item_update_information['type'] = agenda_type
|
||||
assignment.agenda_item_update_information['parent_id'] = agenda_parent_id
|
||||
assignment.agenda_item_update_information["type"] = agenda_type
|
||||
assignment.agenda_item_update_information["parent_id"] = agenda_parent_id
|
||||
assignment.save()
|
||||
return assignment
|
||||
|
@ -5,8 +5,11 @@ def get_permission_change_data(sender, permissions=None, **kwargs):
|
||||
"""
|
||||
Yields all necessary collections if 'assignments.can_see' permission changes.
|
||||
"""
|
||||
assignments_app = apps.get_app_config(app_label='assignments')
|
||||
assignments_app = apps.get_app_config(app_label="assignments")
|
||||
for permission in permissions:
|
||||
# There could be only one 'assignment.can_see' and then we want to return data.
|
||||
if permission.content_type.app_label == assignments_app.label and permission.codename == 'can_see':
|
||||
if (
|
||||
permission.content_type.app_label == assignments_app.label
|
||||
and permission.codename == "can_see"
|
||||
):
|
||||
yield from assignments_app.get_startup_elements()
|
||||
|
@ -21,6 +21,7 @@ from .serializers import AssignmentAllPollSerializer
|
||||
|
||||
# Viewsets for the REST API
|
||||
|
||||
|
||||
class AssignmentViewSet(ModelViewSet):
|
||||
"""
|
||||
API endpoint for assignments.
|
||||
@ -29,6 +30,7 @@ class AssignmentViewSet(ModelViewSet):
|
||||
partial_update, update, destroy, candidature_self, candidature_other,
|
||||
mark_elected and create_poll.
|
||||
"""
|
||||
|
||||
access_permissions = AssignmentAccessPermissions()
|
||||
queryset = Assignment.objects.all()
|
||||
|
||||
@ -36,26 +38,36 @@ class AssignmentViewSet(ModelViewSet):
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
if self.action in ('list', 'retrieve'):
|
||||
if self.action in ("list", "retrieve"):
|
||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||
elif self.action == 'metadata':
|
||||
elif self.action == "metadata":
|
||||
# Everybody is allowed to see the metadata.
|
||||
result = True
|
||||
elif self.action in ('create', 'partial_update', 'update', 'destroy',
|
||||
'mark_elected', 'create_poll', 'sort_related_users'):
|
||||
result = (has_perm(self.request.user, 'assignments.can_see') and
|
||||
has_perm(self.request.user, 'assignments.can_manage'))
|
||||
elif self.action == 'candidature_self':
|
||||
result = (has_perm(self.request.user, 'assignments.can_see') and
|
||||
has_perm(self.request.user, 'assignments.can_nominate_self'))
|
||||
elif self.action == 'candidature_other':
|
||||
result = (has_perm(self.request.user, 'assignments.can_see') and
|
||||
has_perm(self.request.user, 'assignments.can_nominate_other'))
|
||||
elif self.action in (
|
||||
"create",
|
||||
"partial_update",
|
||||
"update",
|
||||
"destroy",
|
||||
"mark_elected",
|
||||
"create_poll",
|
||||
"sort_related_users",
|
||||
):
|
||||
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
||||
self.request.user, "assignments.can_manage"
|
||||
)
|
||||
elif self.action == "candidature_self":
|
||||
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
||||
self.request.user, "assignments.can_nominate_self"
|
||||
)
|
||||
elif self.action == "candidature_other":
|
||||
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
||||
self.request.user, "assignments.can_nominate_other"
|
||||
)
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
|
||||
@detail_route(methods=['post', 'delete'])
|
||||
@detail_route(methods=["post", "delete"])
|
||||
def candidature_self(self, request, pk=None):
|
||||
"""
|
||||
View to nominate self as candidate (POST) or withdraw own
|
||||
@ -63,18 +75,26 @@ class AssignmentViewSet(ModelViewSet):
|
||||
"""
|
||||
assignment = self.get_object()
|
||||
if assignment.is_elected(request.user):
|
||||
raise ValidationError({'detail': _('You are already elected.')})
|
||||
if request.method == 'POST':
|
||||
raise ValidationError({"detail": _("You are already elected.")})
|
||||
if request.method == "POST":
|
||||
message = self.nominate_self(request, assignment)
|
||||
else:
|
||||
# request.method == 'DELETE'
|
||||
message = self.withdraw_self(request, assignment)
|
||||
return Response({'detail': message})
|
||||
return Response({"detail": message})
|
||||
|
||||
def nominate_self(self, request, assignment):
|
||||
if assignment.phase == assignment.PHASE_FINISHED:
|
||||
raise ValidationError({'detail': _('You can not candidate to this election because it is finished.')})
|
||||
if assignment.phase == assignment.PHASE_VOTING and not has_perm(request.user, 'assignments.can_manage'):
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": _(
|
||||
"You can not candidate to this election because it is finished."
|
||||
)
|
||||
}
|
||||
)
|
||||
if assignment.phase == assignment.PHASE_VOTING and not has_perm(
|
||||
request.user, "assignments.can_manage"
|
||||
):
|
||||
# To nominate self during voting you have to be a manager.
|
||||
self.permission_denied(request)
|
||||
# If the request.user is already a candidate he can nominate himself nevertheless.
|
||||
@ -82,19 +102,29 @@ class AssignmentViewSet(ModelViewSet):
|
||||
# Send new candidate via autoupdate because users without permission
|
||||
# to see users may not have it but can get it now.
|
||||
inform_changed_data([request.user])
|
||||
return _('You were nominated successfully.')
|
||||
return _("You were nominated successfully.")
|
||||
|
||||
def withdraw_self(self, request, assignment):
|
||||
# Withdraw candidature.
|
||||
if assignment.phase == assignment.PHASE_FINISHED:
|
||||
raise ValidationError({'detail': _('You can not withdraw your candidature to this election because it is finished.')})
|
||||
if assignment.phase == assignment.PHASE_VOTING and not has_perm(request.user, 'assignments.can_manage'):
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": _(
|
||||
"You can not withdraw your candidature to this election because it is finished."
|
||||
)
|
||||
}
|
||||
)
|
||||
if assignment.phase == assignment.PHASE_VOTING and not has_perm(
|
||||
request.user, "assignments.can_manage"
|
||||
):
|
||||
# To withdraw self during voting you have to be a manager.
|
||||
self.permission_denied(request)
|
||||
if not assignment.is_candidate(request.user):
|
||||
raise ValidationError({'detail': _('You are not a candidate of this election.')})
|
||||
raise ValidationError(
|
||||
{"detail": _("You are not a candidate of this election.")}
|
||||
)
|
||||
assignment.delete_related_user(request.user)
|
||||
return _('You have withdrawn your candidature successfully.')
|
||||
return _("You have withdrawn your candidature successfully.")
|
||||
|
||||
def get_user_from_request_data(self, request):
|
||||
"""
|
||||
@ -103,20 +133,26 @@ class AssignmentViewSet(ModelViewSet):
|
||||
self.mark_elected can play with it.
|
||||
"""
|
||||
if not isinstance(request.data, dict):
|
||||
detail = _('Invalid data. Expected dictionary, got %s.') % type(request.data)
|
||||
raise ValidationError({'detail': detail})
|
||||
user_str = request.data.get('user', '')
|
||||
detail = _("Invalid data. Expected dictionary, got %s.") % type(
|
||||
request.data
|
||||
)
|
||||
raise ValidationError({"detail": detail})
|
||||
user_str = request.data.get("user", "")
|
||||
try:
|
||||
user_pk = int(user_str)
|
||||
except ValueError:
|
||||
raise ValidationError({'detail': _('Invalid data. Expected something like {"user": <id>}.')})
|
||||
raise ValidationError(
|
||||
{"detail": _('Invalid data. Expected something like {"user": <id>}.')}
|
||||
)
|
||||
try:
|
||||
user = get_user_model().objects.get(pk=user_pk)
|
||||
except get_user_model().DoesNotExist:
|
||||
raise ValidationError({'detail': _('Invalid data. User %d does not exist.') % user_pk})
|
||||
raise ValidationError(
|
||||
{"detail": _("Invalid data. User %d does not exist.") % user_pk}
|
||||
)
|
||||
return user
|
||||
|
||||
@detail_route(methods=['post', 'delete'])
|
||||
@detail_route(methods=["post", "delete"])
|
||||
def candidature_other(self, request, pk=None):
|
||||
"""
|
||||
View to nominate other users (POST) or delete their candidature
|
||||
@ -124,43 +160,51 @@ class AssignmentViewSet(ModelViewSet):
|
||||
"""
|
||||
user = self.get_user_from_request_data(request)
|
||||
assignment = self.get_object()
|
||||
if request.method == 'POST':
|
||||
if request.method == "POST":
|
||||
message = self.nominate_other(request, user, assignment)
|
||||
else:
|
||||
# request.method == 'DELETE'
|
||||
message = self.delete_other(request, user, assignment)
|
||||
return Response({'detail': message})
|
||||
return Response({"detail": message})
|
||||
|
||||
def nominate_other(self, request, user, assignment):
|
||||
if assignment.is_elected(user):
|
||||
raise ValidationError({'detail': _('User %s is already elected.') % user})
|
||||
raise ValidationError({"detail": _("User %s is already elected.") % user})
|
||||
if assignment.phase == assignment.PHASE_FINISHED:
|
||||
detail = _('You can not nominate someone to this election because it is finished.')
|
||||
raise ValidationError({'detail': detail})
|
||||
if assignment.phase == assignment.PHASE_VOTING and not has_perm(request.user, 'assignments.can_manage'):
|
||||
detail = _(
|
||||
"You can not nominate someone to this election because it is finished."
|
||||
)
|
||||
raise ValidationError({"detail": detail})
|
||||
if assignment.phase == assignment.PHASE_VOTING and not has_perm(
|
||||
request.user, "assignments.can_manage"
|
||||
):
|
||||
# To nominate another user during voting you have to be a manager.
|
||||
self.permission_denied(request)
|
||||
if assignment.is_candidate(user):
|
||||
raise ValidationError({'detail': _('User %s is already nominated.') % user})
|
||||
raise ValidationError({"detail": _("User %s is already nominated.") % user})
|
||||
assignment.set_candidate(user)
|
||||
# Send new candidate via autoupdate because users without permission
|
||||
# to see users may not have it but can get it now.
|
||||
inform_changed_data(user)
|
||||
return _('User %s was nominated successfully.') % user
|
||||
return _("User %s was nominated successfully.") % user
|
||||
|
||||
def delete_other(self, request, user, assignment):
|
||||
# To delete candidature status you have to be a manager.
|
||||
if not has_perm(request.user, 'assignments.can_manage'):
|
||||
if not has_perm(request.user, "assignments.can_manage"):
|
||||
self.permission_denied(request)
|
||||
if assignment.phase == assignment.PHASE_FINISHED:
|
||||
detail = _("You can not delete someone's candidature to this election because it is finished.")
|
||||
raise ValidationError({'detail': detail})
|
||||
detail = _(
|
||||
"You can not delete someone's candidature to this election because it is finished."
|
||||
)
|
||||
raise ValidationError({"detail": detail})
|
||||
if not assignment.is_candidate(user) and not assignment.is_elected(user):
|
||||
raise ValidationError({'detail': _('User %s has no status in this election.') % user})
|
||||
raise ValidationError(
|
||||
{"detail": _("User %s has no status in this election.") % user}
|
||||
)
|
||||
assignment.delete_related_user(user)
|
||||
return _('Candidate %s was withdrawn successfully.') % user
|
||||
return _("Candidate %s was withdrawn successfully.") % user
|
||||
|
||||
@detail_route(methods=['post', 'delete'])
|
||||
@detail_route(methods=["post", "delete"])
|
||||
def mark_elected(self, request, pk=None):
|
||||
"""
|
||||
View to mark other users as elected (POST) or undo this (DELETE).
|
||||
@ -168,35 +212,41 @@ class AssignmentViewSet(ModelViewSet):
|
||||
"""
|
||||
user = self.get_user_from_request_data(request)
|
||||
assignment = self.get_object()
|
||||
if request.method == 'POST':
|
||||
if request.method == "POST":
|
||||
if not assignment.is_candidate(user):
|
||||
raise ValidationError({'detail': _('User %s is not a candidate of this election.') % user})
|
||||
raise ValidationError(
|
||||
{"detail": _("User %s is not a candidate of this election.") % user}
|
||||
)
|
||||
assignment.set_elected(user)
|
||||
message = _('User %s was successfully elected.') % user
|
||||
message = _("User %s was successfully elected.") % user
|
||||
else:
|
||||
# request.method == 'DELETE'
|
||||
if not assignment.is_elected(user):
|
||||
detail = _('User %s is not an elected candidate of this election.') % user
|
||||
raise ValidationError({'detail': detail})
|
||||
detail = (
|
||||
_("User %s is not an elected candidate of this election.") % user
|
||||
)
|
||||
raise ValidationError({"detail": detail})
|
||||
assignment.set_candidate(user)
|
||||
message = _('User %s was successfully unelected.') % user
|
||||
return Response({'detail': message})
|
||||
message = _("User %s was successfully unelected.") % user
|
||||
return Response({"detail": message})
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def create_poll(self, request, pk=None):
|
||||
"""
|
||||
View to create a poll. It is a POST request without any data.
|
||||
"""
|
||||
assignment = self.get_object()
|
||||
if not assignment.candidates.exists():
|
||||
raise ValidationError({'detail': _('Can not create ballot because there are no candidates.')})
|
||||
raise ValidationError(
|
||||
{"detail": _("Can not create ballot because there are no candidates.")}
|
||||
)
|
||||
with transaction.atomic():
|
||||
poll = assignment.create_poll()
|
||||
return Response({
|
||||
'detail': _('Ballot created successfully.'),
|
||||
'createdPollId': poll.pk})
|
||||
return Response(
|
||||
{"detail": _("Ballot created successfully."), "createdPollId": poll.pk}
|
||||
)
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def sort_related_users(self, request, pk=None):
|
||||
"""
|
||||
Special view endpoint to sort the assignment related users.
|
||||
@ -206,22 +256,25 @@ class AssignmentViewSet(ModelViewSet):
|
||||
assignment = self.get_object()
|
||||
|
||||
# Check data
|
||||
related_user_ids = request.data.get('related_users')
|
||||
related_user_ids = request.data.get("related_users")
|
||||
if not isinstance(related_user_ids, list):
|
||||
raise ValidationError(
|
||||
{'detail': _('users has to be a list of IDs.')})
|
||||
raise ValidationError({"detail": _("users has to be a list of IDs.")})
|
||||
|
||||
# Get all related users from AssignmentRelatedUser.
|
||||
related_users = {}
|
||||
for related_user in AssignmentRelatedUser.objects.filter(assignment__id=assignment.id):
|
||||
for related_user in AssignmentRelatedUser.objects.filter(
|
||||
assignment__id=assignment.id
|
||||
):
|
||||
related_users[related_user.pk] = related_user
|
||||
|
||||
# Check all given candidates from the request
|
||||
valid_related_users = []
|
||||
for related_user_id in related_user_ids:
|
||||
if not isinstance(related_user_id, int) or related_users.get(related_user_id) is None:
|
||||
raise ValidationError(
|
||||
{'detail': _('Invalid data.')})
|
||||
if (
|
||||
not isinstance(related_user_id, int)
|
||||
or related_users.get(related_user_id) is None
|
||||
):
|
||||
raise ValidationError({"detail": _("Invalid data.")})
|
||||
valid_related_users.append(related_users[related_user_id])
|
||||
|
||||
# Sort the related users
|
||||
@ -236,7 +289,7 @@ class AssignmentViewSet(ModelViewSet):
|
||||
inform_changed_data(assignment)
|
||||
|
||||
# Initiate response.
|
||||
return Response({'detail': _('Assignment related users successfully sorted.')})
|
||||
return Response({"detail": _("Assignment related users successfully sorted.")})
|
||||
|
||||
|
||||
class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
||||
@ -245,6 +298,7 @@ class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet)
|
||||
|
||||
There are the following views: update, partial_update and destroy.
|
||||
"""
|
||||
|
||||
queryset = AssignmentPoll.objects.all()
|
||||
serializer_class = AssignmentAllPollSerializer
|
||||
|
||||
@ -252,5 +306,6 @@ class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet)
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
return (has_perm(self.request.user, 'assignments.can_see') and
|
||||
has_perm(self.request.user, 'assignments.can_manage'))
|
||||
return has_perm(self.request.user, "assignments.can_see") and has_perm(
|
||||
self.request.user, "assignments.can_manage"
|
||||
)
|
||||
|
@ -1 +1 @@
|
||||
default_app_config = 'openslides.core.apps.CoreAppConfig'
|
||||
default_app_config = "openslides.core.apps.CoreAppConfig"
|
||||
|
@ -6,7 +6,8 @@ class ProjectorAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for Projector and ProjectorViewSet.
|
||||
"""
|
||||
base_permission = 'core.can_see_projector'
|
||||
|
||||
base_permission = "core.can_see_projector"
|
||||
|
||||
|
||||
class TagAccessPermissions(BaseAccessPermissions):
|
||||
@ -19,21 +20,24 @@ class ChatMessageAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for ChatMessage and ChatMessageViewSet.
|
||||
"""
|
||||
base_permission = 'core.can_use_chat'
|
||||
|
||||
base_permission = "core.can_use_chat"
|
||||
|
||||
|
||||
class ProjectorMessageAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions for ProjectorMessage.
|
||||
"""
|
||||
base_permission = 'core.can_see_projector'
|
||||
|
||||
base_permission = "core.can_see_projector"
|
||||
|
||||
|
||||
class CountdownAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions for Countdown.
|
||||
"""
|
||||
base_permission = 'core.can_see_projector'
|
||||
|
||||
base_permission = "core.can_see_projector"
|
||||
|
||||
|
||||
class ConfigAccessPermissions(BaseAccessPermissions):
|
||||
|
@ -11,8 +11,8 @@ from ..utils.projector import register_projector_elements
|
||||
|
||||
|
||||
class CoreAppConfig(AppConfig):
|
||||
name = 'openslides.core'
|
||||
verbose_name = 'OpenSlides Core'
|
||||
name = "openslides.core"
|
||||
verbose_name = "OpenSlides Core"
|
||||
angular_site_module = True
|
||||
angular_projector_module = True
|
||||
|
||||
@ -54,7 +54,7 @@ class CoreAppConfig(AppConfig):
|
||||
# Skip all database related accesses during migrations.
|
||||
is_normal_server_start = False
|
||||
for sys_part in sys.argv:
|
||||
for entry in ('runserver', 'gunicorn', 'daphne', 'create-example-data'):
|
||||
for entry in ("runserver", "gunicorn", "daphne", "create-example-data"):
|
||||
if sys_part.endswith(entry):
|
||||
is_normal_server_start = True
|
||||
break
|
||||
@ -68,27 +68,46 @@ class CoreAppConfig(AppConfig):
|
||||
|
||||
# Connect signals.
|
||||
post_permission_creation.connect(
|
||||
delete_django_app_permissions,
|
||||
dispatch_uid='delete_django_app_permissions')
|
||||
delete_django_app_permissions, dispatch_uid="delete_django_app_permissions"
|
||||
)
|
||||
permission_change.connect(
|
||||
get_permission_change_data,
|
||||
dispatch_uid='core_get_permission_change_data')
|
||||
get_permission_change_data, dispatch_uid="core_get_permission_change_data"
|
||||
)
|
||||
|
||||
post_migrate.connect(call_save_default_values, sender=self, dispatch_uid='core_save_config_default_values')
|
||||
post_migrate.connect(
|
||||
call_save_default_values,
|
||||
sender=self,
|
||||
dispatch_uid="core_save_config_default_values",
|
||||
)
|
||||
|
||||
# Register viewsets.
|
||||
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet)
|
||||
router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet)
|
||||
router.register(self.get_model('Tag').get_collection_string(), TagViewSet)
|
||||
router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config')
|
||||
router.register(self.get_model('ProjectorMessage').get_collection_string(), ProjectorMessageViewSet)
|
||||
router.register(self.get_model('Countdown').get_collection_string(), CountdownViewSet)
|
||||
router.register(self.get_model('History').get_collection_string(), HistoryViewSet)
|
||||
router.register(
|
||||
self.get_model("Projector").get_collection_string(), ProjectorViewSet
|
||||
)
|
||||
router.register(
|
||||
self.get_model("ChatMessage").get_collection_string(), ChatMessageViewSet
|
||||
)
|
||||
router.register(self.get_model("Tag").get_collection_string(), TagViewSet)
|
||||
router.register(
|
||||
self.get_model("ConfigStore").get_collection_string(),
|
||||
ConfigViewSet,
|
||||
"config",
|
||||
)
|
||||
router.register(
|
||||
self.get_model("ProjectorMessage").get_collection_string(),
|
||||
ProjectorMessageViewSet,
|
||||
)
|
||||
router.register(
|
||||
self.get_model("Countdown").get_collection_string(), CountdownViewSet
|
||||
)
|
||||
router.register(
|
||||
self.get_model("History").get_collection_string(), HistoryViewSet
|
||||
)
|
||||
|
||||
# Sets the cache and builds the startup history
|
||||
if is_normal_server_start:
|
||||
element_cache.ensure_cache()
|
||||
self.get_model('History').objects.build_history()
|
||||
self.get_model("History").objects.build_history()
|
||||
|
||||
# Register client messages
|
||||
register_client_message(NotifyWebsocketClientMessage())
|
||||
@ -97,10 +116,13 @@ class CoreAppConfig(AppConfig):
|
||||
register_client_message(AutoupdateWebsocketClientMessage())
|
||||
|
||||
# register required_users
|
||||
required_user.add_collection_string(self.get_model('ChatMessage').get_collection_string(), required_users)
|
||||
required_user.add_collection_string(
|
||||
self.get_model("ChatMessage").get_collection_string(), required_users
|
||||
)
|
||||
|
||||
def get_config_variables(self):
|
||||
from .config_variables import get_config_variables
|
||||
|
||||
return get_config_variables()
|
||||
|
||||
def get_startup_elements(self):
|
||||
@ -108,7 +130,15 @@ class CoreAppConfig(AppConfig):
|
||||
Yields all Cachables required on startup i. e. opening the websocket
|
||||
connection.
|
||||
"""
|
||||
for model_name in ('Projector', 'ChatMessage', 'Tag', 'ProjectorMessage', 'Countdown', 'ConfigStore', 'History'):
|
||||
for model_name in (
|
||||
"Projector",
|
||||
"ChatMessage",
|
||||
"Tag",
|
||||
"ProjectorMessage",
|
||||
"Countdown",
|
||||
"ConfigStore",
|
||||
"History",
|
||||
):
|
||||
yield self.get_model(model_name)
|
||||
|
||||
def get_angular_constants(self):
|
||||
@ -118,9 +148,9 @@ class CoreAppConfig(AppConfig):
|
||||
|
||||
# Client settings
|
||||
client_settings_keys = [
|
||||
'MOTION_IDENTIFIER_MIN_DIGITS',
|
||||
'MOTION_IDENTIFIER_WITHOUT_BLANKS',
|
||||
'MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS'
|
||||
"MOTION_IDENTIFIER_MIN_DIGITS",
|
||||
"MOTION_IDENTIFIER_WITHOUT_BLANKS",
|
||||
"MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS",
|
||||
]
|
||||
client_settings_dict = {}
|
||||
for key in client_settings_keys:
|
||||
@ -130,33 +160,40 @@ class CoreAppConfig(AppConfig):
|
||||
# Settings key does not exist. Do nothing. The client will
|
||||
# treat this as undefined.
|
||||
pass
|
||||
constants['OpenSlidesSettings'] = client_settings_dict
|
||||
constants["OpenSlidesSettings"] = client_settings_dict
|
||||
|
||||
# Config variables
|
||||
config_groups: List[Any] = []
|
||||
for config_variable in sorted(config.config_variables.values(), key=attrgetter('weight')):
|
||||
for config_variable in sorted(
|
||||
config.config_variables.values(), key=attrgetter("weight")
|
||||
):
|
||||
if config_variable.is_hidden():
|
||||
# Skip hidden config variables. Do not even check groups and subgroups.
|
||||
continue
|
||||
if not config_groups or config_groups[-1]['name'] != config_variable.group:
|
||||
if not config_groups or config_groups[-1]["name"] != config_variable.group:
|
||||
# Add new group.
|
||||
config_groups.append(OrderedDict(
|
||||
name=config_variable.group,
|
||||
subgroups=[]))
|
||||
if not config_groups[-1]['subgroups'] or config_groups[-1]['subgroups'][-1]['name'] != config_variable.subgroup:
|
||||
config_groups.append(
|
||||
OrderedDict(name=config_variable.group, subgroups=[])
|
||||
)
|
||||
if (
|
||||
not config_groups[-1]["subgroups"]
|
||||
or config_groups[-1]["subgroups"][-1]["name"]
|
||||
!= config_variable.subgroup
|
||||
):
|
||||
# Add new subgroup.
|
||||
config_groups[-1]['subgroups'].append(OrderedDict(
|
||||
name=config_variable.subgroup,
|
||||
items=[]))
|
||||
config_groups[-1]["subgroups"].append(
|
||||
OrderedDict(name=config_variable.subgroup, items=[])
|
||||
)
|
||||
# Add the config variable to the current group and subgroup.
|
||||
config_groups[-1]['subgroups'][-1]['items'].append(config_variable.data)
|
||||
constants['OpenSlidesConfigVariables'] = config_groups
|
||||
config_groups[-1]["subgroups"][-1]["items"].append(config_variable.data)
|
||||
constants["OpenSlidesConfigVariables"] = config_groups
|
||||
|
||||
return constants
|
||||
|
||||
|
||||
def call_save_default_values(**kwargs):
|
||||
from .config import config
|
||||
|
||||
config.save_default_values()
|
||||
|
||||
|
||||
@ -164,4 +201,4 @@ def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||
"""
|
||||
Returns all user ids that are displayed as chatters.
|
||||
"""
|
||||
return set(element['user_id'])
|
||||
return set(element["user_id"])
|
||||
|
@ -1,13 +1,4 @@
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
from typing import Any, Callable, Dict, Iterable, Optional, TypeVar, Union, cast
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.apps import apps
|
||||
@ -21,16 +12,16 @@ from .models import ConfigStore
|
||||
|
||||
|
||||
INPUT_TYPE_MAPPING = {
|
||||
'string': str,
|
||||
'text': str,
|
||||
'markupText': str,
|
||||
'integer': int,
|
||||
'boolean': bool,
|
||||
'choice': str,
|
||||
'colorpicker': str,
|
||||
'datetimepicker': int,
|
||||
'static': dict,
|
||||
'translations': list,
|
||||
"string": str,
|
||||
"text": str,
|
||||
"markupText": str,
|
||||
"integer": int,
|
||||
"boolean": bool,
|
||||
"choice": str,
|
||||
"colorpicker": str,
|
||||
"datetimepicker": int,
|
||||
"static": dict,
|
||||
"translations": list,
|
||||
}
|
||||
|
||||
|
||||
@ -54,9 +45,11 @@ class ConfigHandler:
|
||||
Returns the value of the config variable.
|
||||
"""
|
||||
if not self.exists(key):
|
||||
raise ConfigNotFound(_('The config variable {} was not found.').format(key))
|
||||
raise ConfigNotFound(_("The config variable {} was not found.").format(key))
|
||||
|
||||
return async_to_sync(element_cache.get_element_full_data)(self.get_collection_string(), self.get_key_to_id()[key])['value']
|
||||
return async_to_sync(element_cache.get_element_full_data)(
|
||||
self.get_collection_string(), self.get_key_to_id()[key]
|
||||
)["value"]
|
||||
|
||||
def get_key_to_id(self) -> Dict[str, int]:
|
||||
"""
|
||||
@ -80,7 +73,7 @@ class ConfigHandler:
|
||||
all_data = await element_cache.get_all_full_data()
|
||||
elements = all_data[self.get_collection_string()]
|
||||
for element in elements:
|
||||
self.key_to_id[element['key']] = element['id']
|
||||
self.key_to_id[element["key"]] = element["id"]
|
||||
|
||||
def exists(self, key: str) -> bool:
|
||||
"""
|
||||
@ -102,7 +95,7 @@ class ConfigHandler:
|
||||
try:
|
||||
config_variable = self.config_variables[key]
|
||||
except KeyError:
|
||||
raise ConfigNotFound(_('The config variable {} was not found.').format(key))
|
||||
raise ConfigNotFound(_("The config variable {} was not found.").format(key))
|
||||
|
||||
# Validate datatype and run validators.
|
||||
expected_type = INPUT_TYPE_MAPPING[config_variable.input_type]
|
||||
@ -111,17 +104,21 @@ class ConfigHandler:
|
||||
try:
|
||||
value = expected_type(value)
|
||||
except ValueError:
|
||||
raise ConfigError(_('Wrong datatype. Expected %(expected_type)s, got %(got_type)s.') % {
|
||||
'expected_type': expected_type, 'got_type': type(value)})
|
||||
raise ConfigError(
|
||||
_("Wrong datatype. Expected %(expected_type)s, got %(got_type)s.")
|
||||
% {"expected_type": expected_type, "got_type": type(value)}
|
||||
)
|
||||
|
||||
if config_variable.input_type == 'choice':
|
||||
if config_variable.input_type == "choice":
|
||||
# Choices can be a callable. In this case call it at this place
|
||||
if callable(config_variable.choices):
|
||||
choices = config_variable.choices()
|
||||
else:
|
||||
choices = config_variable.choices
|
||||
if choices is None or value not in map(lambda choice: choice['value'], choices):
|
||||
raise ConfigError(_('Invalid input. Choice does not match.'))
|
||||
if choices is None or value not in map(
|
||||
lambda choice: choice["value"], choices
|
||||
):
|
||||
raise ConfigError(_("Invalid input. Choice does not match."))
|
||||
|
||||
for validator in config_variable.validators:
|
||||
try:
|
||||
@ -129,34 +126,36 @@ class ConfigHandler:
|
||||
except DjangoValidationError as e:
|
||||
raise ConfigError(e.messages[0])
|
||||
|
||||
if config_variable.input_type == 'static':
|
||||
if config_variable.input_type == "static":
|
||||
if not isinstance(value, dict):
|
||||
raise ConfigError(_('This has to be a dict.'))
|
||||
whitelist = (
|
||||
'path',
|
||||
'display_name',
|
||||
)
|
||||
raise ConfigError(_("This has to be a dict."))
|
||||
whitelist = ("path", "display_name")
|
||||
for required_entry in whitelist:
|
||||
if required_entry not in value:
|
||||
raise ConfigError(_('{} has to be given.'.format(required_entry)))
|
||||
raise ConfigError(_("{} has to be given.".format(required_entry)))
|
||||
if not isinstance(value[required_entry], str):
|
||||
raise ConfigError(_('{} has to be a string.'.format(required_entry)))
|
||||
raise ConfigError(
|
||||
_("{} has to be a string.".format(required_entry))
|
||||
)
|
||||
|
||||
if config_variable.input_type == 'translations':
|
||||
if config_variable.input_type == "translations":
|
||||
if not isinstance(value, list):
|
||||
raise ConfigError(_('Translations has to be a list.'))
|
||||
raise ConfigError(_("Translations has to be a list."))
|
||||
for entry in value:
|
||||
if not isinstance(entry, dict):
|
||||
raise ConfigError(_('Every value has to be a dict, not {}.'.format(type(entry))))
|
||||
whitelist = (
|
||||
'original',
|
||||
'translation',
|
||||
)
|
||||
raise ConfigError(
|
||||
_("Every value has to be a dict, not {}.".format(type(entry)))
|
||||
)
|
||||
whitelist = ("original", "translation")
|
||||
for required_entry in whitelist:
|
||||
if required_entry not in entry:
|
||||
raise ConfigError(_('{} has to be given.'.format(required_entry)))
|
||||
raise ConfigError(
|
||||
_("{} has to be given.".format(required_entry))
|
||||
)
|
||||
if not isinstance(entry[required_entry], str):
|
||||
raise ConfigError(_('{} has to be a string.'.format(required_entry)))
|
||||
raise ConfigError(
|
||||
_("{} has to be a string.".format(required_entry))
|
||||
)
|
||||
|
||||
# Save the new value to the database.
|
||||
db_value = ConfigStore.objects.get(key=key)
|
||||
@ -178,7 +177,7 @@ class ConfigHandler:
|
||||
continue
|
||||
self.update_config_variables(get_config_variables())
|
||||
|
||||
def update_config_variables(self, items: Iterable['ConfigVariable']) -> None:
|
||||
def update_config_variables(self, items: Iterable["ConfigVariable"]) -> None:
|
||||
"""
|
||||
Updates the config_variables dict.
|
||||
"""
|
||||
@ -189,7 +188,9 @@ class ConfigHandler:
|
||||
# be in already in self.config_variables
|
||||
intersection = set(item_index.keys()).intersection(self.config_variables.keys())
|
||||
if intersection:
|
||||
raise ConfigError(_('Too many values for config variables {} found.').format(intersection))
|
||||
raise ConfigError(
|
||||
_("Too many values for config variables {} found.").format(intersection)
|
||||
)
|
||||
|
||||
self.config_variables.update(item_index)
|
||||
|
||||
@ -224,19 +225,22 @@ use x = config[...], to set it use config[...] = x.
|
||||
"""
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
T = TypeVar("T")
|
||||
ChoiceType = Optional[Iterable[Dict[str, str]]]
|
||||
ChoiceCallableType = Union[ChoiceType, Callable[[], ChoiceType]]
|
||||
ValidatorsType = Iterable[Callable[[T], None]]
|
||||
OnChangeType = Callable[[], None]
|
||||
ConfigVariableDict = TypedDict('ConfigVariableDict', {
|
||||
'key': str,
|
||||
'default_value': Any,
|
||||
'input_type': str,
|
||||
'label': str,
|
||||
'help_text': str,
|
||||
'choices': ChoiceType,
|
||||
})
|
||||
ConfigVariableDict = TypedDict(
|
||||
"ConfigVariableDict",
|
||||
{
|
||||
"key": str,
|
||||
"default_value": Any,
|
||||
"input_type": str,
|
||||
"label": str,
|
||||
"help_text": str,
|
||||
"choices": ChoiceType,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class ConfigVariable:
|
||||
@ -265,27 +269,47 @@ class ConfigVariable:
|
||||
the value during setup of the database if the admin uses the respective
|
||||
command line option.
|
||||
"""
|
||||
def __init__(self, name: str, default_value: T, input_type: str = 'string',
|
||||
label: str = None, help_text: str = None, choices: ChoiceCallableType = None,
|
||||
hidden: bool = False, weight: int = 0, group: str = None, subgroup: str = None,
|
||||
validators: ValidatorsType = None, on_change: OnChangeType = None) -> None:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
default_value: T,
|
||||
input_type: str = "string",
|
||||
label: str = None,
|
||||
help_text: str = None,
|
||||
choices: ChoiceCallableType = None,
|
||||
hidden: bool = False,
|
||||
weight: int = 0,
|
||||
group: str = None,
|
||||
subgroup: str = None,
|
||||
validators: ValidatorsType = None,
|
||||
on_change: OnChangeType = None,
|
||||
) -> None:
|
||||
if input_type not in INPUT_TYPE_MAPPING:
|
||||
raise ValueError(_('Invalid value for config attribute input_type.'))
|
||||
if input_type == 'choice' and choices is None:
|
||||
raise ConfigError(_("Either config attribute 'choices' must not be None or "
|
||||
"'input_type' must not be 'choice'."))
|
||||
elif input_type != 'choice' and choices is not None:
|
||||
raise ConfigError(_("Either config attribute 'choices' must be None or "
|
||||
"'input_type' must be 'choice'."))
|
||||
raise ValueError(_("Invalid value for config attribute input_type."))
|
||||
if input_type == "choice" and choices is None:
|
||||
raise ConfigError(
|
||||
_(
|
||||
"Either config attribute 'choices' must not be None or "
|
||||
"'input_type' must not be 'choice'."
|
||||
)
|
||||
)
|
||||
elif input_type != "choice" and choices is not None:
|
||||
raise ConfigError(
|
||||
_(
|
||||
"Either config attribute 'choices' must be None or "
|
||||
"'input_type' must be 'choice'."
|
||||
)
|
||||
)
|
||||
self.name = name
|
||||
self.default_value = default_value
|
||||
self.input_type = input_type
|
||||
self.label = label or name
|
||||
self.help_text = help_text or ''
|
||||
self.help_text = help_text or ""
|
||||
self.choices = choices
|
||||
self.hidden = hidden
|
||||
self.weight = weight
|
||||
self.group = group or _('General')
|
||||
self.group = group or _("General")
|
||||
self.subgroup = subgroup
|
||||
self.validators = validators or ()
|
||||
self.on_change = on_change
|
||||
@ -301,7 +325,7 @@ class ConfigVariable:
|
||||
input_type=self.input_type,
|
||||
label=self.label,
|
||||
help_text=self.help_text,
|
||||
choices=self.choices() if callable(self.choices) else self.choices
|
||||
choices=self.choices() if callable(self.choices) else self.choices,
|
||||
)
|
||||
|
||||
def is_hidden(self) -> bool:
|
||||
|
@ -12,401 +12,429 @@ def get_config_variables():
|
||||
(see apps.py).
|
||||
"""
|
||||
yield ConfigVariable(
|
||||
name='general_event_name',
|
||||
default_value='OpenSlides',
|
||||
label='Event name',
|
||||
name="general_event_name",
|
||||
default_value="OpenSlides",
|
||||
label="Event name",
|
||||
weight=110,
|
||||
group='General',
|
||||
subgroup='Event',
|
||||
validators=(MaxLengthValidator(100),))
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
validators=(MaxLengthValidator(100),),
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='general_event_description',
|
||||
default_value='Presentation and assembly system',
|
||||
label='Short description of event',
|
||||
name="general_event_description",
|
||||
default_value="Presentation and assembly system",
|
||||
label="Short description of event",
|
||||
weight=115,
|
||||
group='General',
|
||||
subgroup='Event',
|
||||
validators=(MaxLengthValidator(100),))
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
validators=(MaxLengthValidator(100),),
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='general_event_date',
|
||||
default_value='',
|
||||
label='Event date',
|
||||
name="general_event_date",
|
||||
default_value="",
|
||||
label="Event date",
|
||||
weight=120,
|
||||
group='General',
|
||||
subgroup='Event')
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='general_event_location',
|
||||
default_value='',
|
||||
label='Event location',
|
||||
name="general_event_location",
|
||||
default_value="",
|
||||
label="Event location",
|
||||
weight=125,
|
||||
group='General',
|
||||
subgroup='Event')
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='general_event_legal_notice',
|
||||
name="general_event_legal_notice",
|
||||
default_value='<a href="http://www.openslides.org">OpenSlides</a> is a '
|
||||
'free web based presentation and assembly system for '
|
||||
'visualizing and controlling agenda, motions and '
|
||||
'elections of an assembly.',
|
||||
input_type='markupText',
|
||||
label='Legal notice',
|
||||
"free web based presentation and assembly system for "
|
||||
"visualizing and controlling agenda, motions and "
|
||||
"elections of an assembly.",
|
||||
input_type="markupText",
|
||||
label="Legal notice",
|
||||
weight=132,
|
||||
group='General',
|
||||
subgroup='Event')
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='general_event_privacy_policy',
|
||||
default_value='',
|
||||
input_type='markupText',
|
||||
label='Privacy policy',
|
||||
name="general_event_privacy_policy",
|
||||
default_value="",
|
||||
input_type="markupText",
|
||||
label="Privacy policy",
|
||||
weight=132,
|
||||
group='General',
|
||||
subgroup='Event')
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='general_event_welcome_title',
|
||||
default_value='Welcome to OpenSlides',
|
||||
label='Front page title',
|
||||
name="general_event_welcome_title",
|
||||
default_value="Welcome to OpenSlides",
|
||||
label="Front page title",
|
||||
weight=134,
|
||||
group='General',
|
||||
subgroup='Event')
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='general_event_welcome_text',
|
||||
default_value='[Space for your welcome text.]',
|
||||
input_type='markupText',
|
||||
label='Front page text',
|
||||
name="general_event_welcome_text",
|
||||
default_value="[Space for your welcome text.]",
|
||||
input_type="markupText",
|
||||
label="Front page text",
|
||||
weight=136,
|
||||
group='General',
|
||||
subgroup='Event')
|
||||
group="General",
|
||||
subgroup="Event",
|
||||
)
|
||||
|
||||
# General System
|
||||
|
||||
yield ConfigVariable(
|
||||
name='general_system_enable_anonymous',
|
||||
name="general_system_enable_anonymous",
|
||||
default_value=False,
|
||||
input_type='boolean',
|
||||
label='Allow access for anonymous guest users',
|
||||
input_type="boolean",
|
||||
label="Allow access for anonymous guest users",
|
||||
weight=138,
|
||||
group='General',
|
||||
subgroup='System')
|
||||
group="General",
|
||||
subgroup="System",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='general_login_info_text',
|
||||
default_value='',
|
||||
label='Show this text on the login page',
|
||||
name="general_login_info_text",
|
||||
default_value="",
|
||||
label="Show this text on the login page",
|
||||
weight=140,
|
||||
group='General',
|
||||
subgroup='System')
|
||||
group="General",
|
||||
subgroup="System",
|
||||
)
|
||||
|
||||
# General export settings
|
||||
|
||||
yield ConfigVariable(
|
||||
name='general_csv_separator',
|
||||
default_value=',',
|
||||
label='Separator used for all csv exports and examples',
|
||||
name="general_csv_separator",
|
||||
default_value=",",
|
||||
label="Separator used for all csv exports and examples",
|
||||
weight=142,
|
||||
group='General',
|
||||
subgroup='Export')
|
||||
group="General",
|
||||
subgroup="Export",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='general_export_pdf_pagenumber_alignment',
|
||||
default_value='center',
|
||||
input_type='choice',
|
||||
label='Page number alignment in PDF',
|
||||
name="general_export_pdf_pagenumber_alignment",
|
||||
default_value="center",
|
||||
input_type="choice",
|
||||
label="Page number alignment in PDF",
|
||||
choices=(
|
||||
{'value': 'left', 'display_name': 'Left'},
|
||||
{'value': 'center', 'display_name': 'Center'},
|
||||
{'value': 'right', 'display_name': 'Right'}),
|
||||
{"value": "left", "display_name": "Left"},
|
||||
{"value": "center", "display_name": "Center"},
|
||||
{"value": "right", "display_name": "Right"},
|
||||
),
|
||||
weight=144,
|
||||
group='General',
|
||||
subgroup='Export')
|
||||
group="General",
|
||||
subgroup="Export",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='general_export_pdf_fontsize',
|
||||
default_value='10',
|
||||
input_type='choice',
|
||||
label='Standard font size in PDF',
|
||||
name="general_export_pdf_fontsize",
|
||||
default_value="10",
|
||||
input_type="choice",
|
||||
label="Standard font size in PDF",
|
||||
choices=(
|
||||
{'value': '10', 'display_name': '10'},
|
||||
{'value': '11', 'display_name': '11'},
|
||||
{'value': '12', 'display_name': '12'}),
|
||||
{"value": "10", "display_name": "10"},
|
||||
{"value": "11", "display_name": "11"},
|
||||
{"value": "12", "display_name": "12"},
|
||||
),
|
||||
weight=146,
|
||||
group='General',
|
||||
subgroup='Export')
|
||||
group="General",
|
||||
subgroup="Export",
|
||||
)
|
||||
|
||||
# Projector
|
||||
|
||||
yield ConfigVariable(
|
||||
name='projector_language',
|
||||
default_value='browser',
|
||||
input_type='choice',
|
||||
label='Projector language',
|
||||
name="projector_language",
|
||||
default_value="browser",
|
||||
input_type="choice",
|
||||
label="Projector language",
|
||||
choices=(
|
||||
{'value': 'browser', 'display_name': 'Current browser language'},
|
||||
{'value': 'en', 'display_name': 'English'},
|
||||
{'value': 'de', 'display_name': 'Deutsch'},
|
||||
{'value': 'fr', 'display_name': 'Français'},
|
||||
{'value': 'es', 'display_name': 'Español'},
|
||||
{'value': 'pt', 'display_name': 'Português'},
|
||||
{'value': 'cs', 'display_name': 'Čeština'},
|
||||
{'value': 'ru', 'display_name': 'русский'}),
|
||||
{"value": "browser", "display_name": "Current browser language"},
|
||||
{"value": "en", "display_name": "English"},
|
||||
{"value": "de", "display_name": "Deutsch"},
|
||||
{"value": "fr", "display_name": "Français"},
|
||||
{"value": "es", "display_name": "Español"},
|
||||
{"value": "pt", "display_name": "Português"},
|
||||
{"value": "cs", "display_name": "Čeština"},
|
||||
{"value": "ru", "display_name": "русский"},
|
||||
),
|
||||
weight=150,
|
||||
group='Projector')
|
||||
group="Projector",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='projector_enable_logo',
|
||||
name="projector_enable_logo",
|
||||
default_value=True,
|
||||
input_type='boolean',
|
||||
label='Show logo on projector',
|
||||
help_text='You can replace the logo by uploading an image and set it as '
|
||||
'the "Projector logo" in "files".',
|
||||
input_type="boolean",
|
||||
label="Show logo on projector",
|
||||
help_text="You can replace the logo by uploading an image and set it as "
|
||||
'the "Projector logo" in "files".',
|
||||
weight=152,
|
||||
group='Projector')
|
||||
group="Projector",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='projector_enable_clock',
|
||||
name="projector_enable_clock",
|
||||
default_value=True,
|
||||
input_type='boolean',
|
||||
label='Show the clock on projector',
|
||||
input_type="boolean",
|
||||
label="Show the clock on projector",
|
||||
weight=154,
|
||||
group='Projector')
|
||||
group="Projector",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='projector_enable_title',
|
||||
name="projector_enable_title",
|
||||
default_value=True,
|
||||
input_type='boolean',
|
||||
label='Show title and description of event on projector',
|
||||
input_type="boolean",
|
||||
label="Show title and description of event on projector",
|
||||
weight=155,
|
||||
group='Projector')
|
||||
group="Projector",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='projector_enable_header_footer',
|
||||
name="projector_enable_header_footer",
|
||||
default_value=True,
|
||||
input_type='boolean',
|
||||
label='Display header and footer',
|
||||
input_type="boolean",
|
||||
label="Display header and footer",
|
||||
weight=157,
|
||||
group='Projector')
|
||||
group="Projector",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='projector_header_backgroundcolor',
|
||||
default_value='#317796',
|
||||
input_type='colorpicker',
|
||||
label='Background color of projector header and footer',
|
||||
name="projector_header_backgroundcolor",
|
||||
default_value="#317796",
|
||||
input_type="colorpicker",
|
||||
label="Background color of projector header and footer",
|
||||
weight=160,
|
||||
group='Projector')
|
||||
group="Projector",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='projector_header_fontcolor',
|
||||
default_value='#F5F5F5',
|
||||
input_type='colorpicker',
|
||||
label='Font color of projector header and footer',
|
||||
name="projector_header_fontcolor",
|
||||
default_value="#F5F5F5",
|
||||
input_type="colorpicker",
|
||||
label="Font color of projector header and footer",
|
||||
weight=165,
|
||||
group='Projector')
|
||||
group="Projector",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='projector_h1_fontcolor',
|
||||
default_value='#317796',
|
||||
input_type='colorpicker',
|
||||
label='Font color of projector headline',
|
||||
name="projector_h1_fontcolor",
|
||||
default_value="#317796",
|
||||
input_type="colorpicker",
|
||||
label="Font color of projector headline",
|
||||
weight=170,
|
||||
group='Projector')
|
||||
group="Projector",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='projector_default_countdown',
|
||||
name="projector_default_countdown",
|
||||
default_value=60,
|
||||
input_type='integer',
|
||||
label='Predefined seconds of new countdowns',
|
||||
input_type="integer",
|
||||
label="Predefined seconds of new countdowns",
|
||||
weight=185,
|
||||
group='Projector')
|
||||
group="Projector",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='projector_blank_color',
|
||||
default_value='#FFFFFF',
|
||||
input_type='colorpicker',
|
||||
label='Color for blanked projector',
|
||||
name="projector_blank_color",
|
||||
default_value="#FFFFFF",
|
||||
input_type="colorpicker",
|
||||
label="Color for blanked projector",
|
||||
weight=190,
|
||||
group='Projector')
|
||||
group="Projector",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='projector_broadcast',
|
||||
name="projector_broadcast",
|
||||
default_value=0,
|
||||
input_type='integer',
|
||||
label='Projector which is broadcasted',
|
||||
input_type="integer",
|
||||
label="Projector which is broadcasted",
|
||||
weight=200,
|
||||
group='Projector',
|
||||
hidden=True)
|
||||
group="Projector",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='projector_currentListOfSpeakers_reference',
|
||||
name="projector_currentListOfSpeakers_reference",
|
||||
default_value=1,
|
||||
input_type='integer',
|
||||
label='Projector reference for list of speakers',
|
||||
input_type="integer",
|
||||
label="Projector reference for list of speakers",
|
||||
weight=201,
|
||||
group='Projector',
|
||||
hidden=True)
|
||||
group="Projector",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
# Logos.
|
||||
yield ConfigVariable(
|
||||
name='logos_available',
|
||||
name="logos_available",
|
||||
default_value=[
|
||||
'logo_projector_main',
|
||||
'logo_projector_header',
|
||||
'logo_web_header',
|
||||
'logo_pdf_header_L',
|
||||
'logo_pdf_header_R',
|
||||
'logo_pdf_footer_L',
|
||||
'logo_pdf_footer_R',
|
||||
'logo_pdf_ballot_paper'],
|
||||
"logo_projector_main",
|
||||
"logo_projector_header",
|
||||
"logo_web_header",
|
||||
"logo_pdf_header_L",
|
||||
"logo_pdf_header_R",
|
||||
"logo_pdf_footer_L",
|
||||
"logo_pdf_footer_R",
|
||||
"logo_pdf_ballot_paper",
|
||||
],
|
||||
weight=300,
|
||||
group='Logo',
|
||||
hidden=True)
|
||||
group="Logo",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='logo_projector_main',
|
||||
default_value={
|
||||
'display_name': 'Projector logo',
|
||||
'path': ''},
|
||||
input_type='static',
|
||||
name="logo_projector_main",
|
||||
default_value={"display_name": "Projector logo", "path": ""},
|
||||
input_type="static",
|
||||
weight=301,
|
||||
group='Logo',
|
||||
hidden=True)
|
||||
group="Logo",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='logo_projector_header',
|
||||
default_value={
|
||||
'display_name': 'Projector header image',
|
||||
'path': ''},
|
||||
input_type='static',
|
||||
name="logo_projector_header",
|
||||
default_value={"display_name": "Projector header image", "path": ""},
|
||||
input_type="static",
|
||||
weight=302,
|
||||
group='Logo',
|
||||
hidden=True)
|
||||
group="Logo",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='logo_web_header',
|
||||
default_value={
|
||||
'display_name': 'Web interface header logo',
|
||||
'path': ''},
|
||||
input_type='static',
|
||||
name="logo_web_header",
|
||||
default_value={"display_name": "Web interface header logo", "path": ""},
|
||||
input_type="static",
|
||||
weight=303,
|
||||
group='Logo',
|
||||
hidden=True)
|
||||
group="Logo",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
# PDF logos
|
||||
yield ConfigVariable(
|
||||
name='logo_pdf_header_L',
|
||||
default_value={
|
||||
'display_name': 'PDF header logo (left)',
|
||||
'path': ''},
|
||||
input_type='static',
|
||||
name="logo_pdf_header_L",
|
||||
default_value={"display_name": "PDF header logo (left)", "path": ""},
|
||||
input_type="static",
|
||||
weight=310,
|
||||
group='Logo',
|
||||
hidden=True)
|
||||
group="Logo",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='logo_pdf_header_R',
|
||||
default_value={
|
||||
'display_name': 'PDF header logo (right)',
|
||||
'path': ''},
|
||||
input_type='static',
|
||||
name="logo_pdf_header_R",
|
||||
default_value={"display_name": "PDF header logo (right)", "path": ""},
|
||||
input_type="static",
|
||||
weight=311,
|
||||
group='Logo',
|
||||
hidden=True)
|
||||
group="Logo",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='logo_pdf_footer_L',
|
||||
default_value={
|
||||
'display_name': 'PDF footer logo (left)',
|
||||
'path': ''},
|
||||
input_type='static',
|
||||
name="logo_pdf_footer_L",
|
||||
default_value={"display_name": "PDF footer logo (left)", "path": ""},
|
||||
input_type="static",
|
||||
weight=312,
|
||||
group='Logo',
|
||||
hidden=True)
|
||||
group="Logo",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='logo_pdf_footer_R',
|
||||
default_value={
|
||||
'display_name': 'PDF footer logo (right)',
|
||||
'path': ''},
|
||||
input_type='static',
|
||||
name="logo_pdf_footer_R",
|
||||
default_value={"display_name": "PDF footer logo (right)", "path": ""},
|
||||
input_type="static",
|
||||
weight=313,
|
||||
group='Logo',
|
||||
hidden=True)
|
||||
group="Logo",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='logo_pdf_ballot_paper',
|
||||
default_value={
|
||||
'display_name': 'PDF ballot paper logo',
|
||||
'path': ''},
|
||||
input_type='static',
|
||||
name="logo_pdf_ballot_paper",
|
||||
default_value={"display_name": "PDF ballot paper logo", "path": ""},
|
||||
input_type="static",
|
||||
weight=314,
|
||||
group='Logo',
|
||||
hidden=True)
|
||||
group="Logo",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
# Fonts
|
||||
yield ConfigVariable(
|
||||
name='fonts_available',
|
||||
default_value=[
|
||||
'font_regular',
|
||||
'font_italic',
|
||||
'font_bold',
|
||||
'font_bold_italic'],
|
||||
name="fonts_available",
|
||||
default_value=["font_regular", "font_italic", "font_bold", "font_bold_italic"],
|
||||
weight=320,
|
||||
group='Font',
|
||||
hidden=True)
|
||||
group="Font",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='font_regular',
|
||||
name="font_regular",
|
||||
default_value={
|
||||
'display_name': 'Font regular',
|
||||
'default': 'static/fonts/Roboto-Regular.woff',
|
||||
'path': ''},
|
||||
input_type='static',
|
||||
"display_name": "Font regular",
|
||||
"default": "static/fonts/Roboto-Regular.woff",
|
||||
"path": "",
|
||||
},
|
||||
input_type="static",
|
||||
weight=321,
|
||||
group='Font',
|
||||
hidden=True)
|
||||
group="Font",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='font_italic',
|
||||
name="font_italic",
|
||||
default_value={
|
||||
'display_name': 'Font italic',
|
||||
'default': 'static/fonts/Roboto-Medium.woff',
|
||||
'path': ''},
|
||||
input_type='static',
|
||||
"display_name": "Font italic",
|
||||
"default": "static/fonts/Roboto-Medium.woff",
|
||||
"path": "",
|
||||
},
|
||||
input_type="static",
|
||||
weight=321,
|
||||
group='Font',
|
||||
hidden=True)
|
||||
group="Font",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='font_bold',
|
||||
name="font_bold",
|
||||
default_value={
|
||||
'display_name': 'Font bold',
|
||||
'default': 'static/fonts/Roboto-Condensed-Regular.woff',
|
||||
'path': ''},
|
||||
input_type='static',
|
||||
"display_name": "Font bold",
|
||||
"default": "static/fonts/Roboto-Condensed-Regular.woff",
|
||||
"path": "",
|
||||
},
|
||||
input_type="static",
|
||||
weight=321,
|
||||
group='Font',
|
||||
hidden=True)
|
||||
group="Font",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='font_bold_italic',
|
||||
name="font_bold_italic",
|
||||
default_value={
|
||||
'display_name': 'Font bold italic',
|
||||
'default': 'static/fonts/Roboto-Condensed-Light.woff',
|
||||
'path': ''},
|
||||
input_type='static',
|
||||
"display_name": "Font bold italic",
|
||||
"default": "static/fonts/Roboto-Condensed-Light.woff",
|
||||
"path": "",
|
||||
},
|
||||
input_type="static",
|
||||
weight=321,
|
||||
group='Font',
|
||||
hidden=True)
|
||||
group="Font",
|
||||
hidden=True,
|
||||
)
|
||||
|
||||
# Custom translations
|
||||
yield ConfigVariable(
|
||||
name='translations',
|
||||
label='Custom translations',
|
||||
name="translations",
|
||||
label="Custom translations",
|
||||
default_value=[],
|
||||
input_type='translations',
|
||||
input_type="translations",
|
||||
weight=1000,
|
||||
group='Custom translations')
|
||||
group="Custom translations",
|
||||
)
|
||||
|
@ -10,17 +10,18 @@ class Command(BaseCommand):
|
||||
"""
|
||||
Command to backup the SQLite3 database.
|
||||
"""
|
||||
help = 'Backups the SQLite3 database.'
|
||||
|
||||
help = "Backups the SQLite3 database."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--path',
|
||||
default='database_backup.sqlite',
|
||||
help='Path for the backup file (Default: database_backup.sqlite).'
|
||||
"--path",
|
||||
default="database_backup.sqlite",
|
||||
help="Path for the backup file (Default: database_backup.sqlite).",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
path = options.get('path')
|
||||
path = options.get("path")
|
||||
|
||||
@transaction.atomic
|
||||
def do_backup(src_path, dest_path):
|
||||
@ -39,8 +40,11 @@ class Command(BaseCommand):
|
||||
database_path = get_database_path_from_settings()
|
||||
if database_path:
|
||||
do_backup(database_path, path)
|
||||
self.stdout.write('Database %s successfully stored at %s.' % (database_path, path))
|
||||
self.stdout.write(
|
||||
"Database %s successfully stored at %s." % (database_path, path)
|
||||
)
|
||||
else:
|
||||
raise CommandError(
|
||||
'Default database is not SQLite3. Only SQLite3 databases'
|
||||
'can currently be backuped.')
|
||||
"Default database is not SQLite3. Only SQLite3 databases"
|
||||
"can currently be backuped."
|
||||
)
|
||||
|
@ -8,25 +8,28 @@ class Command(BaseCommand):
|
||||
"""
|
||||
Command to change OpenSlides config values.
|
||||
"""
|
||||
help = 'Changes OpenSlides config values.'
|
||||
|
||||
help = "Changes OpenSlides config values."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'key',
|
||||
help='Config key. See config_variables.py in every app.'
|
||||
"key", help="Config key. See config_variables.py in every app."
|
||||
)
|
||||
parser.add_argument(
|
||||
'value',
|
||||
help='New config value. For a falsy boolean use "False".'
|
||||
"value", help='New config value. For a falsy boolean use "False".'
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options['value'].lower() == 'false':
|
||||
options['value'] = False
|
||||
if options["value"].lower() == "false":
|
||||
options["value"] = False
|
||||
try:
|
||||
config[options['key']] = options['value']
|
||||
config[options["key"]] = options["value"]
|
||||
except (ConfigError, ConfigNotFound) as e:
|
||||
raise CommandError(str(e))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS('Config {key} successfully changed to {value}.'.format(
|
||||
key=options['key'], value=config[options['key']])))
|
||||
self.style.SUCCESS(
|
||||
"Config {key} successfully changed to {value}.".format(
|
||||
key=options["key"], value=config[options["key"]]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -7,19 +7,16 @@ class Command(BaseCommand):
|
||||
"""
|
||||
Command to change a user's password.
|
||||
"""
|
||||
help = 'Changes user password.'
|
||||
|
||||
help = "Changes user password."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'username',
|
||||
help='The name of the user to set the password for'
|
||||
)
|
||||
parser.add_argument(
|
||||
'password',
|
||||
help='The new password of the user'
|
||||
"username", help="The name of the user to set the password for"
|
||||
)
|
||||
parser.add_argument("password", help="The new password of the user")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
user = User.objects.get(username=options['username'])
|
||||
user.set_password(options['password'])
|
||||
user = User.objects.get(username=options["username"])
|
||||
user.set_password(options["password"])
|
||||
user.save()
|
||||
|
@ -10,10 +10,12 @@ class Command(_Command):
|
||||
Migration command that does nearly the same as Django's migration command
|
||||
but also calls the post_permission_creation signal.
|
||||
"""
|
||||
|
||||
def handle(self, *args, **options):
|
||||
from django.conf import settings
|
||||
|
||||
# Creates the folder for a SQLite3 database if necessary.
|
||||
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3':
|
||||
if settings.DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3":
|
||||
try:
|
||||
os.makedirs(settings.OPENSLIDES_USER_DATA_PATH)
|
||||
except (FileExistsError, AttributeError):
|
||||
|
@ -18,11 +18,9 @@ def add_default_projector(apps, schema_editor):
|
||||
"""
|
||||
# We get the model from the versioned app registry;
|
||||
# if we directly import it, it will be the wrong version.
|
||||
Projector = apps.get_model('core', 'Projector')
|
||||
Projector = apps.get_model("core", "Projector")
|
||||
projector_config = {}
|
||||
projector_config[uuid.uuid4().hex] = {
|
||||
'name': 'core/clock',
|
||||
'stable': True}
|
||||
projector_config[uuid.uuid4().hex] = {"name": "core/clock", "stable": True}
|
||||
# We use bulk_create here because we do not want model's save() method
|
||||
# to be called because we do not want our autoupdate signals to be
|
||||
# triggered.
|
||||
@ -34,79 +32,126 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('mediafiles', '0001_initial'),
|
||||
("mediafiles", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ChatMessage',
|
||||
name="ChatMessage",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('message', models.TextField()),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("message", models.TextField()),
|
||||
("timestamp", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'permissions': (('can_use_chat', 'Can use the chat'),),
|
||||
"default_permissions": (),
|
||||
"permissions": (("can_use_chat", "Can use the chat"),),
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConfigStore',
|
||||
name="ConfigStore",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.CharField(db_index=True, max_length=255, unique=True)),
|
||||
('value', jsonfield.fields.JSONField()),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("key", models.CharField(db_index=True, max_length=255, unique=True)),
|
||||
("value", jsonfield.fields.JSONField()),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'permissions': (('can_manage_config', 'Can manage configuration'),),
|
||||
"default_permissions": (),
|
||||
"permissions": (("can_manage_config", "Can manage configuration"),),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomSlide',
|
||||
name="CustomSlide",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=256)),
|
||||
('text', models.TextField(blank=True)),
|
||||
('weight', models.IntegerField(default=0)),
|
||||
('attachments', models.ManyToManyField(blank=True, to='mediafiles.Mediafile')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=256)),
|
||||
("text", models.TextField(blank=True)),
|
||||
("weight", models.IntegerField(default=0)),
|
||||
(
|
||||
"attachments",
|
||||
models.ManyToManyField(blank=True, to="mediafiles.Mediafile"),
|
||||
),
|
||||
],
|
||||
options={"default_permissions": (), "ordering": ("weight", "title")},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Projector",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("config", jsonfield.fields.JSONField()),
|
||||
("scale", models.IntegerField(default=0)),
|
||||
("scroll", models.IntegerField(default=0)),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'ordering': ('weight', 'title'),
|
||||
"default_permissions": (),
|
||||
"permissions": (
|
||||
("can_see_projector", "Can see the projector"),
|
||||
("can_manage_projector", "Can manage the projector"),
|
||||
("can_see_frontpage", "Can see the front page"),
|
||||
),
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Projector',
|
||||
name="Tag",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('config', jsonfield.fields.JSONField()),
|
||||
('scale', models.IntegerField(default=0)),
|
||||
('scroll', models.IntegerField(default=0)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255, unique=True)),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'permissions': (
|
||||
('can_see_projector', 'Can see the projector'),
|
||||
('can_manage_projector', 'Can manage the projector'),
|
||||
('can_see_frontpage', 'Can see the front page')),
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tag',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True)),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'permissions': (('can_manage_tags', 'Can manage tags'),),
|
||||
'ordering': ('name',),
|
||||
"default_permissions": (),
|
||||
"permissions": (("can_manage_tags", "Can manage tags"),),
|
||||
"ordering": ("name",),
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
|
@ -15,10 +15,10 @@ def move_custom_slides_to_topics(apps, schema_editor):
|
||||
"""
|
||||
# We get the model from the versioned app registry;
|
||||
# if we directly import it, it will be the wrong version.
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
CustomSlide = apps.get_model('core', 'CustomSlide')
|
||||
Item = apps.get_model('agenda', 'Item')
|
||||
Topic = apps.get_model('topics', 'Topic')
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
CustomSlide = apps.get_model("core", "CustomSlide")
|
||||
Item = apps.get_model("agenda", "Item")
|
||||
Topic = apps.get_model("topics", "Topic")
|
||||
|
||||
# Copy data.
|
||||
content_type_custom_slide = ContentType.objects.get_for_model(CustomSlide)
|
||||
@ -28,7 +28,9 @@ def move_custom_slides_to_topics(apps, schema_editor):
|
||||
# no method 'get_agenda_title()'. See agenda/signals.py.
|
||||
topic = Topic.objects.create(title=custom_slide.title, text=custom_slide.text)
|
||||
topic.attachments.add(*custom_slide.attachments.all())
|
||||
item = Item.objects.get(object_id=custom_slide.pk, content_type=content_type_custom_slide)
|
||||
item = Item.objects.get(
|
||||
object_id=custom_slide.pk, content_type=content_type_custom_slide
|
||||
)
|
||||
item.object_id = topic.pk
|
||||
item.content_type = content_type_topic
|
||||
item.save(skip_autoupdate=True)
|
||||
@ -42,20 +44,20 @@ def name_default_projector(apps, schema_editor):
|
||||
"""
|
||||
Set the name of the default projector to 'Defaultprojector'
|
||||
"""
|
||||
Projector = apps.get_model('core', 'Projector')
|
||||
Projector.objects.filter(pk=1).update(name='Default projector')
|
||||
Projector = apps.get_model("core", "Projector")
|
||||
Projector.objects.filter(pk=1).update(name="Default projector")
|
||||
|
||||
|
||||
def remove_old_countdowns_messages(apps, schema_editor):
|
||||
"""
|
||||
Remove old countdowns and messages created by 2.0 from projector elements which are unusable in 2.1.
|
||||
"""
|
||||
Projector = apps.get_model('core', 'Projector')
|
||||
Projector = apps.get_model("core", "Projector")
|
||||
projector = Projector.objects.get(pk=1)
|
||||
|
||||
projector_config = projector.config
|
||||
for key, value in list(projector.config.items()):
|
||||
if value.get('name') in ('core/countdown', 'core/message'):
|
||||
if value.get("name") in ("core/countdown", "core/message"):
|
||||
del projector_config[key]
|
||||
projector.config = projector_config
|
||||
projector.save(skip_autoupdate=True)
|
||||
@ -65,57 +67,74 @@ def add_projection_defaults(apps, schema_editor):
|
||||
"""
|
||||
Adds projectiondefaults for messages and countdowns.
|
||||
"""
|
||||
Projector = apps.get_model('core', 'Projector')
|
||||
ProjectionDefault = apps.get_model('core', 'ProjectionDefault')
|
||||
Projector = apps.get_model("core", "Projector")
|
||||
ProjectionDefault = apps.get_model("core", "ProjectionDefault")
|
||||
# The default projector (pk=1) is always available.
|
||||
default_projector = Projector.objects.get(pk=1)
|
||||
|
||||
projectiondefaults = []
|
||||
|
||||
projectiondefaults.append(ProjectionDefault(
|
||||
name='agenda_all_items',
|
||||
display_name='Agenda',
|
||||
projector=default_projector))
|
||||
projectiondefaults.append(ProjectionDefault(
|
||||
name='topics',
|
||||
display_name='Topics',
|
||||
projector=default_projector))
|
||||
projectiondefaults.append(ProjectionDefault(
|
||||
name='agenda_list_of_speakers',
|
||||
display_name='List of speakers',
|
||||
projector=default_projector))
|
||||
projectiondefaults.append(ProjectionDefault(
|
||||
name='agenda_current_list_of_speakers',
|
||||
display_name='Current list of speakers',
|
||||
projector=default_projector))
|
||||
projectiondefaults.append(ProjectionDefault(
|
||||
name='motions',
|
||||
display_name='Motions',
|
||||
projector=default_projector))
|
||||
projectiondefaults.append(ProjectionDefault(
|
||||
name='motionBlocks',
|
||||
display_name='Motion blocks',
|
||||
projector=default_projector))
|
||||
projectiondefaults.append(ProjectionDefault(
|
||||
name='assignments',
|
||||
display_name='Elections',
|
||||
projector=default_projector))
|
||||
projectiondefaults.append(ProjectionDefault(
|
||||
name='users',
|
||||
display_name='Participants',
|
||||
projector=default_projector))
|
||||
projectiondefaults.append(ProjectionDefault(
|
||||
name='mediafiles',
|
||||
display_name='Files',
|
||||
projector=default_projector))
|
||||
projectiondefaults.append(ProjectionDefault(
|
||||
name='messages',
|
||||
display_name='Messages',
|
||||
projector=default_projector))
|
||||
projectiondefaults.append(ProjectionDefault(
|
||||
name='countdowns',
|
||||
display_name='Countdowns',
|
||||
projector=default_projector))
|
||||
projectiondefaults.append(
|
||||
ProjectionDefault(
|
||||
name="agenda_all_items", display_name="Agenda", projector=default_projector
|
||||
)
|
||||
)
|
||||
projectiondefaults.append(
|
||||
ProjectionDefault(
|
||||
name="topics", display_name="Topics", projector=default_projector
|
||||
)
|
||||
)
|
||||
projectiondefaults.append(
|
||||
ProjectionDefault(
|
||||
name="agenda_list_of_speakers",
|
||||
display_name="List of speakers",
|
||||
projector=default_projector,
|
||||
)
|
||||
)
|
||||
projectiondefaults.append(
|
||||
ProjectionDefault(
|
||||
name="agenda_current_list_of_speakers",
|
||||
display_name="Current list of speakers",
|
||||
projector=default_projector,
|
||||
)
|
||||
)
|
||||
projectiondefaults.append(
|
||||
ProjectionDefault(
|
||||
name="motions", display_name="Motions", projector=default_projector
|
||||
)
|
||||
)
|
||||
projectiondefaults.append(
|
||||
ProjectionDefault(
|
||||
name="motionBlocks",
|
||||
display_name="Motion blocks",
|
||||
projector=default_projector,
|
||||
)
|
||||
)
|
||||
projectiondefaults.append(
|
||||
ProjectionDefault(
|
||||
name="assignments", display_name="Elections", projector=default_projector
|
||||
)
|
||||
)
|
||||
projectiondefaults.append(
|
||||
ProjectionDefault(
|
||||
name="users", display_name="Participants", projector=default_projector
|
||||
)
|
||||
)
|
||||
projectiondefaults.append(
|
||||
ProjectionDefault(
|
||||
name="mediafiles", display_name="Files", projector=default_projector
|
||||
)
|
||||
)
|
||||
projectiondefaults.append(
|
||||
ProjectionDefault(
|
||||
name="messages", display_name="Messages", projector=default_projector
|
||||
)
|
||||
)
|
||||
projectiondefaults.append(
|
||||
ProjectionDefault(
|
||||
name="countdowns", display_name="Countdowns", projector=default_projector
|
||||
)
|
||||
)
|
||||
|
||||
# Create all new projectiondefaults
|
||||
ProjectionDefault.objects.bulk_create(projectiondefaults)
|
||||
@ -125,114 +144,141 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('sessions', '0001_initial'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('core', '0001_initial'),
|
||||
('agenda', '0001_initial'), # ('agenda', '0002_item_duration') is not required but would be also ok.
|
||||
('topics', '0001_initial'),
|
||||
("sessions", "0001_initial"),
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("core", "0001_initial"),
|
||||
(
|
||||
"agenda",
|
||||
"0001_initial",
|
||||
), # ('agenda', '0002_item_duration') is not required but would be also ok.
|
||||
("topics", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Countdown',
|
||||
name="Countdown",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('description', models.CharField(blank=True, max_length=256)),
|
||||
('running', models.BooleanField(default=False)),
|
||||
('default_time', models.PositiveIntegerField(default=60)),
|
||||
('countdown_time', models.FloatField(default=60)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("description", models.CharField(blank=True, max_length=256)),
|
||||
("running", models.BooleanField(default=False)),
|
||||
("default_time", models.PositiveIntegerField(default=60)),
|
||||
("countdown_time", models.FloatField(default=60)),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProjectionDefault',
|
||||
name="ProjectionDefault",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=256)),
|
||||
('display_name', models.CharField(max_length=256)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=256)),
|
||||
("display_name", models.CharField(max_length=256)),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProjectorMessage',
|
||||
name="ProjectorMessage",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('message', models.TextField(blank=True)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("message", models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Session',
|
||||
name="Session",
|
||||
fields=[
|
||||
('session_ptr', models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to='sessions.Session')),
|
||||
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"session_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="sessions.Session",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
bases=('sessions.session',),
|
||||
),
|
||||
migrations.RunPython(
|
||||
move_custom_slides_to_topics
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='customslide',
|
||||
name='attachments',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='CustomSlide',
|
||||
options={"default_permissions": ()},
|
||||
bases=("sessions.session",),
|
||||
),
|
||||
migrations.RunPython(move_custom_slides_to_topics),
|
||||
migrations.RemoveField(model_name="customslide", name="attachments"),
|
||||
migrations.DeleteModel(name="CustomSlide"),
|
||||
migrations.AlterModelOptions(
|
||||
name='chatmessage',
|
||||
options={'default_permissions': (), 'permissions': (('can_use_chat', 'Can use the chat'), ('can_manage_chat', 'Can manage the chat'))},
|
||||
name="chatmessage",
|
||||
options={
|
||||
"default_permissions": (),
|
||||
"permissions": (
|
||||
("can_use_chat", "Can use the chat"),
|
||||
("can_manage_chat", "Can manage the chat"),
|
||||
),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='projector',
|
||||
name='blank',
|
||||
model_name="projector",
|
||||
name="blank",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='projector',
|
||||
name='height',
|
||||
model_name="projector",
|
||||
name="height",
|
||||
field=models.PositiveIntegerField(default=915),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='projector',
|
||||
name='name',
|
||||
model_name="projector",
|
||||
name="name",
|
||||
field=models.CharField(blank=True, max_length=255, unique=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='projector',
|
||||
name='width',
|
||||
model_name="projector",
|
||||
name="width",
|
||||
field=models.PositiveIntegerField(default=1220),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='projectiondefault',
|
||||
name='projector',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projectiondefaults', to='core.Projector'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
name_default_projector
|
||||
),
|
||||
migrations.RunPython(
|
||||
remove_old_countdowns_messages
|
||||
),
|
||||
migrations.RunPython(
|
||||
add_projection_defaults
|
||||
model_name="projectiondefault",
|
||||
name="projector",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="projectiondefaults",
|
||||
to="core.Projector",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(name_default_projector),
|
||||
migrations.RunPython(remove_old_countdowns_messages),
|
||||
migrations.RunPython(add_projection_defaults),
|
||||
]
|
||||
|
@ -11,22 +11,16 @@ def remove_session_content_type(apps, schema_editor):
|
||||
"""
|
||||
# We get the model from the versioned app registry;
|
||||
# if we directly import it, it will be the wrong version.
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Session = apps.get_model('core', 'Session')
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
Session = apps.get_model("core", "Session")
|
||||
ContentType.objects.get_for_model(Session).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_misc_features'),
|
||||
]
|
||||
dependencies = [("core", "0002_misc_features")]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
remove_session_content_type
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Session',
|
||||
),
|
||||
migrations.RunPython(remove_session_content_type),
|
||||
migrations.DeleteModel(name="Session"),
|
||||
]
|
||||
|
@ -13,25 +13,19 @@ def rename_projector_message_slides(apps, schema_editor):
|
||||
"""
|
||||
# We get the model from the versioned app registry;
|
||||
# if we directly import it, it will be the wrong version.
|
||||
Projector = apps.get_model('core', 'Projector')
|
||||
Projector = apps.get_model("core", "Projector")
|
||||
for projector in Projector.objects.all():
|
||||
new_config = {}
|
||||
for key, value in projector.config.items():
|
||||
new_config[key] = value
|
||||
if value['name'] == 'core/projectormessage':
|
||||
new_config[key]['name'] = 'core/projector-message'
|
||||
if value["name"] == "core/projectormessage":
|
||||
new_config[key]["name"] = "core/projector-message"
|
||||
projector.config = new_config
|
||||
projector.save(skip_autoupdate=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_auto_20161217_1158'),
|
||||
]
|
||||
dependencies = [("core", "0003_auto_20161217_1158")]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
rename_projector_message_slides
|
||||
),
|
||||
]
|
||||
operations = [migrations.RunPython(rename_projector_message_slides)]
|
||||
|
@ -11,22 +11,26 @@ from openslides.utils.migrations import (
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_auto_20170215_1624'),
|
||||
]
|
||||
dependencies = [("core", "0004_auto_20170215_1624")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='configstore',
|
||||
name="configstore",
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'permissions': (
|
||||
('can_manage_config', 'Can manage configuration'),
|
||||
('can_manage_logos', 'Can manage logos')
|
||||
)
|
||||
"default_permissions": (),
|
||||
"permissions": (
|
||||
("can_manage_config", "Can manage configuration"),
|
||||
("can_manage_logos", "Can manage logos"),
|
||||
),
|
||||
},
|
||||
),
|
||||
migrations.RunPython(add_permission_to_groups_based_on_existing_permission(
|
||||
'can_manage_config', 'configstore', 'core', 'can_manage_logos', 'Can manage logos'
|
||||
)),
|
||||
migrations.RunPython(
|
||||
add_permission_to_groups_based_on_existing_permission(
|
||||
"can_manage_config",
|
||||
"configstore",
|
||||
"core",
|
||||
"can_manage_logos",
|
||||
"Can manage logos",
|
||||
)
|
||||
),
|
||||
]
|
||||
|
@ -7,19 +7,17 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_auto_20170412_1258'),
|
||||
]
|
||||
dependencies = [("core", "0005_auto_20170412_1258")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='projector',
|
||||
name='height',
|
||||
model_name="projector",
|
||||
name="height",
|
||||
field=models.PositiveIntegerField(default=768),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='projector',
|
||||
name='width',
|
||||
model_name="projector",
|
||||
name="width",
|
||||
field=models.PositiveIntegerField(default=1024),
|
||||
),
|
||||
]
|
||||
|
@ -15,7 +15,7 @@ def delete_old_logo_permission(apps, schema_editor):
|
||||
If this is an old database, the new permission will be created and the old
|
||||
one deleted. Also it will be assigned to the groups, which had the old permission.
|
||||
"""
|
||||
perm = Permission.objects.filter(codename='can_manage_logos')
|
||||
perm = Permission.objects.filter(codename="can_manage_logos")
|
||||
|
||||
if len(perm):
|
||||
perm = perm.get()
|
||||
@ -30,9 +30,10 @@ def delete_old_logo_permission(apps, schema_editor):
|
||||
|
||||
# Create new permission
|
||||
perm = Permission.objects.create(
|
||||
codename='can_manage_logos_and_fonts',
|
||||
name='Can manage logos and fonts',
|
||||
content_type=content_type)
|
||||
codename="can_manage_logos_and_fonts",
|
||||
name="Can manage logos and fonts",
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
for group in groups:
|
||||
group.permissions.add(perm)
|
||||
@ -41,22 +42,18 @@ def delete_old_logo_permission(apps, schema_editor):
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_auto_20180123_0903'),
|
||||
]
|
||||
dependencies = [("core", "0006_auto_20180123_0903")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='configstore',
|
||||
name="configstore",
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'permissions': (
|
||||
('can_manage_config', 'Can manage configuration'),
|
||||
('can_manage_logos_and_fonts', 'Can manage logos and fonts')
|
||||
)
|
||||
"default_permissions": (),
|
||||
"permissions": (
|
||||
("can_manage_config", "Can manage configuration"),
|
||||
("can_manage_logos_and_fonts", "Can manage logos and fonts"),
|
||||
),
|
||||
},
|
||||
),
|
||||
migrations.RunPython(
|
||||
delete_old_logo_permission
|
||||
),
|
||||
migrations.RunPython(delete_old_logo_permission),
|
||||
]
|
||||
|
@ -11,14 +11,14 @@ def logos_available_default_to_database(apps, schema_editor):
|
||||
"""
|
||||
Writes the new default value of the 'logos_available' into the database.
|
||||
"""
|
||||
ConfigStore = apps.get_model('core', 'ConfigStore')
|
||||
ConfigStore = apps.get_model("core", "ConfigStore")
|
||||
|
||||
try:
|
||||
logos_available = ConfigStore.objects.get(key='logos_available')
|
||||
logos_available = ConfigStore.objects.get(key="logos_available")
|
||||
except ConfigStore.DoesNotExist:
|
||||
return # The key is not in the database, nothing to change here
|
||||
|
||||
default_value = config.config_variables['logos_available'].default_value
|
||||
default_value = config.config_variables["logos_available"].default_value
|
||||
logos_available.value = default_value
|
||||
logos_available.save()
|
||||
|
||||
@ -28,16 +28,16 @@ def move_old_logo_settings(apps, schema_editor):
|
||||
moves the value of 'logo_pdf_header' to 'logo_pdf_header_L' and the same
|
||||
for the footer. The old ones are deleted.
|
||||
"""
|
||||
ConfigStore = apps.get_model('core', 'ConfigStore')
|
||||
ConfigStore = apps.get_model("core", "ConfigStore")
|
||||
|
||||
for place in ('header', 'footer'):
|
||||
for place in ("header", "footer"):
|
||||
try:
|
||||
logo_pdf = ConfigStore.objects.get(key='logo_pdf_{}'.format(place))
|
||||
logo_pdf = ConfigStore.objects.get(key="logo_pdf_{}".format(place))
|
||||
except ConfigStore.DoesNotExist:
|
||||
continue # The old entry is not in the database, nothing to change here
|
||||
|
||||
# The key of the new entry
|
||||
new_value_key = 'logo_pdf_{}_L'.format(place)
|
||||
new_value_key = "logo_pdf_{}_L".format(place)
|
||||
try:
|
||||
logo_pdf_L = ConfigStore.objects.get(key=new_value_key)
|
||||
except ConfigStore.DoesNotExist:
|
||||
@ -45,7 +45,7 @@ def move_old_logo_settings(apps, schema_editor):
|
||||
logo_pdf_L.value = {}
|
||||
|
||||
# Move the path to the new configentry
|
||||
logo_pdf_L.value['path'] = logo_pdf.value.get('path', '')
|
||||
logo_pdf_L.value["path"] = logo_pdf.value.get("path", "")
|
||||
# Save the new one, delete the old
|
||||
logo_pdf_L.save()
|
||||
logo_pdf.delete()
|
||||
@ -53,15 +53,9 @@ def move_old_logo_settings(apps, schema_editor):
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0007_auto_20180130_1400'),
|
||||
]
|
||||
dependencies = [("core", "0007_auto_20180130_1400")]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
logos_available_default_to_database
|
||||
),
|
||||
migrations.RunPython(
|
||||
move_old_logo_settings
|
||||
),
|
||||
migrations.RunPython(logos_available_default_to_database),
|
||||
migrations.RunPython(move_old_logo_settings),
|
||||
]
|
||||
|
@ -13,42 +13,68 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('core', '0008_changed_logo_fields'),
|
||||
("core", "0008_changed_logo_fields"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='History',
|
||||
name="History",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('element_id', models.CharField(max_length=255)),
|
||||
('now', models.DateTimeField(auto_now_add=True)),
|
||||
('information', models.CharField(max_length=255)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("element_id", models.CharField(max_length=255)),
|
||||
("now", models.DateTimeField(auto_now_add=True)),
|
||||
("information", models.CharField(max_length=255)),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HistoryData',
|
||||
name="HistoryData",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('full_data', jsonfield.fields.JSONField(
|
||||
dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={})),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"full_data",
|
||||
jsonfield.fields.JSONField(
|
||||
dump_kwargs={
|
||||
"cls": jsonfield.encoder.JSONEncoder,
|
||||
"separators": (",", ":"),
|
||||
},
|
||||
load_kwargs={},
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='history',
|
||||
name='full_data',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='core.HistoryData'),
|
||||
model_name="history",
|
||||
name="full_data",
|
||||
field=models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="core.HistoryData"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='history',
|
||||
name='user',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||
model_name="history",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -24,13 +24,13 @@ class ProjectorManager(models.Manager):
|
||||
"""
|
||||
Customized model manager to support our get_full_queryset method.
|
||||
"""
|
||||
|
||||
def get_full_queryset(self):
|
||||
"""
|
||||
Returns the normal queryset with all projectors. In the background
|
||||
projector defaults are prefetched from the database.
|
||||
"""
|
||||
return self.get_queryset().prefetch_related(
|
||||
'projectiondefaults')
|
||||
return self.get_queryset().prefetch_related("projectiondefaults")
|
||||
|
||||
|
||||
class Projector(RESTModelMixin, models.Model):
|
||||
@ -72,6 +72,7 @@ class Projector(RESTModelMixin, models.Model):
|
||||
The projector can be controlled using the REST API with POST requests
|
||||
on e. g. the URL /rest/core/projector/1/activate_elements/.
|
||||
"""
|
||||
|
||||
access_permissions = ProjectorAccessPermissions()
|
||||
|
||||
objects = ProjectorManager()
|
||||
@ -86,24 +87,21 @@ class Projector(RESTModelMixin, models.Model):
|
||||
|
||||
height = models.PositiveIntegerField(default=768)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
blank=True)
|
||||
name = models.CharField(max_length=255, unique=True, blank=True)
|
||||
|
||||
blank = models.BooleanField(
|
||||
blank=False,
|
||||
default=False)
|
||||
blank = models.BooleanField(blank=False, default=False)
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
Contains general permissions that can not be placed in a specific app.
|
||||
"""
|
||||
|
||||
default_permissions = ()
|
||||
permissions = (
|
||||
('can_see_projector', 'Can see the projector'),
|
||||
('can_manage_projector', 'Can manage the projector'),
|
||||
('can_see_frontpage', 'Can see the front page'),)
|
||||
("can_see_projector", "Can see the projector"),
|
||||
("can_manage_projector", "Can manage the projector"),
|
||||
("can_see_frontpage", "Can see the front page"),
|
||||
)
|
||||
|
||||
@property
|
||||
def elements(self):
|
||||
@ -120,17 +118,19 @@ class Projector(RESTModelMixin, models.Model):
|
||||
for key, value in self.config.items():
|
||||
# Use a copy here not to change the origin value in the config field.
|
||||
result[key] = value.copy()
|
||||
result[key]['uuid'] = key
|
||||
element = elements.get(value['name'])
|
||||
result[key]["uuid"] = key
|
||||
element = elements.get(value["name"])
|
||||
if element is None:
|
||||
result[key]['error'] = 'Projector element does not exist.'
|
||||
result[key]["error"] = "Projector element does not exist."
|
||||
else:
|
||||
try:
|
||||
result[key].update(element.check_and_update_data(
|
||||
projector_object=self,
|
||||
config_entry=value))
|
||||
result[key].update(
|
||||
element.check_and_update_data(
|
||||
projector_object=self, config_entry=value
|
||||
)
|
||||
)
|
||||
except ProjectorException as e:
|
||||
result[key]['error'] = str(e)
|
||||
result[key]["error"] = str(e)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
@ -173,14 +173,14 @@ class ProjectionDefault(RESTModelMixin, models.Model):
|
||||
special name like 'list_of_speakers'. The display_name is the shown
|
||||
name on the front end for the user.
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=256)
|
||||
|
||||
display_name = models.CharField(max_length=256)
|
||||
|
||||
projector = models.ForeignKey(
|
||||
Projector,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='projectiondefaults')
|
||||
Projector, on_delete=models.CASCADE, related_name="projectiondefaults"
|
||||
)
|
||||
|
||||
def get_root_rest_element(self):
|
||||
return self.projector
|
||||
@ -197,17 +197,15 @@ class Tag(RESTModelMixin, models.Model):
|
||||
Model for tags. This tags can be used for other models like agenda items,
|
||||
motions or assignments.
|
||||
"""
|
||||
|
||||
access_permissions = TagAccessPermissions()
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
unique=True)
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
ordering = ("name",)
|
||||
default_permissions = ()
|
||||
permissions = (
|
||||
('can_manage_tags', 'Can manage tags'),)
|
||||
permissions = (("can_manage_tags", "Can manage tags"),)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -217,6 +215,7 @@ class ConfigStore(RESTModelMixin, models.Model):
|
||||
"""
|
||||
A model class to store all config variables in the database.
|
||||
"""
|
||||
|
||||
access_permissions = ConfigAccessPermissions()
|
||||
|
||||
key = models.CharField(max_length=255, unique=True, db_index=True)
|
||||
@ -228,12 +227,13 @@ class ConfigStore(RESTModelMixin, models.Model):
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
permissions = (
|
||||
('can_manage_config', 'Can manage configuration'),
|
||||
('can_manage_logos_and_fonts', 'Can manage logos and fonts'))
|
||||
("can_manage_config", "Can manage configuration"),
|
||||
("can_manage_logos_and_fonts", "Can manage logos and fonts"),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_collection_string(cls):
|
||||
return 'core/config'
|
||||
return "core/config"
|
||||
|
||||
|
||||
class ChatMessage(RESTModelMixin, models.Model):
|
||||
@ -242,31 +242,32 @@ class ChatMessage(RESTModelMixin, models.Model):
|
||||
|
||||
At the moment we only have one global chat room for managers.
|
||||
"""
|
||||
|
||||
access_permissions = ChatMessageAccessPermissions()
|
||||
can_see_permission = 'core.can_use_chat'
|
||||
can_see_permission = "core.can_use_chat"
|
||||
|
||||
message = models.TextField()
|
||||
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
permissions = (
|
||||
('can_use_chat', 'Can use the chat'),
|
||||
('can_manage_chat', 'Can manage the chat'),)
|
||||
("can_use_chat", "Can use the chat"),
|
||||
("can_manage_chat", "Can manage the chat"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return 'Message {}'.format(self.timestamp)
|
||||
return "Message {}".format(self.timestamp)
|
||||
|
||||
|
||||
class ProjectorMessage(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Model for ProjectorMessages.
|
||||
"""
|
||||
|
||||
access_permissions = ProjectorMessageAccessPermissions()
|
||||
|
||||
message = models.TextField(blank=True)
|
||||
@ -280,16 +281,18 @@ class ProjectorMessage(RESTModelMixin, models.Model):
|
||||
projector message projector element is disabled.
|
||||
"""
|
||||
Projector.remove_any(
|
||||
skip_autoupdate=skip_autoupdate,
|
||||
name='core/projector-message',
|
||||
id=self.pk)
|
||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, name="core/projector-message", id=self.pk
|
||||
)
|
||||
return super().delete( # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class Countdown(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Model for countdowns.
|
||||
"""
|
||||
|
||||
access_permissions = CountdownAccessPermissions()
|
||||
|
||||
description = models.CharField(max_length=256, blank=True)
|
||||
@ -309,19 +312,22 @@ class Countdown(RESTModelMixin, models.Model):
|
||||
countdown projector element is disabled.
|
||||
"""
|
||||
Projector.remove_any(
|
||||
skip_autoupdate=skip_autoupdate,
|
||||
name='core/countdown',
|
||||
id=self.pk)
|
||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, name="core/countdown", id=self.pk
|
||||
)
|
||||
return super().delete( # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||
)
|
||||
|
||||
def control(self, action, skip_autoupdate=False):
|
||||
if action not in ('start', 'stop', 'reset'):
|
||||
raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action))
|
||||
if action not in ("start", "stop", "reset"):
|
||||
raise ValueError(
|
||||
"Action must be 'start', 'stop' or 'reset', not {}.".format(action)
|
||||
)
|
||||
|
||||
if action == 'start':
|
||||
if action == "start":
|
||||
self.running = True
|
||||
self.countdown_time = now().timestamp() + self.default_time
|
||||
elif action == 'stop' and self.running:
|
||||
elif action == "stop" and self.running:
|
||||
self.running = False
|
||||
self.countdown_time = self.countdown_time - now().timestamp()
|
||||
else: # reset
|
||||
@ -337,6 +343,7 @@ class HistoryData(models.Model):
|
||||
This is not a RESTModel. It is not cachable and can only be reached by a
|
||||
special viewset.
|
||||
"""
|
||||
|
||||
full_data = JSONField()
|
||||
|
||||
class Meta:
|
||||
@ -347,6 +354,7 @@ class HistoryManager(models.Manager):
|
||||
"""
|
||||
Customized model manager for the history model.
|
||||
"""
|
||||
|
||||
def add_elements(self, elements):
|
||||
"""
|
||||
Method to add elements to the history. This does not trigger autoupdate.
|
||||
@ -354,18 +362,26 @@ class HistoryManager(models.Manager):
|
||||
with transaction.atomic():
|
||||
instances = []
|
||||
for element in elements:
|
||||
if element['disable_history'] or element['collection_string'] == self.model.get_collection_string():
|
||||
if (
|
||||
element["disable_history"]
|
||||
or element["collection_string"]
|
||||
== self.model.get_collection_string()
|
||||
):
|
||||
# Do not update history for history elements itself or if history is disabled.
|
||||
continue
|
||||
# HistoryData is not a root rest element so there is no autoupdate and not history saving here.
|
||||
data = HistoryData.objects.create(full_data=element['full_data'])
|
||||
data = HistoryData.objects.create(full_data=element["full_data"])
|
||||
instance = self.model(
|
||||
element_id=get_element_id(element['collection_string'], element['id']),
|
||||
information=element['information'],
|
||||
user_id=element['user_id'],
|
||||
element_id=get_element_id(
|
||||
element["collection_string"], element["id"]
|
||||
),
|
||||
information=element["information"],
|
||||
user_id=element["user_id"],
|
||||
full_data=data,
|
||||
)
|
||||
instance.save(skip_autoupdate=True) # Skip autoupdate and of course history saving.
|
||||
instance.save(
|
||||
skip_autoupdate=True
|
||||
) # Skip autoupdate and of course history saving.
|
||||
instances.append(instance)
|
||||
return instances
|
||||
|
||||
@ -380,14 +396,16 @@ class HistoryManager(models.Manager):
|
||||
all_full_data = async_to_sync(element_cache.get_all_full_data)()
|
||||
for collection_string, data in all_full_data.items():
|
||||
for full_data in data:
|
||||
elements.append(Element(
|
||||
id=full_data['id'],
|
||||
collection_string=collection_string,
|
||||
full_data=full_data,
|
||||
information='',
|
||||
user_id=None,
|
||||
disable_history=False,
|
||||
))
|
||||
elements.append(
|
||||
Element(
|
||||
id=full_data["id"],
|
||||
collection_string=collection_string,
|
||||
full_data=full_data,
|
||||
information="",
|
||||
user_id=None,
|
||||
disable_history=False,
|
||||
)
|
||||
)
|
||||
instances = self.add_elements(elements)
|
||||
return instances
|
||||
|
||||
@ -399,29 +417,22 @@ class History(RESTModelMixin, models.Model):
|
||||
This model itself is not part of the history. This means that if you
|
||||
delete a user you may lose the information of the user field here.
|
||||
"""
|
||||
|
||||
access_permissions = HistoryAccessPermissions()
|
||||
|
||||
objects = HistoryManager()
|
||||
|
||||
element_id = models.CharField(
|
||||
max_length=255,
|
||||
)
|
||||
element_id = models.CharField(max_length=255)
|
||||
|
||||
now = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
information = models.CharField(
|
||||
max_length=255,
|
||||
)
|
||||
information = models.CharField(max_length=255)
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL)
|
||||
|
||||
full_data = models.OneToOneField(
|
||||
HistoryData,
|
||||
on_delete=models.CASCADE,
|
||||
settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
full_data = models.OneToOneField(HistoryData, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
|
@ -9,29 +9,32 @@ class Clock(ProjectorElement):
|
||||
"""
|
||||
Clock on the projector.
|
||||
"""
|
||||
name = 'core/clock'
|
||||
|
||||
name = "core/clock"
|
||||
|
||||
|
||||
class CountdownElement(ProjectorElement):
|
||||
"""
|
||||
Countdown slide for the projector.
|
||||
"""
|
||||
name = 'core/countdown'
|
||||
|
||||
name = "core/countdown"
|
||||
|
||||
def check_data(self):
|
||||
if not Countdown.objects.filter(pk=self.config_entry.get('id')).exists():
|
||||
raise ProjectorException('Countdown does not exists.')
|
||||
if not Countdown.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||
raise ProjectorException("Countdown does not exists.")
|
||||
|
||||
|
||||
class ProjectorMessageElement(ProjectorElement):
|
||||
"""
|
||||
Short message on the projector. Rendered as overlay.
|
||||
"""
|
||||
name = 'core/projector-message'
|
||||
|
||||
name = "core/projector-message"
|
||||
|
||||
def check_data(self):
|
||||
if not ProjectorMessage.objects.filter(pk=self.config_entry.get('id')).exists():
|
||||
raise ProjectorException('Message does not exists.')
|
||||
if not ProjectorMessage.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||
raise ProjectorException("Message does not exists.")
|
||||
|
||||
|
||||
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
||||
|
@ -17,18 +17,21 @@ class JSONSerializerField(Field):
|
||||
"""
|
||||
Serializer for projector's and config JSONField.
|
||||
"""
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""
|
||||
Checks that data is a dictionary. The key is a hex UUID and the
|
||||
value is a dictionary with must have a key 'name'.
|
||||
"""
|
||||
if type(data) is not dict:
|
||||
raise ValidationError({'detail': 'Data must be a dictionary.'})
|
||||
raise ValidationError({"detail": "Data must be a dictionary."})
|
||||
for element in data.values():
|
||||
if type(element) is not dict:
|
||||
raise ValidationError({'detail': 'Data must be a dictionary.'})
|
||||
elif element.get('name') is None:
|
||||
raise ValidationError({'detail': "Every dictionary must have a key 'name'."})
|
||||
raise ValidationError({"detail": "Data must be a dictionary."})
|
||||
elif element.get("name") is None:
|
||||
raise ValidationError(
|
||||
{"detail": "Every dictionary must have a key 'name'."}
|
||||
)
|
||||
return data
|
||||
|
||||
def to_representation(self, value):
|
||||
@ -42,65 +45,82 @@ class ProjectionDefaultSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for core.models.ProjectionDefault objects.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ProjectionDefault
|
||||
fields = ('id', 'name', 'display_name', 'projector', )
|
||||
fields = ("id", "name", "display_name", "projector")
|
||||
|
||||
|
||||
class ProjectorSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for core.models.Projector objects.
|
||||
"""
|
||||
|
||||
config = JSONSerializerField(write_only=True)
|
||||
projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Projector
|
||||
fields = ('id', 'config', 'elements', 'scale', 'scroll', 'name', 'blank', 'width', 'height', 'projectiondefaults', )
|
||||
read_only_fields = ('scale', 'scroll', 'blank', 'width', 'height', )
|
||||
fields = (
|
||||
"id",
|
||||
"config",
|
||||
"elements",
|
||||
"scale",
|
||||
"scroll",
|
||||
"name",
|
||||
"blank",
|
||||
"width",
|
||||
"height",
|
||||
"projectiondefaults",
|
||||
)
|
||||
read_only_fields = ("scale", "scroll", "blank", "width", "height")
|
||||
|
||||
|
||||
class TagSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for core.models.Tag objects.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ('id', 'name', )
|
||||
fields = ("id", "name")
|
||||
|
||||
|
||||
class ConfigSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for core.models.Tag objects.
|
||||
"""
|
||||
|
||||
value = JSONSerializerField()
|
||||
|
||||
class Meta:
|
||||
model = ConfigStore
|
||||
fields = ('id', 'key', 'value')
|
||||
fields = ("id", "key", "value")
|
||||
|
||||
|
||||
class ChatMessageSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for core.models.ChatMessage objects.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ChatMessage
|
||||
fields = ('id', 'message', 'timestamp', 'user', )
|
||||
read_only_fields = ('user', )
|
||||
fields = ("id", "message", "timestamp", "user")
|
||||
read_only_fields = ("user",)
|
||||
|
||||
|
||||
class ProjectorMessageSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for core.models.ProjectorMessage objects.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ProjectorMessage
|
||||
fields = ('id', 'message', )
|
||||
fields = ("id", "message")
|
||||
|
||||
def validate(self, data):
|
||||
if 'message' in data:
|
||||
data['message'] = validate_html(data['message'])
|
||||
if "message" in data:
|
||||
data["message"] = validate_html(data["message"])
|
||||
return data
|
||||
|
||||
|
||||
@ -108,9 +128,10 @@ class CountdownSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for core.models.Countdown objects.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Countdown
|
||||
fields = ('id', 'description', 'default_time', 'countdown_time', 'running', )
|
||||
fields = ("id", "description", "default_time", "countdown_time", "running")
|
||||
|
||||
|
||||
class HistorySerializer(ModelSerializer):
|
||||
@ -119,6 +140,7 @@ class HistorySerializer(ModelSerializer):
|
||||
|
||||
Does not contain full data of history object.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = History
|
||||
fields = ('id', 'element_id', 'now', 'information', 'user', )
|
||||
fields = ("id", "element_id", "now", "information", "user")
|
||||
|
@ -21,9 +21,8 @@ def delete_django_app_permissions(sender, **kwargs):
|
||||
for auth, contenttypes and sessions.
|
||||
"""
|
||||
contenttypes = ContentType.objects.filter(
|
||||
Q(app_label='auth') |
|
||||
Q(app_label='contenttypes') |
|
||||
Q(app_label='sessions'))
|
||||
Q(app_label="auth") | Q(app_label="contenttypes") | Q(app_label="sessions")
|
||||
)
|
||||
Permission.objects.filter(content_type__in=contenttypes).delete()
|
||||
|
||||
|
||||
@ -31,13 +30,13 @@ def get_permission_change_data(sender, permissions, **kwargs):
|
||||
"""
|
||||
Yields all necessary Cachables if the respective permissions change.
|
||||
"""
|
||||
core_app = apps.get_app_config(app_label='core')
|
||||
core_app = apps.get_app_config(app_label="core")
|
||||
for permission in permissions:
|
||||
if permission.content_type.app_label == core_app.label:
|
||||
if permission.codename == 'can_see_projector':
|
||||
yield core_app.get_model('Projector')
|
||||
elif permission.codename == 'can_manage_projector':
|
||||
yield core_app.get_model('ProjectorMessage')
|
||||
yield core_app.get_model('Countdown')
|
||||
elif permission.codename == 'can_use_chat':
|
||||
yield core_app.get_model('ChatMessage')
|
||||
if permission.codename == "can_see_projector":
|
||||
yield core_app.get_model("Projector")
|
||||
elif permission.codename == "can_manage_projector":
|
||||
yield core_app.get_model("ProjectorMessage")
|
||||
yield core_app.get_model("Countdown")
|
||||
elif permission.codename == "can_use_chat":
|
||||
yield core_app.get_model("ChatMessage")
|
||||
|
@ -4,15 +4,7 @@ from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^servertime/$',
|
||||
views.ServerTime.as_view(),
|
||||
name='core_servertime'),
|
||||
|
||||
url(r'^version/$',
|
||||
views.VersionView.as_view(),
|
||||
name='core_version'),
|
||||
|
||||
url(r'^history/$',
|
||||
views.HistoryView.as_view(),
|
||||
name='core_history'),
|
||||
url(r"^servertime/$", views.ServerTime.as_view(), name="core_servertime"),
|
||||
url(r"^version/$", views.VersionView.as_view(), name="core_version"),
|
||||
url(r"^history/$", views.HistoryView.as_view(), name="core_history"),
|
||||
]
|
||||
|
@ -17,12 +17,7 @@ from mypy_extensions import TypedDict
|
||||
from .. import __license__ as license, __url__ as url, __version__ as version
|
||||
from ..utils import views as utils_views
|
||||
from ..utils.arguments import arguments
|
||||
from ..utils.auth import (
|
||||
GROUP_ADMIN_PK,
|
||||
anonymous_is_enabled,
|
||||
has_perm,
|
||||
in_some_groups,
|
||||
)
|
||||
from ..utils.auth import GROUP_ADMIN_PK, anonymous_is_enabled, has_perm, in_some_groups
|
||||
from ..utils.autoupdate import inform_changed_data, inform_deleted_data
|
||||
from ..utils.plugins import (
|
||||
get_plugin_description,
|
||||
@ -67,6 +62,7 @@ from .models import (
|
||||
|
||||
# Special Django views
|
||||
|
||||
|
||||
class IndexView(View):
|
||||
"""
|
||||
The primary view for the OpenSlides client. Serves static files. If a file
|
||||
@ -83,11 +79,11 @@ class IndexView(View):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
no_caching = arguments.get('no_template_caching', False)
|
||||
if 'index' not in self.cache or no_caching:
|
||||
self.cache['index'] = finders.find('index.html')
|
||||
no_caching = arguments.get("no_template_caching", False)
|
||||
if "index" not in self.cache or no_caching:
|
||||
self.cache["index"] = finders.find("index.html")
|
||||
|
||||
self.index_document_root, self.index_path = os.path.split(self.cache['index'])
|
||||
self.index_document_root, self.index_path = os.path.split(self.cache["index"])
|
||||
|
||||
def get(self, request, path, **kwargs) -> HttpResponse:
|
||||
"""
|
||||
@ -97,18 +93,25 @@ class IndexView(View):
|
||||
try:
|
||||
response = serve(request, path, **kwargs)
|
||||
except Http404:
|
||||
response = static.serve(request, self.index_path, document_root=self.index_document_root, **kwargs)
|
||||
response = static.serve(
|
||||
request,
|
||||
self.index_path,
|
||||
document_root=self.index_document_root,
|
||||
**kwargs,
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
# Viewsets for the REST API
|
||||
|
||||
|
||||
class ProjectorViewSet(ModelViewSet):
|
||||
"""
|
||||
API endpoint for the projector slide info.
|
||||
|
||||
There are the following views: See strings in check_view_permissions().
|
||||
"""
|
||||
|
||||
access_permissions = ProjectorAccessPermissions()
|
||||
queryset = Projector.objects.all()
|
||||
|
||||
@ -116,18 +119,31 @@ class ProjectorViewSet(ModelViewSet):
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
if self.action in ('list', 'retrieve'):
|
||||
if self.action in ("list", "retrieve"):
|
||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||
elif self.action == 'metadata':
|
||||
result = has_perm(self.request.user, 'core.can_see_projector')
|
||||
elif self.action == "metadata":
|
||||
result = has_perm(self.request.user, "core.can_see_projector")
|
||||
elif self.action in (
|
||||
'create', 'update', 'partial_update', 'destroy',
|
||||
'activate_elements', 'prune_elements', 'update_elements', 'deactivate_elements', 'clear_elements',
|
||||
'project', 'control_view', 'set_resolution', 'set_scroll', 'control_blank',
|
||||
'broadcast', 'set_projectiondefault',
|
||||
"create",
|
||||
"update",
|
||||
"partial_update",
|
||||
"destroy",
|
||||
"activate_elements",
|
||||
"prune_elements",
|
||||
"update_elements",
|
||||
"deactivate_elements",
|
||||
"clear_elements",
|
||||
"project",
|
||||
"control_view",
|
||||
"set_resolution",
|
||||
"set_scroll",
|
||||
"control_blank",
|
||||
"broadcast",
|
||||
"set_projectiondefault",
|
||||
):
|
||||
result = (has_perm(self.request.user, 'core.can_see_projector') and
|
||||
has_perm(self.request.user, 'core.can_manage_projector'))
|
||||
result = has_perm(self.request.user, "core.can_see_projector") and has_perm(
|
||||
self.request.user, "core.can_manage_projector"
|
||||
)
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
@ -144,11 +160,11 @@ class ProjectorViewSet(ModelViewSet):
|
||||
if projection_default.projector.id == projector_instance.id:
|
||||
projection_default.projector_id = 1
|
||||
projection_default.save()
|
||||
if config['projector_broadcast'] == projector_instance.pk:
|
||||
config['projector_broadcast'] = 0
|
||||
if config["projector_broadcast"] == projector_instance.pk:
|
||||
config["projector_broadcast"] = 0
|
||||
return super(ProjectorViewSet, self).destroy(*args, **kwargs)
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def activate_elements(self, request, pk):
|
||||
"""
|
||||
REST API operation to activate projector elements. It expects a POST
|
||||
@ -156,21 +172,25 @@ class ProjectorViewSet(ModelViewSet):
|
||||
of dictionaries to be appended to the projector config entry.
|
||||
"""
|
||||
if not isinstance(request.data, list):
|
||||
raise ValidationError({'detail': 'Data must be a list.'})
|
||||
raise ValidationError({"detail": "Data must be a list."})
|
||||
|
||||
projector_instance = self.get_object()
|
||||
projector_config = projector_instance.config
|
||||
for element in request.data:
|
||||
if element.get('name') is None:
|
||||
raise ValidationError({'detail': 'Invalid projector element. Name is missing.'})
|
||||
if element.get("name") is None:
|
||||
raise ValidationError(
|
||||
{"detail": "Invalid projector element. Name is missing."}
|
||||
)
|
||||
projector_config[uuid.uuid4().hex] = element
|
||||
|
||||
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
||||
serializer = self.get_serializer(
|
||||
projector_instance, data={"config": projector_config}, partial=False
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def prune_elements(self, request, pk):
|
||||
"""
|
||||
REST API operation to activate projector elements. It expects a POST
|
||||
@ -179,17 +199,19 @@ class ProjectorViewSet(ModelViewSet):
|
||||
entries are deleted but not entries with stable == True.
|
||||
"""
|
||||
if not isinstance(request.data, list):
|
||||
raise ValidationError({'detail': 'Data must be a list.'})
|
||||
raise ValidationError({"detail": "Data must be a list."})
|
||||
|
||||
projector = self.get_object()
|
||||
elements = request.data
|
||||
if not isinstance(elements, list):
|
||||
raise ValidationError({'detail': _('The data has to be a list.')})
|
||||
raise ValidationError({"detail": _("The data has to be a list.")})
|
||||
for element in elements:
|
||||
if not isinstance(element, dict):
|
||||
raise ValidationError({'detail': _('All elements have to be dicts.')})
|
||||
if element.get('name') is None:
|
||||
raise ValidationError({'detail': 'Invalid projector element. Name is missing.'})
|
||||
raise ValidationError({"detail": _("All elements have to be dicts.")})
|
||||
if element.get("name") is None:
|
||||
raise ValidationError(
|
||||
{"detail": "Invalid projector element. Name is missing."}
|
||||
)
|
||||
return Response(self.prune(projector, elements))
|
||||
|
||||
def prune(self, projector, elements):
|
||||
@ -201,21 +223,23 @@ class ProjectorViewSet(ModelViewSet):
|
||||
"""
|
||||
projector_config = {}
|
||||
for key, value in projector.config.items():
|
||||
if value.get('stable'):
|
||||
if value.get("stable"):
|
||||
projector_config[key] = value
|
||||
for element in elements:
|
||||
projector_config[uuid.uuid4().hex] = element
|
||||
|
||||
serializer = self.get_serializer(projector, data={'config': projector_config}, partial=False)
|
||||
serializer = self.get_serializer(
|
||||
projector, data={"config": projector_config}, partial=False
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
# reset scroll level
|
||||
if (projector.scroll != 0):
|
||||
if projector.scroll != 0:
|
||||
projector.scroll = 0
|
||||
projector.save()
|
||||
return serializer.data
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def update_elements(self, request, pk):
|
||||
"""
|
||||
REST API operation to update projector elements. It expects a POST
|
||||
@ -237,8 +261,10 @@ class ProjectorViewSet(ModelViewSet):
|
||||
}
|
||||
"""
|
||||
if not isinstance(request.data, dict):
|
||||
raise ValidationError({'detail': 'Data must be a dictionary.'})
|
||||
error = {'detail': 'Data must be a dictionary with UUIDs as keys and dictionaries as values.'}
|
||||
raise ValidationError({"detail": "Data must be a dictionary."})
|
||||
error = {
|
||||
"detail": "Data must be a dictionary with UUIDs as keys and dictionaries as values."
|
||||
}
|
||||
for key, value in request.data.items():
|
||||
try:
|
||||
uuid.UUID(hex=str(key))
|
||||
@ -251,15 +277,19 @@ class ProjectorViewSet(ModelViewSet):
|
||||
projector_config = projector_instance.config
|
||||
for key, value in request.data.items():
|
||||
if key not in projector_config:
|
||||
raise ValidationError({'detail': 'Invalid projector element. Wrong UUID.'})
|
||||
raise ValidationError(
|
||||
{"detail": "Invalid projector element. Wrong UUID."}
|
||||
)
|
||||
projector_config[key].update(request.data[key])
|
||||
|
||||
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
||||
serializer = self.get_serializer(
|
||||
projector_instance, data={"config": projector_config}, partial=False
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def deactivate_elements(self, request, pk):
|
||||
"""
|
||||
REST API operation to deactivate projector elements. It expects a
|
||||
@ -268,12 +298,12 @@ class ProjectorViewSet(ModelViewSet):
|
||||
that should be deleted.
|
||||
"""
|
||||
if not isinstance(request.data, list):
|
||||
raise ValidationError({'detail': 'Data must be a list of hex UUIDs.'})
|
||||
raise ValidationError({"detail": "Data must be a list of hex UUIDs."})
|
||||
for item in request.data:
|
||||
try:
|
||||
uuid.UUID(hex=str(item))
|
||||
except ValueError:
|
||||
raise ValidationError({'detail': 'Data must be a list of hex UUIDs.'})
|
||||
raise ValidationError({"detail": "Data must be a list of hex UUIDs."})
|
||||
|
||||
projector_instance = self.get_object()
|
||||
projector_config = projector_instance.config
|
||||
@ -281,14 +311,16 @@ class ProjectorViewSet(ModelViewSet):
|
||||
try:
|
||||
del projector_config[key]
|
||||
except KeyError:
|
||||
raise ValidationError({'detail': 'Invalid UUID.'})
|
||||
raise ValidationError({"detail": "Invalid UUID."})
|
||||
|
||||
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
||||
serializer = self.get_serializer(
|
||||
projector_instance, data={"config": projector_config}, partial=False
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def clear_elements(self, request, pk):
|
||||
"""
|
||||
REST API operation to deactivate all projector elements but not
|
||||
@ -301,15 +333,17 @@ class ProjectorViewSet(ModelViewSet):
|
||||
def clear(self, projector):
|
||||
projector_config = {}
|
||||
for key, value in projector.config.items():
|
||||
if value.get('stable'):
|
||||
if value.get("stable"):
|
||||
projector_config[key] = value
|
||||
|
||||
serializer = self.get_serializer(projector, data={'config': projector_config}, partial=False)
|
||||
serializer = self.get_serializer(
|
||||
projector, data={"config": projector_config}, partial=False
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return serializer.data
|
||||
|
||||
@list_route(methods=['post'])
|
||||
@list_route(methods=["post"])
|
||||
def project(self, request, *args, **kwargs):
|
||||
"""
|
||||
REST API operation. Does a combination of clear_elements and prune_elements:
|
||||
@ -327,33 +361,46 @@ class ProjectorViewSet(ModelViewSet):
|
||||
"""
|
||||
# The data has to be a dict.
|
||||
if not isinstance(request.data, dict):
|
||||
raise ValidationError({'detail': _('The data has to be a dict.')})
|
||||
raise ValidationError({"detail": _("The data has to be a dict.")})
|
||||
|
||||
# Get projector ids to clear
|
||||
clear_projector_ids = request.data.get('clear_ids', [])
|
||||
clear_projector_ids = request.data.get("clear_ids", [])
|
||||
for id in clear_projector_ids:
|
||||
if not isinstance(id, int):
|
||||
raise ValidationError({'detail': _('The id "{}" has to be int.').format(id)})
|
||||
raise ValidationError(
|
||||
{"detail": _('The id "{}" has to be int.').format(id)}
|
||||
)
|
||||
|
||||
# Get the projector id and validate element to prune. This is optional.
|
||||
prune = request.data.get('prune')
|
||||
prune = request.data.get("prune")
|
||||
if prune is not None:
|
||||
if not isinstance(prune, dict):
|
||||
raise ValidationError({'detail': _('Prune has to be an object.')})
|
||||
prune_projector_id = prune.get('id')
|
||||
raise ValidationError({"detail": _("Prune has to be an object.")})
|
||||
prune_projector_id = prune.get("id")
|
||||
if not isinstance(prune_projector_id, int):
|
||||
raise ValidationError({'detail': _('The prune projector id has to be int.')})
|
||||
raise ValidationError(
|
||||
{"detail": _("The prune projector id has to be int.")}
|
||||
)
|
||||
|
||||
# Get the projector after all clear operations, but check, if it exist.
|
||||
if not Projector.objects.filter(pk=prune_projector_id).exists():
|
||||
raise ValidationError({
|
||||
'detail': _('The projector with id "{}" does not exist').format(prune_projector_id)})
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": _('The projector with id "{}" does not exist').format(
|
||||
prune_projector_id
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
prune_element = prune.get('element', {})
|
||||
prune_element = prune.get("element", {})
|
||||
if not isinstance(prune_element, dict):
|
||||
raise ValidationError({'detail': _('Prune element has to be a dict or not given.')})
|
||||
if prune_element.get('name') is None:
|
||||
raise ValidationError({'detail': 'Invalid projector element. Name is missing.'})
|
||||
raise ValidationError(
|
||||
{"detail": _("Prune element has to be a dict or not given.")}
|
||||
)
|
||||
if prune_element.get("name") is None:
|
||||
raise ValidationError(
|
||||
{"detail": "Invalid projector element. Name is missing."}
|
||||
)
|
||||
|
||||
# First step: Clear all given projectors
|
||||
for projector in Projector.objects.filter(pk__in=clear_projector_ids):
|
||||
@ -367,7 +414,7 @@ class ProjectorViewSet(ModelViewSet):
|
||||
|
||||
return Response()
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def set_resolution(self, request, pk):
|
||||
"""
|
||||
REST API operation to set the resolution.
|
||||
@ -388,26 +435,34 @@ class ProjectorViewSet(ModelViewSet):
|
||||
}
|
||||
"""
|
||||
if not isinstance(request.data, dict):
|
||||
raise ValidationError({'detail': 'Data must be a dictionary.'})
|
||||
if request.data.get('width') is None or request.data.get('height') is None:
|
||||
raise ValidationError({'detail': 'A width and a height have to be given.'})
|
||||
if not isinstance(request.data['width'], int) or not isinstance(request.data['height'], int):
|
||||
raise ValidationError({'detail': 'Data has to be integers.'})
|
||||
if (request.data['width'] < 800 or request.data['width'] > 3840 or
|
||||
request.data['height'] < 340 or request.data['height'] > 2880):
|
||||
raise ValidationError({'detail': 'The Resolution have to be between 800x340 and 3840x2880.'})
|
||||
raise ValidationError({"detail": "Data must be a dictionary."})
|
||||
if request.data.get("width") is None or request.data.get("height") is None:
|
||||
raise ValidationError({"detail": "A width and a height have to be given."})
|
||||
if not isinstance(request.data["width"], int) or not isinstance(
|
||||
request.data["height"], int
|
||||
):
|
||||
raise ValidationError({"detail": "Data has to be integers."})
|
||||
if (
|
||||
request.data["width"] < 800
|
||||
or request.data["width"] > 3840
|
||||
or request.data["height"] < 340
|
||||
or request.data["height"] > 2880
|
||||
):
|
||||
raise ValidationError(
|
||||
{"detail": "The Resolution have to be between 800x340 and 3840x2880."}
|
||||
)
|
||||
|
||||
projector_instance = self.get_object()
|
||||
projector_instance.width = request.data['width']
|
||||
projector_instance.height = request.data['height']
|
||||
projector_instance.width = request.data["width"]
|
||||
projector_instance.height = request.data["height"]
|
||||
projector_instance.save()
|
||||
|
||||
message = 'Changing resolution to {width}x{height} was successful.'.format(
|
||||
width=request.data['width'],
|
||||
height=request.data['height'])
|
||||
return Response({'detail': message})
|
||||
message = "Changing resolution to {width}x{height} was successful.".format(
|
||||
width=request.data["width"], height=request.data["height"]
|
||||
)
|
||||
return Response({"detail": message})
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def control_view(self, request, pk):
|
||||
"""
|
||||
REST API operation to control the projector view, i. e. scale and
|
||||
@ -426,27 +481,32 @@ class ProjectorViewSet(ModelViewSet):
|
||||
}
|
||||
"""
|
||||
if not isinstance(request.data, dict):
|
||||
raise ValidationError({'detail': 'Data must be a dictionary.'})
|
||||
if (request.data.get('action') not in ('scale', 'scroll') or
|
||||
request.data.get('direction') not in ('up', 'down', 'reset')):
|
||||
raise ValidationError({'detail': "Data must be a dictionary with an action ('scale' or 'scroll') "
|
||||
"and a direction ('up', 'down' or 'reset')."})
|
||||
raise ValidationError({"detail": "Data must be a dictionary."})
|
||||
if request.data.get("action") not in ("scale", "scroll") or request.data.get(
|
||||
"direction"
|
||||
) not in ("up", "down", "reset"):
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": "Data must be a dictionary with an action ('scale' or 'scroll') "
|
||||
"and a direction ('up', 'down' or 'reset')."
|
||||
}
|
||||
)
|
||||
|
||||
projector_instance = self.get_object()
|
||||
if request.data['action'] == 'scale':
|
||||
if request.data['direction'] == 'up':
|
||||
projector_instance.scale = F('scale') + 1
|
||||
elif request.data['direction'] == 'down':
|
||||
projector_instance.scale = F('scale') - 1
|
||||
if request.data["action"] == "scale":
|
||||
if request.data["direction"] == "up":
|
||||
projector_instance.scale = F("scale") + 1
|
||||
elif request.data["direction"] == "down":
|
||||
projector_instance.scale = F("scale") - 1
|
||||
else:
|
||||
# request.data['direction'] == 'reset'
|
||||
projector_instance.scale = 0
|
||||
else:
|
||||
# request.data['action'] == 'scroll'
|
||||
if request.data['direction'] == 'up':
|
||||
projector_instance.scroll = F('scroll') + 1
|
||||
elif request.data['direction'] == 'down':
|
||||
projector_instance.scroll = F('scroll') - 1
|
||||
if request.data["direction"] == "up":
|
||||
projector_instance.scroll = F("scroll") + 1
|
||||
elif request.data["direction"] == "down":
|
||||
projector_instance.scroll = F("scroll") - 1
|
||||
else:
|
||||
# request.data['direction'] == 'reset'
|
||||
projector_instance.scroll = 0
|
||||
@ -454,12 +514,13 @@ class ProjectorViewSet(ModelViewSet):
|
||||
projector_instance.save(skip_autoupdate=True)
|
||||
projector_instance.refresh_from_db()
|
||||
inform_changed_data(projector_instance)
|
||||
message = '{action} {direction} was successful.'.format(
|
||||
action=request.data['action'].capitalize(),
|
||||
direction=request.data['direction'])
|
||||
return Response({'detail': message})
|
||||
message = "{action} {direction} was successful.".format(
|
||||
action=request.data["action"].capitalize(),
|
||||
direction=request.data["direction"],
|
||||
)
|
||||
return Response({"detail": message})
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def set_scroll(self, request, pk):
|
||||
"""
|
||||
REST API operation to scroll the projector.
|
||||
@ -468,17 +529,18 @@ class ProjectorViewSet(ModelViewSet):
|
||||
/rest/core/projector/<pk>/set_scroll/ with a new value for scroll.
|
||||
"""
|
||||
if not isinstance(request.data, int):
|
||||
raise ValidationError({'detail': 'Data must be an int.'})
|
||||
raise ValidationError({"detail": "Data must be an int."})
|
||||
|
||||
projector_instance = self.get_object()
|
||||
projector_instance.scroll = request.data
|
||||
|
||||
projector_instance.save()
|
||||
message = 'Setting scroll to {scroll} was successful.'.format(
|
||||
scroll=request.data)
|
||||
return Response({'detail': message})
|
||||
message = "Setting scroll to {scroll} was successful.".format(
|
||||
scroll=request.data
|
||||
)
|
||||
return Response({"detail": message})
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def control_blank(self, request, pk):
|
||||
"""
|
||||
REST API operation to blank the projector.
|
||||
@ -487,16 +549,17 @@ class ProjectorViewSet(ModelViewSet):
|
||||
/rest/core/projector/<pk>/control_blank/ with a value for blank.
|
||||
"""
|
||||
if not isinstance(request.data, bool):
|
||||
raise ValidationError({'detail': 'Data must be a bool.'})
|
||||
raise ValidationError({"detail": "Data must be a bool."})
|
||||
|
||||
projector_instance = self.get_object()
|
||||
projector_instance.blank = request.data
|
||||
projector_instance.save()
|
||||
message = "Setting 'blank' to {blank} was successful.".format(
|
||||
blank=request.data)
|
||||
return Response({'detail': message})
|
||||
blank=request.data
|
||||
)
|
||||
return Response({"detail": message})
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def broadcast(self, request, pk):
|
||||
"""
|
||||
REST API operation to (un-)broadcast the given projector.
|
||||
@ -505,16 +568,17 @@ class ProjectorViewSet(ModelViewSet):
|
||||
It expects a POST request to
|
||||
/rest/core/projector/<pk>/broadcast/ without an argument
|
||||
"""
|
||||
if config['projector_broadcast'] == 0:
|
||||
config['projector_broadcast'] = pk
|
||||
if config["projector_broadcast"] == 0:
|
||||
config["projector_broadcast"] = pk
|
||||
message = "Setting projector {id} as broadcast projector was successful.".format(
|
||||
id=pk)
|
||||
id=pk
|
||||
)
|
||||
else:
|
||||
config['projector_broadcast'] = 0
|
||||
config["projector_broadcast"] = 0
|
||||
message = "Disabling broadcast was successful."
|
||||
return Response({'detail': message})
|
||||
return Response({"detail": message})
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
@detail_route(methods=["post"])
|
||||
def set_projectiondefault(self, request, pk):
|
||||
"""
|
||||
REST API operation to set a projectiondefault to the requested projector. The argument
|
||||
@ -524,21 +588,28 @@ class ProjectorViewSet(ModelViewSet):
|
||||
/rest/core/projector/<pk>/set_projectiondefault/ with the projectiondefault id as the argument
|
||||
"""
|
||||
if not isinstance(request.data, int):
|
||||
raise ValidationError({'detail': 'Data must be an int.'})
|
||||
raise ValidationError({"detail": "Data must be an int."})
|
||||
|
||||
try:
|
||||
projectiondefault = ProjectionDefault.objects.get(pk=request.data)
|
||||
except ProjectionDefault.DoesNotExist:
|
||||
raise ValidationError({'detail': 'The projectiondefault with pk={pk} was not found.'.format(
|
||||
pk=request.data)})
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": "The projectiondefault with pk={pk} was not found.".format(
|
||||
pk=request.data
|
||||
)
|
||||
}
|
||||
)
|
||||
else:
|
||||
projector_instance = self.get_object()
|
||||
projectiondefault.projector = projector_instance
|
||||
projectiondefault.save()
|
||||
|
||||
return Response('Setting projectiondefault "{name}" to projector {projector_id} was successful.'.format(
|
||||
name=projectiondefault.display_name,
|
||||
projector_id=projector_instance.pk))
|
||||
return Response(
|
||||
'Setting projectiondefault "{name}" to projector {projector_id} was successful.'.format(
|
||||
name=projectiondefault.display_name, projector_id=projector_instance.pk
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TagViewSet(ModelViewSet):
|
||||
@ -548,6 +619,7 @@ class TagViewSet(ModelViewSet):
|
||||
There are the following views: metadata, list, retrieve, create,
|
||||
partial_update, update and destroy.
|
||||
"""
|
||||
|
||||
access_permissions = TagAccessPermissions()
|
||||
queryset = Tag.objects.all()
|
||||
|
||||
@ -555,14 +627,14 @@ class TagViewSet(ModelViewSet):
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
if self.action in ('list', 'retrieve'):
|
||||
if self.action in ("list", "retrieve"):
|
||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||
elif self.action == 'metadata':
|
||||
elif self.action == "metadata":
|
||||
# Every authenticated user can see the metadata.
|
||||
# Anonymous users can do so if they are enabled.
|
||||
result = self.request.user.is_authenticated or anonymous_is_enabled()
|
||||
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
||||
result = has_perm(self.request.user, 'core.can_manage_tags')
|
||||
elif self.action in ("create", "partial_update", "update", "destroy"):
|
||||
result = has_perm(self.request.user, "core.can_manage_tags")
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
@ -575,6 +647,7 @@ class ConfigViewSet(ModelViewSet):
|
||||
There are the following views: metadata, list, retrieve, update and
|
||||
partial_update.
|
||||
"""
|
||||
|
||||
access_permissions = ConfigAccessPermissions()
|
||||
queryset = ConfigStore.objects.all()
|
||||
|
||||
@ -582,22 +655,22 @@ class ConfigViewSet(ModelViewSet):
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
if self.action in ('list', 'retrieve'):
|
||||
if self.action in ("list", "retrieve"):
|
||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||
elif self.action == 'metadata':
|
||||
elif self.action == "metadata":
|
||||
# Every authenticated user can see the metadata and list or
|
||||
# retrieve the config. Anonymous users can do so if they are
|
||||
# enabled.
|
||||
result = self.request.user.is_authenticated or anonymous_is_enabled()
|
||||
elif self.action in ('partial_update', 'update'):
|
||||
elif self.action in ("partial_update", "update"):
|
||||
# The user needs 'core.can_manage_logos_and_fonts' for all config values
|
||||
# starting with 'logo' and 'font'. For all other config values th euser needs
|
||||
# the default permissions 'core.can_manage_config'.
|
||||
pk = self.kwargs['pk']
|
||||
if pk.startswith('logo') or pk.startswith('font'):
|
||||
result = has_perm(self.request.user, 'core.can_manage_logos_and_fonts')
|
||||
pk = self.kwargs["pk"]
|
||||
if pk.startswith("logo") or pk.startswith("font"):
|
||||
result = has_perm(self.request.user, "core.can_manage_logos_and_fonts")
|
||||
else:
|
||||
result = has_perm(self.request.user, 'core.can_manage_config')
|
||||
result = has_perm(self.request.user, "core.can_manage_config")
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
@ -608,10 +681,10 @@ class ConfigViewSet(ModelViewSet):
|
||||
|
||||
Example: {"value": 42}
|
||||
"""
|
||||
key = kwargs['pk']
|
||||
value = request.data.get('value')
|
||||
key = kwargs["pk"]
|
||||
value = request.data.get("value")
|
||||
if value is None:
|
||||
raise ValidationError({'detail': 'Invalid input. Config value is missing.'})
|
||||
raise ValidationError({"detail": "Invalid input. Config value is missing."})
|
||||
|
||||
# Validate and change value.
|
||||
try:
|
||||
@ -619,10 +692,10 @@ class ConfigViewSet(ModelViewSet):
|
||||
except ConfigNotFound:
|
||||
raise Http404
|
||||
except ConfigError as e:
|
||||
raise ValidationError({'detail': str(e)})
|
||||
raise ValidationError({"detail": str(e)})
|
||||
|
||||
# Return response.
|
||||
return Response({'key': key, 'value': value})
|
||||
return Response({"key": key, "value": value})
|
||||
|
||||
|
||||
class ChatMessageViewSet(ModelViewSet):
|
||||
@ -632,6 +705,7 @@ class ChatMessageViewSet(ModelViewSet):
|
||||
There are the following views: metadata, list, retrieve and create.
|
||||
The views partial_update, update and destroy are disabled.
|
||||
"""
|
||||
|
||||
access_permissions = ChatMessageAccessPermissions()
|
||||
queryset = ChatMessage.objects.all()
|
||||
|
||||
@ -639,18 +713,18 @@ class ChatMessageViewSet(ModelViewSet):
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
if self.action in ('list', 'retrieve'):
|
||||
if self.action in ("list", "retrieve"):
|
||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||
elif self.action in ('metadata', 'create'):
|
||||
elif self.action in ("metadata", "create"):
|
||||
# We do not want anonymous users to use the chat even the anonymous
|
||||
# group has the permission core.can_use_chat.
|
||||
result = (
|
||||
self.request.user.is_authenticated and
|
||||
has_perm(self.request.user, 'core.can_use_chat'))
|
||||
elif self.action == 'clear':
|
||||
result = (
|
||||
has_perm(self.request.user, 'core.can_use_chat') and
|
||||
has_perm(self.request.user, 'core.can_manage_chat'))
|
||||
result = self.request.user.is_authenticated and has_perm(
|
||||
self.request.user, "core.can_use_chat"
|
||||
)
|
||||
elif self.action == "clear":
|
||||
result = has_perm(self.request.user, "core.can_use_chat") and has_perm(
|
||||
self.request.user, "core.can_manage_chat"
|
||||
)
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
@ -665,7 +739,7 @@ class ChatMessageViewSet(ModelViewSet):
|
||||
# to see users may not have it but can get it now.
|
||||
inform_changed_data([self.request.user])
|
||||
|
||||
@list_route(methods=['post'])
|
||||
@list_route(methods=["post"])
|
||||
def clear(self, request):
|
||||
"""
|
||||
Deletes all chat messages.
|
||||
@ -679,7 +753,7 @@ class ChatMessageViewSet(ModelViewSet):
|
||||
# Trigger autoupdate and setup response.
|
||||
if len(args) > 0:
|
||||
inform_deleted_data(args)
|
||||
return Response({'detail': _('All chat messages deleted successfully.')})
|
||||
return Response({"detail": _("All chat messages deleted successfully.")})
|
||||
|
||||
|
||||
class ProjectorMessageViewSet(ModelViewSet):
|
||||
@ -689,6 +763,7 @@ class ProjectorMessageViewSet(ModelViewSet):
|
||||
There are the following views: list, retrieve, create, update,
|
||||
partial_update and destroy.
|
||||
"""
|
||||
|
||||
access_permissions = ProjectorMessageAccessPermissions()
|
||||
queryset = ProjectorMessage.objects.all()
|
||||
|
||||
@ -696,10 +771,10 @@ class ProjectorMessageViewSet(ModelViewSet):
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
if self.action in ('list', 'retrieve'):
|
||||
if self.action in ("list", "retrieve"):
|
||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
||||
result = has_perm(self.request.user, 'core.can_manage_projector')
|
||||
elif self.action in ("create", "partial_update", "update", "destroy"):
|
||||
result = has_perm(self.request.user, "core.can_manage_projector")
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
@ -712,6 +787,7 @@ class CountdownViewSet(ModelViewSet):
|
||||
There are the following views: list, retrieve, create, update,
|
||||
partial_update and destroy.
|
||||
"""
|
||||
|
||||
access_permissions = CountdownAccessPermissions()
|
||||
queryset = Countdown.objects.all()
|
||||
|
||||
@ -719,10 +795,10 @@ class CountdownViewSet(ModelViewSet):
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
if self.action in ('list', 'retrieve'):
|
||||
if self.action in ("list", "retrieve"):
|
||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
||||
result = has_perm(self.request.user, 'core.can_manage_projector')
|
||||
elif self.action in ("create", "partial_update", "update", "destroy"):
|
||||
result = has_perm(self.request.user, "core.can_manage_projector")
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
@ -734,6 +810,7 @@ class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||
|
||||
There are the following views: list, retrieve, clear_history.
|
||||
"""
|
||||
|
||||
access_permissions = HistoryAccessPermissions()
|
||||
queryset = History.objects.all()
|
||||
|
||||
@ -741,13 +818,13 @@ class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
if self.action in ('list', 'retrieve', 'clear_history'):
|
||||
if self.action in ("list", "retrieve", "clear_history"):
|
||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
|
||||
@list_route(methods=['post'])
|
||||
@list_route(methods=["post"])
|
||||
def clear_history(self, request):
|
||||
"""
|
||||
Deletes and rebuilds the history.
|
||||
@ -769,16 +846,18 @@ class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||
inform_changed_data(history_instances)
|
||||
|
||||
# Setup response.
|
||||
return Response({'detail': _('History was deleted successfully.')})
|
||||
return Response({"detail": _("History was deleted successfully.")})
|
||||
|
||||
|
||||
# Special API views
|
||||
|
||||
|
||||
class ServerTime(utils_views.APIView):
|
||||
"""
|
||||
Returns the server time as UNIX timestamp.
|
||||
"""
|
||||
http_method_names = ['get']
|
||||
|
||||
http_method_names = ["get"]
|
||||
|
||||
def get_context_data(self, **context):
|
||||
return now().timestamp()
|
||||
@ -789,27 +868,36 @@ class VersionView(utils_views.APIView):
|
||||
Returns a dictionary with the OpenSlides version and the version of all
|
||||
plugins.
|
||||
"""
|
||||
http_method_names = ['get']
|
||||
|
||||
http_method_names = ["get"]
|
||||
|
||||
def get_context_data(self, **context):
|
||||
Result = TypedDict('Result', {
|
||||
'openslides_version': str,
|
||||
'openslides_license': str,
|
||||
'openslides_url': str,
|
||||
'plugins': List[Dict[str, str]]})
|
||||
Result = TypedDict(
|
||||
"Result",
|
||||
{
|
||||
"openslides_version": str,
|
||||
"openslides_license": str,
|
||||
"openslides_url": str,
|
||||
"plugins": List[Dict[str, str]],
|
||||
},
|
||||
)
|
||||
result: Result = dict(
|
||||
openslides_version=version,
|
||||
openslides_license=license,
|
||||
openslides_url=url,
|
||||
plugins=[])
|
||||
plugins=[],
|
||||
)
|
||||
# Versions of plugins.
|
||||
for plugin in settings.INSTALLED_PLUGINS:
|
||||
result['plugins'].append({
|
||||
'verbose_name': get_plugin_verbose_name(plugin),
|
||||
'description': get_plugin_description(plugin),
|
||||
'version': get_plugin_version(plugin),
|
||||
'license': get_plugin_license(plugin),
|
||||
'url': get_plugin_url(plugin)})
|
||||
result["plugins"].append(
|
||||
{
|
||||
"verbose_name": get_plugin_verbose_name(plugin),
|
||||
"description": get_plugin_description(plugin),
|
||||
"version": get_plugin_version(plugin),
|
||||
"license": get_plugin_license(plugin),
|
||||
"url": get_plugin_url(plugin),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@ -820,7 +908,8 @@ class HistoryView(utils_views.APIView):
|
||||
Use query paramter timestamp (UNIX timestamp) to get all elements from begin
|
||||
until (including) this timestamp.
|
||||
"""
|
||||
http_method_names = ['get']
|
||||
|
||||
http_method_names = ["get"]
|
||||
|
||||
def get_context_data(self, **context):
|
||||
"""
|
||||
@ -830,19 +919,25 @@ class HistoryView(utils_views.APIView):
|
||||
if not in_some_groups(self.request.user.pk or 0, [GROUP_ADMIN_PK]):
|
||||
self.permission_denied(self.request)
|
||||
try:
|
||||
timestamp = int(self.request.query_params.get('timestamp', 0))
|
||||
timestamp = int(self.request.query_params.get("timestamp", 0))
|
||||
except (ValueError):
|
||||
raise ValidationError({'detail': 'Invalid input. Timestamp should be an integer.'})
|
||||
raise ValidationError(
|
||||
{"detail": "Invalid input. Timestamp should be an integer."}
|
||||
)
|
||||
data = []
|
||||
queryset = History.objects.select_related('full_data')
|
||||
queryset = History.objects.select_related("full_data")
|
||||
if timestamp:
|
||||
queryset = queryset.filter(now__lte=datetime.datetime.fromtimestamp(timestamp))
|
||||
queryset = queryset.filter(
|
||||
now__lte=datetime.datetime.fromtimestamp(timestamp)
|
||||
)
|
||||
for instance in queryset:
|
||||
data.append({
|
||||
'full_data': instance.full_data.full_data,
|
||||
'element_id': instance.element_id,
|
||||
'timestamp': instance.now.timestamp(),
|
||||
'information': instance.information,
|
||||
'user_id': instance.user.pk if instance.user else None,
|
||||
})
|
||||
data.append(
|
||||
{
|
||||
"full_data": instance.full_data.full_data,
|
||||
"element_id": instance.element_id,
|
||||
"timestamp": instance.now.timestamp(),
|
||||
"information": instance.information,
|
||||
"user_id": instance.user.pk if instance.user else None,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
@ -12,7 +12,8 @@ class NotifyWebsocketClientMessage(BaseWebsocketClientMessage):
|
||||
"""
|
||||
Websocket message from a client to send a message to other clients.
|
||||
"""
|
||||
identifier = 'notify'
|
||||
|
||||
identifier = "notify"
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Notify elements.",
|
||||
@ -21,31 +22,24 @@ class NotifyWebsocketClientMessage(BaseWebsocketClientMessage):
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"projectors": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
},
|
||||
"reply_channels": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"users": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
}
|
||||
}
|
||||
"projectors": {"type": "array", "items": {"type": "integer"}},
|
||||
"reply_channels": {"type": "array", "items": {"type": "string"}},
|
||||
"users": {"type": "array", "items": {"type": "integer"}},
|
||||
},
|
||||
},
|
||||
"minItems": 1,
|
||||
}
|
||||
|
||||
async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str) -> None:
|
||||
async def receive_content(
|
||||
self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str
|
||||
) -> None:
|
||||
await consumer.channel_layer.group_send(
|
||||
"site",
|
||||
{
|
||||
"type": "send_notify",
|
||||
"incomming": content,
|
||||
"senderReplyChannelName": consumer.channel_name,
|
||||
"senderUserId": consumer.scope['user']['id'],
|
||||
"senderUserId": consumer.scope["user"]["id"],
|
||||
},
|
||||
)
|
||||
|
||||
@ -54,19 +48,25 @@ class ConstantsWebsocketClientMessage(BaseWebsocketClientMessage):
|
||||
"""
|
||||
The Client requests the constants.
|
||||
"""
|
||||
identifier = 'constants'
|
||||
|
||||
identifier = "constants"
|
||||
content_required = False
|
||||
|
||||
async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str) -> None:
|
||||
async def receive_content(
|
||||
self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str
|
||||
) -> None:
|
||||
# Return all constants to the client.
|
||||
await consumer.send_json(type='constants', content=get_constants(), in_response=id)
|
||||
await consumer.send_json(
|
||||
type="constants", content=get_constants(), in_response=id
|
||||
)
|
||||
|
||||
|
||||
class GetElementsWebsocketClientMessage(BaseWebsocketClientMessage):
|
||||
"""
|
||||
The Client request database elements.
|
||||
"""
|
||||
identifier = 'getElements'
|
||||
|
||||
identifier = "getElements"
|
||||
schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"titel": "getElement request",
|
||||
@ -74,31 +74,40 @@ class GetElementsWebsocketClientMessage(BaseWebsocketClientMessage):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
# change_id is not required
|
||||
"change_id": {
|
||||
"type": "integer",
|
||||
}
|
||||
"change_id": {"type": "integer"}
|
||||
},
|
||||
}
|
||||
|
||||
async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str) -> None:
|
||||
requested_change_id = content.get('change_id', 0)
|
||||
async def receive_content(
|
||||
self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str
|
||||
) -> None:
|
||||
requested_change_id = content.get("change_id", 0)
|
||||
try:
|
||||
element_data = await get_element_data(consumer.scope['user']['id'], requested_change_id)
|
||||
element_data = await get_element_data(
|
||||
consumer.scope["user"]["id"], requested_change_id
|
||||
)
|
||||
except ValueError as error:
|
||||
await consumer.send_json(type='error', content=str(error), in_response=id)
|
||||
await consumer.send_json(type="error", content=str(error), in_response=id)
|
||||
else:
|
||||
await consumer.send_json(type='autoupdate', content=element_data, in_response=id)
|
||||
await consumer.send_json(
|
||||
type="autoupdate", content=element_data, in_response=id
|
||||
)
|
||||
|
||||
|
||||
class AutoupdateWebsocketClientMessage(BaseWebsocketClientMessage):
|
||||
"""
|
||||
The Client turns autoupdate on or off.
|
||||
"""
|
||||
identifier = 'autoupdate'
|
||||
|
||||
async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str) -> None:
|
||||
identifier = "autoupdate"
|
||||
|
||||
async def receive_content(
|
||||
self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str
|
||||
) -> None:
|
||||
# Turn on or off the autoupdate for the client
|
||||
if content: # accept any value, that can be interpreted as bool
|
||||
await consumer.channel_layer.group_add('autoupdate', consumer.channel_name)
|
||||
await consumer.channel_layer.group_add("autoupdate", consumer.channel_name)
|
||||
else:
|
||||
await consumer.channel_layer.group_discard('autoupdate', consumer.channel_name)
|
||||
await consumer.channel_layer.group_discard(
|
||||
"autoupdate", consumer.channel_name
|
||||
)
|
||||
|
@ -9,68 +9,70 @@ MODULE_DIR = os.path.realpath(os.path.dirname(os.path.abspath(__file__)))
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'openslides.core',
|
||||
'openslides.users',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'channels',
|
||||
'openslides.agenda',
|
||||
'openslides.topics',
|
||||
'openslides.motions',
|
||||
'openslides.assignments',
|
||||
'openslides.mediafiles',
|
||||
"openslides.core",
|
||||
"openslides.users",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.staticfiles",
|
||||
"rest_framework",
|
||||
"channels",
|
||||
"openslides.agenda",
|
||||
"openslides.topics",
|
||||
"openslides.motions",
|
||||
"openslides.assignments",
|
||||
"openslides.mediafiles",
|
||||
]
|
||||
|
||||
INSTALLED_PLUGINS = collect_plugins() # Adds all automaticly collected plugins
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'openslides.utils.autoupdate.AutoupdateBundleMiddleware',
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.locale.LocaleMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"openslides.utils.autoupdate.AutoupdateBundleMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'openslides.urls'
|
||||
ROOT_URLCONF = "openslides.urls"
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
},
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
}
|
||||
]
|
||||
|
||||
# Email
|
||||
# https://docs.djangoproject.com/en/1.10/topics/email/
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_TIMEOUT = 5 # Timeout in seconds for blocking operations like the connection attempt
|
||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||
EMAIL_TIMEOUT = (
|
||||
5
|
||||
) # Timeout in seconds for blocking operations like the connection attempt
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.10/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en'
|
||||
LANGUAGE_CODE = "en"
|
||||
|
||||
LANGUAGES = (
|
||||
('en', 'English'),
|
||||
('de', 'Deutsch'),
|
||||
('fr', 'Français'),
|
||||
('es', 'Español'),
|
||||
('pt', 'Português'),
|
||||
('cs', 'Český'),
|
||||
('ru', 'русский'),
|
||||
("en", "English"),
|
||||
("de", "Deutsch"),
|
||||
("fr", "Français"),
|
||||
("es", "Español"),
|
||||
("pt", "Português"),
|
||||
("cs", "Český"),
|
||||
("ru", "русский"),
|
||||
)
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@ -78,62 +80,54 @@ USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
LOCALE_PATHS = [
|
||||
os.path.join(MODULE_DIR, 'locale'),
|
||||
]
|
||||
LOCALE_PATHS = [os.path.join(MODULE_DIR, "locale")]
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(MODULE_DIR, 'static'),
|
||||
]
|
||||
STATICFILES_DIRS = [os.path.join(MODULE_DIR, "static")]
|
||||
|
||||
|
||||
# Sessions and user authentication
|
||||
# https://docs.djangoproject.com/en/1.10/topics/http/sessions/
|
||||
# https://docs.djangoproject.com/en/1.10/topics/auth/
|
||||
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
AUTH_USER_MODEL = "users.User"
|
||||
|
||||
AUTH_GROUP_MODEL = 'users.Group'
|
||||
AUTH_GROUP_MODEL = "users.Group"
|
||||
|
||||
SESSION_COOKIE_NAME = 'OpenSlidesSessionID'
|
||||
SESSION_COOKIE_NAME = "OpenSlidesSessionID"
|
||||
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||
|
||||
CSRF_COOKIE_NAME = 'OpenSlidesCsrfToken'
|
||||
CSRF_COOKIE_NAME = "OpenSlidesCsrfToken"
|
||||
|
||||
CSRF_COOKIE_AGE = None
|
||||
|
||||
PASSWORD_HASHERS = [
|
||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
||||
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
|
||||
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
||||
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
|
||||
"django.contrib.auth.hashers.BCryptPasswordHasher",
|
||||
]
|
||||
|
||||
|
||||
# Files
|
||||
# https://docs.djangoproject.com/en/1.10/topics/files/
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_URL = "/media/"
|
||||
|
||||
|
||||
# Django Channels
|
||||
# http://channels.readthedocs.io/en/latest/
|
||||
|
||||
ASGI_APPLICATION = 'openslides.routing.application'
|
||||
ASGI_APPLICATION = "openslides.routing.application"
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels.layers.InMemoryChannelLayer',
|
||||
},
|
||||
}
|
||||
CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
|
||||
|
||||
|
||||
# Enable updating the last_login field for users on every login.
|
||||
|
@ -1 +1 @@
|
||||
default_app_config = 'openslides.mediafiles.apps.MediafilesAppConfig'
|
||||
default_app_config = "openslides.mediafiles.apps.MediafilesAppConfig"
|
||||
|
@ -8,22 +8,24 @@ class MediafileAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for Mediafile and MediafileViewSet.
|
||||
"""
|
||||
base_permission = 'mediafiles.can_see'
|
||||
|
||||
base_permission = "mediafiles.can_see"
|
||||
|
||||
async def get_restricted_data(
|
||||
self,
|
||||
full_data: List[Dict[str, Any]],
|
||||
user_id: int) -> List[Dict[str, Any]]:
|
||||
self, full_data: List[Dict[str, Any]], user_id: int
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns the restricted serialized data for the instance prepared
|
||||
for the user. Removes hidden mediafiles for some users.
|
||||
"""
|
||||
# Parse data.
|
||||
if await async_has_perm(user_id, 'mediafiles.can_see') and await async_has_perm(user_id, 'mediafiles.can_see_hidden'):
|
||||
if await async_has_perm(user_id, "mediafiles.can_see") and await async_has_perm(
|
||||
user_id, "mediafiles.can_see_hidden"
|
||||
):
|
||||
data = full_data
|
||||
elif await async_has_perm(user_id, 'mediafiles.can_see'):
|
||||
elif await async_has_perm(user_id, "mediafiles.can_see"):
|
||||
# Exclude hidden mediafiles.
|
||||
data = [full for full in full_data if not full['hidden']]
|
||||
data = [full for full in full_data if not full["hidden"]]
|
||||
else:
|
||||
data = []
|
||||
|
||||
|
@ -6,8 +6,8 @@ from ..utils.projector import register_projector_elements
|
||||
|
||||
|
||||
class MediafilesAppConfig(AppConfig):
|
||||
name = 'openslides.mediafiles'
|
||||
verbose_name = 'OpenSlides Mediafiles'
|
||||
name = "openslides.mediafiles"
|
||||
verbose_name = "OpenSlides Mediafiles"
|
||||
angular_site_module = True
|
||||
angular_projector_module = True
|
||||
|
||||
@ -27,20 +27,25 @@ class MediafilesAppConfig(AppConfig):
|
||||
# Connect signals.
|
||||
permission_change.connect(
|
||||
get_permission_change_data,
|
||||
dispatch_uid='mediafiles_get_permission_change_data')
|
||||
dispatch_uid="mediafiles_get_permission_change_data",
|
||||
)
|
||||
|
||||
# Register viewsets.
|
||||
router.register(self.get_model('Mediafile').get_collection_string(), MediafileViewSet)
|
||||
router.register(
|
||||
self.get_model("Mediafile").get_collection_string(), MediafileViewSet
|
||||
)
|
||||
|
||||
# register required_users
|
||||
required_user.add_collection_string(self.get_model('Mediafile').get_collection_string(), required_users)
|
||||
required_user.add_collection_string(
|
||||
self.get_model("Mediafile").get_collection_string(), required_users
|
||||
)
|
||||
|
||||
def get_startup_elements(self):
|
||||
"""
|
||||
Yields all Cachables required on startup i. e. opening the websocket
|
||||
connection.
|
||||
"""
|
||||
yield self.get_model('Mediafile')
|
||||
yield self.get_model("Mediafile")
|
||||
|
||||
|
||||
def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||
@ -49,4 +54,4 @@ def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||
if request_user can see mediafiles. This function may return an empty
|
||||
set.
|
||||
"""
|
||||
return set(element['uploader_id'])
|
||||
return set(element["uploader_id"])
|
||||
|
@ -13,25 +13,43 @@ class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Mediafile',
|
||||
name="Mediafile",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mediafile', models.FileField(upload_to='file')),
|
||||
('title', models.CharField(max_length=255, unique=True)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('uploader', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("mediafile", models.FileField(upload_to="file")),
|
||||
("title", models.CharField(max_length=255, unique=True)),
|
||||
("timestamp", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"uploader",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'permissions': (('can_see', 'Can see the list of files'), ('can_upload', 'Can upload files'), ('can_manage', 'Can manage files')),
|
||||
'default_permissions': (),
|
||||
'ordering': ['title'],
|
||||
"permissions": (
|
||||
("can_see", "Can see the list of files"),
|
||||
("can_upload", "Can upload files"),
|
||||
("can_manage", "Can manage files"),
|
||||
),
|
||||
"default_permissions": (),
|
||||
"ordering": ["title"],
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
)
|
||||
]
|
||||
|
@ -7,22 +7,25 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mediafiles', '0001_initial'),
|
||||
]
|
||||
dependencies = [("mediafiles", "0001_initial")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='mediafile',
|
||||
options={'default_permissions': (), 'ordering': ['title'], 'permissions': (
|
||||
('can_see', 'Can see the list of files'),
|
||||
('can_see_hidden', 'Can see hidden files'),
|
||||
('can_upload', 'Can upload files'),
|
||||
('can_manage', 'Can manage files'))},
|
||||
name="mediafile",
|
||||
options={
|
||||
"default_permissions": (),
|
||||
"ordering": ["title"],
|
||||
"permissions": (
|
||||
("can_see", "Can see the list of files"),
|
||||
("can_see_hidden", "Can see hidden files"),
|
||||
("can_upload", "Can upload files"),
|
||||
("can_manage", "Can manage files"),
|
||||
),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mediafile',
|
||||
name='hidden',
|
||||
model_name="mediafile",
|
||||
name="hidden",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
|
@ -13,10 +13,11 @@ class Mediafile(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Class for uploaded files which can be delivered under a certain url.
|
||||
"""
|
||||
access_permissions = MediafileAccessPermissions()
|
||||
can_see_permission = 'mediafiles.can_see'
|
||||
|
||||
mediafile = models.FileField(upload_to='file')
|
||||
access_permissions = MediafileAccessPermissions()
|
||||
can_see_permission = "mediafiles.can_see"
|
||||
|
||||
mediafile = models.FileField(upload_to="file")
|
||||
"""
|
||||
See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield
|
||||
for more information.
|
||||
@ -26,10 +27,8 @@ class Mediafile(RESTModelMixin, models.Model):
|
||||
"""A string representing the title of the file."""
|
||||
|
||||
uploader = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True)
|
||||
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
"""A user – the uploader of a file."""
|
||||
|
||||
hidden = models.BooleanField(default=False)
|
||||
@ -42,13 +41,15 @@ class Mediafile(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Meta class for the mediafile model.
|
||||
"""
|
||||
ordering = ['title']
|
||||
|
||||
ordering = ["title"]
|
||||
default_permissions = ()
|
||||
permissions = (
|
||||
('can_see', 'Can see the list of files'),
|
||||
('can_see_hidden', 'Can see hidden files'),
|
||||
('can_upload', 'Can upload files'),
|
||||
('can_manage', 'Can manage files'))
|
||||
("can_see", "Can see the list of files"),
|
||||
("can_see_hidden", "Can see hidden files"),
|
||||
("can_upload", "Can upload files"),
|
||||
("can_manage", "Can manage files"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
@ -72,10 +73,11 @@ class Mediafile(RESTModelMixin, models.Model):
|
||||
mediafile projector element is disabled.
|
||||
"""
|
||||
Projector.remove_any(
|
||||
skip_autoupdate=skip_autoupdate,
|
||||
name='mediafiles/mediafile',
|
||||
id=self.pk)
|
||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, name="mediafiles/mediafile", id=self.pk
|
||||
)
|
||||
return super().delete( # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||
)
|
||||
|
||||
def get_filesize(self):
|
||||
"""
|
||||
@ -85,26 +87,26 @@ class Mediafile(RESTModelMixin, models.Model):
|
||||
try:
|
||||
size = self.mediafile.size
|
||||
except OSError:
|
||||
size_string = _('unknown')
|
||||
size_string = _("unknown")
|
||||
else:
|
||||
if size < 1024:
|
||||
size_string = '< 1 kB'
|
||||
size_string = "< 1 kB"
|
||||
elif size >= 1024 * 1024:
|
||||
mB = size / 1024 / 1024
|
||||
size_string = '%d MB' % mB
|
||||
size_string = "%d MB" % mB
|
||||
else:
|
||||
kB = size / 1024
|
||||
size_string = '%d kB' % kB
|
||||
size_string = "%d kB" % kB
|
||||
return size_string
|
||||
|
||||
def is_logo(self):
|
||||
for key in config['logos_available']:
|
||||
if config[key]['path'] == self.mediafile.url:
|
||||
for key in config["logos_available"]:
|
||||
if config[key]["path"] == self.mediafile.url:
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_font(self):
|
||||
for key in config['fonts_available']:
|
||||
if config[key]['path'] == self.mediafile.url:
|
||||
for key in config["fonts_available"]:
|
||||
if config[key]["path"] == self.mediafile.url:
|
||||
return True
|
||||
return False
|
||||
|
@ -9,11 +9,12 @@ class MediafileSlide(ProjectorElement):
|
||||
"""
|
||||
Slide definitions for Mediafile model.
|
||||
"""
|
||||
name = 'mediafiles/mediafile'
|
||||
|
||||
name = "mediafiles/mediafile"
|
||||
|
||||
def check_data(self):
|
||||
if not Mediafile.objects.filter(pk=self.config_entry.get('id')).exists():
|
||||
raise ProjectorException('File does not exist.')
|
||||
if not Mediafile.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||
raise ProjectorException("File does not exist.")
|
||||
|
||||
|
||||
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
||||
|
@ -10,9 +10,8 @@ from .models import Mediafile
|
||||
|
||||
|
||||
class AngularCompatibleFileField(FileField):
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if data == '':
|
||||
if data == "":
|
||||
return None
|
||||
return super(AngularCompatibleFileField, self).to_internal_value(data)
|
||||
|
||||
@ -20,20 +19,17 @@ class AngularCompatibleFileField(FileField):
|
||||
if value is None:
|
||||
return None
|
||||
filetype = mimetypes.guess_type(value.path)[0]
|
||||
result = {
|
||||
'name': value.name,
|
||||
'type': filetype
|
||||
}
|
||||
if filetype == 'application/pdf':
|
||||
result = {"name": value.name, "type": filetype}
|
||||
if filetype == "application/pdf":
|
||||
try:
|
||||
result['pages'] = PdfFileReader(open(value.path, 'rb')).getNumPages()
|
||||
result["pages"] = PdfFileReader(open(value.path, "rb")).getNumPages()
|
||||
except FileNotFoundError:
|
||||
# File was deleted from server. Set 'pages' to 0.
|
||||
result['pages'] = 0
|
||||
result["pages"] = 0
|
||||
except PdfReadError:
|
||||
# File could be encrypted but not be detected by PyPDF.
|
||||
result['pages'] = 0
|
||||
result['encrypted'] = True
|
||||
result["pages"] = 0
|
||||
result["encrypted"] = True
|
||||
return result
|
||||
|
||||
|
||||
@ -41,6 +37,7 @@ class MediafileSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for mediafile.models.Mediafile objects.
|
||||
"""
|
||||
|
||||
media_url_prefix = SerializerMethodField()
|
||||
filesize = SerializerMethodField()
|
||||
|
||||
@ -52,19 +49,20 @@ class MediafileSerializer(ModelSerializer):
|
||||
super(MediafileSerializer, self).__init__(*args, **kwargs)
|
||||
self.serializer_field_mapping[dbmodels.FileField] = AngularCompatibleFileField
|
||||
if self.instance is not None:
|
||||
self.fields['mediafile'].read_only = True
|
||||
self.fields["mediafile"].read_only = True
|
||||
|
||||
class Meta:
|
||||
model = Mediafile
|
||||
fields = (
|
||||
'id',
|
||||
'title',
|
||||
'mediafile',
|
||||
'media_url_prefix',
|
||||
'uploader',
|
||||
'filesize',
|
||||
'hidden',
|
||||
'timestamp',)
|
||||
"id",
|
||||
"title",
|
||||
"mediafile",
|
||||
"media_url_prefix",
|
||||
"uploader",
|
||||
"filesize",
|
||||
"hidden",
|
||||
"timestamp",
|
||||
)
|
||||
|
||||
def get_filesize(self, mediafile):
|
||||
return mediafile.get_filesize()
|
||||
|
@ -5,8 +5,11 @@ def get_permission_change_data(sender, permissions=None, **kwargs):
|
||||
"""
|
||||
Yields all necessary collections if 'mediafiles.can_see' permission changes.
|
||||
"""
|
||||
mediafiles_app = apps.get_app_config(app_label='mediafiles')
|
||||
mediafiles_app = apps.get_app_config(app_label="mediafiles")
|
||||
for permission in permissions:
|
||||
# There could be only one 'mediafiles.can_see' and then we want to return data.
|
||||
if permission.content_type.app_label == mediafiles_app.label and permission.codename == 'can_see':
|
||||
if (
|
||||
permission.content_type.app_label == mediafiles_app.label
|
||||
and permission.codename == "can_see"
|
||||
):
|
||||
yield from mediafiles_app.get_startup_elements()
|
||||
|
@ -9,6 +9,7 @@ from .models import Mediafile
|
||||
|
||||
# Viewsets for the REST API
|
||||
|
||||
|
||||
class MediafileViewSet(ModelViewSet):
|
||||
"""
|
||||
API endpoint for mediafile objects.
|
||||
@ -16,6 +17,7 @@ class MediafileViewSet(ModelViewSet):
|
||||
There are the following views: metadata, list, retrieve, create,
|
||||
partial_update, update and destroy.
|
||||
"""
|
||||
|
||||
access_permissions = MediafileAccessPermissions()
|
||||
queryset = Mediafile.objects.all()
|
||||
|
||||
@ -23,20 +25,24 @@ class MediafileViewSet(ModelViewSet):
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
if self.action in ('list', 'retrieve'):
|
||||
if self.action in ("list", "retrieve"):
|
||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||
elif self.action == 'metadata':
|
||||
result = has_perm(self.request.user, 'mediafiles.can_see')
|
||||
elif self.action == 'create':
|
||||
result = (has_perm(self.request.user, 'mediafiles.can_see') and
|
||||
has_perm(self.request.user, 'mediafiles.can_upload'))
|
||||
elif self.action in ('partial_update', 'update'):
|
||||
result = (has_perm(self.request.user, 'mediafiles.can_see') and
|
||||
has_perm(self.request.user, 'mediafiles.can_upload') and
|
||||
has_perm(self.request.user, 'mediafiles.can_manage'))
|
||||
elif self.action == 'destroy':
|
||||
result = (has_perm(self.request.user, 'mediafiles.can_see') and
|
||||
has_perm(self.request.user, 'mediafiles.can_manage'))
|
||||
elif self.action == "metadata":
|
||||
result = has_perm(self.request.user, "mediafiles.can_see")
|
||||
elif self.action == "create":
|
||||
result = has_perm(self.request.user, "mediafiles.can_see") and has_perm(
|
||||
self.request.user, "mediafiles.can_upload"
|
||||
)
|
||||
elif self.action in ("partial_update", "update"):
|
||||
result = (
|
||||
has_perm(self.request.user, "mediafiles.can_see")
|
||||
and has_perm(self.request.user, "mediafiles.can_upload")
|
||||
and has_perm(self.request.user, "mediafiles.can_manage")
|
||||
)
|
||||
elif self.action == "destroy":
|
||||
result = has_perm(self.request.user, "mediafiles.can_see") and has_perm(
|
||||
self.request.user, "mediafiles.can_manage"
|
||||
)
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
@ -46,13 +52,15 @@ class MediafileViewSet(ModelViewSet):
|
||||
Customized view endpoint to upload a new file.
|
||||
"""
|
||||
# Check permission to check if the uploader has to be changed.
|
||||
uploader_id = self.request.data.get('uploader_id')
|
||||
if (uploader_id and
|
||||
not has_perm(request.user, 'mediafiles.can_manage') and
|
||||
str(self.request.user.pk) != str(uploader_id)):
|
||||
uploader_id = self.request.data.get("uploader_id")
|
||||
if (
|
||||
uploader_id
|
||||
and not has_perm(request.user, "mediafiles.can_manage")
|
||||
and str(self.request.user.pk) != str(uploader_id)
|
||||
):
|
||||
self.permission_denied(request)
|
||||
if not self.request.data.get('mediafile'):
|
||||
raise ValidationError({'detail': 'You forgot to provide a file.'})
|
||||
if not self.request.data.get("mediafile"):
|
||||
raise ValidationError({"detail": "You forgot to provide a file."})
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
@ -77,9 +85,11 @@ def protected_serve(request, path, document_root=None, show_indexes=False):
|
||||
except Mediafile.DoesNotExist:
|
||||
return HttpResponseNotFound(content="Not found.")
|
||||
|
||||
can_see = has_perm(request.user, 'mediafiles.can_see')
|
||||
can_see = has_perm(request.user, "mediafiles.can_see")
|
||||
is_special_file = mediafile.is_logo() or mediafile.is_font()
|
||||
is_hidden_but_no_perms = mediafile.hidden and not has_perm(request.user, 'mediafiles.can_see_hidden')
|
||||
is_hidden_but_no_perms = mediafile.hidden and not has_perm(
|
||||
request.user, "mediafiles.can_see_hidden"
|
||||
)
|
||||
|
||||
if not is_special_file and (not can_see or is_hidden_but_no_perms):
|
||||
return HttpResponseForbidden(content="Forbidden.")
|
||||
|
@ -1 +1 @@
|
||||
default_app_config = 'openslides.motions.apps.MotionsAppConfig'
|
||||
default_app_config = "openslides.motions.apps.MotionsAppConfig"
|
||||
|
@ -9,12 +9,12 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for Motion and MotionViewSet.
|
||||
"""
|
||||
base_permission = 'motions.can_see'
|
||||
|
||||
base_permission = "motions.can_see"
|
||||
|
||||
async def get_restricted_data(
|
||||
self,
|
||||
full_data: List[Dict[str, Any]],
|
||||
user_id: int) -> List[Dict[str, Any]]:
|
||||
self, full_data: List[Dict[str, Any]], user_id: int
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns the restricted serialized data for the instance prepared for
|
||||
the user. Removes motion if the user has not the permission to see
|
||||
@ -23,33 +23,37 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
||||
personal notes.
|
||||
"""
|
||||
# Parse data.
|
||||
if await async_has_perm(user_id, 'motions.can_see'):
|
||||
if await async_has_perm(user_id, "motions.can_see"):
|
||||
# TODO: Refactor this after personal_notes system is refactored.
|
||||
data = []
|
||||
for full in full_data:
|
||||
# Check if user is submitter of this motion.
|
||||
if user_id:
|
||||
is_submitter = user_id in [
|
||||
submitter['user_id'] for submitter in full.get('submitters', [])]
|
||||
submitter["user_id"] for submitter in full.get("submitters", [])
|
||||
]
|
||||
else:
|
||||
# Anonymous users can not be submitters.
|
||||
is_submitter = False
|
||||
|
||||
# Check see permission for this motion.
|
||||
required_permission_to_see = full['state_required_permission_to_see']
|
||||
required_permission_to_see = full["state_required_permission_to_see"]
|
||||
permission = (
|
||||
not required_permission_to_see or
|
||||
await async_has_perm(user_id, required_permission_to_see) or
|
||||
await async_has_perm(user_id, 'motions.can_manage') or
|
||||
is_submitter)
|
||||
not required_permission_to_see
|
||||
or await async_has_perm(user_id, required_permission_to_see)
|
||||
or await async_has_perm(user_id, "motions.can_manage")
|
||||
or is_submitter
|
||||
)
|
||||
|
||||
# Parse single motion.
|
||||
if permission:
|
||||
full_copy = deepcopy(full)
|
||||
full_copy['comments'] = []
|
||||
for comment in full['comments']:
|
||||
if await async_in_some_groups(user_id, comment['read_groups_id']):
|
||||
full_copy['comments'].append(comment)
|
||||
full_copy["comments"] = []
|
||||
for comment in full["comments"]:
|
||||
if await async_in_some_groups(
|
||||
user_id, comment["read_groups_id"]
|
||||
):
|
||||
full_copy["comments"].append(comment)
|
||||
data.append(full_copy)
|
||||
else:
|
||||
data = []
|
||||
@ -61,23 +65,23 @@ class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for MotionChangeRecommendation and MotionChangeRecommendationViewSet.
|
||||
"""
|
||||
base_permission = 'motions.can_see'
|
||||
|
||||
base_permission = "motions.can_see"
|
||||
|
||||
async def get_restricted_data(
|
||||
self,
|
||||
full_data: List[Dict[str, Any]],
|
||||
user_id: int) -> List[Dict[str, Any]]:
|
||||
self, full_data: List[Dict[str, Any]], user_id: int
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Removes change recommendations if they are internal and the user has
|
||||
not the can_manage permission. To see change recommendation the user needs
|
||||
the can_see permission.
|
||||
"""
|
||||
# Parse data.
|
||||
if await async_has_perm(user_id, 'motions.can_see'):
|
||||
has_manage_perms = await async_has_perm(user_id, 'motion.can_manage')
|
||||
if await async_has_perm(user_id, "motions.can_see"):
|
||||
has_manage_perms = await async_has_perm(user_id, "motion.can_manage")
|
||||
data = []
|
||||
for full in full_data:
|
||||
if not full['internal'] or has_manage_perms:
|
||||
if not full["internal"] or has_manage_perms:
|
||||
data.append(full)
|
||||
else:
|
||||
data = []
|
||||
@ -89,22 +93,22 @@ class MotionCommentSectionAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for MotionCommentSection and MotionCommentSectionViewSet.
|
||||
"""
|
||||
base_permission = 'motions.can_see'
|
||||
|
||||
base_permission = "motions.can_see"
|
||||
|
||||
async def get_restricted_data(
|
||||
self,
|
||||
full_data: List[Dict[str, Any]],
|
||||
user_id: int) -> List[Dict[str, Any]]:
|
||||
self, full_data: List[Dict[str, Any]], user_id: int
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
If the user has manage rights, he can see all sections. If not all sections
|
||||
will be removed, when the user is not in at least one of the read_groups.
|
||||
"""
|
||||
data: List[Dict[str, Any]] = []
|
||||
if await async_has_perm(user_id, 'motions.can_manage'):
|
||||
if await async_has_perm(user_id, "motions.can_manage"):
|
||||
data = full_data
|
||||
else:
|
||||
for full in full_data:
|
||||
read_groups = full.get('read_groups_id', [])
|
||||
read_groups = full.get("read_groups_id", [])
|
||||
if await async_in_some_groups(user_id, read_groups):
|
||||
data.append(full)
|
||||
return data
|
||||
@ -114,25 +118,29 @@ class StatuteParagraphAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for StatuteParagraph and StatuteParagraphViewSet.
|
||||
"""
|
||||
base_permission = 'motions.can_see'
|
||||
|
||||
base_permission = "motions.can_see"
|
||||
|
||||
|
||||
class CategoryAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for Category and CategoryViewSet.
|
||||
"""
|
||||
base_permission = 'motions.can_see'
|
||||
|
||||
base_permission = "motions.can_see"
|
||||
|
||||
|
||||
class MotionBlockAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for Category and CategoryViewSet.
|
||||
"""
|
||||
base_permission = 'motions.can_see'
|
||||
|
||||
base_permission = "motions.can_see"
|
||||
|
||||
|
||||
class WorkflowAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for Workflow and WorkflowViewSet.
|
||||
"""
|
||||
base_permission = 'motions.can_see'
|
||||
|
||||
base_permission = "motions.can_see"
|
||||
|
@ -7,8 +7,8 @@ from ..utils.projector import register_projector_elements
|
||||
|
||||
|
||||
class MotionsAppConfig(AppConfig):
|
||||
name = 'openslides.motions'
|
||||
verbose_name = 'OpenSlides Motion'
|
||||
name = "openslides.motions"
|
||||
verbose_name = "OpenSlides Motion"
|
||||
angular_site_module = True
|
||||
angular_projector_module = True
|
||||
|
||||
@ -17,10 +17,7 @@ class MotionsAppConfig(AppConfig):
|
||||
from openslides.core.signals import permission_change
|
||||
from openslides.utils.rest_api import router
|
||||
from .projector import get_projector_elements
|
||||
from .signals import (
|
||||
create_builtin_workflows,
|
||||
get_permission_change_data,
|
||||
)
|
||||
from .signals import create_builtin_workflows, get_permission_change_data
|
||||
from . import serializers # noqa
|
||||
from .views import (
|
||||
CategoryViewSet,
|
||||
@ -40,29 +37,49 @@ class MotionsAppConfig(AppConfig):
|
||||
|
||||
# Connect signals.
|
||||
post_migrate.connect(
|
||||
create_builtin_workflows,
|
||||
dispatch_uid='motion_create_builtin_workflows')
|
||||
create_builtin_workflows, dispatch_uid="motion_create_builtin_workflows"
|
||||
)
|
||||
permission_change.connect(
|
||||
get_permission_change_data,
|
||||
dispatch_uid='motions_get_permission_change_data')
|
||||
dispatch_uid="motions_get_permission_change_data",
|
||||
)
|
||||
|
||||
# Register viewsets.
|
||||
router.register(self.get_model('Category').get_collection_string(), CategoryViewSet)
|
||||
router.register(self.get_model('StatuteParagraph').get_collection_string(), StatuteParagraphViewSet)
|
||||
router.register(self.get_model('Motion').get_collection_string(), MotionViewSet)
|
||||
router.register(self.get_model('MotionBlock').get_collection_string(), MotionBlockViewSet)
|
||||
router.register(self.get_model('MotionCommentSection').get_collection_string(), MotionCommentSectionViewSet)
|
||||
router.register(self.get_model('Workflow').get_collection_string(), WorkflowViewSet)
|
||||
router.register(self.get_model('MotionChangeRecommendation').get_collection_string(),
|
||||
MotionChangeRecommendationViewSet)
|
||||
router.register(self.get_model('MotionPoll').get_collection_string(), MotionPollViewSet)
|
||||
router.register(self.get_model('State').get_collection_string(), StateViewSet)
|
||||
router.register(
|
||||
self.get_model("Category").get_collection_string(), CategoryViewSet
|
||||
)
|
||||
router.register(
|
||||
self.get_model("StatuteParagraph").get_collection_string(),
|
||||
StatuteParagraphViewSet,
|
||||
)
|
||||
router.register(self.get_model("Motion").get_collection_string(), MotionViewSet)
|
||||
router.register(
|
||||
self.get_model("MotionBlock").get_collection_string(), MotionBlockViewSet
|
||||
)
|
||||
router.register(
|
||||
self.get_model("MotionCommentSection").get_collection_string(),
|
||||
MotionCommentSectionViewSet,
|
||||
)
|
||||
router.register(
|
||||
self.get_model("Workflow").get_collection_string(), WorkflowViewSet
|
||||
)
|
||||
router.register(
|
||||
self.get_model("MotionChangeRecommendation").get_collection_string(),
|
||||
MotionChangeRecommendationViewSet,
|
||||
)
|
||||
router.register(
|
||||
self.get_model("MotionPoll").get_collection_string(), MotionPollViewSet
|
||||
)
|
||||
router.register(self.get_model("State").get_collection_string(), StateViewSet)
|
||||
|
||||
# Register required_users
|
||||
required_user.add_collection_string(self.get_model('Motion').get_collection_string(), required_users)
|
||||
required_user.add_collection_string(
|
||||
self.get_model("Motion").get_collection_string(), required_users
|
||||
)
|
||||
|
||||
def get_config_variables(self):
|
||||
from .config_variables import get_config_variables
|
||||
|
||||
return get_config_variables()
|
||||
|
||||
def get_startup_elements(self):
|
||||
@ -70,8 +87,15 @@ class MotionsAppConfig(AppConfig):
|
||||
Yields all Cachables required on startup i. e. opening the websocket
|
||||
connection.
|
||||
"""
|
||||
for model_name in ('Category', 'StatuteParagraph', 'Motion', 'MotionBlock',
|
||||
'Workflow', 'MotionChangeRecommendation', 'MotionCommentSection'):
|
||||
for model_name in (
|
||||
"Category",
|
||||
"StatuteParagraph",
|
||||
"Motion",
|
||||
"MotionBlock",
|
||||
"Workflow",
|
||||
"MotionChangeRecommendation",
|
||||
"MotionCommentSection",
|
||||
):
|
||||
yield self.get_model(model_name)
|
||||
|
||||
|
||||
@ -81,6 +105,8 @@ def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||
any motion if request_user can see motions. This function may return an
|
||||
empty set.
|
||||
"""
|
||||
submitters_supporters = set([submitter['user_id'] for submitter in element['submitters']])
|
||||
submitters_supporters.update(element['supporters_id'])
|
||||
submitters_supporters = set(
|
||||
[submitter["user_id"] for submitter in element["submitters"]]
|
||||
)
|
||||
submitters_supporters.update(element["supporters_id"])
|
||||
return submitters_supporters
|
||||
|
@ -11,8 +11,10 @@ def get_workflow_choices():
|
||||
Returns a list of all workflows to be used as choices for the config variable
|
||||
'motions_workflow'. Each list item contains the pk and the display name.
|
||||
"""
|
||||
return [{'value': str(workflow.pk), 'display_name': workflow.name}
|
||||
for workflow in Workflow.objects.all()]
|
||||
return [
|
||||
{"value": str(workflow.pk), "display_name": workflow.name}
|
||||
for workflow in Workflow.objects.all()
|
||||
]
|
||||
|
||||
|
||||
def get_config_variables():
|
||||
@ -26,300 +28,339 @@ def get_config_variables():
|
||||
|
||||
# General
|
||||
yield ConfigVariable(
|
||||
name='motions_workflow',
|
||||
default_value='1',
|
||||
input_type='choice',
|
||||
label='Workflow of new motions',
|
||||
name="motions_workflow",
|
||||
default_value="1",
|
||||
input_type="choice",
|
||||
label="Workflow of new motions",
|
||||
choices=get_workflow_choices,
|
||||
weight=310,
|
||||
group='Motions',
|
||||
subgroup='General')
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_statute_amendments_workflow',
|
||||
default_value='1',
|
||||
input_type='choice',
|
||||
label='Workflow of new statute amendments',
|
||||
name="motions_statute_amendments_workflow",
|
||||
default_value="1",
|
||||
input_type="choice",
|
||||
label="Workflow of new statute amendments",
|
||||
choices=get_workflow_choices,
|
||||
weight=312,
|
||||
group='Motions',
|
||||
subgroup='General')
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_identifier',
|
||||
default_value='per_category',
|
||||
input_type='choice',
|
||||
label='Identifier',
|
||||
name="motions_identifier",
|
||||
default_value="per_category",
|
||||
input_type="choice",
|
||||
label="Identifier",
|
||||
choices=(
|
||||
{'value': 'per_category', 'display_name': 'Numbered per category'},
|
||||
{'value': 'serially_numbered', 'display_name': 'Serially numbered'},
|
||||
{'value': 'manually', 'display_name': 'Set it manually'}),
|
||||
{"value": "per_category", "display_name": "Numbered per category"},
|
||||
{"value": "serially_numbered", "display_name": "Serially numbered"},
|
||||
{"value": "manually", "display_name": "Set it manually"},
|
||||
),
|
||||
weight=315,
|
||||
group='Motions',
|
||||
subgroup='General')
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_preamble',
|
||||
default_value='The assembly may decide:',
|
||||
label='Motion preamble',
|
||||
name="motions_preamble",
|
||||
default_value="The assembly may decide:",
|
||||
label="Motion preamble",
|
||||
weight=320,
|
||||
group='Motions',
|
||||
subgroup='General')
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_default_line_numbering',
|
||||
default_value='none',
|
||||
input_type='choice',
|
||||
label='Default line numbering',
|
||||
name="motions_default_line_numbering",
|
||||
default_value="none",
|
||||
input_type="choice",
|
||||
label="Default line numbering",
|
||||
choices=(
|
||||
{'value': 'outside', 'display_name': 'outside'},
|
||||
{'value': 'inline', 'display_name': 'inline'},
|
||||
{'value': 'none', 'display_name': 'Disabled'}),
|
||||
{"value": "outside", "display_name": "outside"},
|
||||
{"value": "inline", "display_name": "inline"},
|
||||
{"value": "none", "display_name": "Disabled"},
|
||||
),
|
||||
weight=322,
|
||||
group='Motions',
|
||||
subgroup='General')
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_line_length',
|
||||
name="motions_line_length",
|
||||
default_value=90,
|
||||
input_type='integer',
|
||||
label='Line length',
|
||||
help_text='The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40',
|
||||
input_type="integer",
|
||||
label="Line length",
|
||||
help_text="The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40",
|
||||
weight=323,
|
||||
group='Motions',
|
||||
subgroup='General',
|
||||
validators=(MinValueValidator(40),))
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
validators=(MinValueValidator(40),),
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_disable_reason_on_projector',
|
||||
name="motions_disable_reason_on_projector",
|
||||
default_value=False,
|
||||
input_type='boolean',
|
||||
label='Hide reason on projector',
|
||||
input_type="boolean",
|
||||
label="Hide reason on projector",
|
||||
weight=325,
|
||||
group='Motions',
|
||||
subgroup='General')
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_disable_sidebox_on_projector',
|
||||
name="motions_disable_sidebox_on_projector",
|
||||
default_value=False,
|
||||
input_type='boolean',
|
||||
label='Hide meta information box on projector',
|
||||
input_type="boolean",
|
||||
label="Hide meta information box on projector",
|
||||
weight=326,
|
||||
group='Motions',
|
||||
subgroup='General')
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_disable_recommendation_on_projector',
|
||||
name="motions_disable_recommendation_on_projector",
|
||||
default_value=False,
|
||||
input_type='boolean',
|
||||
label='Hide recommendation on projector',
|
||||
input_type="boolean",
|
||||
label="Hide recommendation on projector",
|
||||
weight=327,
|
||||
group='Motions',
|
||||
subgroup='General')
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_stop_submitting',
|
||||
name="motions_stop_submitting",
|
||||
default_value=False,
|
||||
input_type='boolean',
|
||||
label='Stop submitting new motions by non-staff users',
|
||||
input_type="boolean",
|
||||
label="Stop submitting new motions by non-staff users",
|
||||
weight=331,
|
||||
group='Motions',
|
||||
subgroup='General')
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_recommendations_by',
|
||||
default_value='',
|
||||
label='Name of recommender',
|
||||
help_text='Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.',
|
||||
name="motions_recommendations_by",
|
||||
default_value="",
|
||||
label="Name of recommender",
|
||||
help_text="Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.",
|
||||
weight=332,
|
||||
group='Motions',
|
||||
subgroup='General')
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_statute_recommendations_by',
|
||||
default_value='',
|
||||
label='Name of recommender for statute amendments',
|
||||
help_text='Will be displayed as label before selected recommendation in statute amendments. ' +
|
||||
'Use an empty value to disable the recommendation system for statute amendments.',
|
||||
name="motions_statute_recommendations_by",
|
||||
default_value="",
|
||||
label="Name of recommender for statute amendments",
|
||||
help_text="Will be displayed as label before selected recommendation in statute amendments. "
|
||||
+ "Use an empty value to disable the recommendation system for statute amendments.",
|
||||
weight=333,
|
||||
group='Motions',
|
||||
subgroup='General')
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_recommendation_text_mode',
|
||||
default_value='original',
|
||||
input_type='choice',
|
||||
label='Default text version for change recommendations',
|
||||
name="motions_recommendation_text_mode",
|
||||
default_value="original",
|
||||
input_type="choice",
|
||||
label="Default text version for change recommendations",
|
||||
choices=(
|
||||
{'value': 'original', 'display_name': 'Original version'},
|
||||
{'value': 'changed', 'display_name': 'Changed version'},
|
||||
{'value': 'diff', 'display_name': 'Diff version'},
|
||||
{'value': 'agreed', 'display_name': 'Final version'}),
|
||||
{"value": "original", "display_name": "Original version"},
|
||||
{"value": "changed", "display_name": "Changed version"},
|
||||
{"value": "diff", "display_name": "Diff version"},
|
||||
{"value": "agreed", "display_name": "Final version"},
|
||||
),
|
||||
weight=334,
|
||||
group='Motions',
|
||||
subgroup='General')
|
||||
group="Motions",
|
||||
subgroup="General",
|
||||
)
|
||||
|
||||
# Amendments
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_statutes_enabled',
|
||||
name="motions_statutes_enabled",
|
||||
default_value=False,
|
||||
input_type='boolean',
|
||||
label='Activate statute amendments',
|
||||
input_type="boolean",
|
||||
label="Activate statute amendments",
|
||||
weight=335,
|
||||
group='Motions',
|
||||
subgroup='Amendments')
|
||||
group="Motions",
|
||||
subgroup="Amendments",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_amendments_enabled',
|
||||
name="motions_amendments_enabled",
|
||||
default_value=False,
|
||||
input_type='boolean',
|
||||
label='Activate amendments',
|
||||
input_type="boolean",
|
||||
label="Activate amendments",
|
||||
weight=336,
|
||||
group='Motions',
|
||||
subgroup='Amendments')
|
||||
group="Motions",
|
||||
subgroup="Amendments",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_amendments_main_table',
|
||||
name="motions_amendments_main_table",
|
||||
default_value=False,
|
||||
input_type='boolean',
|
||||
label='Show amendments together with motions',
|
||||
input_type="boolean",
|
||||
label="Show amendments together with motions",
|
||||
weight=337,
|
||||
group='Motions',
|
||||
subgroup='Amendments')
|
||||
group="Motions",
|
||||
subgroup="Amendments",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_amendments_prefix',
|
||||
default_value='-',
|
||||
label='Prefix for the identifier for amendments',
|
||||
name="motions_amendments_prefix",
|
||||
default_value="-",
|
||||
label="Prefix for the identifier for amendments",
|
||||
weight=340,
|
||||
group='Motions',
|
||||
subgroup='Amendments')
|
||||
group="Motions",
|
||||
subgroup="Amendments",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_amendments_text_mode',
|
||||
default_value='freestyle',
|
||||
input_type='choice',
|
||||
label='How to create new amendments',
|
||||
name="motions_amendments_text_mode",
|
||||
default_value="freestyle",
|
||||
input_type="choice",
|
||||
label="How to create new amendments",
|
||||
choices=(
|
||||
{'value': 'freestyle', 'display_name': 'Empty text field'},
|
||||
{'value': 'fulltext', 'display_name': 'Edit the whole motion text'},
|
||||
{'value': 'paragraph', 'display_name': 'Paragraph-based, Diff-enabled'},
|
||||
{"value": "freestyle", "display_name": "Empty text field"},
|
||||
{"value": "fulltext", "display_name": "Edit the whole motion text"},
|
||||
{"value": "paragraph", "display_name": "Paragraph-based, Diff-enabled"},
|
||||
),
|
||||
weight=342,
|
||||
group='Motions',
|
||||
subgroup='Amendments')
|
||||
group="Motions",
|
||||
subgroup="Amendments",
|
||||
)
|
||||
|
||||
# Supporters
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_min_supporters',
|
||||
name="motions_min_supporters",
|
||||
default_value=0,
|
||||
input_type='integer',
|
||||
label='Number of (minimum) required supporters for a motion',
|
||||
help_text='Choose 0 to disable the supporting system.',
|
||||
input_type="integer",
|
||||
label="Number of (minimum) required supporters for a motion",
|
||||
help_text="Choose 0 to disable the supporting system.",
|
||||
weight=345,
|
||||
group='Motions',
|
||||
subgroup='Supporters',
|
||||
validators=(MinValueValidator(0),))
|
||||
group="Motions",
|
||||
subgroup="Supporters",
|
||||
validators=(MinValueValidator(0),),
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_remove_supporters',
|
||||
name="motions_remove_supporters",
|
||||
default_value=False,
|
||||
input_type='boolean',
|
||||
label='Remove all supporters of a motion if a submitter edits his motion in early state',
|
||||
input_type="boolean",
|
||||
label="Remove all supporters of a motion if a submitter edits his motion in early state",
|
||||
weight=350,
|
||||
group='Motions',
|
||||
subgroup='Supporters')
|
||||
group="Motions",
|
||||
subgroup="Supporters",
|
||||
)
|
||||
|
||||
# Voting and ballot papers
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_poll_100_percent_base',
|
||||
default_value='YES_NO_ABSTAIN',
|
||||
input_type='choice',
|
||||
label='The 100 % base of a voting result consists of',
|
||||
name="motions_poll_100_percent_base",
|
||||
default_value="YES_NO_ABSTAIN",
|
||||
input_type="choice",
|
||||
label="The 100 % base of a voting result consists of",
|
||||
choices=(
|
||||
{'value': 'YES_NO_ABSTAIN', 'display_name': 'Yes/No/Abstain'},
|
||||
{'value': 'YES_NO', 'display_name': 'Yes/No'},
|
||||
{'value': 'VALID', 'display_name': 'All valid ballots'},
|
||||
{'value': 'CAST', 'display_name': 'All casted ballots'},
|
||||
{'value': 'DISABLED', 'display_name': 'Disabled (no percents)'}
|
||||
),
|
||||
{"value": "YES_NO_ABSTAIN", "display_name": "Yes/No/Abstain"},
|
||||
{"value": "YES_NO", "display_name": "Yes/No"},
|
||||
{"value": "VALID", "display_name": "All valid ballots"},
|
||||
{"value": "CAST", "display_name": "All casted ballots"},
|
||||
{"value": "DISABLED", "display_name": "Disabled (no percents)"},
|
||||
),
|
||||
weight=355,
|
||||
group='Motions',
|
||||
subgroup='Voting and ballot papers')
|
||||
group="Motions",
|
||||
subgroup="Voting and ballot papers",
|
||||
)
|
||||
|
||||
# TODO: Add server side validation of the choices.
|
||||
yield ConfigVariable(
|
||||
name='motions_poll_default_majority_method',
|
||||
default_value=majorityMethods[0]['value'],
|
||||
input_type='choice',
|
||||
name="motions_poll_default_majority_method",
|
||||
default_value=majorityMethods[0]["value"],
|
||||
input_type="choice",
|
||||
choices=majorityMethods,
|
||||
label='Required majority',
|
||||
help_text='Default method to check whether a motion has reached the required majority.',
|
||||
label="Required majority",
|
||||
help_text="Default method to check whether a motion has reached the required majority.",
|
||||
weight=357,
|
||||
group='Motions',
|
||||
subgroup='Voting and ballot papers')
|
||||
group="Motions",
|
||||
subgroup="Voting and ballot papers",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_pdf_ballot_papers_selection',
|
||||
default_value='CUSTOM_NUMBER',
|
||||
input_type='choice',
|
||||
label='Number of ballot papers (selection)',
|
||||
name="motions_pdf_ballot_papers_selection",
|
||||
default_value="CUSTOM_NUMBER",
|
||||
input_type="choice",
|
||||
label="Number of ballot papers (selection)",
|
||||
choices=(
|
||||
{'value': 'NUMBER_OF_DELEGATES', 'display_name': 'Number of all delegates'},
|
||||
{'value': 'NUMBER_OF_ALL_PARTICIPANTS', 'display_name': 'Number of all participants'},
|
||||
{'value': 'CUSTOM_NUMBER', 'display_name': 'Use the following custom number'}),
|
||||
{"value": "NUMBER_OF_DELEGATES", "display_name": "Number of all delegates"},
|
||||
{
|
||||
"value": "NUMBER_OF_ALL_PARTICIPANTS",
|
||||
"display_name": "Number of all participants",
|
||||
},
|
||||
{
|
||||
"value": "CUSTOM_NUMBER",
|
||||
"display_name": "Use the following custom number",
|
||||
},
|
||||
),
|
||||
weight=360,
|
||||
group='Motions',
|
||||
subgroup='Voting and ballot papers')
|
||||
group="Motions",
|
||||
subgroup="Voting and ballot papers",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_pdf_ballot_papers_number',
|
||||
name="motions_pdf_ballot_papers_number",
|
||||
default_value=8,
|
||||
input_type='integer',
|
||||
label='Custom number of ballot papers',
|
||||
input_type="integer",
|
||||
label="Custom number of ballot papers",
|
||||
weight=365,
|
||||
group='Motions',
|
||||
subgroup='Voting and ballot papers',
|
||||
validators=(MinValueValidator(1),))
|
||||
group="Motions",
|
||||
subgroup="Voting and ballot papers",
|
||||
validators=(MinValueValidator(1),),
|
||||
)
|
||||
|
||||
# PDF and DOCX export
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_export_title',
|
||||
default_value='Motions',
|
||||
label='Title for PDF and DOCX documents (all motions)',
|
||||
name="motions_export_title",
|
||||
default_value="Motions",
|
||||
label="Title for PDF and DOCX documents (all motions)",
|
||||
weight=370,
|
||||
group='Motions',
|
||||
subgroup='Export')
|
||||
group="Motions",
|
||||
subgroup="Export",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_export_preamble',
|
||||
default_value='',
|
||||
label='Preamble text for PDF and DOCX documents (all motions)',
|
||||
name="motions_export_preamble",
|
||||
default_value="",
|
||||
label="Preamble text for PDF and DOCX documents (all motions)",
|
||||
weight=375,
|
||||
group='Motions',
|
||||
subgroup='Export')
|
||||
group="Motions",
|
||||
subgroup="Export",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_export_category_sorting',
|
||||
default_value='prefix',
|
||||
input_type='choice',
|
||||
label='Sort categories by',
|
||||
name="motions_export_category_sorting",
|
||||
default_value="prefix",
|
||||
input_type="choice",
|
||||
label="Sort categories by",
|
||||
choices=(
|
||||
{'value': 'prefix', 'display_name': 'Prefix'},
|
||||
{'value': 'name', 'display_name': 'Name'}),
|
||||
{"value": "prefix", "display_name": "Prefix"},
|
||||
{"value": "name", "display_name": "Name"},
|
||||
),
|
||||
weight=380,
|
||||
group='Motions',
|
||||
subgroup='Export')
|
||||
group="Motions",
|
||||
subgroup="Export",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_export_sequential_number',
|
||||
name="motions_export_sequential_number",
|
||||
default_value=True,
|
||||
input_type='boolean',
|
||||
label='Include the sequential number in PDF and DOCX',
|
||||
input_type="boolean",
|
||||
label="Include the sequential number in PDF and DOCX",
|
||||
weight=385,
|
||||
group='Motions',
|
||||
subgroup='Export')
|
||||
group="Motions",
|
||||
subgroup="Export",
|
||||
)
|
||||
|
@ -3,4 +3,5 @@ from openslides.utils.exceptions import OpenSlidesError
|
||||
|
||||
class WorkflowError(OpenSlidesError):
|
||||
"""Exception raised when errors in a workflow or state accure."""
|
||||
|
||||
pass
|
||||
|
@ -15,200 +15,344 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('mediafiles', '0001_initial'),
|
||||
("mediafiles", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('core', '0001_initial'),
|
||||
("core", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
name="Category",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('prefix', models.CharField(blank=True, max_length=32)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("prefix", models.CharField(blank=True, max_length=32)),
|
||||
],
|
||||
options={"ordering": ["prefix"], "default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Motion",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"identifier",
|
||||
models.CharField(
|
||||
blank=True, max_length=255, null=True, unique=True
|
||||
),
|
||||
),
|
||||
("identifier_number", models.IntegerField(null=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['prefix'],
|
||||
'default_permissions': (),
|
||||
"verbose_name": "Motion",
|
||||
"permissions": (
|
||||
("can_see", "Can see motions"),
|
||||
("can_create", "Can create motions"),
|
||||
("can_support", "Can support motions"),
|
||||
("can_manage", "Can manage motions"),
|
||||
),
|
||||
"ordering": ("identifier",),
|
||||
"default_permissions": (),
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Motion',
|
||||
name="MotionLog",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('identifier', models.CharField(blank=True, max_length=255, null=True, unique=True)),
|
||||
('identifier_number', models.IntegerField(null=True)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("message_list", jsonfield.fields.JSONField()),
|
||||
("time", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"motion",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="log_messages",
|
||||
to="motions.Motion",
|
||||
),
|
||||
),
|
||||
(
|
||||
"person",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Motion',
|
||||
'permissions': (
|
||||
('can_see', 'Can see motions'),
|
||||
('can_create', 'Can create motions'),
|
||||
('can_support', 'Can support motions'),
|
||||
('can_manage', 'Can manage motions')),
|
||||
'ordering': ('identifier',),
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"ordering": ["-time"], "default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MotionLog',
|
||||
name="MotionOption",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('message_list', jsonfield.fields.JSONField()),
|
||||
('time', models.DateTimeField(auto_now=True)),
|
||||
('motion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='log_messages', to='motions.Motion')),
|
||||
('person', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
)
|
||||
],
|
||||
options={
|
||||
'ordering': ['-time'],
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MotionOption',
|
||||
name="MotionPoll",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"votesvalid",
|
||||
openslides.utils.models.MinMaxIntegerField(blank=True, null=True),
|
||||
),
|
||||
(
|
||||
"votesinvalid",
|
||||
openslides.utils.models.MinMaxIntegerField(blank=True, null=True),
|
||||
),
|
||||
(
|
||||
"votescast",
|
||||
openslides.utils.models.MinMaxIntegerField(blank=True, null=True),
|
||||
),
|
||||
(
|
||||
"motion",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="polls",
|
||||
to="motions.Motion",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MotionPoll',
|
||||
name="MotionVersion",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('votesvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)),
|
||||
('votesinvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)),
|
||||
('votescast', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)),
|
||||
('motion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polls', to='motions.Motion')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("version_number", models.PositiveIntegerField(default=1)),
|
||||
("title", models.CharField(max_length=255)),
|
||||
("text", models.TextField()),
|
||||
("reason", models.TextField(blank=True, null=True)),
|
||||
("creation_time", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"motion",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="versions",
|
||||
to="motions.Motion",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MotionVersion',
|
||||
name="MotionVote",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('version_number', models.PositiveIntegerField(default=1)),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('text', models.TextField()),
|
||||
('reason', models.TextField(blank=True, null=True)),
|
||||
('creation_time', models.DateTimeField(auto_now=True)),
|
||||
('motion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='motions.Motion')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("weight", models.IntegerField(default=1, null=True)),
|
||||
("value", models.CharField(max_length=255, null=True)),
|
||||
(
|
||||
"option",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="motions.MotionOption",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MotionVote',
|
||||
name="State",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('weight', models.IntegerField(default=1, null=True)),
|
||||
('value', models.CharField(max_length=255, null=True)),
|
||||
('option', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='motions.MotionOption')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("action_word", models.CharField(max_length=255)),
|
||||
("css_class", models.CharField(default="primary", max_length=255)),
|
||||
(
|
||||
"required_permission_to_see",
|
||||
models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
("allow_support", models.BooleanField(default=False)),
|
||||
("allow_create_poll", models.BooleanField(default=False)),
|
||||
("allow_submitter_edit", models.BooleanField(default=False)),
|
||||
("versioning", models.BooleanField(default=False)),
|
||||
("leave_old_version_active", models.BooleanField(default=False)),
|
||||
("dont_set_identifier", models.BooleanField(default=False)),
|
||||
("next_states", models.ManyToManyField(to="motions.State")),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='State',
|
||||
name="Workflow",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('action_word', models.CharField(max_length=255)),
|
||||
('css_class', models.CharField(default='primary', max_length=255)),
|
||||
('required_permission_to_see', models.CharField(blank=True, max_length=255)),
|
||||
('allow_support', models.BooleanField(default=False)),
|
||||
('allow_create_poll', models.BooleanField(default=False)),
|
||||
('allow_submitter_edit', models.BooleanField(default=False)),
|
||||
('versioning', models.BooleanField(default=False)),
|
||||
('leave_old_version_active', models.BooleanField(default=False)),
|
||||
('dont_set_identifier', models.BooleanField(default=False)),
|
||||
('next_states', models.ManyToManyField(to='motions.State')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
(
|
||||
"first_state",
|
||||
models.OneToOneField(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="motions.State",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Workflow',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('first_state', models.OneToOneField(
|
||||
null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='motions.State')),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='state',
|
||||
name='workflow',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='states', to='motions.Workflow'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motionoption',
|
||||
name='poll',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='motions.MotionPoll'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='active_version',
|
||||
model_name="state",
|
||||
name="workflow",
|
||||
field=models.ForeignKey(
|
||||
null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='active_version', to='motions.MotionVersion'),
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="states",
|
||||
to="motions.Workflow",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='attachments',
|
||||
field=models.ManyToManyField(blank=True, to='mediafiles.Mediafile'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='category',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='motions.Category'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='parent',
|
||||
model_name="motionoption",
|
||||
name="poll",
|
||||
field=models.ForeignKey(
|
||||
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='amendments', to='motions.Motion'),
|
||||
on_delete=django.db.models.deletion.CASCADE, to="motions.MotionPoll"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='state',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='motions.State'),
|
||||
model_name="motion",
|
||||
name="active_version",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="active_version",
|
||||
to="motions.MotionVersion",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='submitters',
|
||||
field=models.ManyToManyField(blank=True, related_name='motion_submitters', to=settings.AUTH_USER_MODEL),
|
||||
model_name="motion",
|
||||
name="attachments",
|
||||
field=models.ManyToManyField(blank=True, to="mediafiles.Mediafile"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='supporters',
|
||||
field=models.ManyToManyField(blank=True, related_name='motion_supporters', to=settings.AUTH_USER_MODEL),
|
||||
model_name="motion",
|
||||
name="category",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="motions.Category",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='tags',
|
||||
field=models.ManyToManyField(blank=True, to='core.Tag'),
|
||||
model_name="motion",
|
||||
name="parent",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="amendments",
|
||||
to="motions.Motion",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="motion",
|
||||
name="state",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="motions.State",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="motion",
|
||||
name="submitters",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="motion_submitters",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="motion",
|
||||
name="supporters",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="motion_supporters",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="motion",
|
||||
name="tags",
|
||||
field=models.ManyToManyField(blank=True, to="core.Tag"),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='motionversion',
|
||||
unique_together=set([('motion', 'version_number')]),
|
||||
name="motionversion", unique_together=set([("motion", "version_number")])
|
||||
),
|
||||
]
|
||||
|
@ -16,16 +16,16 @@ def change_label_of_state(apps, schema_editor):
|
||||
"""
|
||||
# We get the model from the versioned app registry;
|
||||
# if we directly import it, it will be the wrong version.
|
||||
State = apps.get_model('motions', 'State')
|
||||
State = apps.get_model("motions", "State")
|
||||
|
||||
try:
|
||||
state = State.objects.get(name='commited a bill')
|
||||
state = State.objects.get(name="commited a bill")
|
||||
except State.DoesNotExist:
|
||||
# State does not exists, there is nothing to change.
|
||||
pass
|
||||
else:
|
||||
state.name = 'refered to committee'
|
||||
state.action_word = 'Refer to committee'
|
||||
state.name = "refered to committee"
|
||||
state.action_word = "Refer to committee"
|
||||
state.save(skip_autoupdate=True)
|
||||
|
||||
|
||||
@ -35,17 +35,17 @@ def add_recommendation_labels(apps, schema_editor):
|
||||
"""
|
||||
# We get the model from the versioned app registry;
|
||||
# if we directly import it, it will be the wrong version.
|
||||
State = apps.get_model('motions', 'State')
|
||||
State = apps.get_model("motions", "State")
|
||||
|
||||
name_label_map = {
|
||||
'accepted': 'Acceptance',
|
||||
'rejected': 'Rejection',
|
||||
'not decided': 'No decision',
|
||||
'permitted': 'Permission',
|
||||
'adjourned': 'Adjournment',
|
||||
'not concerned': 'No concernment',
|
||||
'refered to committee': 'Referral to committee',
|
||||
'rejected (not authorized)': 'Rejection (not authorized)',
|
||||
"accepted": "Acceptance",
|
||||
"rejected": "Rejection",
|
||||
"not decided": "No decision",
|
||||
"permitted": "Permission",
|
||||
"adjourned": "Adjournment",
|
||||
"not concerned": "No concernment",
|
||||
"refered to committee": "Referral to committee",
|
||||
"rejected (not authorized)": "Rejection (not authorized)",
|
||||
}
|
||||
for state in State.objects.all():
|
||||
if name_label_map.get(state.name):
|
||||
@ -57,101 +57,135 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('motions', '0001_initial'),
|
||||
("motions", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MotionBlock',
|
||||
name="MotionBlock",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=255)),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MotionChangeRecommendation',
|
||||
name="MotionChangeRecommendation",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('rejected', models.BooleanField(default=False)),
|
||||
('type', models.PositiveIntegerField(default=0)),
|
||||
('line_from', models.PositiveIntegerField()),
|
||||
('line_to', models.PositiveIntegerField()),
|
||||
('text', models.TextField(blank=True)),
|
||||
('creation_time', models.DateTimeField(auto_now=True)),
|
||||
('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
('motion_version', models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name='change_recommendations', to='motions.MotionVersion')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("rejected", models.BooleanField(default=False)),
|
||||
("type", models.PositiveIntegerField(default=0)),
|
||||
("line_from", models.PositiveIntegerField()),
|
||||
("line_to", models.PositiveIntegerField()),
|
||||
("text", models.TextField(blank=True)),
|
||||
("creation_time", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"author",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"motion_version",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="change_recommendations",
|
||||
to="motions.MotionVersion",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='motion',
|
||||
name="motion",
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'ordering': (
|
||||
'identifier',
|
||||
"default_permissions": (),
|
||||
"ordering": ("identifier",),
|
||||
"permissions": (
|
||||
("can_see", "Can see motions"),
|
||||
("can_create", "Can create motions"),
|
||||
("can_support", "Can support motions"),
|
||||
("can_see_and_manage_comments", "Can see and manage comments"),
|
||||
("can_manage", "Can manage motions"),
|
||||
),
|
||||
'permissions': (
|
||||
('can_see', 'Can see motions'),
|
||||
('can_create', 'Can create motions'),
|
||||
('can_support', 'Can support motions'),
|
||||
('can_see_and_manage_comments', 'Can see and manage comments'),
|
||||
('can_manage', 'Can manage motions')
|
||||
),
|
||||
'verbose_name': 'Motion',
|
||||
"verbose_name": "Motion",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='comments',
|
||||
model_name="motion",
|
||||
name="comments",
|
||||
field=jsonfield.fields.JSONField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='origin',
|
||||
model_name="motion",
|
||||
name="origin",
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='recommendation',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='motions.State'),
|
||||
model_name="motion",
|
||||
name="recommendation",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="motions.State",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='state',
|
||||
name='recommendation_label',
|
||||
model_name="state",
|
||||
name="recommendation_label",
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='state',
|
||||
name='show_recommendation_extension_field',
|
||||
model_name="state",
|
||||
name="show_recommendation_extension_field",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='state',
|
||||
name='show_state_extension_field',
|
||||
model_name="state",
|
||||
name="show_state_extension_field",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='motion',
|
||||
name='state',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='motions.State'),
|
||||
model_name="motion",
|
||||
name="state",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="motions.State",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='motion_block',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='motions.MotionBlock'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
change_label_of_state
|
||||
),
|
||||
migrations.RunPython(
|
||||
add_recommendation_labels
|
||||
model_name="motion",
|
||||
name="motion_block",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="motions.MotionBlock",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(change_label_of_state),
|
||||
migrations.RunPython(add_recommendation_labels),
|
||||
]
|
||||
|
@ -12,24 +12,24 @@ def change_motions_comments(apps, schema_editor):
|
||||
"""
|
||||
# We get the model from the versioned app registry;
|
||||
# if we directly import it, it will be the wrong version.
|
||||
ConfigStore = apps.get_model('core', 'ConfigStore')
|
||||
Motion = apps.get_model('motions', 'Motion')
|
||||
ConfigStore = apps.get_model("core", "ConfigStore")
|
||||
Motion = apps.get_model("motions", "Motion")
|
||||
|
||||
try:
|
||||
config_comments_fields = ConfigStore.objects.get(key='motions_comments').value
|
||||
config_comments_fields = ConfigStore.objects.get(key="motions_comments").value
|
||||
except ConfigStore.DoesNotExist:
|
||||
config_comments_fields = [] # The old default: An empty list.
|
||||
|
||||
comments_fields = {}
|
||||
for index, field in enumerate(config_comments_fields):
|
||||
comments_fields[index+1] = field
|
||||
comments_fields[index + 1] = field
|
||||
|
||||
max_index = len(config_comments_fields)-1
|
||||
max_index = len(config_comments_fields) - 1
|
||||
|
||||
try:
|
||||
db_value = ConfigStore.objects.get(key='motions_comments')
|
||||
db_value = ConfigStore.objects.get(key="motions_comments")
|
||||
except ConfigStore.DoesNotExist:
|
||||
db_value = ConfigStore(key='motions_comments')
|
||||
db_value = ConfigStore(key="motions_comments")
|
||||
db_value.value = comments_fields
|
||||
# We cannot provide skip_autoupdate=True here, becuase this object is a fake object. It does *not*
|
||||
# inherit from the RESTModelMixin, so the save() methos from base_model.py (django's default)
|
||||
@ -42,19 +42,13 @@ def change_motions_comments(apps, schema_editor):
|
||||
for index, comment in enumerate(motion.comments or []):
|
||||
if index > max_index:
|
||||
break
|
||||
comments[index+1] = comment
|
||||
comments[index + 1] = comment
|
||||
motion.comments = comments
|
||||
motion.save(skip_autoupdate=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0002_misc_features'),
|
||||
]
|
||||
dependencies = [("motions", "0002_misc_features")]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
change_motions_comments
|
||||
),
|
||||
]
|
||||
operations = [migrations.RunPython(change_motions_comments)]
|
||||
|
@ -7,14 +7,12 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0003_motion_comments'),
|
||||
]
|
||||
dependencies = [("motions", "0003_motion_comments")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='motionchangerecommendation',
|
||||
name='other_description',
|
||||
model_name="motionchangerecommendation",
|
||||
name="other_description",
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
)
|
||||
]
|
||||
|
@ -11,7 +11,7 @@ def delete_old_comment_permission(apps, schema_editor):
|
||||
Deletes the old 'can_see_and_manage_comments' permission which is
|
||||
split up into two seperate permissions.
|
||||
"""
|
||||
perm = Permission.objects.filter(codename='can_see_and_manage_comments')
|
||||
perm = Permission.objects.filter(codename="can_see_and_manage_comments")
|
||||
|
||||
if len(perm):
|
||||
perm = perm.get()
|
||||
@ -26,13 +26,15 @@ def delete_old_comment_permission(apps, schema_editor):
|
||||
|
||||
# Create new permission
|
||||
perm_see = Permission.objects.create(
|
||||
codename='can_see_comments',
|
||||
name='Can see comments',
|
||||
content_type=content_type)
|
||||
codename="can_see_comments",
|
||||
name="Can see comments",
|
||||
content_type=content_type,
|
||||
)
|
||||
perm_manage = Permission.objects.create(
|
||||
codename='can_manage_comments',
|
||||
name='Can manage comments',
|
||||
content_type=content_type)
|
||||
codename="can_manage_comments",
|
||||
name="Can manage comments",
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
for group in groups:
|
||||
group.permissions.add(perm_see)
|
||||
@ -42,28 +44,24 @@ def delete_old_comment_permission(apps, schema_editor):
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0004_motionchangerecommendation_other_description'),
|
||||
]
|
||||
dependencies = [("motions", "0004_motionchangerecommendation_other_description")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='motion',
|
||||
name="motion",
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'ordering': ('identifier',),
|
||||
'permissions': (
|
||||
('can_see', 'Can see motions'),
|
||||
('can_create', 'Can create motions'),
|
||||
('can_support', 'Can support motions'),
|
||||
('can_see_comments', 'Can see comments'),
|
||||
('can_manage_comments', 'Can manage comments'),
|
||||
('can_manage', 'Can manage motions')
|
||||
"default_permissions": (),
|
||||
"ordering": ("identifier",),
|
||||
"permissions": (
|
||||
("can_see", "Can see motions"),
|
||||
("can_create", "Can create motions"),
|
||||
("can_support", "Can support motions"),
|
||||
("can_see_comments", "Can see comments"),
|
||||
("can_manage_comments", "Can manage comments"),
|
||||
("can_manage", "Can manage motions"),
|
||||
),
|
||||
'verbose_name': 'Motion'
|
||||
"verbose_name": "Motion",
|
||||
},
|
||||
),
|
||||
migrations.RunPython(
|
||||
delete_old_comment_permission
|
||||
),
|
||||
migrations.RunPython(delete_old_comment_permission),
|
||||
]
|
||||
|
@ -11,8 +11,8 @@ import openslides.utils.models
|
||||
|
||||
|
||||
def move_submitters_to_own_model(apps, schema_editor):
|
||||
Motion = apps.get_model('motions', 'Motion')
|
||||
Submitter = apps.get_model('motions', 'Submitter')
|
||||
Motion = apps.get_model("motions", "Motion")
|
||||
Submitter = apps.get_model("motions", "Submitter")
|
||||
|
||||
for motion in Motion.objects.all():
|
||||
weight = 0
|
||||
@ -32,41 +32,46 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('motions', '0005_auto_20180202_1318'),
|
||||
("motions", "0005_auto_20180202_1318"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='motion',
|
||||
old_name='submitters',
|
||||
new_name='submittersOld',
|
||||
model_name="motion", old_name="submitters", new_name="submittersOld"
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Submitter',
|
||||
name="Submitter",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('weight', models.IntegerField(null=True)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("weight", models.IntegerField(null=True)),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='submitter',
|
||||
name='motion',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submitters', to='motions.Motion'),
|
||||
model_name="submitter",
|
||||
name="motion",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="submitters",
|
||||
to="motions.Motion",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='submitter',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.RunPython(
|
||||
move_submitters_to_own_model
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='motion',
|
||||
name='submittersOld',
|
||||
model_name="submitter",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
migrations.RunPython(move_submitters_to_own_model),
|
||||
migrations.RemoveField(model_name="motion", name="submittersOld"),
|
||||
]
|
||||
|
@ -8,14 +8,12 @@ from django.db import migrations
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0006_submitter_model'),
|
||||
]
|
||||
dependencies = [("motions", "0006_submitter_model")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='motionversion',
|
||||
name='amendment_paragraphs',
|
||||
model_name="motionversion",
|
||||
name="amendment_paragraphs",
|
||||
field=jsonfield.fields.JSONField(null=True),
|
||||
),
|
||||
)
|
||||
]
|
||||
|
@ -8,38 +8,38 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0007_motionversion_amendment_data'),
|
||||
]
|
||||
dependencies = [("motions", "0007_motionversion_amendment_data")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='workflow',
|
||||
name='first_state',
|
||||
model_name="workflow",
|
||||
name="first_state",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='+',
|
||||
to='motions.State'),
|
||||
related_name="+",
|
||||
to="motions.State",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='motion',
|
||||
name='state',
|
||||
model_name="motion",
|
||||
name="state",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='motions.State'),
|
||||
related_name="+",
|
||||
to="motions.State",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='state',
|
||||
name='next_states',
|
||||
field=models.ManyToManyField(blank=True, to='motions.State'),
|
||||
model_name="state",
|
||||
name="next_states",
|
||||
field=models.ManyToManyField(blank=True, to="motions.State"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='state',
|
||||
name='action_word',
|
||||
model_name="state",
|
||||
name="action_word",
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
]
|
||||
|
@ -7,14 +7,12 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0008_auto_20180702_1128'),
|
||||
]
|
||||
dependencies = [("motions", "0008_auto_20180702_1128")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='motionversion',
|
||||
name='modified_final_version',
|
||||
model_name="motionversion",
|
||||
name="modified_final_version",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
)
|
||||
]
|
||||
|
@ -8,49 +8,51 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0009_motionversion_modified_final_version'),
|
||||
]
|
||||
dependencies = [("motions", "0009_motionversion_modified_final_version")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='motionpoll',
|
||||
name='votescast',
|
||||
model_name="motionpoll",
|
||||
name="votescast",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=6,
|
||||
max_digits=15,
|
||||
null=True,
|
||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
||||
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='motionpoll',
|
||||
name='votesinvalid',
|
||||
model_name="motionpoll",
|
||||
name="votesinvalid",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=6,
|
||||
max_digits=15,
|
||||
null=True,
|
||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
||||
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='motionpoll',
|
||||
name='votesvalid',
|
||||
model_name="motionpoll",
|
||||
name="votesvalid",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=6,
|
||||
max_digits=15,
|
||||
null=True,
|
||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
||||
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='motionvote',
|
||||
name='weight',
|
||||
model_name="motionvote",
|
||||
name="weight",
|
||||
field=models.DecimalField(
|
||||
decimal_places=6,
|
||||
default=Decimal('1'),
|
||||
default=Decimal("1"),
|
||||
max_digits=15,
|
||||
null=True,
|
||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
||||
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -10,7 +10,7 @@ def copy_motion_version_content_to_motion(apps, schema_editor):
|
||||
"""
|
||||
Move all motion version content of the active version to the motion.
|
||||
"""
|
||||
Motion = apps.get_model('motions', 'Motion')
|
||||
Motion = apps.get_model("motions", "Motion")
|
||||
|
||||
for motion in Motion.objects.all():
|
||||
motion.title = motion.active_version.title
|
||||
@ -26,7 +26,7 @@ def migrate_active_change_recommendations(apps, schema_editor):
|
||||
Delete all change recommendation of motion versions, that are not active. For active
|
||||
change recommendations the motion id will be set.
|
||||
"""
|
||||
MotionChangeRecommendation = apps.get_model('motions', 'MotionChangeRecommendation')
|
||||
MotionChangeRecommendation = apps.get_model("motions", "MotionChangeRecommendation")
|
||||
to_delete = []
|
||||
for cr in MotionChangeRecommendation.objects.all():
|
||||
# chack if version id matches the active version of the motion
|
||||
@ -43,89 +43,65 @@ def migrate_active_change_recommendations(apps, schema_editor):
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0010_auto_20180822_1042'),
|
||||
]
|
||||
dependencies = [("motions", "0010_auto_20180822_1042")]
|
||||
|
||||
operations = [
|
||||
# Create new fields. Title and Text have empty defaults, but the values
|
||||
# should be overwritten by copy_motion_version_content_to_motion. In the next
|
||||
# migration file these defaults are removed.
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='title',
|
||||
field=models.CharField(max_length=255, default=''),
|
||||
model_name="motion",
|
||||
name="title",
|
||||
field=models.CharField(max_length=255, default=""),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='text',
|
||||
field=models.TextField(default=''),
|
||||
model_name="motion", name="text", field=models.TextField(default="")
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='reason',
|
||||
model_name="motion",
|
||||
name="reason",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='modified_final_version',
|
||||
model_name="motion",
|
||||
name="modified_final_version",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='amendment_paragraphs',
|
||||
model_name="motion",
|
||||
name="amendment_paragraphs",
|
||||
field=jsonfield.fields.JSONField(
|
||||
dump_kwargs={
|
||||
'cls': jsonfield.encoder.JSONEncoder,
|
||||
'separators': (',', ':')
|
||||
"cls": jsonfield.encoder.JSONEncoder,
|
||||
"separators": (",", ":"),
|
||||
},
|
||||
load_kwargs={},
|
||||
null=True),
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
|
||||
# Copy old motion version data
|
||||
migrations.RunPython(copy_motion_version_content_to_motion),
|
||||
|
||||
# Change recommendations
|
||||
migrations.AddField(
|
||||
model_name='motionchangerecommendation',
|
||||
name='motion',
|
||||
model_name="motionchangerecommendation",
|
||||
name="motion",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
null=True, # This is reverted in the next migration
|
||||
related_name='change_recommendations',
|
||||
to='motions.Motion'),
|
||||
related_name="change_recommendations",
|
||||
to="motions.Motion",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(migrate_active_change_recommendations),
|
||||
migrations.RemoveField(
|
||||
model_name='motionchangerecommendation',
|
||||
name='motion_version',
|
||||
model_name="motionchangerecommendation", name="motion_version"
|
||||
),
|
||||
|
||||
# remove motion version references from motion and state.
|
||||
migrations.RemoveField(
|
||||
model_name='motion',
|
||||
name='active_version',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='motionversion',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='motionversion',
|
||||
name='motion',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='state',
|
||||
name='leave_old_version_active',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='state',
|
||||
name='versioning',
|
||||
),
|
||||
|
||||
migrations.RemoveField(model_name="motion", name="active_version"),
|
||||
migrations.AlterUniqueTogether(name="motionversion", unique_together=set()),
|
||||
migrations.RemoveField(model_name="motionversion", name="motion"),
|
||||
migrations.RemoveField(model_name="state", name="leave_old_version_active"),
|
||||
migrations.RemoveField(model_name="state", name="versioning"),
|
||||
# Delete motion version.
|
||||
migrations.DeleteModel(
|
||||
name='MotionVersion',
|
||||
),
|
||||
migrations.DeleteModel(name="MotionVersion"),
|
||||
]
|
||||
|
@ -8,16 +8,18 @@ from django.db import migrations, models
|
||||
import openslides
|
||||
|
||||
|
||||
def create_comment_sections_from_config_and_move_comments_to_own_model(apps, schema_editor):
|
||||
ConfigStore = apps.get_model('core', 'ConfigStore')
|
||||
Motion = apps.get_model('motions', 'Motion')
|
||||
MotionComment = apps.get_model('motions', 'MotionComment')
|
||||
MotionCommentSection = apps.get_model('motions', 'MotionCommentSection')
|
||||
def create_comment_sections_from_config_and_move_comments_to_own_model(
|
||||
apps, schema_editor
|
||||
):
|
||||
ConfigStore = apps.get_model("core", "ConfigStore")
|
||||
Motion = apps.get_model("motions", "Motion")
|
||||
MotionComment = apps.get_model("motions", "MotionComment")
|
||||
MotionCommentSection = apps.get_model("motions", "MotionCommentSection")
|
||||
Group = apps.get_model(settings.AUTH_GROUP_MODEL)
|
||||
|
||||
# try to get old motions_comments config variable, where all comment fields are saved
|
||||
try:
|
||||
motions_comments = ConfigStore.objects.get(key='motions_comments')
|
||||
motions_comments = ConfigStore.objects.get(key="motions_comments")
|
||||
except ConfigStore.DoesNotExist:
|
||||
return
|
||||
comments_sections = motions_comments.value
|
||||
@ -26,14 +28,14 @@ def create_comment_sections_from_config_and_move_comments_to_own_model(apps, sch
|
||||
motions_comments.delete()
|
||||
|
||||
# Get can_see_comments and can_manage_comments permissions and the associated groups
|
||||
can_see_comments = Permission.objects.filter(codename='can_see_comments')
|
||||
can_see_comments = Permission.objects.filter(codename="can_see_comments")
|
||||
if len(can_see_comments) == 1:
|
||||
# Save groups. list() is necessary to evaluate the database query right now.
|
||||
can_see_groups = list(can_see_comments.get().group_set.all())
|
||||
else:
|
||||
can_see_groups = Group.objects.all()
|
||||
|
||||
can_manage_comments = Permission.objects.filter(codename='can_manage_comments')
|
||||
can_manage_comments = Permission.objects.filter(codename="can_manage_comments")
|
||||
if len(can_manage_comments) == 1:
|
||||
# Save groups. list() is necessary to evaluate the database query right now.
|
||||
can_manage_groups = list(can_manage_comments.get().group_set.all())
|
||||
@ -50,12 +52,12 @@ def create_comment_sections_from_config_and_move_comments_to_own_model(apps, sch
|
||||
for id, section in comments_sections.items():
|
||||
if section is None:
|
||||
continue
|
||||
if section.get('forState', False):
|
||||
if section.get("forState", False):
|
||||
forStateId = id
|
||||
elif section.get('forRecommendation', False):
|
||||
elif section.get("forRecommendation", False):
|
||||
forRecommendationId = id
|
||||
else:
|
||||
comment_section = MotionCommentSection(name=section['name'])
|
||||
comment_section = MotionCommentSection(name=section["name"])
|
||||
comment_section.save(skip_autoupdate=True)
|
||||
comment_section.read_groups.add(*[group.id for group in can_see_groups])
|
||||
comment_section.write_groups.add(*[group.id for group in can_manage_groups])
|
||||
@ -70,7 +72,7 @@ def create_comment_sections_from_config_and_move_comments_to_own_model(apps, sch
|
||||
for section_id, comment_value in motion.comments.items():
|
||||
# Skip empty sections.
|
||||
comment_value = comment_value.strip()
|
||||
if comment_value == '':
|
||||
if comment_value == "":
|
||||
continue
|
||||
# Special comments will be moved to separate fields.
|
||||
if section_id == forStateId:
|
||||
@ -83,136 +85,147 @@ def create_comment_sections_from_config_and_move_comments_to_own_model(apps, sch
|
||||
comment = MotionComment(
|
||||
comment=comment_value,
|
||||
motion=motion,
|
||||
section=old_id_mapping[section_id])
|
||||
section=old_id_mapping[section_id],
|
||||
)
|
||||
comments.append(comment)
|
||||
MotionComment.objects.bulk_create(comments)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0006_user_email'),
|
||||
('motions', '0011_motion_version'),
|
||||
]
|
||||
dependencies = [("users", "0006_user_email"), ("motions", "0011_motion_version")]
|
||||
|
||||
operations = [
|
||||
# Cleanup from last migration. Somehow cannot be done there.
|
||||
migrations.AlterField( # remove default=''
|
||||
model_name='motion',
|
||||
name='text',
|
||||
field=models.TextField(),
|
||||
model_name="motion", name="text", field=models.TextField()
|
||||
),
|
||||
migrations.AlterField( # remove default=''
|
||||
model_name='motion',
|
||||
name='title',
|
||||
field=models.CharField(max_length=255),
|
||||
model_name="motion", name="title", field=models.CharField(max_length=255)
|
||||
),
|
||||
migrations.AlterField( # remove null=True
|
||||
model_name='motionchangerecommendation',
|
||||
name='motion',
|
||||
model_name="motionchangerecommendation",
|
||||
name="motion",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='change_recommendations',
|
||||
to='motions.Motion'),
|
||||
related_name="change_recommendations",
|
||||
to="motions.Motion",
|
||||
),
|
||||
),
|
||||
|
||||
# Add extension fields for former "special comments". No hack anymore..
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='recommendation_extension',
|
||||
model_name="motion",
|
||||
name="recommendation_extension",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='state_extension',
|
||||
model_name="motion",
|
||||
name="state_extension",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
|
||||
migrations.AlterModelOptions(
|
||||
name='motion',
|
||||
name="motion",
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'ordering': ('identifier',),
|
||||
'permissions': (
|
||||
('can_see', 'Can see motions'),
|
||||
('can_create', 'Can create motions'),
|
||||
('can_support', 'Can support motions'),
|
||||
('can_manage', 'Can manage motions')),
|
||||
'verbose_name': 'Motion'},
|
||||
"default_permissions": (),
|
||||
"ordering": ("identifier",),
|
||||
"permissions": (
|
||||
("can_see", "Can see motions"),
|
||||
("can_create", "Can create motions"),
|
||||
("can_support", "Can support motions"),
|
||||
("can_manage", "Can manage motions"),
|
||||
),
|
||||
"verbose_name": "Motion",
|
||||
},
|
||||
),
|
||||
# Comments and CommentsSection models
|
||||
migrations.CreateModel(
|
||||
name='MotionComment',
|
||||
name="MotionComment",
|
||||
fields=[
|
||||
('id', models.AutoField(
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID')),
|
||||
('comment', models.TextField()),
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("comment", models.TextField()),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model), # type: ignore
|
||||
options={"default_permissions": ()},
|
||||
bases=(
|
||||
openslides.utils.models.RESTModelMixin, # type: ignore
|
||||
models.Model,
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MotionCommentSection',
|
||||
name="MotionCommentSection",
|
||||
fields=[
|
||||
('id', models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('read_groups', models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name='read_comments',
|
||||
to=settings.AUTH_GROUP_MODEL)),
|
||||
('write_groups', models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name='write_comments',
|
||||
to=settings.AUTH_GROUP_MODEL)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
(
|
||||
"read_groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="read_comments",
|
||||
to=settings.AUTH_GROUP_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"write_groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="write_comments",
|
||||
to=settings.AUTH_GROUP_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model), # type: ignore
|
||||
options={"default_permissions": ()},
|
||||
bases=(
|
||||
openslides.utils.models.RESTModelMixin, # type: ignore
|
||||
models.Model,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motioncomment',
|
||||
name='section',
|
||||
model_name="motioncomment",
|
||||
name="section",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='comments',
|
||||
to='motions.MotionCommentSection'),
|
||||
related_name="comments",
|
||||
to="motions.MotionCommentSection",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motioncomment',
|
||||
name='motion',
|
||||
model_name="motioncomment",
|
||||
name="motion",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='motions.Motion'),
|
||||
on_delete=django.db.models.deletion.CASCADE, to="motions.Motion"
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='motioncomment',
|
||||
unique_together={('motion', 'section')},
|
||||
name="motioncomment", unique_together={("motion", "section")}
|
||||
),
|
||||
|
||||
# Move the comments and sections
|
||||
migrations.RunPython(create_comment_sections_from_config_and_move_comments_to_own_model),
|
||||
|
||||
# Remove old comment field from motion, use the new model instead
|
||||
migrations.RemoveField(
|
||||
model_name='motion',
|
||||
name='comments',
|
||||
migrations.RunPython(
|
||||
create_comment_sections_from_config_and_move_comments_to_own_model
|
||||
),
|
||||
# Remove old comment field from motion, use the new model instead
|
||||
migrations.RemoveField(model_name="motion", name="comments"),
|
||||
migrations.AlterField(
|
||||
model_name='motioncomment',
|
||||
name='motion',
|
||||
model_name="motioncomment",
|
||||
name="motion",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='comments',
|
||||
to='motions.Motion'),
|
||||
related_name="comments",
|
||||
to="motions.Motion",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -8,58 +8,55 @@ import openslides.utils.models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0012_motion_comments'),
|
||||
]
|
||||
dependencies = [("motions", "0012_motion_comments")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='motionblock',
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'verbose_name': 'Motion block'},
|
||||
name="motionblock",
|
||||
options={"default_permissions": (), "verbose_name": "Motion block"},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='sort_parent',
|
||||
model_name="motion",
|
||||
name="sort_parent",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='children',
|
||||
to='motions.Motion'),
|
||||
related_name="children",
|
||||
to="motions.Motion",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='weight',
|
||||
field=models.IntegerField(default=10000),
|
||||
model_name="motion", name="weight", field=models.IntegerField(default=10000)
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StatuteParagraph',
|
||||
name="StatuteParagraph",
|
||||
fields=[
|
||||
('id', models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('text', models.TextField()),
|
||||
('weight', models.IntegerField(default=10000)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=255)),
|
||||
("text", models.TextField()),
|
||||
("weight", models.IntegerField(default=10000)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['weight', 'title'],
|
||||
'default_permissions': (),
|
||||
},
|
||||
options={"ordering": ["weight", "title"], "default_permissions": ()},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='statute_paragraph',
|
||||
model_name="motion",
|
||||
name="statute_paragraph",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='motions',
|
||||
to='motions.StatuteParagraph'),
|
||||
related_name="motions",
|
||||
to="motions.StatuteParagraph",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -5,14 +5,12 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0013_motion_sorting_and_statute'),
|
||||
]
|
||||
dependencies = [("motions", "0013_motion_sorting_and_statute")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='motionchangerecommendation',
|
||||
name='internal',
|
||||
model_name="motionchangerecommendation",
|
||||
name="internal",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
)
|
||||
]
|
||||
|
@ -5,24 +5,22 @@ from django.db import migrations
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0014_motionchangerecommendation_internal'),
|
||||
]
|
||||
dependencies = [("motions", "0014_motionchangerecommendation_internal")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='motion',
|
||||
name="motion",
|
||||
options={
|
||||
'default_permissions': (),
|
||||
'ordering': ('identifier',),
|
||||
'permissions': (
|
||||
('can_see', 'Can see motions'),
|
||||
('can_create', 'Can create motions'),
|
||||
('can_support', 'Can support motions'),
|
||||
('can_manage_metadata', 'Can manage motion metadata'),
|
||||
('can_manage', 'Can manage motions')
|
||||
"default_permissions": (),
|
||||
"ordering": ("identifier",),
|
||||
"permissions": (
|
||||
("can_see", "Can see motions"),
|
||||
("can_create", "Can create motions"),
|
||||
("can_support", "Can support motions"),
|
||||
("can_manage_metadata", "Can manage motion metadata"),
|
||||
("can_manage", "Can manage motions"),
|
||||
),
|
||||
'verbose_name': 'Motion'
|
||||
"verbose_name": "Motion",
|
||||
},
|
||||
),
|
||||
)
|
||||
]
|
||||
|
@ -5,14 +5,12 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0015_metadata_permission'),
|
||||
]
|
||||
dependencies = [("motions", "0015_metadata_permission")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='state',
|
||||
name='merge_amendment_into_final',
|
||||
model_name="state",
|
||||
name="merge_amendment_into_final",
|
||||
field=models.SmallIntegerField(default=0),
|
||||
),
|
||||
)
|
||||
]
|
||||
|
@ -5,13 +5,6 @@ from django.db import migrations
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0016_merge_amendment_into_final'),
|
||||
]
|
||||
dependencies = [("motions", "0016_merge_amendment_into_final")]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='state',
|
||||
name='action_word',
|
||||
),
|
||||
]
|
||||
operations = [migrations.RemoveField(model_name="state", name="action_word")]
|
||||
|
@ -40,6 +40,7 @@ class StatuteParagraph(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Model for parts of the statute
|
||||
"""
|
||||
|
||||
access_permissions = StatuteParagraphAccessPermissions()
|
||||
|
||||
title = models.CharField(max_length=255)
|
||||
@ -55,7 +56,7 @@ class StatuteParagraph(RESTModelMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
ordering = ['weight', 'title']
|
||||
ordering = ["weight", "title"]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@ -65,25 +66,29 @@ class MotionManager(models.Manager):
|
||||
"""
|
||||
Customized model manager to support our get_full_queryset method.
|
||||
"""
|
||||
|
||||
def get_full_queryset(self):
|
||||
"""
|
||||
Returns the normal queryset with all motions. In the background we
|
||||
join and prefetch all related models.
|
||||
"""
|
||||
return (self.get_queryset()
|
||||
.select_related('state')
|
||||
.prefetch_related(
|
||||
'state__workflow',
|
||||
'comments',
|
||||
'comments__section',
|
||||
'comments__section__read_groups',
|
||||
'agenda_items',
|
||||
'log_messages',
|
||||
'polls',
|
||||
'attachments',
|
||||
'tags',
|
||||
'submitters',
|
||||
'supporters'))
|
||||
return (
|
||||
self.get_queryset()
|
||||
.select_related("state")
|
||||
.prefetch_related(
|
||||
"state__workflow",
|
||||
"comments",
|
||||
"comments__section",
|
||||
"comments__section__read_groups",
|
||||
"agenda_items",
|
||||
"log_messages",
|
||||
"polls",
|
||||
"attachments",
|
||||
"tags",
|
||||
"submitters",
|
||||
"supporters",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Motion(RESTModelMixin, models.Model):
|
||||
@ -92,8 +97,9 @@ class Motion(RESTModelMixin, models.Model):
|
||||
|
||||
This class is the main entry point to all other classes related to a motion.
|
||||
"""
|
||||
|
||||
access_permissions = MotionAccessPermissions()
|
||||
can_see_permission = 'motions.can_see'
|
||||
can_see_permission = "motions.can_see"
|
||||
|
||||
objects = MotionManager()
|
||||
|
||||
@ -119,10 +125,11 @@ class Motion(RESTModelMixin, models.Model):
|
||||
"""The reason for a motion."""
|
||||
|
||||
state = models.ForeignKey(
|
||||
'State',
|
||||
related_name='+',
|
||||
"State",
|
||||
related_name="+",
|
||||
on_delete=models.PROTECT, # Do not let the user delete states, that are used for motions
|
||||
null=True) # TODO: Check whether null=True is necessary.
|
||||
null=True,
|
||||
) # TODO: Check whether null=True is necessary.
|
||||
"""
|
||||
The related state object.
|
||||
|
||||
@ -135,10 +142,8 @@ class Motion(RESTModelMixin, models.Model):
|
||||
"""
|
||||
|
||||
recommendation = models.ForeignKey(
|
||||
'State',
|
||||
related_name='+',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True)
|
||||
"State", related_name="+", on_delete=models.SET_NULL, null=True
|
||||
)
|
||||
"""
|
||||
The recommendation of a person or committee for this motion.
|
||||
"""
|
||||
@ -148,8 +153,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
A text field fo additional information about the recommendation.
|
||||
"""
|
||||
|
||||
identifier = models.CharField(max_length=255, null=True, blank=True,
|
||||
unique=True)
|
||||
identifier = models.CharField(max_length=255, null=True, blank=True, unique=True)
|
||||
"""
|
||||
A string as human readable identifier for the motion.
|
||||
"""
|
||||
@ -167,29 +171,26 @@ class Motion(RESTModelMixin, models.Model):
|
||||
"""
|
||||
|
||||
sort_parent = models.ForeignKey(
|
||||
'self',
|
||||
"self",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='children')
|
||||
related_name="children",
|
||||
)
|
||||
"""
|
||||
A parent field for multi-depth sorting of motions.
|
||||
"""
|
||||
|
||||
category = models.ForeignKey(
|
||||
'Category',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True)
|
||||
"Category", on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
"""
|
||||
ForeignKey to one category of motions.
|
||||
"""
|
||||
|
||||
motion_block = models.ForeignKey(
|
||||
'MotionBlock',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True)
|
||||
"MotionBlock", on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
"""
|
||||
ForeignKey to one block of motions.
|
||||
"""
|
||||
@ -206,11 +207,12 @@ class Motion(RESTModelMixin, models.Model):
|
||||
"""
|
||||
|
||||
parent = models.ForeignKey(
|
||||
'self',
|
||||
"self",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='amendments')
|
||||
related_name="amendments",
|
||||
)
|
||||
"""
|
||||
Field for amendments to reference to the motion that should be altered.
|
||||
|
||||
@ -222,7 +224,8 @@ class Motion(RESTModelMixin, models.Model):
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='motions')
|
||||
related_name="motions",
|
||||
)
|
||||
"""
|
||||
Field to reference to a statute paragraph if this motion is a
|
||||
statute-amendment.
|
||||
@ -235,26 +238,28 @@ class Motion(RESTModelMixin, models.Model):
|
||||
Tags to categorise motions.
|
||||
"""
|
||||
|
||||
supporters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='motion_supporters', blank=True)
|
||||
supporters = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL, related_name="motion_supporters", blank=True
|
||||
)
|
||||
"""
|
||||
Users who support this motion.
|
||||
"""
|
||||
|
||||
# In theory there could be one then more agenda_item. But we support only
|
||||
# one. See the property agenda_item.
|
||||
agenda_items = GenericRelation(Item, related_name='motions')
|
||||
agenda_items = GenericRelation(Item, related_name="motions")
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
permissions = (
|
||||
('can_see', 'Can see motions'),
|
||||
('can_create', 'Can create motions'),
|
||||
('can_support', 'Can support motions'),
|
||||
('can_manage_metadata', 'Can manage motion metadata'),
|
||||
('can_manage', 'Can manage motions'),
|
||||
("can_see", "Can see motions"),
|
||||
("can_create", "Can create motions"),
|
||||
("can_support", "Can support motions"),
|
||||
("can_manage_metadata", "Can manage motion metadata"),
|
||||
("can_manage", "Can manage motions"),
|
||||
)
|
||||
ordering = ('identifier', )
|
||||
verbose_name = ugettext_noop('Motion')
|
||||
ordering = ("identifier",)
|
||||
verbose_name = ugettext_noop("Motion")
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
@ -284,14 +289,15 @@ class Motion(RESTModelMixin, models.Model):
|
||||
try:
|
||||
# Always skip autoupdate. Maybe we run it later in this method.
|
||||
with transaction.atomic():
|
||||
super(Motion, self).save(skip_autoupdate=True, *args, **kwargs) # type: ignore
|
||||
super(Motion, self).save( # type: ignore
|
||||
skip_autoupdate=True, *args, **kwargs
|
||||
)
|
||||
except IntegrityError:
|
||||
# Identifier is already used.
|
||||
if hasattr(self, '_identifier_prefix'):
|
||||
if hasattr(self, "_identifier_prefix"):
|
||||
# Calculate a new one and try again.
|
||||
self.identifier_number, self.identifier = self.increment_identifier_number(
|
||||
self.identifier_number,
|
||||
self._identifier_prefix,
|
||||
self.identifier_number, self._identifier_prefix
|
||||
)
|
||||
else:
|
||||
# Do not calculate a new one but reraise the IntegrityError.
|
||||
@ -310,10 +316,11 @@ class Motion(RESTModelMixin, models.Model):
|
||||
motion projector element is disabled.
|
||||
"""
|
||||
Projector.remove_any(
|
||||
skip_autoupdate=skip_autoupdate,
|
||||
name='motions/motion',
|
||||
id=self.pk)
|
||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, name="motions/motion", id=self.pk
|
||||
)
|
||||
return super().delete( # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||
)
|
||||
|
||||
def set_identifier(self):
|
||||
"""
|
||||
@ -321,27 +328,36 @@ class Motion(RESTModelMixin, models.Model):
|
||||
it is not set yet.
|
||||
"""
|
||||
# The identifier is already set or should be set manually.
|
||||
if config['motions_identifier'] == 'manually' or self.identifier:
|
||||
if config["motions_identifier"] == "manually" or self.identifier:
|
||||
# Do not set an identifier.
|
||||
return
|
||||
|
||||
# If MOTION_IDENTIFIER_WITHOUT_BLANKS is set, don't use blanks when building identifier.
|
||||
without_blank = hasattr(settings, 'MOTION_IDENTIFIER_WITHOUT_BLANKS') and settings.MOTION_IDENTIFIER_WITHOUT_BLANKS
|
||||
without_blank = (
|
||||
hasattr(settings, "MOTION_IDENTIFIER_WITHOUT_BLANKS")
|
||||
and settings.MOTION_IDENTIFIER_WITHOUT_BLANKS
|
||||
)
|
||||
|
||||
# Build prefix.
|
||||
if self.is_amendment():
|
||||
parent_identifier = self.parent.identifier or ''
|
||||
parent_identifier = self.parent.identifier or ""
|
||||
if without_blank:
|
||||
prefix = '%s%s' % (parent_identifier, config['motions_amendments_prefix'])
|
||||
prefix = "%s%s" % (
|
||||
parent_identifier,
|
||||
config["motions_amendments_prefix"],
|
||||
)
|
||||
else:
|
||||
prefix = '%s %s ' % (parent_identifier, config['motions_amendments_prefix'])
|
||||
prefix = "%s %s " % (
|
||||
parent_identifier,
|
||||
config["motions_amendments_prefix"],
|
||||
)
|
||||
elif self.category is None or not self.category.prefix:
|
||||
prefix = ''
|
||||
prefix = ""
|
||||
else:
|
||||
if without_blank:
|
||||
prefix = '%s' % self.category.prefix
|
||||
prefix = "%s" % self.category.prefix
|
||||
else:
|
||||
prefix = '%s ' % self.category.prefix
|
||||
prefix = "%s " % self.category.prefix
|
||||
self._identifier_prefix = prefix
|
||||
|
||||
# Use the already assigned identifier_number, if the motion has one.
|
||||
@ -354,20 +370,22 @@ class Motion(RESTModelMixin, models.Model):
|
||||
if self.is_amendment():
|
||||
motions = self.parent.amendments.all()
|
||||
# The motions should be counted per category.
|
||||
elif config['motions_identifier'] == 'per_category':
|
||||
elif config["motions_identifier"] == "per_category":
|
||||
motions = Motion.objects.filter(category=self.category)
|
||||
# The motions should be counted over all.
|
||||
else:
|
||||
motions = Motion.objects.all()
|
||||
|
||||
number = motions.aggregate(Max('identifier_number'))['identifier_number__max'] or 0
|
||||
number = (
|
||||
motions.aggregate(Max("identifier_number"))["identifier_number__max"]
|
||||
or 0
|
||||
)
|
||||
initial_increment = True
|
||||
|
||||
# Calculate new identifier.
|
||||
number, identifier = self.increment_identifier_number(
|
||||
number,
|
||||
prefix,
|
||||
initial_increment=initial_increment)
|
||||
number, prefix, initial_increment=initial_increment
|
||||
)
|
||||
|
||||
# Set identifier and identifier_number.
|
||||
self.identifier = identifier
|
||||
@ -380,10 +398,10 @@ class Motion(RESTModelMixin, models.Model):
|
||||
"""
|
||||
if initial_increment:
|
||||
number += 1
|
||||
identifier = '%s%s' % (prefix, self.extend_identifier_number(number))
|
||||
identifier = "%s%s" % (prefix, self.extend_identifier_number(number))
|
||||
while Motion.objects.filter(identifier=identifier).exists():
|
||||
number += 1
|
||||
identifier = '%s%s' % (prefix, self.extend_identifier_number(number))
|
||||
identifier = "%s%s" % (prefix, self.extend_identifier_number(number))
|
||||
return number, identifier
|
||||
|
||||
def extend_identifier_number(self, number):
|
||||
@ -393,10 +411,18 @@ class Motion(RESTModelMixin, models.Model):
|
||||
MOTION_IDENTIFIER_MIN_DIGITS.
|
||||
"""
|
||||
result = str(number)
|
||||
if hasattr(settings, 'MOTION_IDENTIFIER_MIN_DIGITS') and settings.MOTION_IDENTIFIER_MIN_DIGITS:
|
||||
if (
|
||||
hasattr(settings, "MOTION_IDENTIFIER_MIN_DIGITS")
|
||||
and settings.MOTION_IDENTIFIER_MIN_DIGITS
|
||||
):
|
||||
if not isinstance(settings.MOTION_IDENTIFIER_MIN_DIGITS, int):
|
||||
raise ImproperlyConfigured('Settings value MOTION_IDENTIFIER_MIN_DIGITS must be an integer.')
|
||||
result = '0' * (settings.MOTION_IDENTIFIER_MIN_DIGITS - len(str(number))) + result
|
||||
raise ImproperlyConfigured(
|
||||
"Settings value MOTION_IDENTIFIER_MIN_DIGITS must be an integer."
|
||||
)
|
||||
result = (
|
||||
"0" * (settings.MOTION_IDENTIFIER_MIN_DIGITS - len(str(number)))
|
||||
+ result
|
||||
)
|
||||
return result
|
||||
|
||||
def is_submitter(self, user):
|
||||
@ -423,7 +449,9 @@ class Motion(RESTModelMixin, models.Model):
|
||||
poll.set_options(skip_autoupdate=skip_autoupdate)
|
||||
return poll
|
||||
else:
|
||||
raise WorkflowError('You can not create a poll in state %s.' % self.state.name)
|
||||
raise WorkflowError(
|
||||
"You can not create a poll in state %s." % self.state.name
|
||||
)
|
||||
|
||||
@property
|
||||
def workflow_id(self):
|
||||
@ -464,8 +492,10 @@ class Motion(RESTModelMixin, models.Model):
|
||||
elif self.state:
|
||||
new_state = self.state.workflow.first_state
|
||||
else:
|
||||
new_state = (Workflow.objects.get(pk=config['motions_workflow']).first_state or
|
||||
Workflow.objects.get(pk=config['motions_workflow']).states.all()[0])
|
||||
new_state = (
|
||||
Workflow.objects.get(pk=config["motions_workflow"]).first_state
|
||||
or Workflow.objects.get(pk=config["motions_workflow"]).states.all()[0]
|
||||
)
|
||||
self.set_state(new_state)
|
||||
|
||||
def set_recommendation(self, recommendation):
|
||||
@ -499,7 +529,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
Note: It has to be the same return value like in JavaScript.
|
||||
"""
|
||||
if self.identifier:
|
||||
title = '%s %s' % (_(self._meta.verbose_name), self.identifier)
|
||||
title = "%s %s" % (_(self._meta.verbose_name), self.identifier)
|
||||
else:
|
||||
title = self.title
|
||||
return title
|
||||
@ -512,9 +542,9 @@ class Motion(RESTModelMixin, models.Model):
|
||||
Note: It has to be the same return value like in JavaScript.
|
||||
"""
|
||||
if self.identifier:
|
||||
title = '%s %s' % (_(self._meta.verbose_name), self.identifier)
|
||||
title = "%s %s" % (_(self._meta.verbose_name), self.identifier)
|
||||
else:
|
||||
title = '%s (%s)' % (self.title, _(self._meta.verbose_name))
|
||||
title = "%s (%s)" % (self.title, _(self._meta.verbose_name))
|
||||
return title
|
||||
|
||||
@property
|
||||
@ -552,7 +582,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
A motion is a amendment if amendments are activated in the config and
|
||||
the motion has a parent.
|
||||
"""
|
||||
return config['motions_amendments_enabled'] and self.parent is not None
|
||||
return config["motions_amendments_enabled"] and self.parent is not None
|
||||
|
||||
def is_paragraph_based_amendment(self):
|
||||
"""
|
||||
@ -574,7 +604,12 @@ class Motion(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Returns a list of all paragraph-based amendments to this motion
|
||||
"""
|
||||
return list(filter(lambda amend: amend.is_paragraph_based_amendment(), self.amendments.all()))
|
||||
return list(
|
||||
filter(
|
||||
lambda amend: amend.is_paragraph_based_amendment(),
|
||||
self.amendments.all(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class MotionCommentSection(RESTModelMixin, models.Model):
|
||||
@ -582,6 +617,7 @@ class MotionCommentSection(RESTModelMixin, models.Model):
|
||||
The model for comment sections for motions. Each comment is related to one section, so
|
||||
each motions has the ability to have comments from the same section.
|
||||
"""
|
||||
|
||||
access_permissions = MotionCommentSectionAccessPermissions()
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
@ -590,17 +626,15 @@ class MotionCommentSection(RESTModelMixin, models.Model):
|
||||
"""
|
||||
|
||||
read_groups = models.ManyToManyField(
|
||||
settings.AUTH_GROUP_MODEL,
|
||||
blank=True,
|
||||
related_name='read_comments')
|
||||
settings.AUTH_GROUP_MODEL, blank=True, related_name="read_comments"
|
||||
)
|
||||
"""
|
||||
These groups have read-access to the section.
|
||||
"""
|
||||
|
||||
write_groups = models.ManyToManyField(
|
||||
settings.AUTH_GROUP_MODEL,
|
||||
blank=True,
|
||||
related_name='write_comments')
|
||||
settings.AUTH_GROUP_MODEL, blank=True, related_name="write_comments"
|
||||
)
|
||||
"""
|
||||
These groups have write-access to the section.
|
||||
"""
|
||||
@ -621,24 +655,22 @@ class MotionComment(RESTModelMixin, models.Model):
|
||||
"""
|
||||
|
||||
motion = models.ForeignKey(
|
||||
Motion,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='comments')
|
||||
Motion, on_delete=models.CASCADE, related_name="comments"
|
||||
)
|
||||
"""
|
||||
The motion where this comment belongs to.
|
||||
"""
|
||||
|
||||
section = models.ForeignKey(
|
||||
MotionCommentSection,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='comments')
|
||||
MotionCommentSection, on_delete=models.PROTECT, related_name="comments"
|
||||
)
|
||||
"""
|
||||
The section of the comment.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
unique_together = ('motion', 'section')
|
||||
unique_together = ("motion", "section")
|
||||
|
||||
def get_root_rest_element(self):
|
||||
"""
|
||||
@ -651,6 +683,7 @@ class SubmitterManager(models.Manager):
|
||||
"""
|
||||
Manager for Submitter model. Provides a customized add method.
|
||||
"""
|
||||
|
||||
def add(self, user, motion, skip_autoupdate=False):
|
||||
"""
|
||||
Customized manager method to prevent anonymous users to be a
|
||||
@ -658,13 +691,13 @@ class SubmitterManager(models.Manager):
|
||||
for the initial sorting of the submitters.
|
||||
"""
|
||||
if self.filter(user=user, motion=motion).exists():
|
||||
raise OpenSlidesError(
|
||||
_('{user} is already a submitter.').format(user=user))
|
||||
raise OpenSlidesError(_("{user} is already a submitter.").format(user=user))
|
||||
if isinstance(user, AnonymousUser):
|
||||
raise OpenSlidesError(
|
||||
_('An anonymous user can not be a submitter.'))
|
||||
weight = (self.filter(motion=motion).aggregate(
|
||||
models.Max('weight'))['weight__max'] or 0)
|
||||
raise OpenSlidesError(_("An anonymous user can not be a submitter."))
|
||||
weight = (
|
||||
self.filter(motion=motion).aggregate(models.Max("weight"))["weight__max"]
|
||||
or 0
|
||||
)
|
||||
submitter = self.model(user=user, motion=motion, weight=weight + 1)
|
||||
submitter.save(force_insert=True, skip_autoupdate=skip_autoupdate)
|
||||
return submitter
|
||||
@ -680,17 +713,14 @@ class Submitter(RESTModelMixin, models.Model):
|
||||
Use custom Manager.
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
"""
|
||||
ForeignKey to the user who is the submitter.
|
||||
"""
|
||||
|
||||
motion = models.ForeignKey(
|
||||
Motion,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='submitters')
|
||||
Motion, on_delete=models.CASCADE, related_name="submitters"
|
||||
)
|
||||
"""
|
||||
ForeignKey to the motion.
|
||||
"""
|
||||
@ -714,6 +744,7 @@ class MotionChangeRecommendationManager(models.Manager):
|
||||
"""
|
||||
Customized model manager to support our get_full_queryset method.
|
||||
"""
|
||||
|
||||
def get_full_queryset(self):
|
||||
"""
|
||||
Returns the normal queryset with all change recommendations. In the background we
|
||||
@ -732,9 +763,8 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
||||
objects = MotionChangeRecommendationManager()
|
||||
|
||||
motion = models.ForeignKey(
|
||||
Motion,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='change_recommendations')
|
||||
Motion, on_delete=models.CASCADE, related_name="change_recommendations"
|
||||
)
|
||||
"""The motion to which the change recommendation belongs."""
|
||||
|
||||
rejected = models.BooleanField(default=False)
|
||||
@ -759,9 +789,8 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
||||
"""The replacement for the section of the original text specified by motion, line_from and line_to"""
|
||||
|
||||
author = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True)
|
||||
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True
|
||||
)
|
||||
"""A user object, who created this change recommendation. Optional."""
|
||||
|
||||
creation_time = models.DateTimeField(auto_now=True)
|
||||
@ -769,20 +798,27 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
||||
|
||||
def collides_with_other_recommendation(self, recommendations):
|
||||
for recommendation in recommendations:
|
||||
if (not (self.line_from < recommendation.line_from and self.line_to <= recommendation.line_from) and
|
||||
not (self.line_from >= recommendation.line_to and self.line_to > recommendation.line_to)):
|
||||
if not (
|
||||
self.line_from < recommendation.line_from
|
||||
and self.line_to <= recommendation.line_from
|
||||
) and not (
|
||||
self.line_from >= recommendation.line_to
|
||||
and self.line_to > recommendation.line_to
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
recommendations = (MotionChangeRecommendation.objects
|
||||
.filter(motion=self.motion)
|
||||
.exclude(pk=self.pk))
|
||||
recommendations = MotionChangeRecommendation.objects.filter(
|
||||
motion=self.motion
|
||||
).exclude(pk=self.pk)
|
||||
|
||||
if self.collides_with_other_recommendation(recommendations):
|
||||
raise ValidationError('The recommendation collides with an existing one (line %s - %s).' %
|
||||
(self.line_from, self.line_to))
|
||||
raise ValidationError(
|
||||
"The recommendation collides with an existing one (line %s - %s)."
|
||||
% (self.line_from, self.line_to)
|
||||
)
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@ -791,13 +827,18 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string, representing this object."""
|
||||
return "Recommendation for Motion %s, line %s - %s" % (self.motion_id, self.line_from, self.line_to)
|
||||
return "Recommendation for Motion %s, line %s - %s" % (
|
||||
self.motion_id,
|
||||
self.line_from,
|
||||
self.line_to,
|
||||
)
|
||||
|
||||
|
||||
class Category(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Model for categories of motions.
|
||||
"""
|
||||
|
||||
access_permissions = CategoryAccessPermissions()
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
@ -811,7 +852,7 @@ class Category(RESTModelMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
ordering = ['prefix']
|
||||
ordering = ["prefix"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -821,18 +862,20 @@ class MotionBlockManager(models.Manager):
|
||||
"""
|
||||
Customized model manager to support our get_full_queryset method.
|
||||
"""
|
||||
|
||||
def get_full_queryset(self):
|
||||
"""
|
||||
Returns the normal queryset with all motion blocks. In the
|
||||
background the related agenda item is prefetched from the database.
|
||||
"""
|
||||
return self.get_queryset().prefetch_related('agenda_items')
|
||||
return self.get_queryset().prefetch_related("agenda_items")
|
||||
|
||||
|
||||
class MotionBlock(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Model for blocks of motions.
|
||||
"""
|
||||
|
||||
access_permissions = MotionBlockAccessPermissions()
|
||||
|
||||
objects = MotionBlockManager()
|
||||
@ -841,10 +884,10 @@ class MotionBlock(RESTModelMixin, models.Model):
|
||||
|
||||
# In theory there could be one then more agenda_item. But we support only
|
||||
# one. See the property agenda_item.
|
||||
agenda_items = GenericRelation(Item, related_name='topics')
|
||||
agenda_items = GenericRelation(Item, related_name="topics")
|
||||
|
||||
class Meta:
|
||||
verbose_name = ugettext_noop('Motion block')
|
||||
verbose_name = ugettext_noop("Motion block")
|
||||
default_permissions = ()
|
||||
|
||||
def __str__(self):
|
||||
@ -856,10 +899,11 @@ class MotionBlock(RESTModelMixin, models.Model):
|
||||
motion block projector element is disabled.
|
||||
"""
|
||||
Projector.remove_any(
|
||||
skip_autoupdate=skip_autoupdate,
|
||||
name='motions/motion-block',
|
||||
id=self.pk)
|
||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, name="motions/motion-block", id=self.pk
|
||||
)
|
||||
return super().delete( # type: ignore
|
||||
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||
)
|
||||
|
||||
"""
|
||||
Container for runtime information for agenda app (on create or update of this instance).
|
||||
@ -886,16 +930,15 @@ class MotionBlock(RESTModelMixin, models.Model):
|
||||
return self.title
|
||||
|
||||
def get_agenda_title_with_type(self):
|
||||
return '%s (%s)' % (self.get_agenda_title(), _(self._meta.verbose_name))
|
||||
return "%s (%s)" % (self.get_agenda_title(), _(self._meta.verbose_name))
|
||||
|
||||
|
||||
class MotionLog(RESTModelMixin, models.Model):
|
||||
"""Save a logmessage for a motion."""
|
||||
|
||||
motion = models.ForeignKey(
|
||||
Motion,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='log_messages')
|
||||
Motion, on_delete=models.CASCADE, related_name="log_messages"
|
||||
)
|
||||
"""The motion to witch the object belongs."""
|
||||
|
||||
message_list = JSONField()
|
||||
@ -904,9 +947,8 @@ class MotionLog(RESTModelMixin, models.Model):
|
||||
"""
|
||||
|
||||
person = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True)
|
||||
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True
|
||||
)
|
||||
"""A user object, who created the log message. Optional."""
|
||||
|
||||
time = models.DateTimeField(auto_now=True)
|
||||
@ -914,18 +956,20 @@ class MotionLog(RESTModelMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
ordering = ['-time']
|
||||
ordering = ["-time"]
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Return a string, representing the log message.
|
||||
"""
|
||||
localtime = timezone.localtime(self.time)
|
||||
time = formats.date_format(localtime, 'DATETIME_FORMAT')
|
||||
time_and_messages = '%s ' % time + ''.join(map(_, self.message_list))
|
||||
time = formats.date_format(localtime, "DATETIME_FORMAT")
|
||||
time_and_messages = "%s " % time + "".join(map(_, self.message_list))
|
||||
if self.person is not None:
|
||||
return _('%(time_and_messages)s by %(person)s') % {'time_and_messages': time_and_messages,
|
||||
'person': self.person}
|
||||
return _("%(time_and_messages)s by %(person)s") % {
|
||||
"time_and_messages": time_and_messages,
|
||||
"person": self.person,
|
||||
}
|
||||
return time_and_messages
|
||||
|
||||
def get_root_rest_element(self):
|
||||
@ -941,9 +985,7 @@ class MotionVote(RESTModelMixin, BaseVote):
|
||||
There should allways be three MotionVote objects for each poll,
|
||||
one for 'yes', 'no', and 'abstain'."""
|
||||
|
||||
option = models.ForeignKey(
|
||||
'MotionOption',
|
||||
on_delete=models.CASCADE)
|
||||
option = models.ForeignKey("MotionOption", on_delete=models.CASCADE)
|
||||
"""The option object, to witch the vote belongs."""
|
||||
|
||||
class Meta:
|
||||
@ -961,9 +1003,7 @@ class MotionOption(RESTModelMixin, BaseOption):
|
||||
|
||||
There should be one MotionOption object for each poll."""
|
||||
|
||||
poll = models.ForeignKey(
|
||||
'MotionPoll',
|
||||
on_delete=models.CASCADE)
|
||||
poll = models.ForeignKey("MotionPoll", on_delete=models.CASCADE)
|
||||
"""The poll object, to witch the object belongs."""
|
||||
|
||||
vote_class = MotionVote
|
||||
@ -984,16 +1024,13 @@ class MotionOption(RESTModelMixin, BaseOption):
|
||||
class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: ignore
|
||||
"""The Class to saves the vote result for a motion poll."""
|
||||
|
||||
motion = models.ForeignKey(
|
||||
Motion,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='polls')
|
||||
motion = models.ForeignKey(Motion, on_delete=models.CASCADE, related_name="polls")
|
||||
"""The motion to witch the object belongs."""
|
||||
|
||||
option_class = MotionOption
|
||||
"""The option class, witch links between this object the the votes."""
|
||||
|
||||
vote_values = ['Yes', 'No', 'Abstain']
|
||||
vote_values = ["Yes", "No", "Abstain"]
|
||||
"""The possible anwers for the poll. 'Yes, 'No' and 'Abstain'."""
|
||||
|
||||
class Meta:
|
||||
@ -1003,7 +1040,7 @@ class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: i
|
||||
"""
|
||||
Representation method only for debugging purposes.
|
||||
"""
|
||||
return 'MotionPoll for motion %s' % self.motion
|
||||
return "MotionPoll for motion %s" % self.motion
|
||||
|
||||
def set_options(self, skip_autoupdate=False):
|
||||
"""Create the option class for this poll."""
|
||||
@ -1012,7 +1049,7 @@ class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: i
|
||||
self.get_option_class()(poll=self).save(skip_autoupdate=skip_autoupdate)
|
||||
|
||||
def get_percent_base_choice(self):
|
||||
return config['motions_poll_100_percent_base']
|
||||
return config["motions_poll_100_percent_base"]
|
||||
|
||||
def get_slide_context(self, **context):
|
||||
return super(MotionPoll, self).get_slide_context(poll=self)
|
||||
@ -1047,15 +1084,14 @@ class State(RESTModelMixin, models.Model):
|
||||
"""A string for a recommendation to set the motion to this state."""
|
||||
|
||||
workflow = models.ForeignKey(
|
||||
'Workflow',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='states')
|
||||
"Workflow", on_delete=models.CASCADE, related_name="states"
|
||||
)
|
||||
"""A many-to-one relation to a workflow."""
|
||||
|
||||
next_states = models.ManyToManyField('self', symmetrical=False, blank=True)
|
||||
next_states = models.ManyToManyField("self", symmetrical=False, blank=True)
|
||||
"""A many-to-many relation to all states, that can be choosen from this state."""
|
||||
|
||||
css_class = models.CharField(max_length=255, default='primary')
|
||||
css_class = models.CharField(max_length=255, default="primary")
|
||||
"""
|
||||
A css class string for showing the state name in a coloured label based on bootstrap,
|
||||
e.g. 'danger' (red), 'success' (green), 'warning' (yellow), 'default' (grey).
|
||||
@ -1131,9 +1167,11 @@ class State(RESTModelMixin, models.Model):
|
||||
recommendation_label is not an empty string.
|
||||
"""
|
||||
self.check_next_states()
|
||||
if self.recommendation_label == '':
|
||||
raise WorkflowError('The field recommendation_label of {} must not '
|
||||
'be an empty string.'.format(self))
|
||||
if self.recommendation_label == "":
|
||||
raise WorkflowError(
|
||||
"The field recommendation_label of {} must not "
|
||||
"be an empty string.".format(self)
|
||||
)
|
||||
super(State, self).save(**kwargs)
|
||||
|
||||
def check_next_states(self):
|
||||
@ -1143,7 +1181,10 @@ class State(RESTModelMixin, models.Model):
|
||||
return
|
||||
for state in self.next_states.all():
|
||||
if not state.workflow == self.workflow:
|
||||
raise WorkflowError('%s can not be next state of %s because it does not belong to the same workflow.' % (state, self))
|
||||
raise WorkflowError(
|
||||
"%s can not be next state of %s because it does not belong to the same workflow."
|
||||
% (state, self)
|
||||
)
|
||||
|
||||
def get_root_rest_element(self):
|
||||
"""
|
||||
@ -1156,21 +1197,25 @@ class WorkflowManager(models.Manager):
|
||||
"""
|
||||
Customized model manager to support our get_full_queryset method.
|
||||
"""
|
||||
|
||||
def get_full_queryset(self):
|
||||
"""
|
||||
Returns the normal queryset with all workflows. In the background
|
||||
the first state is joined and all states and next states are
|
||||
prefetched from the database.
|
||||
"""
|
||||
return (self.get_queryset()
|
||||
.select_related('first_state')
|
||||
.prefetch_related('states', 'states__next_states'))
|
||||
return (
|
||||
self.get_queryset()
|
||||
.select_related("first_state")
|
||||
.prefetch_related("states", "states__next_states")
|
||||
)
|
||||
|
||||
|
||||
class Workflow(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Defines a workflow for a motion.
|
||||
"""
|
||||
|
||||
access_permissions = WorkflowAccessPermissions()
|
||||
|
||||
objects = WorkflowManager()
|
||||
@ -1179,11 +1224,8 @@ class Workflow(RESTModelMixin, models.Model):
|
||||
"""A string representing the workflow."""
|
||||
|
||||
first_state = models.OneToOneField(
|
||||
State,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='+',
|
||||
null=True,
|
||||
blank=True)
|
||||
State, on_delete=models.SET_NULL, related_name="+", null=True, blank=True
|
||||
)
|
||||
"""A one-to-one relation to a state, the starting point for the workflow."""
|
||||
|
||||
class Meta:
|
||||
@ -1205,5 +1247,6 @@ class Workflow(RESTModelMixin, models.Model):
|
||||
"""Checks whether the first_state itself belongs to the workflow."""
|
||||
if self.first_state and not self.first_state.workflow == self:
|
||||
raise WorkflowError(
|
||||
'%s can not be first state of %s because it '
|
||||
'does not belong to it.' % (self.first_state, self))
|
||||
"%s can not be first state of %s because it "
|
||||
"does not belong to it." % (self.first_state, self)
|
||||
)
|
||||
|
@ -9,21 +9,22 @@ class MotionSlide(ProjectorElement):
|
||||
"""
|
||||
Slide definitions for Motion model.
|
||||
"""
|
||||
name = 'motions/motion'
|
||||
|
||||
name = "motions/motion"
|
||||
|
||||
def check_data(self):
|
||||
if not Motion.objects.filter(pk=self.config_entry.get('id')).exists():
|
||||
raise ProjectorException('Motion does not exist.')
|
||||
if not Motion.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||
raise ProjectorException("Motion does not exist.")
|
||||
|
||||
def update_data(self):
|
||||
data = None
|
||||
try:
|
||||
motion = Motion.objects.get(pk=self.config_entry.get('id'))
|
||||
motion = Motion.objects.get(pk=self.config_entry.get("id"))
|
||||
except Motion.DoesNotExist:
|
||||
# Motion does not exist, so just do nothing.
|
||||
pass
|
||||
else:
|
||||
data = {'agenda_item_id': motion.agenda_item_id}
|
||||
data = {"agenda_item_id": motion.agenda_item_id}
|
||||
return data
|
||||
|
||||
|
||||
@ -31,21 +32,22 @@ class MotionBlockSlide(ProjectorElement):
|
||||
"""
|
||||
Slide definitions for a block of motions (MotionBlock model).
|
||||
"""
|
||||
name = 'motions/motion-block'
|
||||
|
||||
name = "motions/motion-block"
|
||||
|
||||
def check_data(self):
|
||||
if not MotionBlock.objects.filter(pk=self.config_entry.get('id')).exists():
|
||||
raise ProjectorException('MotionBlock does not exist.')
|
||||
if not MotionBlock.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||
raise ProjectorException("MotionBlock does not exist.")
|
||||
|
||||
def update_data(self):
|
||||
data = None
|
||||
try:
|
||||
motion_block = MotionBlock.objects.get(pk=self.config_entry.get('id'))
|
||||
motion_block = MotionBlock.objects.get(pk=self.config_entry.get("id"))
|
||||
except MotionBlock.DoesNotExist:
|
||||
# MotionBlock does not exist, so just do nothing.
|
||||
pass
|
||||
else:
|
||||
data = {'agenda_item_id': motion_block.agenda_item_id}
|
||||
data = {"agenda_item_id": motion_block.agenda_item_id}
|
||||
return data
|
||||
|
||||
|
||||
|
@ -39,48 +39,55 @@ def validate_workflow_field(value):
|
||||
Validator to ensure that the workflow with the given id exists.
|
||||
"""
|
||||
if not Workflow.objects.filter(pk=value).exists():
|
||||
raise ValidationError({'detail': _('Workflow %(pk)d does not exist.') % {'pk': value}})
|
||||
raise ValidationError(
|
||||
{"detail": _("Workflow %(pk)d does not exist.") % {"pk": value}}
|
||||
)
|
||||
|
||||
|
||||
class StatuteParagraphSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.StatuteParagraph objects.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = StatuteParagraph
|
||||
fields = ('id', 'title', 'text', 'weight')
|
||||
fields = ("id", "title", "text", "weight")
|
||||
|
||||
|
||||
class CategorySerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.Category objects.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ('id', 'name', 'prefix',)
|
||||
fields = ("id", "name", "prefix")
|
||||
|
||||
|
||||
class MotionBlockSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.Category objects.
|
||||
"""
|
||||
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3)
|
||||
|
||||
agenda_type = IntegerField(
|
||||
write_only=True, required=False, min_value=1, max_value=3
|
||||
)
|
||||
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
|
||||
|
||||
class Meta:
|
||||
model = MotionBlock
|
||||
fields = ('id', 'title', 'agenda_item_id', 'agenda_type', 'agenda_parent_id',)
|
||||
fields = ("id", "title", "agenda_item_id", "agenda_type", "agenda_parent_id")
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
Customized create method. Set information about related agenda item
|
||||
into agenda_item_update_information container.
|
||||
"""
|
||||
agenda_type = validated_data.pop('agenda_type', None)
|
||||
agenda_parent_id = validated_data.pop('agenda_parent_id', None)
|
||||
agenda_type = validated_data.pop("agenda_type", None)
|
||||
agenda_parent_id = validated_data.pop("agenda_parent_id", None)
|
||||
motion_block = MotionBlock(**validated_data)
|
||||
motion_block.agenda_item_update_information['type'] = agenda_type
|
||||
motion_block.agenda_item_update_information['parent_id'] = agenda_parent_id
|
||||
motion_block.agenda_item_update_information["type"] = agenda_type
|
||||
motion_block.agenda_item_update_information["parent_id"] = agenda_parent_id
|
||||
motion_block.save()
|
||||
return motion_block
|
||||
|
||||
@ -89,35 +96,38 @@ class StateSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.State objects.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = State
|
||||
fields = (
|
||||
'id',
|
||||
'name',
|
||||
'recommendation_label',
|
||||
'css_class',
|
||||
'required_permission_to_see',
|
||||
'allow_support',
|
||||
'allow_create_poll',
|
||||
'allow_submitter_edit',
|
||||
'dont_set_identifier',
|
||||
'show_state_extension_field',
|
||||
'merge_amendment_into_final',
|
||||
'show_recommendation_extension_field',
|
||||
'next_states',
|
||||
'workflow')
|
||||
"id",
|
||||
"name",
|
||||
"recommendation_label",
|
||||
"css_class",
|
||||
"required_permission_to_see",
|
||||
"allow_support",
|
||||
"allow_create_poll",
|
||||
"allow_submitter_edit",
|
||||
"dont_set_identifier",
|
||||
"show_state_extension_field",
|
||||
"merge_amendment_into_final",
|
||||
"show_recommendation_extension_field",
|
||||
"next_states",
|
||||
"workflow",
|
||||
)
|
||||
|
||||
|
||||
class WorkflowSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.Workflow objects.
|
||||
"""
|
||||
|
||||
states = StateSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Workflow
|
||||
fields = ('id', 'name', 'states', 'first_state',)
|
||||
read_only_fields = ('first_state',)
|
||||
fields = ("id", "name", "states", "first_state")
|
||||
read_only_fields = ("first_state",)
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
@ -127,11 +137,11 @@ class WorkflowSerializer(ModelSerializer):
|
||||
"""
|
||||
workflow = super().create(validated_data)
|
||||
first_state = State.objects.create(
|
||||
name='new',
|
||||
name="new",
|
||||
workflow=workflow,
|
||||
allow_create_poll=True,
|
||||
allow_support=True,
|
||||
allow_submitter_edit=True
|
||||
allow_submitter_edit=True,
|
||||
)
|
||||
workflow.first_state = first_state
|
||||
workflow.save()
|
||||
@ -142,6 +152,7 @@ class AmendmentParagraphsJSONSerializerField(Field):
|
||||
"""
|
||||
Serializer for motions's amendment_paragraphs JSONField.
|
||||
"""
|
||||
|
||||
def to_representation(self, obj):
|
||||
"""
|
||||
Returns the value of the field.
|
||||
@ -153,10 +164,12 @@ class AmendmentParagraphsJSONSerializerField(Field):
|
||||
Checks that data is a list of strings.
|
||||
"""
|
||||
if type(data) is not list:
|
||||
raise ValidationError({'detail': 'Data must be a list.'})
|
||||
raise ValidationError({"detail": "Data must be a list."})
|
||||
for paragraph in data:
|
||||
if type(paragraph) is not str and paragraph is not None:
|
||||
raise ValidationError({'detail': 'Paragraph must be either a string or null/None.'})
|
||||
raise ValidationError(
|
||||
{"detail": "Paragraph must be either a string or null/None."}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@ -164,11 +177,12 @@ class MotionLogSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.MotionLog objects.
|
||||
"""
|
||||
|
||||
message = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = MotionLog
|
||||
fields = ('message_list', 'person', 'time', 'message',)
|
||||
fields = ("message_list", "person", "time", "message")
|
||||
|
||||
def get_message(self, obj):
|
||||
"""
|
||||
@ -181,27 +195,32 @@ class MotionPollSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.MotionPoll objects.
|
||||
"""
|
||||
|
||||
yes = SerializerMethodField()
|
||||
no = SerializerMethodField()
|
||||
abstain = SerializerMethodField()
|
||||
votes = DictField(
|
||||
child=DecimalField(max_digits=15, decimal_places=6, min_value=-2, allow_null=True),
|
||||
write_only=True)
|
||||
child=DecimalField(
|
||||
max_digits=15, decimal_places=6, min_value=-2, allow_null=True
|
||||
),
|
||||
write_only=True,
|
||||
)
|
||||
has_votes = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = MotionPoll
|
||||
fields = (
|
||||
'id',
|
||||
'motion',
|
||||
'yes',
|
||||
'no',
|
||||
'abstain',
|
||||
'votesvalid',
|
||||
'votesinvalid',
|
||||
'votescast',
|
||||
'votes',
|
||||
'has_votes')
|
||||
"id",
|
||||
"motion",
|
||||
"yes",
|
||||
"no",
|
||||
"abstain",
|
||||
"votesvalid",
|
||||
"votesinvalid",
|
||||
"votescast",
|
||||
"votes",
|
||||
"has_votes",
|
||||
)
|
||||
validators = (default_votes_validator,)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -211,21 +230,21 @@ class MotionPollSerializer(ModelSerializer):
|
||||
|
||||
def get_yes(self, obj):
|
||||
try:
|
||||
result: Optional[str] = str(self.get_votes_dict(obj)['Yes'])
|
||||
result: Optional[str] = str(self.get_votes_dict(obj)["Yes"])
|
||||
except KeyError:
|
||||
result = None
|
||||
return result
|
||||
|
||||
def get_no(self, obj):
|
||||
try:
|
||||
result: Optional[str] = str(self.get_votes_dict(obj)['No'])
|
||||
result: Optional[str] = str(self.get_votes_dict(obj)["No"])
|
||||
except KeyError:
|
||||
result = None
|
||||
return result
|
||||
|
||||
def get_abstain(self, obj):
|
||||
try:
|
||||
result: Optional[str] = str(self.get_votes_dict(obj)['Abstain'])
|
||||
result: Optional[str] = str(self.get_votes_dict(obj)["Abstain"])
|
||||
except KeyError:
|
||||
result = None
|
||||
return result
|
||||
@ -256,21 +275,30 @@ class MotionPollSerializer(ModelSerializer):
|
||||
"votes": {"Yes": 10, "No": 4, "Abstain": -2}
|
||||
"""
|
||||
# Update votes.
|
||||
votes = validated_data.get('votes')
|
||||
votes = validated_data.get("votes")
|
||||
if votes:
|
||||
if len(votes) != len(instance.get_vote_values()):
|
||||
raise ValidationError({
|
||||
'detail': _('You have to submit data for %d vote values.') % len(instance.get_vote_values())})
|
||||
raise ValidationError(
|
||||
{
|
||||
"detail": _("You have to submit data for %d vote values.")
|
||||
% len(instance.get_vote_values())
|
||||
}
|
||||
)
|
||||
for vote_value, vote_weight in votes.items():
|
||||
if vote_value not in instance.get_vote_values():
|
||||
raise ValidationError({
|
||||
'detail': _('Vote value %s is invalid.') % vote_value})
|
||||
instance.set_vote_objects_with_values(instance.get_options().get(), votes, skip_autoupdate=True)
|
||||
raise ValidationError(
|
||||
{"detail": _("Vote value %s is invalid.") % vote_value}
|
||||
)
|
||||
instance.set_vote_objects_with_values(
|
||||
instance.get_options().get(), votes, skip_autoupdate=True
|
||||
)
|
||||
|
||||
# Update remaining writeable fields.
|
||||
instance.votesvalid = validated_data.get('votesvalid', instance.votesvalid)
|
||||
instance.votesinvalid = validated_data.get('votesinvalid', instance.votesinvalid)
|
||||
instance.votescast = validated_data.get('votescast', instance.votescast)
|
||||
instance.votesvalid = validated_data.get("votesvalid", instance.votesvalid)
|
||||
instance.votesinvalid = validated_data.get(
|
||||
"votesinvalid", instance.votesinvalid
|
||||
)
|
||||
instance.votescast = validated_data.get("votescast", instance.votescast)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
@ -279,27 +307,29 @@ class MotionChangeRecommendationSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.MotionChangeRecommendation objects.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = MotionChangeRecommendation
|
||||
fields = (
|
||||
'id',
|
||||
'motion',
|
||||
'rejected',
|
||||
'internal',
|
||||
'type',
|
||||
'other_description',
|
||||
'line_from',
|
||||
'line_to',
|
||||
'text',
|
||||
'creation_time',)
|
||||
"id",
|
||||
"motion",
|
||||
"rejected",
|
||||
"internal",
|
||||
"type",
|
||||
"other_description",
|
||||
"line_from",
|
||||
"line_to",
|
||||
"text",
|
||||
"creation_time",
|
||||
)
|
||||
|
||||
def is_title_cr(self, data):
|
||||
return int(data['line_from']) == 0 and int(data['line_to']) == 0
|
||||
return int(data["line_from"]) == 0 and int(data["line_to"]) == 0
|
||||
|
||||
def validate(self, data):
|
||||
# Change recommendations for titles are stored as plain-text, thus they don't need to be html-escaped
|
||||
if 'text' in data and not self.is_title_cr(data):
|
||||
data['text'] = validate_html(data['text'])
|
||||
if "text" in data and not self.is_title_cr(data):
|
||||
data["text"] = validate_html(data["text"])
|
||||
return data
|
||||
|
||||
|
||||
@ -307,23 +337,18 @@ class MotionCommentSectionSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.MotionCommentSection objects.
|
||||
"""
|
||||
|
||||
read_groups = IdPrimaryKeyRelatedField(
|
||||
many=True,
|
||||
required=False,
|
||||
queryset=get_group_model().objects.all())
|
||||
many=True, required=False, queryset=get_group_model().objects.all()
|
||||
)
|
||||
|
||||
write_groups = IdPrimaryKeyRelatedField(
|
||||
many=True,
|
||||
required=False,
|
||||
queryset=get_group_model().objects.all())
|
||||
many=True, required=False, queryset=get_group_model().objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = MotionCommentSection
|
||||
fields = (
|
||||
'id',
|
||||
'name',
|
||||
'read_groups',
|
||||
'write_groups',)
|
||||
fields = ("id", "name", "read_groups", "write_groups")
|
||||
|
||||
def create(self, validated_data):
|
||||
""" Call inform_changed_data on creation, so the cache includes the groups. """
|
||||
@ -336,15 +361,12 @@ class MotionCommentSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.MotionComment objects.
|
||||
"""
|
||||
|
||||
read_groups_id = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = MotionComment
|
||||
fields = (
|
||||
'id',
|
||||
'comment',
|
||||
'section',
|
||||
'read_groups_id',)
|
||||
fields = ("id", "comment", "section", "read_groups_id")
|
||||
|
||||
def get_read_groups_id(self, comment):
|
||||
return [group.id for group in comment.section.read_groups.all()]
|
||||
@ -354,20 +376,17 @@ class SubmitterSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.Submitter objects.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Submitter
|
||||
fields = (
|
||||
'id',
|
||||
'user',
|
||||
'motion',
|
||||
'weight',
|
||||
)
|
||||
fields = ("id", "user", "motion", "weight")
|
||||
|
||||
|
||||
class MotionSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.Motion objects.
|
||||
"""
|
||||
|
||||
comments = MotionCommentSerializer(many=True, read_only=True)
|
||||
log_messages = MotionLogSerializer(many=True, read_only=True)
|
||||
polls = MotionPollSerializer(many=True, read_only=True)
|
||||
@ -378,67 +397,76 @@ class MotionSerializer(ModelSerializer):
|
||||
title = CharField(max_length=255)
|
||||
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False)
|
||||
workflow_id = IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
validators=[validate_workflow_field])
|
||||
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3)
|
||||
min_value=1, required=False, validators=[validate_workflow_field]
|
||||
)
|
||||
agenda_type = IntegerField(
|
||||
write_only=True, required=False, min_value=1, max_value=3
|
||||
)
|
||||
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
|
||||
submitters = SubmitterSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Motion
|
||||
fields = (
|
||||
'id',
|
||||
'identifier',
|
||||
'title',
|
||||
'text',
|
||||
'amendment_paragraphs',
|
||||
'modified_final_version',
|
||||
'reason',
|
||||
'parent',
|
||||
'category',
|
||||
'comments',
|
||||
'motion_block',
|
||||
'origin',
|
||||
'submitters',
|
||||
'supporters',
|
||||
'state',
|
||||
'state_extension',
|
||||
'state_required_permission_to_see',
|
||||
'statute_paragraph',
|
||||
'workflow_id',
|
||||
'recommendation',
|
||||
'recommendation_extension',
|
||||
'tags',
|
||||
'attachments',
|
||||
'polls',
|
||||
'agenda_item_id',
|
||||
'agenda_type',
|
||||
'agenda_parent_id',
|
||||
'log_messages',
|
||||
'sort_parent',
|
||||
'weight',)
|
||||
read_only_fields = ('state', 'recommendation',) # Some other fields are also read_only. See definitions above.
|
||||
"id",
|
||||
"identifier",
|
||||
"title",
|
||||
"text",
|
||||
"amendment_paragraphs",
|
||||
"modified_final_version",
|
||||
"reason",
|
||||
"parent",
|
||||
"category",
|
||||
"comments",
|
||||
"motion_block",
|
||||
"origin",
|
||||
"submitters",
|
||||
"supporters",
|
||||
"state",
|
||||
"state_extension",
|
||||
"state_required_permission_to_see",
|
||||
"statute_paragraph",
|
||||
"workflow_id",
|
||||
"recommendation",
|
||||
"recommendation_extension",
|
||||
"tags",
|
||||
"attachments",
|
||||
"polls",
|
||||
"agenda_item_id",
|
||||
"agenda_type",
|
||||
"agenda_parent_id",
|
||||
"log_messages",
|
||||
"sort_parent",
|
||||
"weight",
|
||||
)
|
||||
read_only_fields = (
|
||||
"state",
|
||||
"recommendation",
|
||||
) # Some other fields are also read_only. See definitions above.
|
||||
|
||||
def validate(self, data):
|
||||
if 'text'in data:
|
||||
data['text'] = validate_html(data['text'])
|
||||
if "text" in data:
|
||||
data["text"] = validate_html(data["text"])
|
||||
|
||||
if 'modified_final_version' in data:
|
||||
data['modified_final_version'] = validate_html(data['modified_final_version'])
|
||||
if "modified_final_version" in data:
|
||||
data["modified_final_version"] = validate_html(
|
||||
data["modified_final_version"]
|
||||
)
|
||||
|
||||
if 'reason' in data:
|
||||
data['reason'] = validate_html(data['reason'])
|
||||
if "reason" in data:
|
||||
data["reason"] = validate_html(data["reason"])
|
||||
|
||||
if 'amendment_paragraphs' in data:
|
||||
data['amendment_paragraphs'] = list(map(lambda entry: validate_html(entry) if type(entry) is str else None,
|
||||
data['amendment_paragraphs']))
|
||||
data['text'] = ''
|
||||
if "amendment_paragraphs" in data:
|
||||
data["amendment_paragraphs"] = list(
|
||||
map(
|
||||
lambda entry: validate_html(entry) if type(entry) is str else None,
|
||||
data["amendment_paragraphs"],
|
||||
)
|
||||
)
|
||||
data["text"] = ""
|
||||
else:
|
||||
if 'text' in data and len(data['text']) == 0:
|
||||
raise ValidationError({
|
||||
'detail': _('This field may not be blank.')
|
||||
})
|
||||
if "text" in data and len(data["text"]) == 0:
|
||||
raise ValidationError({"detail": _("This field may not be blank.")})
|
||||
|
||||
return data
|
||||
|
||||
@ -451,24 +479,28 @@ class MotionSerializer(ModelSerializer):
|
||||
agenda_item_update_information container.
|
||||
"""
|
||||
motion = Motion()
|
||||
motion.title = validated_data['title']
|
||||
motion.text = validated_data['text']
|
||||
motion.amendment_paragraphs = validated_data.get('amendment_paragraphs')
|
||||
motion.modified_final_version = validated_data.get('modified_final_version', '')
|
||||
motion.reason = validated_data.get('reason', '')
|
||||
motion.identifier = validated_data.get('identifier')
|
||||
motion.category = validated_data.get('category')
|
||||
motion.motion_block = validated_data.get('motion_block')
|
||||
motion.origin = validated_data.get('origin', '')
|
||||
motion.parent = validated_data.get('parent')
|
||||
motion.statute_paragraph = validated_data.get('statute_paragraph')
|
||||
motion.reset_state(validated_data.get('workflow_id'))
|
||||
motion.agenda_item_update_information['type'] = validated_data.get('agenda_type')
|
||||
motion.agenda_item_update_information['parent_id'] = validated_data.get('agenda_parent_id')
|
||||
motion.title = validated_data["title"]
|
||||
motion.text = validated_data["text"]
|
||||
motion.amendment_paragraphs = validated_data.get("amendment_paragraphs")
|
||||
motion.modified_final_version = validated_data.get("modified_final_version", "")
|
||||
motion.reason = validated_data.get("reason", "")
|
||||
motion.identifier = validated_data.get("identifier")
|
||||
motion.category = validated_data.get("category")
|
||||
motion.motion_block = validated_data.get("motion_block")
|
||||
motion.origin = validated_data.get("origin", "")
|
||||
motion.parent = validated_data.get("parent")
|
||||
motion.statute_paragraph = validated_data.get("statute_paragraph")
|
||||
motion.reset_state(validated_data.get("workflow_id"))
|
||||
motion.agenda_item_update_information["type"] = validated_data.get(
|
||||
"agenda_type"
|
||||
)
|
||||
motion.agenda_item_update_information["parent_id"] = validated_data.get(
|
||||
"agenda_parent_id"
|
||||
)
|
||||
motion.save()
|
||||
motion.supporters.add(*validated_data.get('supporters', []))
|
||||
motion.attachments.add(*validated_data.get('attachments', []))
|
||||
motion.tags.add(*validated_data.get('tags', []))
|
||||
motion.supporters.add(*validated_data.get("supporters", []))
|
||||
motion.attachments.add(*validated_data.get("attachments", []))
|
||||
motion.tags.add(*validated_data.get("tags", []))
|
||||
return motion
|
||||
|
||||
@transaction.atomic
|
||||
@ -477,8 +509,8 @@ class MotionSerializer(ModelSerializer):
|
||||
Customized method to update a motion.
|
||||
"""
|
||||
workflow_id = None
|
||||
if 'workflow_id' in validated_data:
|
||||
workflow_id = validated_data.pop('workflow_id')
|
||||
if "workflow_id" in validated_data:
|
||||
workflow_id = validated_data.pop("workflow_id")
|
||||
|
||||
result = super().update(motion, validated_data)
|
||||
|
||||
|
@ -14,98 +14,128 @@ def create_builtin_workflows(sender, **kwargs):
|
||||
# If there is at least one workflow, then do nothing.
|
||||
return
|
||||
|
||||
workflow_1 = Workflow(name='Simple Workflow')
|
||||
workflow_1 = Workflow(name="Simple Workflow")
|
||||
workflow_1.save(skip_autoupdate=True)
|
||||
state_1_1 = State(name=ugettext_noop('submitted'),
|
||||
workflow=workflow_1,
|
||||
allow_create_poll=True,
|
||||
allow_support=True,
|
||||
allow_submitter_edit=True)
|
||||
state_1_1 = State(
|
||||
name=ugettext_noop("submitted"),
|
||||
workflow=workflow_1,
|
||||
allow_create_poll=True,
|
||||
allow_support=True,
|
||||
allow_submitter_edit=True,
|
||||
)
|
||||
state_1_1.save(skip_autoupdate=True)
|
||||
state_1_2 = State(name=ugettext_noop('accepted'),
|
||||
workflow=workflow_1,
|
||||
recommendation_label='Acceptance',
|
||||
css_class='success',
|
||||
merge_amendment_into_final=1)
|
||||
state_1_2 = State(
|
||||
name=ugettext_noop("accepted"),
|
||||
workflow=workflow_1,
|
||||
recommendation_label="Acceptance",
|
||||
css_class="success",
|
||||
merge_amendment_into_final=1,
|
||||
)
|
||||
state_1_2.save(skip_autoupdate=True)
|
||||
state_1_3 = State(name=ugettext_noop('rejected'),
|
||||
workflow=workflow_1,
|
||||
recommendation_label='Rejection',
|
||||
css_class='danger',
|
||||
merge_amendment_into_final=-1)
|
||||
state_1_3 = State(
|
||||
name=ugettext_noop("rejected"),
|
||||
workflow=workflow_1,
|
||||
recommendation_label="Rejection",
|
||||
css_class="danger",
|
||||
merge_amendment_into_final=-1,
|
||||
)
|
||||
state_1_3.save(skip_autoupdate=True)
|
||||
state_1_4 = State(name=ugettext_noop('not decided'),
|
||||
workflow=workflow_1,
|
||||
recommendation_label='No decision',
|
||||
css_class='default',
|
||||
merge_amendment_into_final=-1)
|
||||
state_1_4 = State(
|
||||
name=ugettext_noop("not decided"),
|
||||
workflow=workflow_1,
|
||||
recommendation_label="No decision",
|
||||
css_class="default",
|
||||
merge_amendment_into_final=-1,
|
||||
)
|
||||
state_1_4.save(skip_autoupdate=True)
|
||||
state_1_1.next_states.add(state_1_2, state_1_3, state_1_4)
|
||||
workflow_1.first_state = state_1_1
|
||||
workflow_1.save(skip_autoupdate=True)
|
||||
|
||||
workflow_2 = Workflow(name='Complex Workflow')
|
||||
workflow_2 = Workflow(name="Complex Workflow")
|
||||
workflow_2.save(skip_autoupdate=True)
|
||||
state_2_1 = State(name=ugettext_noop('published'),
|
||||
workflow=workflow_2,
|
||||
allow_support=True,
|
||||
allow_submitter_edit=True,
|
||||
dont_set_identifier=True)
|
||||
state_2_1 = State(
|
||||
name=ugettext_noop("published"),
|
||||
workflow=workflow_2,
|
||||
allow_support=True,
|
||||
allow_submitter_edit=True,
|
||||
dont_set_identifier=True,
|
||||
)
|
||||
state_2_1.save(skip_autoupdate=True)
|
||||
state_2_2 = State(name=ugettext_noop('permitted'),
|
||||
workflow=workflow_2,
|
||||
recommendation_label='Permission',
|
||||
allow_create_poll=True,
|
||||
allow_submitter_edit=True)
|
||||
state_2_2 = State(
|
||||
name=ugettext_noop("permitted"),
|
||||
workflow=workflow_2,
|
||||
recommendation_label="Permission",
|
||||
allow_create_poll=True,
|
||||
allow_submitter_edit=True,
|
||||
)
|
||||
state_2_2.save(skip_autoupdate=True)
|
||||
state_2_3 = State(name=ugettext_noop('accepted'),
|
||||
workflow=workflow_2,
|
||||
recommendation_label='Acceptance',
|
||||
css_class='success',
|
||||
merge_amendment_into_final=1)
|
||||
state_2_3 = State(
|
||||
name=ugettext_noop("accepted"),
|
||||
workflow=workflow_2,
|
||||
recommendation_label="Acceptance",
|
||||
css_class="success",
|
||||
merge_amendment_into_final=1,
|
||||
)
|
||||
state_2_3.save(skip_autoupdate=True)
|
||||
state_2_4 = State(name=ugettext_noop('rejected'),
|
||||
workflow=workflow_2,
|
||||
recommendation_label='Rejection',
|
||||
css_class='danger',
|
||||
merge_amendment_into_final=-1)
|
||||
state_2_4 = State(
|
||||
name=ugettext_noop("rejected"),
|
||||
workflow=workflow_2,
|
||||
recommendation_label="Rejection",
|
||||
css_class="danger",
|
||||
merge_amendment_into_final=-1,
|
||||
)
|
||||
state_2_4.save(skip_autoupdate=True)
|
||||
state_2_5 = State(name=ugettext_noop('withdrawed'),
|
||||
workflow=workflow_2,
|
||||
css_class='default',
|
||||
merge_amendment_into_final=-1)
|
||||
state_2_5 = State(
|
||||
name=ugettext_noop("withdrawed"),
|
||||
workflow=workflow_2,
|
||||
css_class="default",
|
||||
merge_amendment_into_final=-1,
|
||||
)
|
||||
state_2_5.save(skip_autoupdate=True)
|
||||
state_2_6 = State(name=ugettext_noop('adjourned'),
|
||||
workflow=workflow_2,
|
||||
recommendation_label='Adjournment',
|
||||
css_class='default',
|
||||
merge_amendment_into_final=-1)
|
||||
state_2_6 = State(
|
||||
name=ugettext_noop("adjourned"),
|
||||
workflow=workflow_2,
|
||||
recommendation_label="Adjournment",
|
||||
css_class="default",
|
||||
merge_amendment_into_final=-1,
|
||||
)
|
||||
state_2_6.save(skip_autoupdate=True)
|
||||
state_2_7 = State(name=ugettext_noop('not concerned'),
|
||||
workflow=workflow_2,
|
||||
recommendation_label='No concernment',
|
||||
css_class='default',
|
||||
merge_amendment_into_final=-1)
|
||||
state_2_7 = State(
|
||||
name=ugettext_noop("not concerned"),
|
||||
workflow=workflow_2,
|
||||
recommendation_label="No concernment",
|
||||
css_class="default",
|
||||
merge_amendment_into_final=-1,
|
||||
)
|
||||
state_2_7.save(skip_autoupdate=True)
|
||||
state_2_8 = State(name=ugettext_noop('refered to committee'),
|
||||
workflow=workflow_2,
|
||||
recommendation_label='Referral to committee',
|
||||
css_class='default',
|
||||
merge_amendment_into_final=-1)
|
||||
state_2_8 = State(
|
||||
name=ugettext_noop("refered to committee"),
|
||||
workflow=workflow_2,
|
||||
recommendation_label="Referral to committee",
|
||||
css_class="default",
|
||||
merge_amendment_into_final=-1,
|
||||
)
|
||||
state_2_8.save(skip_autoupdate=True)
|
||||
state_2_9 = State(name=ugettext_noop('needs review'),
|
||||
workflow=workflow_2,
|
||||
css_class='default',
|
||||
merge_amendment_into_final=-1)
|
||||
state_2_9 = State(
|
||||
name=ugettext_noop("needs review"),
|
||||
workflow=workflow_2,
|
||||
css_class="default",
|
||||
merge_amendment_into_final=-1,
|
||||
)
|
||||
state_2_9.save(skip_autoupdate=True)
|
||||
state_2_10 = State(name=ugettext_noop('rejected (not authorized)'),
|
||||
workflow=workflow_2,
|
||||
recommendation_label='Rejection (not authorized)',
|
||||
css_class='default',
|
||||
merge_amendment_into_final=-1)
|
||||
state_2_10 = State(
|
||||
name=ugettext_noop("rejected (not authorized)"),
|
||||
workflow=workflow_2,
|
||||
recommendation_label="Rejection (not authorized)",
|
||||
css_class="default",
|
||||
merge_amendment_into_final=-1,
|
||||
)
|
||||
state_2_10.save(skip_autoupdate=True)
|
||||
state_2_1.next_states.add(state_2_2, state_2_5, state_2_10)
|
||||
state_2_2.next_states.add(state_2_3, state_2_4, state_2_5, state_2_6, state_2_7, state_2_8, state_2_9)
|
||||
state_2_2.next_states.add(
|
||||
state_2_3, state_2_4, state_2_5, state_2_6, state_2_7, state_2_8, state_2_9
|
||||
)
|
||||
workflow_2.first_state = state_2_1
|
||||
workflow_2.save(skip_autoupdate=True)
|
||||
|
||||
@ -114,8 +144,11 @@ def get_permission_change_data(sender, permissions, **kwargs):
|
||||
"""
|
||||
Yields all necessary collections if 'motions.can_see' permission changes.
|
||||
"""
|
||||
motions_app = apps.get_app_config(app_label='motions')
|
||||
motions_app = apps.get_app_config(app_label="motions")
|
||||
for permission in permissions:
|
||||
# There could be only one 'motions.can_see' and then we want to return data.
|
||||
if permission.content_type.app_label == motions_app.label and permission.codename == 'can_see':
|
||||
if (
|
||||
permission.content_type.app_label == motions_app.label
|
||||
and permission.codename == "can_see"
|
||||
):
|
||||
yield from motions_app.get_startup_elements()
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
# Common majority methods for all apps using polls. The first one should be the default.
|
||||
majorityMethods = (
|
||||
{'value': 'simple_majority', 'display_name': 'Simple majority'},
|
||||
{'value': 'two-thirds_majority', 'display_name': 'Two-thirds majority'},
|
||||
{'value': 'three-quarters_majority', 'display_name': 'Three-quarters majority'},
|
||||
{'value': 'disabled', 'display_name': 'Disabled'},
|
||||
{"value": "simple_majority", "display_name": "Simple majority"},
|
||||
{"value": "two-thirds_majority", "display_name": "Two-thirds majority"},
|
||||
{"value": "three-quarters_majority", "display_name": "Three-quarters majority"},
|
||||
{"value": "disabled", "display_name": "Disabled"},
|
||||
)
|
||||
|
@ -17,7 +17,8 @@ class BaseOption(models.Model):
|
||||
which has to be a subclass of BaseVote. Otherwise you have to override the
|
||||
get_vote_class method.
|
||||
"""
|
||||
vote_class: Optional[Type['BaseVote']] = None
|
||||
|
||||
vote_class: Optional[Type["BaseVote"]] = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@ -27,7 +28,9 @@ class BaseOption(models.Model):
|
||||
|
||||
def get_vote_class(self):
|
||||
if self.vote_class is None:
|
||||
raise NotImplementedError('The option class %s has to have an attribute vote_class.' % self)
|
||||
raise NotImplementedError(
|
||||
"The option class %s has to have an attribute vote_class." % self
|
||||
)
|
||||
return self.vote_class
|
||||
|
||||
def __getitem__(self, name):
|
||||
@ -44,8 +47,14 @@ class BaseVote(models.Model):
|
||||
Subclasses have to define an option field. This must be a ForeignKeyField
|
||||
to a subclass of BasePoll.
|
||||
"""
|
||||
weight = models.DecimalField(default=Decimal('1'), null=True, validators=[
|
||||
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
|
||||
|
||||
weight = models.DecimalField(
|
||||
default=Decimal("1"),
|
||||
null=True,
|
||||
validators=[MinValueValidator(Decimal("-2"))],
|
||||
max_digits=15,
|
||||
decimal_places=6,
|
||||
)
|
||||
value = models.CharField(max_length=255, null=True)
|
||||
|
||||
class Meta:
|
||||
@ -73,12 +82,28 @@ class CollectDefaultVotesMixin(models.Model):
|
||||
Mixin for a poll to collect the default vote values for valid votes,
|
||||
invalid votes and votes cast.
|
||||
"""
|
||||
votesvalid = models.DecimalField(null=True, blank=True, validators=[
|
||||
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
|
||||
votesinvalid = models.DecimalField(null=True, blank=True, validators=[
|
||||
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
|
||||
votescast = models.DecimalField(null=True, blank=True, validators=[
|
||||
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
|
||||
|
||||
votesvalid = models.DecimalField(
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(Decimal("-2"))],
|
||||
max_digits=15,
|
||||
decimal_places=6,
|
||||
)
|
||||
votesinvalid = models.DecimalField(
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(Decimal("-2"))],
|
||||
max_digits=15,
|
||||
decimal_places=6,
|
||||
)
|
||||
votescast = models.DecimalField(
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(Decimal("-2"))],
|
||||
max_digits=15,
|
||||
decimal_places=6,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@ -87,13 +112,16 @@ class CollectDefaultVotesMixin(models.Model):
|
||||
"""
|
||||
Returns one of the strings of the percent base.
|
||||
"""
|
||||
raise NotImplementedError('You have to provide a get_percent_base_choice() method.')
|
||||
raise NotImplementedError(
|
||||
"You have to provide a get_percent_base_choice() method."
|
||||
)
|
||||
|
||||
|
||||
class PublishPollMixin(models.Model):
|
||||
"""
|
||||
Mixin for a poll to add a flag whether the poll is published or not.
|
||||
"""
|
||||
|
||||
published = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
@ -108,7 +136,8 @@ class BasePoll(models.Model):
|
||||
"""
|
||||
Base poll class.
|
||||
"""
|
||||
vote_values = ['Votes']
|
||||
|
||||
vote_values = ["Votes"]
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@ -183,7 +212,7 @@ class BasePoll(models.Model):
|
||||
try:
|
||||
vote = self.get_votes().filter(option=option_id).get(value=value)
|
||||
except ObjectDoesNotExist:
|
||||
values.append(self.get_vote_class()(value=value, weight=''))
|
||||
values.append(self.get_vote_class()(value=value, weight=""))
|
||||
else:
|
||||
values.append(vote)
|
||||
return values
|
||||
@ -195,15 +224,18 @@ def print_value(value, percent_base=0):
|
||||
'undocumented' or the vote value with percent value if so.
|
||||
"""
|
||||
if value == -1:
|
||||
verbose_value = _('majority')
|
||||
verbose_value = _("majority")
|
||||
elif value == -2:
|
||||
verbose_value = _('undocumented')
|
||||
verbose_value = _("undocumented")
|
||||
elif value is None:
|
||||
verbose_value = _('undocumented')
|
||||
verbose_value = _("undocumented")
|
||||
else:
|
||||
if percent_base:
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
verbose_value = u'%d (%s %%)' % (value, locale.format('%.1f', value * percent_base))
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
verbose_value = "%d (%s %%)" % (
|
||||
value,
|
||||
locale.format("%.1f", value * percent_base),
|
||||
)
|
||||
else:
|
||||
verbose_value = u'%s' % value
|
||||
verbose_value = "%s" % value
|
||||
return verbose_value
|
||||
|
@ -10,8 +10,12 @@ def default_votes_validator(data):
|
||||
than or equal to -2.
|
||||
"""
|
||||
for key in data:
|
||||
if (key in ('votesvalid', 'votesinvalid', 'votescast') and
|
||||
data[key] is not None and
|
||||
data[key] < -2):
|
||||
raise ValidationError({'detail': _('Value for {} must not be less than -2').format(key)})
|
||||
if (
|
||||
key in ("votesvalid", "votesinvalid", "votescast")
|
||||
and data[key] is not None
|
||||
and data[key] < -2
|
||||
):
|
||||
raise ValidationError(
|
||||
{"detail": _("Value for {} must not be less than -2").format(key)}
|
||||
)
|
||||
return data
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user