Run black
This commit is contained in:
parent
800055a5ea
commit
eddbd86d3a
|
@ -5,7 +5,7 @@ from parser import parser
|
||||||
|
|
||||||
|
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
args = parser.parse_args(['--help'])
|
args = parser.parse_args(["--help"])
|
||||||
else:
|
else:
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
|
@ -2,47 +2,48 @@ from parser import command, argument, call
|
||||||
import yaml
|
import yaml
|
||||||
import requirements
|
import requirements
|
||||||
|
|
||||||
FAIL = '\033[91m'
|
FAIL = "\033[91m"
|
||||||
SUCCESS = '\033[92m'
|
SUCCESS = "\033[92m"
|
||||||
RESET = '\033[0m'
|
RESET = "\033[0m"
|
||||||
|
|
||||||
|
|
||||||
@command('check', help='Checks for pep8 errors in openslides and tests')
|
@command("check", help="Checks for pep8 errors in openslides and tests")
|
||||||
def check(args=None):
|
def check(args=None):
|
||||||
"""
|
"""
|
||||||
Checks for pep8 and other code styling conventions.
|
Checks for pep8 and other code styling conventions.
|
||||||
"""
|
"""
|
||||||
value = call('flake8 --max-line-length=150 --statistics openslides tests')
|
value = call("flake8 --max-line-length=150 --statistics openslides tests")
|
||||||
value += call('python -m mypy openslides/ tests/')
|
value += call("python -m mypy openslides/ tests/")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
@command('travis', help='Runs the code that travis does')
|
@command("travis", help="Runs the code that travis does")
|
||||||
def travis(args=None):
|
def travis(args=None):
|
||||||
"""
|
"""
|
||||||
Runs all commands that travis tests.
|
Runs all commands that travis tests.
|
||||||
"""
|
"""
|
||||||
return_codes = []
|
return_codes = []
|
||||||
with open('.travis.yml') as f:
|
with open(".travis.yml") as f:
|
||||||
travis = yaml.load(f)
|
travis = yaml.load(f)
|
||||||
for line in travis['script']:
|
for line in travis["script"]:
|
||||||
print('Run: {}'.format(line))
|
print("Run: {}".format(line))
|
||||||
return_code = call(line)
|
return_code = call(line)
|
||||||
return_codes.append(return_code)
|
return_codes.append(return_code)
|
||||||
if return_code:
|
if return_code:
|
||||||
print(FAIL + 'fail!\n' + RESET)
|
print(FAIL + "fail!\n" + RESET)
|
||||||
else:
|
else:
|
||||||
print(SUCCESS + 'success!\n' + RESET)
|
print(SUCCESS + "success!\n" + RESET)
|
||||||
|
|
||||||
# Retuns True if one command exited with a different statuscode then 1
|
# Retuns True if one command exited with a different statuscode then 1
|
||||||
return bool(list(filter(bool, return_codes)))
|
return bool(list(filter(bool, return_codes)))
|
||||||
|
|
||||||
|
|
||||||
@argument('-r', '--requirements', nargs='?',
|
@argument("-r", "--requirements", nargs="?", default="requirements.txt")
|
||||||
default='requirements.txt')
|
@command(
|
||||||
@command('min_requirements',
|
"min_requirements",
|
||||||
help='Prints a pip line to install the minimum supported versions of '
|
help="Prints a pip line to install the minimum supported versions of "
|
||||||
'the requirements.')
|
"the requirements.",
|
||||||
|
)
|
||||||
def min_requirements(args=None):
|
def min_requirements(args=None):
|
||||||
"""
|
"""
|
||||||
Prints a pip install command to install the minimal supported versions of a
|
Prints a pip install command to install the minimal supported versions of a
|
||||||
|
@ -54,6 +55,7 @@ def min_requirements(args=None):
|
||||||
|
|
||||||
pip install $(python make min_requirements)
|
pip install $(python make min_requirements)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_lowest_versions(requirements_file):
|
def get_lowest_versions(requirements_file):
|
||||||
with open(requirements_file) as f:
|
with open(requirements_file) as f:
|
||||||
for req in requirements.parse(f):
|
for req in requirements.parse(f):
|
||||||
|
@ -62,22 +64,20 @@ def min_requirements(args=None):
|
||||||
if spec == ">=":
|
if spec == ">=":
|
||||||
yield "{}=={}".format(req.name, version)
|
yield "{}=={}".format(req.name, version)
|
||||||
|
|
||||||
print(' '.join(get_lowest_versions(args.requirements)))
|
print(" ".join(get_lowest_versions(args.requirements)))
|
||||||
|
|
||||||
|
|
||||||
@command('clean',
|
@command("clean", help="Deletes unneeded files and folders")
|
||||||
help='Deletes unneeded files and folders')
|
|
||||||
def clean(args=None):
|
def clean(args=None):
|
||||||
"""
|
"""
|
||||||
Deletes all .pyc and .orig files and empty folders.
|
Deletes all .pyc and .orig files and empty folders.
|
||||||
"""
|
"""
|
||||||
call('find -name "*.pyc" -delete')
|
call('find -name "*.pyc" -delete')
|
||||||
call('find -name "*.orig" -delete')
|
call('find -name "*.orig" -delete')
|
||||||
call('find -type d -empty -delete')
|
call("find -type d -empty -delete")
|
||||||
|
|
||||||
|
|
||||||
@command('format',
|
@command("format", help="Format code with isort and black")
|
||||||
help='Format code with isort and black')
|
|
||||||
def isort(args=None):
|
def isort(args=None):
|
||||||
call('isort --recursive openslides tests')
|
call("isort --recursive openslides tests")
|
||||||
call('black --py36 openslides tests')
|
call("black --py36 openslides tests")
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from subprocess import call as _call
|
from subprocess import call as _call
|
||||||
|
|
||||||
parser = ArgumentParser(description='Development scripts for OpenSlides')
|
parser = ArgumentParser(description="Development scripts for OpenSlides")
|
||||||
subparsers = parser.add_subparsers()
|
subparsers = parser.add_subparsers()
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ def command(*args, **kwargs):
|
||||||
The arguments to this decorator are used as arguments for the argparse
|
The arguments to this decorator are used as arguments for the argparse
|
||||||
command.
|
command.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class decorator:
|
class decorator:
|
||||||
def __init__(self, func):
|
def __init__(self, func):
|
||||||
self.parser = subparsers.add_parser(*args, **kwargs)
|
self.parser = subparsers.add_parser(*args, **kwargs)
|
||||||
|
@ -34,11 +35,15 @@ def argument(*args, **kwargs):
|
||||||
Does only work if the decorated function was decorated with the
|
Does only work if the decorated function was decorated with the
|
||||||
command-decorator before.
|
command-decorator before.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
func.parser.add_argument(*args, **kwargs)
|
func.parser.add_argument(*args, **kwargs)
|
||||||
|
|
||||||
def wrapper(*func_args, **func_kwargs):
|
def wrapper(*func_args, **func_kwargs):
|
||||||
return func(*func_args, **func_kwargs)
|
return func(*func_args, **func_kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
__author__ = 'OpenSlides Team <support@openslides.org>'
|
__author__ = "OpenSlides Team <support@openslides.org>"
|
||||||
__description__ = 'Presentation and assembly system'
|
__description__ = "Presentation and assembly system"
|
||||||
__version__ = '3.0-dev'
|
__version__ = "3.0-dev"
|
||||||
__license__ = 'MIT'
|
__license__ = "MIT"
|
||||||
__url__ = 'https://openslides.org'
|
__url__ = "https://openslides.org"
|
||||||
|
|
||||||
args = None
|
args = None
|
||||||
|
|
|
@ -42,7 +42,7 @@ def main():
|
||||||
else:
|
else:
|
||||||
# Check for unknown_args.
|
# Check for unknown_args.
|
||||||
if unknown_args:
|
if unknown_args:
|
||||||
parser.error('Unknown arguments {}'.format(' '.join(unknown_args)))
|
parser.error("Unknown arguments {}".format(" ".join(unknown_args)))
|
||||||
|
|
||||||
# Save arguments, if one wants to access them later.
|
# Save arguments, if one wants to access them later.
|
||||||
arguments.set_arguments(known_args)
|
arguments.set_arguments(known_args)
|
||||||
|
@ -59,11 +59,11 @@ def get_parser():
|
||||||
if len(sys.argv) == 1:
|
if len(sys.argv) == 1:
|
||||||
# Use start subcommand if called by openslides console script without
|
# Use start subcommand if called by openslides console script without
|
||||||
# any other arguments.
|
# any other arguments.
|
||||||
sys.argv.append('start')
|
sys.argv.append("start")
|
||||||
|
|
||||||
# Init parser
|
# Init parser
|
||||||
description = 'Start script for OpenSlides.'
|
description = "Start script for OpenSlides."
|
||||||
if 'manage.py' not in sys.argv[0]:
|
if "manage.py" not in sys.argv[0]:
|
||||||
description += """
|
description += """
|
||||||
If it is called without any argument, this will be treated as
|
If it is called without any argument, this will be treated as
|
||||||
if it is called with the 'start' subcommand. That means
|
if it is called with the 'start' subcommand. That means
|
||||||
|
@ -77,109 +77,116 @@ def get_parser():
|
||||||
(without the two hyphen-minus characters) to list them all. Type
|
(without the two hyphen-minus characters) to list them all. Type
|
||||||
'%(prog)s help <subcommand>' for help on a specific subcommand.
|
'%(prog)s help <subcommand>' for help on a specific subcommand.
|
||||||
"""
|
"""
|
||||||
parser = ExceptionArgumentParser(
|
parser = ExceptionArgumentParser(description=description, epilog=epilog)
|
||||||
description=description,
|
|
||||||
epilog=epilog)
|
|
||||||
|
|
||||||
# Add version argument
|
# Add version argument
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--version',
|
"--version",
|
||||||
action='version',
|
action="version",
|
||||||
version=openslides.__version__,
|
version=openslides.__version__,
|
||||||
help='Show version number and exit.')
|
help="Show version number and exit.",
|
||||||
|
)
|
||||||
|
|
||||||
# Init subparsers
|
# Init subparsers
|
||||||
subparsers = parser.add_subparsers(
|
subparsers = parser.add_subparsers(
|
||||||
dest='subcommand',
|
dest="subcommand",
|
||||||
title='Available subcommands',
|
title="Available subcommands",
|
||||||
description="Type '%s <subcommand> --help' for help on a "
|
description="Type '%s <subcommand> --help' for help on a "
|
||||||
"specific subcommand." % parser.prog, # type: ignore
|
"specific subcommand." % parser.prog, # type: ignore
|
||||||
help='You can choose only one subcommand at once.',
|
help="You can choose only one subcommand at once.",
|
||||||
metavar='')
|
metavar="",
|
||||||
|
)
|
||||||
|
|
||||||
# Subcommand start
|
# Subcommand start
|
||||||
start_help = (
|
start_help = (
|
||||||
'Setup settings and database, start webserver, launch the '
|
"Setup settings and database, start webserver, launch the "
|
||||||
'default web browser and open the webinterface. The environment '
|
"default web browser and open the webinterface. The environment "
|
||||||
'variable DJANGO_SETTINGS_MODULE is ignored.')
|
"variable DJANGO_SETTINGS_MODULE is ignored."
|
||||||
|
)
|
||||||
subcommand_start = subparsers.add_parser(
|
subcommand_start = subparsers.add_parser(
|
||||||
'start',
|
"start", description=start_help, help=start_help
|
||||||
description=start_help,
|
)
|
||||||
help=start_help)
|
|
||||||
subcommand_start.set_defaults(callback=start)
|
subcommand_start.set_defaults(callback=start)
|
||||||
subcommand_start.add_argument(
|
subcommand_start.add_argument(
|
||||||
'--no-browser',
|
"--no-browser",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
help='Do not launch the default web browser.')
|
help="Do not launch the default web browser.",
|
||||||
|
)
|
||||||
subcommand_start.add_argument(
|
subcommand_start.add_argument(
|
||||||
'--debug-email',
|
"--debug-email",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
help='Change the email backend to console output.')
|
help="Change the email backend to console output.",
|
||||||
|
)
|
||||||
subcommand_start.add_argument(
|
subcommand_start.add_argument(
|
||||||
'--no-template-caching',
|
"--no-template-caching",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help='Disables caching of templates.')
|
help="Disables caching of templates.",
|
||||||
|
)
|
||||||
subcommand_start.add_argument(
|
subcommand_start.add_argument(
|
||||||
'--host',
|
"--host",
|
||||||
action='store',
|
action="store",
|
||||||
default='0.0.0.0',
|
default="0.0.0.0",
|
||||||
help='IP address to listen on. Default is 0.0.0.0.')
|
help="IP address to listen on. Default is 0.0.0.0.",
|
||||||
|
)
|
||||||
subcommand_start.add_argument(
|
subcommand_start.add_argument(
|
||||||
'--port',
|
"--port",
|
||||||
action='store',
|
action="store",
|
||||||
default='8000',
|
default="8000",
|
||||||
help='Port to listen on. Default is 8000.')
|
help="Port to listen on. Default is 8000.",
|
||||||
|
)
|
||||||
subcommand_start.add_argument(
|
subcommand_start.add_argument(
|
||||||
'--settings_dir',
|
"--settings_dir", action="store", default=None, help="The settings directory."
|
||||||
action='store',
|
)
|
||||||
default=None,
|
|
||||||
help='The settings directory.')
|
|
||||||
subcommand_start.add_argument(
|
subcommand_start.add_argument(
|
||||||
'--settings_filename',
|
"--settings_filename",
|
||||||
action='store',
|
action="store",
|
||||||
default='settings.py',
|
default="settings.py",
|
||||||
help='The used settings file name. The file is created, if it does not exist.')
|
help="The used settings file name. The file is created, if it does not exist.",
|
||||||
|
)
|
||||||
subcommand_start.add_argument(
|
subcommand_start.add_argument(
|
||||||
'--local-installation',
|
"--local-installation",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
help='Store settings and user files in a local directory.')
|
help="Store settings and user files in a local directory.",
|
||||||
|
)
|
||||||
|
|
||||||
# Subcommand createsettings
|
# Subcommand createsettings
|
||||||
createsettings_help = 'Creates the settings file.'
|
createsettings_help = "Creates the settings file."
|
||||||
subcommand_createsettings = subparsers.add_parser(
|
subcommand_createsettings = subparsers.add_parser(
|
||||||
'createsettings',
|
"createsettings", description=createsettings_help, help=createsettings_help
|
||||||
description=createsettings_help,
|
)
|
||||||
help=createsettings_help)
|
|
||||||
subcommand_createsettings.set_defaults(callback=createsettings)
|
subcommand_createsettings.set_defaults(callback=createsettings)
|
||||||
subcommand_createsettings.add_argument(
|
subcommand_createsettings.add_argument(
|
||||||
'--settings_dir',
|
"--settings_dir",
|
||||||
action='store',
|
action="store",
|
||||||
default=None,
|
default=None,
|
||||||
help='The used settings file directory. All settings files are created, even if they exist.')
|
help="The used settings file directory. All settings files are created, even if they exist.",
|
||||||
|
)
|
||||||
subcommand_createsettings.add_argument(
|
subcommand_createsettings.add_argument(
|
||||||
'--settings_filename',
|
"--settings_filename",
|
||||||
action='store',
|
action="store",
|
||||||
default='settings.py',
|
default="settings.py",
|
||||||
help='The used settings file name. The file is created, if it does not exist.')
|
help="The used settings file name. The file is created, if it does not exist.",
|
||||||
|
)
|
||||||
subcommand_createsettings.add_argument(
|
subcommand_createsettings.add_argument(
|
||||||
'--local-installation',
|
"--local-installation",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
help='Store settings and user files in a local directory.')
|
help="Store settings and user files in a local directory.",
|
||||||
|
)
|
||||||
|
|
||||||
# Help text for several Django subcommands
|
# Help text for several Django subcommands
|
||||||
django_subcommands = (
|
django_subcommands = (
|
||||||
('backupdb', 'Backups the SQLite3 database.'),
|
("backupdb", "Backups the SQLite3 database."),
|
||||||
('createsuperuser', 'Creates or resets the admin user.'),
|
("createsuperuser", "Creates or resets the admin user."),
|
||||||
('migrate', 'Updates database schema.'),
|
("migrate", "Updates database schema."),
|
||||||
('runserver', 'Starts the Tornado webserver.'),
|
("runserver", "Starts the Tornado webserver."),
|
||||||
)
|
)
|
||||||
for django_subcommand, help_text in django_subcommands:
|
for django_subcommand, help_text in django_subcommands:
|
||||||
subparsers._choices_actions.append( # type: ignore
|
subparsers._choices_actions.append( # type: ignore
|
||||||
subparsers._ChoicesPseudoAction( # type: ignore
|
subparsers._ChoicesPseudoAction( # type: ignore
|
||||||
django_subcommand,
|
django_subcommand, (), help_text
|
||||||
(),
|
)
|
||||||
help_text))
|
)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
@ -188,8 +195,10 @@ def start(args):
|
||||||
"""
|
"""
|
||||||
Starts OpenSlides: Runs migrations and runs runserver.
|
Starts OpenSlides: Runs migrations and runs runserver.
|
||||||
"""
|
"""
|
||||||
raise OpenSlidesError('The start command does not work anymore. ' +
|
raise OpenSlidesError(
|
||||||
'Please use `createsettings`, `migrate` and `runserver`.')
|
"The start command does not work anymore. "
|
||||||
|
+ "Please use `createsettings`, `migrate` and `runserver`."
|
||||||
|
)
|
||||||
settings_dir = args.settings_dir
|
settings_dir = args.settings_dir
|
||||||
settings_filename = args.settings_filename
|
settings_filename = args.settings_filename
|
||||||
local_installation = is_local_installation()
|
local_installation = is_local_installation()
|
||||||
|
@ -212,10 +221,10 @@ def start(args):
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
if args.debug_email:
|
if args.debug_email:
|
||||||
settings.EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
settings.EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||||
|
|
||||||
# Migrate database
|
# Migrate database
|
||||||
call_command('migrate')
|
call_command("migrate")
|
||||||
|
|
||||||
# Open the browser
|
# Open the browser
|
||||||
if not args.no_browser:
|
if not args.no_browser:
|
||||||
|
@ -229,8 +238,8 @@ def start(args):
|
||||||
#
|
#
|
||||||
# Use flag --insecure to serve static files even if DEBUG is False.
|
# Use flag --insecure to serve static files even if DEBUG is False.
|
||||||
call_command(
|
call_command(
|
||||||
'runserver',
|
"runserver",
|
||||||
'{}:{}'.format(args.host, args.port),
|
"{}:{}".format(args.host, args.port),
|
||||||
noreload=False, # Means True, see above.
|
noreload=False, # Means True, see above.
|
||||||
insecure=True,
|
insecure=True,
|
||||||
)
|
)
|
||||||
|
@ -248,11 +257,14 @@ def createsettings(args):
|
||||||
if settings_dir is None:
|
if settings_dir is None:
|
||||||
settings_dir = get_local_settings_dir()
|
settings_dir = get_local_settings_dir()
|
||||||
context = {
|
context = {
|
||||||
'openslides_user_data_dir': repr(os.path.join(os.getcwd(), 'personal_data', 'var')),
|
"openslides_user_data_dir": repr(
|
||||||
'debug': 'True'}
|
os.path.join(os.getcwd(), "personal_data", "var")
|
||||||
|
),
|
||||||
|
"debug": "True",
|
||||||
|
}
|
||||||
|
|
||||||
settings_path = write_settings(settings_dir, args.settings_filename, **context)
|
settings_path = write_settings(settings_dir, args.settings_filename, **context)
|
||||||
print('Settings created at %s' % settings_path)
|
print("Settings created at %s" % settings_path)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -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.
|
Access permissions container for Item and ItemViewSet.
|
||||||
"""
|
"""
|
||||||
base_permission = 'agenda.can_see'
|
|
||||||
|
base_permission = "agenda.can_see"
|
||||||
|
|
||||||
# TODO: In the following method we use full_data['is_hidden'] and
|
# TODO: In the following method we use full_data['is_hidden'] and
|
||||||
# full_data['is_internal'] but this can be out of date.
|
# full_data['is_internal'] but this can be out of date.
|
||||||
async def get_restricted_data(
|
async def get_restricted_data(
|
||||||
self,
|
self, full_data: List[Dict[str, Any]], user_id: int
|
||||||
full_data: List[Dict[str, Any]],
|
) -> List[Dict[str, Any]]:
|
||||||
user_id: int) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the user.
|
for the user.
|
||||||
|
@ -25,6 +25,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
||||||
We remove comments for non admins/managers and a lot of fields of
|
We remove comments for non admins/managers and a lot of fields of
|
||||||
internal items for users without permission to see internal items.
|
internal items for users without permission to see internal items.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def filtered_data(full_data, blocked_keys):
|
def filtered_data(full_data, blocked_keys):
|
||||||
"""
|
"""
|
||||||
Returns a new dict like full_data but with all blocked_keys removed.
|
Returns a new dict like full_data but with all blocked_keys removed.
|
||||||
|
@ -33,47 +34,56 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
||||||
return {key: full_data[key] for key in whitelist}
|
return {key: full_data[key] for key in whitelist}
|
||||||
|
|
||||||
# Parse data.
|
# Parse data.
|
||||||
if full_data and await async_has_perm(user_id, 'agenda.can_see'):
|
if full_data and await async_has_perm(user_id, "agenda.can_see"):
|
||||||
if await async_has_perm(user_id, 'agenda.can_manage') and await async_has_perm(user_id, 'agenda.can_see_internal_items'):
|
if await async_has_perm(
|
||||||
|
user_id, "agenda.can_manage"
|
||||||
|
) and await async_has_perm(user_id, "agenda.can_see_internal_items"):
|
||||||
# Managers with special permission can see everything.
|
# Managers with special permission can see everything.
|
||||||
data = full_data
|
data = full_data
|
||||||
elif await async_has_perm(user_id, 'agenda.can_see_internal_items'):
|
elif await async_has_perm(user_id, "agenda.can_see_internal_items"):
|
||||||
# Non managers with special permission can see everything but
|
# Non managers with special permission can see everything but
|
||||||
# comments and hidden items.
|
# comments and hidden items.
|
||||||
data = [full for full in full_data if not full['is_hidden']] # filter hidden items
|
data = [
|
||||||
blocked_keys = ('comment',)
|
full for full in full_data if not full["is_hidden"]
|
||||||
data = [filtered_data(full, blocked_keys) for full in data] # remove blocked_keys
|
] # filter hidden items
|
||||||
|
blocked_keys = ("comment",)
|
||||||
|
data = [
|
||||||
|
filtered_data(full, blocked_keys) for full in data
|
||||||
|
] # remove blocked_keys
|
||||||
else:
|
else:
|
||||||
# Users without special permission for internal items.
|
# Users without special permission for internal items.
|
||||||
|
|
||||||
# In internal and hidden case managers and non managers see only some fields
|
# In internal and hidden case managers and non managers see only some fields
|
||||||
# so that list of speakers is provided regardless. Hidden items can only be seen by managers.
|
# so that list of speakers is provided regardless. Hidden items can only be seen by managers.
|
||||||
# We know that full_data has at least one entry which can be used to parse the keys.
|
# We know that full_data has at least one entry which can be used to parse the keys.
|
||||||
blocked_keys_internal_hidden_case = set(full_data[0].keys()) - set((
|
blocked_keys_internal_hidden_case = set(full_data[0].keys()) - set(
|
||||||
'id',
|
("id", "title", "speakers", "speaker_list_closed", "content_object")
|
||||||
'title',
|
)
|
||||||
'speakers',
|
|
||||||
'speaker_list_closed',
|
|
||||||
'content_object'))
|
|
||||||
|
|
||||||
# In non internal case managers see everything and non managers see
|
# In non internal case managers see everything and non managers see
|
||||||
# everything but comments.
|
# everything but comments.
|
||||||
if await async_has_perm(user_id, 'agenda.can_manage'):
|
if await async_has_perm(user_id, "agenda.can_manage"):
|
||||||
blocked_keys_non_internal_hidden_case: Iterable[str] = []
|
blocked_keys_non_internal_hidden_case: Iterable[str] = []
|
||||||
can_see_hidden = True
|
can_see_hidden = True
|
||||||
else:
|
else:
|
||||||
blocked_keys_non_internal_hidden_case = ('comment',)
|
blocked_keys_non_internal_hidden_case = ("comment",)
|
||||||
can_see_hidden = False
|
can_see_hidden = False
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
for full in full_data:
|
for full in full_data:
|
||||||
if full['is_hidden'] and can_see_hidden:
|
if full["is_hidden"] and can_see_hidden:
|
||||||
# Same filtering for internal and hidden items
|
# Same filtering for internal and hidden items
|
||||||
data.append(filtered_data(full, blocked_keys_internal_hidden_case))
|
data.append(
|
||||||
elif full['is_internal']:
|
filtered_data(full, blocked_keys_internal_hidden_case)
|
||||||
data.append(filtered_data(full, blocked_keys_internal_hidden_case))
|
)
|
||||||
|
elif full["is_internal"]:
|
||||||
|
data.append(
|
||||||
|
filtered_data(full, blocked_keys_internal_hidden_case)
|
||||||
|
)
|
||||||
else: # agenda item
|
else: # agenda item
|
||||||
data.append(filtered_data(full, blocked_keys_non_internal_hidden_case))
|
data.append(
|
||||||
|
filtered_data(full, blocked_keys_non_internal_hidden_case)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ from ..utils.projector import register_projector_elements
|
||||||
|
|
||||||
|
|
||||||
class AgendaAppConfig(AppConfig):
|
class AgendaAppConfig(AppConfig):
|
||||||
name = 'openslides.agenda'
|
name = "openslides.agenda"
|
||||||
verbose_name = 'OpenSlides Agenda'
|
verbose_name = "OpenSlides Agenda"
|
||||||
angular_site_module = True
|
angular_site_module = True
|
||||||
angular_projector_module = True
|
angular_projector_module = True
|
||||||
|
|
||||||
|
@ -20,7 +20,8 @@ class AgendaAppConfig(AppConfig):
|
||||||
from .signals import (
|
from .signals import (
|
||||||
get_permission_change_data,
|
get_permission_change_data,
|
||||||
listen_to_related_object_post_delete,
|
listen_to_related_object_post_delete,
|
||||||
listen_to_related_object_post_save)
|
listen_to_related_object_post_save,
|
||||||
|
)
|
||||||
from .views import ItemViewSet
|
from .views import ItemViewSet
|
||||||
from . import serializers # noqa
|
from . import serializers # noqa
|
||||||
from ..utils.access_permissions import required_user
|
from ..utils.access_permissions import required_user
|
||||||
|
@ -31,22 +32,27 @@ class AgendaAppConfig(AppConfig):
|
||||||
# Connect signals.
|
# Connect signals.
|
||||||
post_save.connect(
|
post_save.connect(
|
||||||
listen_to_related_object_post_save,
|
listen_to_related_object_post_save,
|
||||||
dispatch_uid='listen_to_related_object_post_save')
|
dispatch_uid="listen_to_related_object_post_save",
|
||||||
|
)
|
||||||
pre_delete.connect(
|
pre_delete.connect(
|
||||||
listen_to_related_object_post_delete,
|
listen_to_related_object_post_delete,
|
||||||
dispatch_uid='listen_to_related_object_post_delete')
|
dispatch_uid="listen_to_related_object_post_delete",
|
||||||
|
)
|
||||||
permission_change.connect(
|
permission_change.connect(
|
||||||
get_permission_change_data,
|
get_permission_change_data, dispatch_uid="agenda_get_permission_change_data"
|
||||||
dispatch_uid='agenda_get_permission_change_data')
|
)
|
||||||
|
|
||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register(self.get_model('Item').get_collection_string(), ItemViewSet)
|
router.register(self.get_model("Item").get_collection_string(), ItemViewSet)
|
||||||
|
|
||||||
# register required_users
|
# register required_users
|
||||||
required_user.add_collection_string(self.get_model('Item').get_collection_string(), required_users)
|
required_user.add_collection_string(
|
||||||
|
self.get_model("Item").get_collection_string(), required_users
|
||||||
|
)
|
||||||
|
|
||||||
def get_config_variables(self):
|
def get_config_variables(self):
|
||||||
from .config_variables import get_config_variables
|
from .config_variables import get_config_variables
|
||||||
|
|
||||||
return get_config_variables()
|
return get_config_variables()
|
||||||
|
|
||||||
def get_startup_elements(self):
|
def get_startup_elements(self):
|
||||||
|
@ -54,11 +60,11 @@ class AgendaAppConfig(AppConfig):
|
||||||
Yields all Cachables required on startup i. e. opening the websocket
|
Yields all Cachables required on startup i. e. opening the websocket
|
||||||
connection.
|
connection.
|
||||||
"""
|
"""
|
||||||
yield self.get_model('Item')
|
yield self.get_model("Item")
|
||||||
|
|
||||||
|
|
||||||
def required_users(element: Dict[str, Any]) -> Set[int]:
|
def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||||
"""
|
"""
|
||||||
Returns all user ids that are displayed as speaker in the given element.
|
Returns all user ids that are displayed as speaker in the given element.
|
||||||
"""
|
"""
|
||||||
return set(speaker['user_id'] for speaker in element['speakers'])
|
return set(speaker["user_id"] for speaker in element["speakers"])
|
||||||
|
|
|
@ -10,97 +10,108 @@ def get_config_variables():
|
||||||
It has to be evaluated during app loading (see apps.py).
|
It has to be evaluated during app loading (see apps.py).
|
||||||
"""
|
"""
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='agenda_enable_numbering',
|
name="agenda_enable_numbering",
|
||||||
label='Enable numbering for agenda items',
|
label="Enable numbering for agenda items",
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
default_value=True,
|
default_value=True,
|
||||||
weight=200,
|
weight=200,
|
||||||
group='Agenda',
|
group="Agenda",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='agenda_number_prefix',
|
name="agenda_number_prefix",
|
||||||
default_value='',
|
default_value="",
|
||||||
label='Numbering prefix for agenda items',
|
label="Numbering prefix for agenda items",
|
||||||
help_text='This prefix will be set if you run the automatic agenda numbering.',
|
help_text="This prefix will be set if you run the automatic agenda numbering.",
|
||||||
weight=210,
|
weight=210,
|
||||||
group='Agenda',
|
group="Agenda",
|
||||||
subgroup='General',
|
subgroup="General",
|
||||||
validators=(MaxLengthValidator(20),))
|
validators=(MaxLengthValidator(20),),
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='agenda_numeral_system',
|
name="agenda_numeral_system",
|
||||||
default_value='arabic',
|
default_value="arabic",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='Numeral system for agenda items',
|
label="Numeral system for agenda items",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': 'arabic', 'display_name': 'Arabic'},
|
{"value": "arabic", "display_name": "Arabic"},
|
||||||
{'value': 'roman', 'display_name': 'Roman'}),
|
{"value": "roman", "display_name": "Roman"},
|
||||||
|
),
|
||||||
weight=215,
|
weight=215,
|
||||||
group='Agenda',
|
group="Agenda",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='agenda_start_event_date_time',
|
name="agenda_start_event_date_time",
|
||||||
default_value=None,
|
default_value=None,
|
||||||
input_type='datetimepicker',
|
input_type="datetimepicker",
|
||||||
label='Begin of event',
|
label="Begin of event",
|
||||||
help_text='Input format: DD.MM.YYYY HH:MM',
|
help_text="Input format: DD.MM.YYYY HH:MM",
|
||||||
weight=220,
|
weight=220,
|
||||||
group='Agenda',
|
group="Agenda",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='agenda_hide_internal_items_on_projector',
|
name="agenda_hide_internal_items_on_projector",
|
||||||
default_value=True,
|
default_value=True,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Hide internal items when projecting subitems',
|
label="Hide internal items when projecting subitems",
|
||||||
weight=225,
|
weight=225,
|
||||||
group='Agenda',
|
group="Agenda",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='agenda_new_items_default_visibility',
|
name="agenda_new_items_default_visibility",
|
||||||
default_value='2',
|
default_value="2",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': '1', 'display_name': 'Public item'},
|
{"value": "1", "display_name": "Public item"},
|
||||||
{'value': '2', 'display_name': 'Internal item'},
|
{"value": "2", "display_name": "Internal item"},
|
||||||
{'value': '3', 'display_name': 'Hidden item'}),
|
{"value": "3", "display_name": "Hidden item"},
|
||||||
label='Default visibility for new agenda items (except topics)',
|
),
|
||||||
|
label="Default visibility for new agenda items (except topics)",
|
||||||
weight=227,
|
weight=227,
|
||||||
group='Agenda',
|
group="Agenda",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
# List of speakers
|
# List of speakers
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='agenda_show_last_speakers',
|
name="agenda_show_last_speakers",
|
||||||
default_value=1,
|
default_value=1,
|
||||||
input_type='integer',
|
input_type="integer",
|
||||||
label='Number of last speakers to be shown on the projector',
|
label="Number of last speakers to be shown on the projector",
|
||||||
weight=230,
|
weight=230,
|
||||||
group='Agenda',
|
group="Agenda",
|
||||||
subgroup='List of speakers',
|
subgroup="List of speakers",
|
||||||
validators=(MinValueValidator(0),))
|
validators=(MinValueValidator(0),),
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='agenda_countdown_warning_time',
|
name="agenda_countdown_warning_time",
|
||||||
default_value=0,
|
default_value=0,
|
||||||
input_type='integer',
|
input_type="integer",
|
||||||
label='Show orange countdown in the last x seconds of speaking time',
|
label="Show orange countdown in the last x seconds of speaking time",
|
||||||
help_text='Enter duration in seconds. Choose 0 to disable warning color.',
|
help_text="Enter duration in seconds. Choose 0 to disable warning color.",
|
||||||
weight=235,
|
weight=235,
|
||||||
group='Agenda',
|
group="Agenda",
|
||||||
subgroup='List of speakers',
|
subgroup="List of speakers",
|
||||||
validators=(MinValueValidator(0),))
|
validators=(MinValueValidator(0),),
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='agenda_couple_countdown_and_speakers',
|
name="agenda_couple_countdown_and_speakers",
|
||||||
default_value=False,
|
default_value=False,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Couple countdown with the list of speakers',
|
label="Couple countdown with the list of speakers",
|
||||||
help_text='[Begin speech] starts the countdown, [End speech] stops the countdown.',
|
help_text="[Begin speech] starts the countdown, [End speech] stops the countdown.",
|
||||||
weight=240,
|
weight=240,
|
||||||
group='Agenda',
|
group="Agenda",
|
||||||
subgroup='List of speakers')
|
subgroup="List of speakers",
|
||||||
|
)
|
||||||
|
|
|
@ -14,55 +14,109 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('contenttypes', '0002_remove_content_type_name'),
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Item',
|
name="Item",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('item_number', models.CharField(blank=True, max_length=255)),
|
"id",
|
||||||
('comment', models.TextField(blank=True, null=True)),
|
models.AutoField(
|
||||||
('closed', models.BooleanField(default=False)),
|
auto_created=True,
|
||||||
('type', models.IntegerField(choices=[(1, 'Agenda item'), (2, 'Hidden item')], default=2)),
|
primary_key=True,
|
||||||
('duration', models.CharField(blank=True, max_length=5, null=True)),
|
serialize=False,
|
||||||
('weight', models.IntegerField(default=10000)),
|
verbose_name="ID",
|
||||||
('object_id', models.PositiveIntegerField(blank=True, null=True)),
|
),
|
||||||
('speaker_list_closed', models.BooleanField(default=False)),
|
),
|
||||||
('content_type', models.ForeignKey(
|
("item_number", models.CharField(blank=True, max_length=255)),
|
||||||
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contenttypes.ContentType')),
|
("comment", models.TextField(blank=True, null=True)),
|
||||||
('parent', models.ForeignKey(
|
("closed", models.BooleanField(default=False)),
|
||||||
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='agenda.Item')),
|
(
|
||||||
|
"type",
|
||||||
|
models.IntegerField(
|
||||||
|
choices=[(1, "Agenda item"), (2, "Hidden item")], default=2
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("duration", models.CharField(blank=True, max_length=5, null=True)),
|
||||||
|
("weight", models.IntegerField(default=10000)),
|
||||||
|
("object_id", models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
("speaker_list_closed", models.BooleanField(default=False)),
|
||||||
|
(
|
||||||
|
"content_type",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="contenttypes.ContentType",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"parent",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="children",
|
||||||
|
to="agenda.Item",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'permissions': (
|
"permissions": (
|
||||||
('can_see', 'Can see agenda'),
|
("can_see", "Can see agenda"),
|
||||||
('can_manage', 'Can manage agenda'),
|
("can_manage", "Can manage agenda"),
|
||||||
('can_see_hidden_items', 'Can see hidden items and time scheduling of agenda')),
|
(
|
||||||
'default_permissions': (),
|
"can_see_hidden_items",
|
||||||
|
"Can see hidden items and time scheduling of agenda",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"default_permissions": (),
|
||||||
},
|
},
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Speaker',
|
name="Speaker",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('begin_time', models.DateTimeField(null=True)),
|
"id",
|
||||||
('end_time', models.DateTimeField(null=True)),
|
models.AutoField(
|
||||||
('weight', models.IntegerField(null=True)),
|
auto_created=True,
|
||||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='speakers', to='agenda.Item')),
|
primary_key=True,
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("begin_time", models.DateTimeField(null=True)),
|
||||||
|
("end_time", models.DateTimeField(null=True)),
|
||||||
|
("weight", models.IntegerField(null=True)),
|
||||||
|
(
|
||||||
|
"item",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="speakers",
|
||||||
|
to="agenda.Item",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'permissions': (('can_be_speaker', 'Can put oneself on the list of speakers'),),
|
"permissions": (
|
||||||
'default_permissions': (),
|
("can_be_speaker", "Can put oneself on the list of speakers"),
|
||||||
|
),
|
||||||
|
"default_permissions": (),
|
||||||
},
|
},
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='item',
|
name="item", unique_together=set([("content_type", "object_id")])
|
||||||
unique_together=set([('content_type', 'object_id')]),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,7 +11,7 @@ def convert_duration(apps, schema_editor):
|
||||||
IntegerField. It uses the temporary field for proper renaming the field
|
IntegerField. It uses the temporary field for proper renaming the field
|
||||||
in the end.
|
in the end.
|
||||||
"""
|
"""
|
||||||
Item = apps.get_model('agenda', 'Item')
|
Item = apps.get_model("agenda", "Item")
|
||||||
for item in Item.objects.all():
|
for item in Item.objects.all():
|
||||||
duration = item.duration
|
duration = item.duration
|
||||||
item.duration_tmp = None
|
item.duration_tmp = None
|
||||||
|
@ -20,7 +20,7 @@ def convert_duration(apps, schema_editor):
|
||||||
item.duration_tmp = int(duration)
|
item.duration_tmp = int(duration)
|
||||||
elif isinstance(duration, str):
|
elif isinstance(duration, str):
|
||||||
# Assuming format (h)h:(m)m. If not, new value is None.
|
# Assuming format (h)h:(m)m. If not, new value is None.
|
||||||
split = duration.split(':')
|
split = duration.split(":")
|
||||||
if len(split) == 2 and is_int(split[0]) and is_int(split[1]):
|
if len(split) == 2 and is_int(split[0]) and is_int(split[1]):
|
||||||
# Calculate new duration: hours * 60 + minutes.
|
# Calculate new duration: hours * 60 + minutes.
|
||||||
item.duration_tmp = int(split[0]) * 60 + int(split[1])
|
item.duration_tmp = int(split[0]) * 60 + int(split[1])
|
||||||
|
@ -41,26 +41,17 @@ def is_int(s):
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("agenda", "0001_initial")]
|
||||||
('agenda', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='item',
|
model_name="item",
|
||||||
name='duration_tmp',
|
name="duration_tmp",
|
||||||
field=models.IntegerField(blank=True, null=True),
|
field=models.IntegerField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.RunPython(
|
migrations.RunPython(convert_duration),
|
||||||
convert_duration
|
migrations.RemoveField(model_name="item", name="duration"),
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='item',
|
|
||||||
name='duration',
|
|
||||||
),
|
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name='item',
|
model_name="item", old_name="duration_tmp", new_name="duration"
|
||||||
old_name='duration_tmp',
|
|
||||||
new_name='duration',
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,24 +11,31 @@ from openslides.utils.migrations import (
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("agenda", "0002_item_duration")]
|
||||||
('agenda', '0002_item_duration'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='item',
|
name="item",
|
||||||
options={
|
options={
|
||||||
'default_permissions': (),
|
"default_permissions": (),
|
||||||
'permissions': (
|
"permissions": (
|
||||||
('can_see', 'Can see agenda'),
|
("can_see", "Can see agenda"),
|
||||||
('can_manage', 'Can manage agenda'),
|
("can_manage", "Can manage agenda"),
|
||||||
('can_manage_list_of_speakers', 'Can manage list of speakers'),
|
("can_manage_list_of_speakers", "Can manage list of speakers"),
|
||||||
('can_see_hidden_items', 'Can see hidden items and time scheduling of agenda')
|
(
|
||||||
)
|
"can_see_hidden_items",
|
||||||
|
"Can see hidden items and time scheduling of agenda",
|
||||||
|
),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.RunPython(add_permission_to_groups_based_on_existing_permission(
|
migrations.RunPython(
|
||||||
'can_manage', 'item', 'agenda', 'can_manage_list_of_speakers', 'Can manage list of speakers'
|
add_permission_to_groups_based_on_existing_permission(
|
||||||
)),
|
"can_manage",
|
||||||
|
"item",
|
||||||
|
"agenda",
|
||||||
|
"can_manage_list_of_speakers",
|
||||||
|
"Can manage list of speakers",
|
||||||
|
)
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,14 +7,12 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("agenda", "0003_auto_20170818_1202")]
|
||||||
('agenda', '0003_auto_20170818_1202'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='speaker',
|
model_name="speaker",
|
||||||
name='marked',
|
name="marked",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
)
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,44 +11,47 @@ from openslides.utils.migrations import (
|
||||||
|
|
||||||
|
|
||||||
def delete_old_can_see_hidden_permission(apps, schema_editor):
|
def delete_old_can_see_hidden_permission(apps, schema_editor):
|
||||||
perm = Permission.objects.filter(codename='can_see_hidden_items')
|
perm = Permission.objects.filter(codename="can_see_hidden_items")
|
||||||
if len(perm):
|
if len(perm):
|
||||||
perm = perm.delete()
|
perm = perm.delete()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("agenda", "0004_speaker_marked")]
|
||||||
('agenda', '0004_speaker_marked'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='item',
|
name="item",
|
||||||
options={
|
options={
|
||||||
'default_permissions': (),
|
"default_permissions": (),
|
||||||
'permissions': (
|
"permissions": (
|
||||||
('can_see', 'Can see agenda'),
|
("can_see", "Can see agenda"),
|
||||||
('can_manage', 'Can manage agenda'),
|
("can_manage", "Can manage agenda"),
|
||||||
('can_manage_list_of_speakers', 'Can manage list of speakers'),
|
("can_manage_list_of_speakers", "Can manage list of speakers"),
|
||||||
('can_see_internal_items', 'Can see internal items and time scheduling of agenda')
|
(
|
||||||
)
|
"can_see_internal_items",
|
||||||
|
"Can see internal items and time scheduling of agenda",
|
||||||
|
),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='item',
|
model_name="item",
|
||||||
name='type',
|
name="type",
|
||||||
field=models.IntegerField(
|
field=models.IntegerField(
|
||||||
choices=[
|
choices=[(1, "Agenda item"), (2, "Internal item"), (3, "Hidden item")],
|
||||||
(1, 'Agenda item'),
|
default=3,
|
||||||
(2, 'Internal item'),
|
|
||||||
(3, 'Hidden item')
|
|
||||||
],
|
|
||||||
default=3
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.RunPython(add_permission_to_groups_based_on_existing_permission(
|
migrations.RunPython(
|
||||||
'can_see_hidden_items', 'item', 'agenda', 'can_see_internal_items', 'Can see internal items and time scheduling of agenda'
|
add_permission_to_groups_based_on_existing_permission(
|
||||||
)),
|
"can_see_hidden_items",
|
||||||
|
"item",
|
||||||
|
"agenda",
|
||||||
|
"can_see_internal_items",
|
||||||
|
"Can see internal items and time scheduling of agenda",
|
||||||
|
)
|
||||||
|
),
|
||||||
migrations.RunPython(delete_old_can_see_hidden_permission),
|
migrations.RunPython(delete_old_can_see_hidden_permission),
|
||||||
]
|
]
|
||||||
|
|
|
@ -24,13 +24,14 @@ class ItemManager(models.Manager):
|
||||||
Customized model manager with special methods for agenda tree and
|
Customized model manager with special methods for agenda tree and
|
||||||
numbering.
|
numbering.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_full_queryset(self):
|
def get_full_queryset(self):
|
||||||
"""
|
"""
|
||||||
Returns the normal queryset with all items. In the background all
|
Returns the normal queryset with all items. In the background all
|
||||||
speakers and related items (topics, motions, assignments) are
|
speakers and related items (topics, motions, assignments) are
|
||||||
prefetched from the database.
|
prefetched from the database.
|
||||||
"""
|
"""
|
||||||
return self.get_queryset().prefetch_related('speakers', 'content_object')
|
return self.get_queryset().prefetch_related("speakers", "content_object")
|
||||||
|
|
||||||
def get_only_non_public_items(self):
|
def get_only_non_public_items(self):
|
||||||
"""
|
"""
|
||||||
|
@ -45,14 +46,17 @@ class ItemManager(models.Manager):
|
||||||
Generator that yields a list of items and their children.
|
Generator that yields a list of items and their children.
|
||||||
"""
|
"""
|
||||||
for item in items:
|
for item in items:
|
||||||
if parent_is_not_public or item.type in (item.INTERNAL_ITEM, item.HIDDEN_ITEM):
|
if parent_is_not_public or item.type in (
|
||||||
|
item.INTERNAL_ITEM,
|
||||||
|
item.HIDDEN_ITEM,
|
||||||
|
):
|
||||||
item_is_not_public = True
|
item_is_not_public = True
|
||||||
yield item
|
yield item
|
||||||
else:
|
else:
|
||||||
item_is_not_public = False
|
item_is_not_public = False
|
||||||
yield from yield_items(
|
yield from yield_items(
|
||||||
item_children[item.pk],
|
item_children[item.pk], parent_is_not_public=item_is_not_public
|
||||||
parent_is_not_public=item_is_not_public)
|
)
|
||||||
|
|
||||||
yield from yield_items(root_items)
|
yield from yield_items(root_items)
|
||||||
|
|
||||||
|
@ -64,7 +68,7 @@ class ItemManager(models.Manager):
|
||||||
If only_item_type is given, the tree hides items with other types and
|
If only_item_type is given, the tree hides items with other types and
|
||||||
all of their children.
|
all of their children.
|
||||||
"""
|
"""
|
||||||
queryset = self.order_by('weight')
|
queryset = self.order_by("weight")
|
||||||
item_children: Dict[int, List[Item]] = defaultdict(list)
|
item_children: Dict[int, List[Item]] = defaultdict(list)
|
||||||
root_items = []
|
root_items = []
|
||||||
for item in queryset:
|
for item in queryset:
|
||||||
|
@ -88,7 +92,9 @@ class ItemManager(models.Manager):
|
||||||
If include_content is True, the yielded dictonaries have no key 'id'
|
If include_content is True, the yielded dictonaries have no key 'id'
|
||||||
but a key 'item' with the entire object.
|
but a key 'item' with the entire object.
|
||||||
"""
|
"""
|
||||||
root_items, item_children = self.get_root_and_children(only_item_type=only_item_type)
|
root_items, item_children = self.get_root_and_children(
|
||||||
|
only_item_type=only_item_type
|
||||||
|
)
|
||||||
|
|
||||||
def get_children(items):
|
def get_children(items):
|
||||||
"""
|
"""
|
||||||
|
@ -98,7 +104,9 @@ class ItemManager(models.Manager):
|
||||||
if include_content:
|
if include_content:
|
||||||
yield dict(item=item, children=get_children(item_children[item.pk]))
|
yield dict(item=item, children=get_children(item_children[item.pk]))
|
||||||
else:
|
else:
|
||||||
yield dict(id=item.pk, children=get_children(item_children[item.pk]))
|
yield dict(
|
||||||
|
id=item.pk, children=get_children(item_children[item.pk])
|
||||||
|
)
|
||||||
|
|
||||||
yield from get_children(root_items)
|
yield from get_children(root_items)
|
||||||
|
|
||||||
|
@ -110,6 +118,7 @@ class ItemManager(models.Manager):
|
||||||
The tree has to be a nested object. For example:
|
The tree has to be a nested object. For example:
|
||||||
[{"id": 1}, {"id": 2, "children": [{"id": 3}]}]
|
[{"id": 1}, {"id": 2, "children": [{"id": 3}]}]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def walk_items(tree, parent=None):
|
def walk_items(tree, parent=None):
|
||||||
"""
|
"""
|
||||||
Generator that returns each item in the tree as tuple.
|
Generator that returns each item in the tree as tuple.
|
||||||
|
@ -118,15 +127,17 @@ class ItemManager(models.Manager):
|
||||||
weight of the item.
|
weight of the item.
|
||||||
"""
|
"""
|
||||||
for weight, element in enumerate(tree):
|
for weight, element in enumerate(tree):
|
||||||
yield (element['id'], parent, weight)
|
yield (element["id"], parent, weight)
|
||||||
yield from walk_items(element.get('children', []), element['id'])
|
yield from walk_items(element.get("children", []), element["id"])
|
||||||
|
|
||||||
touched_items: Set[int] = set()
|
touched_items: Set[int] = set()
|
||||||
db_items = dict((item.pk, item) for item in Item.objects.all())
|
db_items = dict((item.pk, item) for item in Item.objects.all())
|
||||||
for item_id, parent_id, weight in walk_items(tree):
|
for item_id, parent_id, weight in walk_items(tree):
|
||||||
# Check that the item is only once in the tree to prevent invalid trees
|
# Check that the item is only once in the tree to prevent invalid trees
|
||||||
if item_id in touched_items:
|
if item_id in touched_items:
|
||||||
raise ValueError("Item {} is more then once in the tree.".format(item_id))
|
raise ValueError(
|
||||||
|
"Item {} is more then once in the tree.".format(item_id)
|
||||||
|
)
|
||||||
touched_items.add(item_id)
|
touched_items.add(item_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -143,36 +154,40 @@ class ItemManager(models.Manager):
|
||||||
db_item.save()
|
db_item.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def number_all(self, numeral_system='arabic'):
|
def number_all(self, numeral_system="arabic"):
|
||||||
"""
|
"""
|
||||||
Auto numbering of the agenda according to the numeral_system. Manually
|
Auto numbering of the agenda according to the numeral_system. Manually
|
||||||
added item numbers will be overwritten.
|
added item numbers will be overwritten.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def walk_tree(tree, number=None):
|
def walk_tree(tree, number=None):
|
||||||
for index, tree_element in enumerate(tree):
|
for index, tree_element in enumerate(tree):
|
||||||
# Calculate number of visable agenda items.
|
# Calculate number of visable agenda items.
|
||||||
if numeral_system == 'roman' and number is None:
|
if numeral_system == "roman" and number is None:
|
||||||
item_number = to_roman(index + 1)
|
item_number = to_roman(index + 1)
|
||||||
else:
|
else:
|
||||||
item_number = str(index + 1)
|
item_number = str(index + 1)
|
||||||
if number is not None:
|
if number is not None:
|
||||||
item_number = '.'.join((number, item_number))
|
item_number = ".".join((number, item_number))
|
||||||
# Add prefix.
|
# Add prefix.
|
||||||
if config['agenda_number_prefix']:
|
if config["agenda_number_prefix"]:
|
||||||
item_number_tmp = "%s %s" % (config['agenda_number_prefix'], item_number)
|
item_number_tmp = "%s %s" % (
|
||||||
|
config["agenda_number_prefix"],
|
||||||
|
item_number,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
item_number_tmp = item_number
|
item_number_tmp = item_number
|
||||||
# Save the new value and go down the tree.
|
# Save the new value and go down the tree.
|
||||||
tree_element['item'].item_number = item_number_tmp
|
tree_element["item"].item_number = item_number_tmp
|
||||||
tree_element['item'].save()
|
tree_element["item"].save()
|
||||||
walk_tree(tree_element['children'], item_number)
|
walk_tree(tree_element["children"], item_number)
|
||||||
|
|
||||||
# Start numbering visable agenda items.
|
# Start numbering visable agenda items.
|
||||||
walk_tree(self.get_tree(only_item_type=Item.AGENDA_ITEM, include_content=True))
|
walk_tree(self.get_tree(only_item_type=Item.AGENDA_ITEM, include_content=True))
|
||||||
|
|
||||||
# Reset number of hidden items.
|
# Reset number of hidden items.
|
||||||
for item in self.get_only_non_public_items():
|
for item in self.get_only_non_public_items():
|
||||||
item.item_number = ''
|
item.item_number = ""
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
|
|
||||||
|
@ -180,18 +195,20 @@ class Item(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
An Agenda Item
|
An Agenda Item
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ItemAccessPermissions()
|
access_permissions = ItemAccessPermissions()
|
||||||
objects = ItemManager()
|
objects = ItemManager()
|
||||||
can_see_permission = 'agenda.can_see'
|
can_see_permission = "agenda.can_see"
|
||||||
|
|
||||||
AGENDA_ITEM = 1
|
AGENDA_ITEM = 1
|
||||||
INTERNAL_ITEM = 2
|
INTERNAL_ITEM = 2
|
||||||
HIDDEN_ITEM = 3
|
HIDDEN_ITEM = 3
|
||||||
|
|
||||||
ITEM_TYPE = (
|
ITEM_TYPE = (
|
||||||
(AGENDA_ITEM, ugettext_lazy('Agenda item')),
|
(AGENDA_ITEM, ugettext_lazy("Agenda item")),
|
||||||
(INTERNAL_ITEM, ugettext_lazy('Internal item')),
|
(INTERNAL_ITEM, ugettext_lazy("Internal item")),
|
||||||
(HIDDEN_ITEM, ugettext_lazy('Hidden item')))
|
(HIDDEN_ITEM, ugettext_lazy("Hidden item")),
|
||||||
|
)
|
||||||
|
|
||||||
item_number = models.CharField(blank=True, max_length=255)
|
item_number = models.CharField(blank=True, max_length=255)
|
||||||
"""
|
"""
|
||||||
|
@ -208,9 +225,7 @@ class Item(RESTModelMixin, models.Model):
|
||||||
Flag, if the item is finished.
|
Flag, if the item is finished.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
type = models.IntegerField(
|
type = models.IntegerField(choices=ITEM_TYPE, default=HIDDEN_ITEM)
|
||||||
choices=ITEM_TYPE,
|
|
||||||
default=HIDDEN_ITEM)
|
|
||||||
"""
|
"""
|
||||||
Type of the agenda item.
|
Type of the agenda item.
|
||||||
|
|
||||||
|
@ -223,11 +238,12 @@ class Item(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
'self',
|
"self",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name='children')
|
related_name="children",
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
The parent item in the agenda tree.
|
The parent item in the agenda tree.
|
||||||
"""
|
"""
|
||||||
|
@ -238,10 +254,8 @@ class Item(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
content_type = models.ForeignKey(
|
content_type = models.ForeignKey(
|
||||||
ContentType,
|
ContentType, on_delete=models.SET_NULL, null=True, blank=True
|
||||||
on_delete=models.SET_NULL,
|
)
|
||||||
null=True,
|
|
||||||
blank=True)
|
|
||||||
"""
|
"""
|
||||||
Field for generic relation to a related object. Type of the object.
|
Field for generic relation to a related object. Type of the object.
|
||||||
"""
|
"""
|
||||||
|
@ -256,8 +270,7 @@ class Item(RESTModelMixin, models.Model):
|
||||||
Field for generic relation to a related object. General field to the related object.
|
Field for generic relation to a related object. General field to the related object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
speaker_list_closed = models.BooleanField(
|
speaker_list_closed = models.BooleanField(default=False)
|
||||||
default=False)
|
|
||||||
"""
|
"""
|
||||||
True, if the list of speakers is closed.
|
True, if the list of speakers is closed.
|
||||||
"""
|
"""
|
||||||
|
@ -265,11 +278,15 @@ class Item(RESTModelMixin, models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
permissions = (
|
permissions = (
|
||||||
('can_see', 'Can see agenda'),
|
("can_see", "Can see agenda"),
|
||||||
('can_manage', 'Can manage agenda'),
|
("can_manage", "Can manage agenda"),
|
||||||
('can_manage_list_of_speakers', 'Can manage list of speakers'),
|
("can_manage_list_of_speakers", "Can manage list of speakers"),
|
||||||
('can_see_internal_items', 'Can see internal items and time scheduling of agenda'))
|
(
|
||||||
unique_together = ('content_type', 'object_id')
|
"can_see_internal_items",
|
||||||
|
"Can see internal items and time scheduling of agenda",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
unique_together = ("content_type", "object_id")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
@ -280,10 +297,11 @@ class Item(RESTModelMixin, models.Model):
|
||||||
list of speakers projector element is disabled.
|
list of speakers projector element is disabled.
|
||||||
"""
|
"""
|
||||||
Projector.remove_any(
|
Projector.remove_any(
|
||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate, name="agenda/list-of-speakers", id=self.pk
|
||||||
name='agenda/list-of-speakers',
|
)
|
||||||
id=self.pk)
|
return super().delete( # type: ignore
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self):
|
def title(self):
|
||||||
|
@ -293,8 +311,10 @@ class Item(RESTModelMixin, models.Model):
|
||||||
try:
|
try:
|
||||||
return self.content_object.get_agenda_title()
|
return self.content_object.get_agenda_title()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise NotImplementedError('You have to provide a get_agenda_title '
|
raise NotImplementedError(
|
||||||
'method on your related model.')
|
"You have to provide a get_agenda_title "
|
||||||
|
"method on your related model."
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title_with_type(self):
|
def title_with_type(self):
|
||||||
|
@ -304,8 +324,10 @@ class Item(RESTModelMixin, models.Model):
|
||||||
try:
|
try:
|
||||||
return self.content_object.get_agenda_title_with_type()
|
return self.content_object.get_agenda_title_with_type()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise NotImplementedError('You have to provide a get_agenda_title_with_type '
|
raise NotImplementedError(
|
||||||
'method on your related model.')
|
"You have to provide a get_agenda_title_with_type "
|
||||||
|
"method on your related model."
|
||||||
|
)
|
||||||
|
|
||||||
def is_internal(self):
|
def is_internal(self):
|
||||||
"""
|
"""
|
||||||
|
@ -314,8 +336,9 @@ class Item(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
Attention! This executes one query for each ancestor of the item.
|
Attention! This executes one query for each ancestor of the item.
|
||||||
"""
|
"""
|
||||||
return (self.type == self.INTERNAL_ITEM or
|
return self.type == self.INTERNAL_ITEM or (
|
||||||
(self.parent is not None and self.parent.is_internal()))
|
self.parent is not None and self.parent.is_internal()
|
||||||
|
)
|
||||||
|
|
||||||
def is_hidden(self):
|
def is_hidden(self):
|
||||||
"""
|
"""
|
||||||
|
@ -324,15 +347,16 @@ class Item(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
Attention! This executes one query for each ancestor of the item.
|
Attention! This executes one query for each ancestor of the item.
|
||||||
"""
|
"""
|
||||||
return (self.type == self.HIDDEN_ITEM or
|
return self.type == self.HIDDEN_ITEM or (
|
||||||
(self.parent is not None and self.parent.is_hidden()))
|
self.parent is not None and self.parent.is_hidden()
|
||||||
|
)
|
||||||
|
|
||||||
def get_next_speaker(self):
|
def get_next_speaker(self):
|
||||||
"""
|
"""
|
||||||
Returns the speaker object of the speaker who is next.
|
Returns the speaker object of the speaker who is next.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return self.speakers.filter(begin_time=None).order_by('weight')[0]
|
return self.speakers.filter(begin_time=None).order_by("weight")[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# The list of speakers is empty.
|
# The list of speakers is empty.
|
||||||
return None
|
return None
|
||||||
|
@ -342,6 +366,7 @@ class SpeakerManager(models.Manager):
|
||||||
"""
|
"""
|
||||||
Manager for Speaker model. Provides a customized add method.
|
Manager for Speaker model. Provides a customized add method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def add(self, user, item, skip_autoupdate=False):
|
def add(self, user, item, skip_autoupdate=False):
|
||||||
"""
|
"""
|
||||||
Customized manager method to prevent anonymous users to be on the
|
Customized manager method to prevent anonymous users to be on the
|
||||||
|
@ -350,12 +375,15 @@ class SpeakerManager(models.Manager):
|
||||||
"""
|
"""
|
||||||
if self.filter(user=user, item=item, begin_time=None).exists():
|
if self.filter(user=user, item=item, begin_time=None).exists():
|
||||||
raise OpenSlidesError(
|
raise OpenSlidesError(
|
||||||
_('{user} is already on the list of speakers.').format(user=user))
|
_("{user} is already on the list of speakers.").format(user=user)
|
||||||
|
)
|
||||||
if isinstance(user, AnonymousUser):
|
if isinstance(user, AnonymousUser):
|
||||||
raise OpenSlidesError(
|
raise OpenSlidesError(
|
||||||
_('An anonymous user can not be on lists of speakers.'))
|
_("An anonymous user can not be on lists of speakers.")
|
||||||
weight = (self.filter(item=item).aggregate(
|
)
|
||||||
models.Max('weight'))['weight__max'] or 0)
|
weight = (
|
||||||
|
self.filter(item=item).aggregate(models.Max("weight"))["weight__max"] or 0
|
||||||
|
)
|
||||||
speaker = self.model(item=item, user=user, weight=weight + 1)
|
speaker = self.model(item=item, user=user, weight=weight + 1)
|
||||||
speaker.save(force_insert=True, skip_autoupdate=skip_autoupdate)
|
speaker.save(force_insert=True, skip_autoupdate=skip_autoupdate)
|
||||||
return speaker
|
return speaker
|
||||||
|
@ -368,17 +396,12 @@ class Speaker(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
objects = SpeakerManager()
|
objects = SpeakerManager()
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
"""
|
"""
|
||||||
ForeinKey to the user who speaks.
|
ForeinKey to the user who speaks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
item = models.ForeignKey(
|
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name="speakers")
|
||||||
Item,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='speakers')
|
|
||||||
"""
|
"""
|
||||||
ForeinKey to the agenda item to which the user want to speak.
|
ForeinKey to the agenda item to which the user want to speak.
|
||||||
"""
|
"""
|
||||||
|
@ -405,9 +428,7 @@ class Speaker(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
permissions = (
|
permissions = (("can_be_speaker", "Can put oneself on the list of speakers"),)
|
||||||
('can_be_speaker', 'Can put oneself on the list of speakers'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.user)
|
return str(self.user)
|
||||||
|
@ -420,8 +441,11 @@ class Speaker(RESTModelMixin, models.Model):
|
||||||
speaking, end his speech.
|
speaking, end his speech.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
current_speaker = (Speaker.objects.filter(item=self.item, end_time=None)
|
current_speaker = (
|
||||||
.exclude(begin_time=None).get())
|
Speaker.objects.filter(item=self.item, end_time=None)
|
||||||
|
.exclude(begin_time=None)
|
||||||
|
.get()
|
||||||
|
)
|
||||||
except Speaker.DoesNotExist:
|
except Speaker.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
@ -431,15 +455,21 @@ class Speaker(RESTModelMixin, models.Model):
|
||||||
self.weight = None
|
self.weight = None
|
||||||
self.begin_time = timezone.now()
|
self.begin_time = timezone.now()
|
||||||
self.save() # Here, the item is saved and causes an autoupdate.
|
self.save() # Here, the item is saved and causes an autoupdate.
|
||||||
if config['agenda_couple_countdown_and_speakers']:
|
if config["agenda_couple_countdown_and_speakers"]:
|
||||||
countdown, created = Countdown.objects.get_or_create(pk=1, defaults={
|
countdown, created = Countdown.objects.get_or_create(
|
||||||
'default_time': config['projector_default_countdown'],
|
pk=1,
|
||||||
'countdown_time': config['projector_default_countdown']})
|
defaults={
|
||||||
|
"default_time": config["projector_default_countdown"],
|
||||||
|
"countdown_time": config["projector_default_countdown"],
|
||||||
|
},
|
||||||
|
)
|
||||||
if not created:
|
if not created:
|
||||||
countdown.control(action='reset', skip_autoupdate=True)
|
countdown.control(action="reset", skip_autoupdate=True)
|
||||||
countdown.control(action='start', skip_autoupdate=True)
|
countdown.control(action="start", skip_autoupdate=True)
|
||||||
|
|
||||||
inform_changed_data(countdown) # Here, the autoupdate for the countdown is triggered.
|
inform_changed_data(
|
||||||
|
countdown
|
||||||
|
) # Here, the autoupdate for the countdown is triggered.
|
||||||
|
|
||||||
def end_speech(self, skip_autoupdate=False):
|
def end_speech(self, skip_autoupdate=False):
|
||||||
"""
|
"""
|
||||||
|
@ -447,13 +477,13 @@ class Speaker(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
self.end_time = timezone.now()
|
self.end_time = timezone.now()
|
||||||
self.save(skip_autoupdate=skip_autoupdate)
|
self.save(skip_autoupdate=skip_autoupdate)
|
||||||
if config['agenda_couple_countdown_and_speakers']:
|
if config["agenda_couple_countdown_and_speakers"]:
|
||||||
try:
|
try:
|
||||||
countdown = Countdown.objects.get(pk=1)
|
countdown = Countdown.objects.get(pk=1)
|
||||||
except Countdown.DoesNotExist:
|
except Countdown.DoesNotExist:
|
||||||
pass # Do not create a new countdown on stop action
|
pass # Do not create a new countdown on stop action
|
||||||
else:
|
else:
|
||||||
countdown.control(action='reset', skip_autoupdate=skip_autoupdate)
|
countdown.control(action="reset", skip_autoupdate=skip_autoupdate)
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
def get_root_rest_element(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -16,14 +16,15 @@ class ItemListSlide(ProjectorElement):
|
||||||
|
|
||||||
Additionally set 'tree' to True to get also children of children.
|
Additionally set 'tree' to True to get also children of children.
|
||||||
"""
|
"""
|
||||||
name = 'agenda/item-list'
|
|
||||||
|
name = "agenda/item-list"
|
||||||
|
|
||||||
def check_data(self):
|
def check_data(self):
|
||||||
pk = self.config_entry.get('id')
|
pk = self.config_entry.get("id")
|
||||||
if pk is not None:
|
if pk is not None:
|
||||||
# Children slide.
|
# Children slide.
|
||||||
if not Item.objects.filter(pk=pk).exists():
|
if not Item.objects.filter(pk=pk).exists():
|
||||||
raise ProjectorException('Item does not exist.')
|
raise ProjectorException("Item does not exist.")
|
||||||
|
|
||||||
|
|
||||||
class ListOfSpeakersSlide(ProjectorElement):
|
class ListOfSpeakersSlide(ProjectorElement):
|
||||||
|
@ -31,21 +32,23 @@ class ListOfSpeakersSlide(ProjectorElement):
|
||||||
Slide definitions for Item model.
|
Slide definitions for Item model.
|
||||||
This is only for list of speakers slide. You have to set 'id'.
|
This is only for list of speakers slide. You have to set 'id'.
|
||||||
"""
|
"""
|
||||||
name = 'agenda/list-of-speakers'
|
|
||||||
|
name = "agenda/list-of-speakers"
|
||||||
|
|
||||||
def check_data(self):
|
def check_data(self):
|
||||||
if not Item.objects.filter(pk=self.config_entry.get('id')).exists():
|
if not Item.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||||
raise ProjectorException('Item does not exist.')
|
raise ProjectorException("Item does not exist.")
|
||||||
|
|
||||||
def update_data(self):
|
def update_data(self):
|
||||||
return {'agenda_item_id': self.config_entry.get('id')}
|
return {"agenda_item_id": self.config_entry.get("id")}
|
||||||
|
|
||||||
|
|
||||||
class CurrentListOfSpeakersSlide(ProjectorElement):
|
class CurrentListOfSpeakersSlide(ProjectorElement):
|
||||||
"""
|
"""
|
||||||
Slide for the current list of speakers.
|
Slide for the current list of speakers.
|
||||||
"""
|
"""
|
||||||
name = 'agenda/current-list-of-speakers'
|
|
||||||
|
name = "agenda/current-list-of-speakers"
|
||||||
|
|
||||||
|
|
||||||
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
||||||
|
|
|
@ -7,16 +7,17 @@ class SpeakerSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for agenda.models.Speaker objects.
|
Serializer for agenda.models.Speaker objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Speaker
|
model = Speaker
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
"id",
|
||||||
'user',
|
"user",
|
||||||
'begin_time',
|
"begin_time",
|
||||||
'end_time',
|
"end_time",
|
||||||
'weight',
|
"weight",
|
||||||
'marked',
|
"marked",
|
||||||
'item', # js-data needs the item-id in the nested object to define relations.
|
"item", # js-data needs the item-id in the nested object to define relations.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,36 +25,39 @@ class RelatedItemRelatedField(RelatedField):
|
||||||
"""
|
"""
|
||||||
A custom field to use for the content_object generic relationship.
|
A custom field to use for the content_object generic relationship.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
"""
|
"""
|
||||||
Returns info concerning the related object extracted from the api URL
|
Returns info concerning the related object extracted from the api URL
|
||||||
of this object.
|
of this object.
|
||||||
"""
|
"""
|
||||||
return {'collection': value.get_collection_string(), 'id': value.get_rest_pk()}
|
return {"collection": value.get_collection_string(), "id": value.get_rest_pk()}
|
||||||
|
|
||||||
|
|
||||||
class ItemSerializer(ModelSerializer):
|
class ItemSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for agenda.models.Item objects.
|
Serializer for agenda.models.Item objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
content_object = RelatedItemRelatedField(read_only=True)
|
content_object = RelatedItemRelatedField(read_only=True)
|
||||||
speakers = SpeakerSerializer(many=True, read_only=True)
|
speakers = SpeakerSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Item
|
model = Item
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
"id",
|
||||||
'item_number',
|
"item_number",
|
||||||
'title',
|
"title",
|
||||||
'title_with_type',
|
"title_with_type",
|
||||||
'comment',
|
"comment",
|
||||||
'closed',
|
"closed",
|
||||||
'type',
|
"type",
|
||||||
'is_internal',
|
"is_internal",
|
||||||
'is_hidden',
|
"is_hidden",
|
||||||
'duration',
|
"duration",
|
||||||
'speakers',
|
"speakers",
|
||||||
'speaker_list_closed',
|
"speaker_list_closed",
|
||||||
'content_object',
|
"content_object",
|
||||||
'weight',
|
"weight",
|
||||||
'parent',)
|
"parent",
|
||||||
|
)
|
||||||
|
|
|
@ -16,18 +16,18 @@ def listen_to_related_object_post_save(sender, instance, created, **kwargs):
|
||||||
Do not run caching and autoupdate if the instance has a key
|
Do not run caching and autoupdate if the instance has a key
|
||||||
skip_autoupdate in the agenda_item_update_information container.
|
skip_autoupdate in the agenda_item_update_information container.
|
||||||
"""
|
"""
|
||||||
if hasattr(instance, 'get_agenda_title'):
|
if hasattr(instance, "get_agenda_title"):
|
||||||
if created:
|
if created:
|
||||||
attrs = {}
|
attrs = {}
|
||||||
for attr in ('type', 'parent_id', 'comment', 'duration', 'weight'):
|
for attr in ("type", "parent_id", "comment", "duration", "weight"):
|
||||||
if instance.agenda_item_update_information.get(attr):
|
if instance.agenda_item_update_information.get(attr):
|
||||||
attrs[attr] = instance.agenda_item_update_information.get(attr)
|
attrs[attr] = instance.agenda_item_update_information.get(attr)
|
||||||
Item.objects.create(content_object=instance, **attrs)
|
Item.objects.create(content_object=instance, **attrs)
|
||||||
|
|
||||||
# If the object is created, the related_object has to be sent again.
|
# If the object is created, the related_object has to be sent again.
|
||||||
if not instance.agenda_item_update_information.get('skip_autoupdate'):
|
if not instance.agenda_item_update_information.get("skip_autoupdate"):
|
||||||
inform_changed_data(instance)
|
inform_changed_data(instance)
|
||||||
elif not instance.agenda_item_update_information.get('skip_autoupdate'):
|
elif not instance.agenda_item_update_information.get("skip_autoupdate"):
|
||||||
# If the object has changed, then also the agenda item has to be sent.
|
# If the object has changed, then also the agenda item has to be sent.
|
||||||
inform_changed_data(instance.agenda_item)
|
inform_changed_data(instance.agenda_item)
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ def listen_to_related_object_post_delete(sender, instance, **kwargs):
|
||||||
Receiver function to delete agenda items. It is connected to the signal
|
Receiver function to delete agenda items. It is connected to the signal
|
||||||
django.db.models.signals.post_delete during app loading.
|
django.db.models.signals.post_delete during app loading.
|
||||||
"""
|
"""
|
||||||
if hasattr(instance, 'get_agenda_title'):
|
if hasattr(instance, "get_agenda_title"):
|
||||||
content_type = ContentType.objects.get_for_model(instance)
|
content_type = ContentType.objects.get_for_model(instance)
|
||||||
try:
|
try:
|
||||||
# Attention: This delete() call is also necessary to remove
|
# Attention: This delete() call is also necessary to remove
|
||||||
|
@ -53,10 +53,12 @@ def get_permission_change_data(sender, permissions, **kwargs):
|
||||||
Yields all necessary collections if 'agenda.can_see' or
|
Yields all necessary collections if 'agenda.can_see' or
|
||||||
'agenda.can_see_internal_items' permissions changes.
|
'agenda.can_see_internal_items' permissions changes.
|
||||||
"""
|
"""
|
||||||
agenda_app = apps.get_app_config(app_label='agenda')
|
agenda_app = apps.get_app_config(app_label="agenda")
|
||||||
for permission in permissions:
|
for permission in permissions:
|
||||||
# There could be only one 'agenda.can_see' and then we want to return data.
|
# There could be only one 'agenda.can_see' and then we want to return data.
|
||||||
if (permission.content_type.app_label == agenda_app.label
|
if (
|
||||||
and permission.codename in ('can_see', 'can_see_internal_items')):
|
permission.content_type.app_label == agenda_app.label
|
||||||
|
and permission.codename in ("can_see", "can_see_internal_items")
|
||||||
|
):
|
||||||
yield from agenda_app.get_startup_elements()
|
yield from agenda_app.get_startup_elements()
|
||||||
break
|
break
|
||||||
|
|
|
@ -24,12 +24,14 @@ from .models import Item, Speaker
|
||||||
|
|
||||||
# Viewsets for the REST API
|
# Viewsets for the REST API
|
||||||
|
|
||||||
|
|
||||||
class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
|
class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint for agenda items.
|
API endpoint for agenda items.
|
||||||
|
|
||||||
There are some views, see check_view_permissions.
|
There are some views, see check_view_permissions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ItemAccessPermissions()
|
access_permissions = ItemAccessPermissions()
|
||||||
queryset = Item.objects.all()
|
queryset = Item.objects.all()
|
||||||
|
|
||||||
|
@ -37,22 +39,26 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ('list', 'retrieve'):
|
if self.action in ("list", "retrieve"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action in ('metadata', 'manage_speaker', 'tree'):
|
elif self.action in ("metadata", "manage_speaker", "tree"):
|
||||||
result = has_perm(self.request.user, 'agenda.can_see')
|
result = has_perm(self.request.user, "agenda.can_see")
|
||||||
# For manage_speaker and tree requests the rest of the check is
|
# For manage_speaker and tree requests the rest of the check is
|
||||||
# done in the specific method. See below.
|
# done in the specific method. See below.
|
||||||
elif self.action in ('partial_update', 'update', 'sort', 'assign'):
|
elif self.action in ("partial_update", "update", "sort", "assign"):
|
||||||
result = (has_perm(self.request.user, 'agenda.can_see') and
|
result = (
|
||||||
has_perm(self.request.user, 'agenda.can_see_internal_items') and
|
has_perm(self.request.user, "agenda.can_see")
|
||||||
has_perm(self.request.user, 'agenda.can_manage'))
|
and has_perm(self.request.user, "agenda.can_see_internal_items")
|
||||||
elif self.action in ('speak', 'sort_speakers'):
|
and has_perm(self.request.user, "agenda.can_manage")
|
||||||
result = (has_perm(self.request.user, 'agenda.can_see') and
|
)
|
||||||
has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'))
|
elif self.action in ("speak", "sort_speakers"):
|
||||||
elif self.action in ('numbering', ):
|
result = has_perm(self.request.user, "agenda.can_see") and has_perm(
|
||||||
result = (has_perm(self.request.user, 'agenda.can_see') and
|
self.request.user, "agenda.can_manage_list_of_speakers"
|
||||||
has_perm(self.request.user, 'agenda.can_manage'))
|
)
|
||||||
|
elif self.action in ("numbering",):
|
||||||
|
result = has_perm(self.request.user, "agenda.can_see") and has_perm(
|
||||||
|
self.request.user, "agenda.can_manage"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
@ -82,7 +88,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@detail_route(methods=['POST', 'PATCH', 'DELETE'])
|
@detail_route(methods=["POST", "PATCH", "DELETE"])
|
||||||
def manage_speaker(self, request, pk=None):
|
def manage_speaker(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
Special view endpoint to add users to the list of speakers or remove
|
Special view endpoint to add users to the list of speakers or remove
|
||||||
|
@ -103,55 +109,61 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
# Retrieve item.
|
# Retrieve item.
|
||||||
item = self.get_object()
|
item = self.get_object()
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
# Retrieve user_id
|
# Retrieve user_id
|
||||||
user_id = request.data.get('user')
|
user_id = request.data.get("user")
|
||||||
|
|
||||||
# Check permissions and other conditions. Get user instance.
|
# Check permissions and other conditions. Get user instance.
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
# Add oneself
|
# Add oneself
|
||||||
if not has_perm(self.request.user, 'agenda.can_be_speaker'):
|
if not has_perm(self.request.user, "agenda.can_be_speaker"):
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
if item.speaker_list_closed:
|
if item.speaker_list_closed:
|
||||||
raise ValidationError({'detail': _('The list of speakers is closed.')})
|
raise ValidationError(
|
||||||
|
{"detail": _("The list of speakers is closed.")}
|
||||||
|
)
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
else:
|
else:
|
||||||
# Add someone else.
|
# Add someone else.
|
||||||
if not has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'):
|
if not has_perm(
|
||||||
|
self.request.user, "agenda.can_manage_list_of_speakers"
|
||||||
|
):
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
try:
|
try:
|
||||||
user = get_user_model().objects.get(pk=int(user_id))
|
user = get_user_model().objects.get(pk=int(user_id))
|
||||||
except (ValueError, get_user_model().DoesNotExist):
|
except (ValueError, get_user_model().DoesNotExist):
|
||||||
raise ValidationError({'detail': _('User does not exist.')})
|
raise ValidationError({"detail": _("User does not exist.")})
|
||||||
|
|
||||||
# Try to add the user. This ensurse that a user is not twice in the
|
# Try to add the user. This ensurse that a user is not twice in the
|
||||||
# list of coming speakers.
|
# list of coming speakers.
|
||||||
try:
|
try:
|
||||||
Speaker.objects.add(user, item)
|
Speaker.objects.add(user, item)
|
||||||
except OpenSlidesError as e:
|
except OpenSlidesError as e:
|
||||||
raise ValidationError({'detail': str(e)})
|
raise ValidationError({"detail": str(e)})
|
||||||
message = _('User %s was successfully added to the list of speakers.') % user
|
message = (
|
||||||
|
_("User %s was successfully added to the list of speakers.") % user
|
||||||
|
)
|
||||||
|
|
||||||
# Send new speaker via autoupdate because users without permission
|
# Send new speaker via autoupdate because users without permission
|
||||||
# to see users may not have it but can get it now.
|
# to see users may not have it but can get it now.
|
||||||
inform_changed_data([user])
|
inform_changed_data([user])
|
||||||
|
|
||||||
# Toggle 'marked' for the speaker
|
# Toggle 'marked' for the speaker
|
||||||
elif request.method == 'PATCH':
|
elif request.method == "PATCH":
|
||||||
# Check permissions
|
# Check permissions
|
||||||
if not has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'):
|
if not has_perm(self.request.user, "agenda.can_manage_list_of_speakers"):
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
|
|
||||||
# Retrieve user_id
|
# Retrieve user_id
|
||||||
user_id = request.data.get('user')
|
user_id = request.data.get("user")
|
||||||
try:
|
try:
|
||||||
user = get_user_model().objects.get(pk=int(user_id))
|
user = get_user_model().objects.get(pk=int(user_id))
|
||||||
except (ValueError, get_user_model().DoesNotExist):
|
except (ValueError, get_user_model().DoesNotExist):
|
||||||
raise ValidationError({'detail': _('User does not exist.')})
|
raise ValidationError({"detail": _("User does not exist.")})
|
||||||
|
|
||||||
marked = request.data.get('marked')
|
marked = request.data.get("marked")
|
||||||
if not isinstance(marked, bool):
|
if not isinstance(marked, bool):
|
||||||
raise ValidationError({'detail': _('Marked has to be a bool.')})
|
raise ValidationError({"detail": _("Marked has to be a bool.")})
|
||||||
|
|
||||||
queryset = Speaker.objects.filter(item=item, user=user)
|
queryset = Speaker.objects.filter(item=item, user=user)
|
||||||
try:
|
try:
|
||||||
|
@ -160,37 +172,46 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
# there is only one speaker instance or none.
|
# there is only one speaker instance or none.
|
||||||
speaker = queryset.get()
|
speaker = queryset.get()
|
||||||
except Speaker.DoesNotExist:
|
except Speaker.DoesNotExist:
|
||||||
raise ValidationError({'detail': _('The user is not in the list of speakers.')})
|
raise ValidationError(
|
||||||
|
{"detail": _("The user is not in the list of speakers.")}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
speaker.marked = marked
|
speaker.marked = marked
|
||||||
speaker.save()
|
speaker.save()
|
||||||
if speaker.marked:
|
if speaker.marked:
|
||||||
message = _('You are successfully marked the speaker.')
|
message = _("You are successfully marked the speaker.")
|
||||||
else:
|
else:
|
||||||
message = _('You are successfully unmarked the speaker.')
|
message = _("You are successfully unmarked the speaker.")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# request.method == 'DELETE'
|
# request.method == 'DELETE'
|
||||||
speaker_ids = request.data.get('speaker')
|
speaker_ids = request.data.get("speaker")
|
||||||
|
|
||||||
# Check permissions and other conditions. Get speaker instance.
|
# Check permissions and other conditions. Get speaker instance.
|
||||||
if speaker_ids is None:
|
if speaker_ids is None:
|
||||||
# Remove oneself
|
# Remove oneself
|
||||||
queryset = Speaker.objects.filter(
|
queryset = Speaker.objects.filter(
|
||||||
item=item, user=self.request.user).exclude(weight=None)
|
item=item, user=self.request.user
|
||||||
|
).exclude(weight=None)
|
||||||
try:
|
try:
|
||||||
# We assume that there aren't multiple entries because this
|
# We assume that there aren't multiple entries because this
|
||||||
# is forbidden by the Manager's add method. We assume that
|
# is forbidden by the Manager's add method. We assume that
|
||||||
# there is only one speaker instance or none.
|
# there is only one speaker instance or none.
|
||||||
speaker = queryset.get()
|
speaker = queryset.get()
|
||||||
except Speaker.DoesNotExist:
|
except Speaker.DoesNotExist:
|
||||||
raise ValidationError({'detail': _('You are not on the list of speakers.')})
|
raise ValidationError(
|
||||||
|
{"detail": _("You are not on the list of speakers.")}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
speaker.delete()
|
speaker.delete()
|
||||||
message = _('You are successfully removed from the list of speakers.')
|
message = _(
|
||||||
|
"You are successfully removed from the list of speakers."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Remove someone else.
|
# Remove someone else.
|
||||||
if not has_perm(self.request.user, 'agenda.can_manage_list_of_speakers'):
|
if not has_perm(
|
||||||
|
self.request.user, "agenda.can_manage_list_of_speakers"
|
||||||
|
):
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
if type(speaker_ids) is int:
|
if type(speaker_ids) is int:
|
||||||
speaker_ids = [speaker_ids]
|
speaker_ids = [speaker_ids]
|
||||||
|
@ -209,15 +230,24 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
inform_changed_data(item)
|
inform_changed_data(item)
|
||||||
|
|
||||||
if deleted_speaker_count > 1:
|
if deleted_speaker_count > 1:
|
||||||
message = str(deleted_speaker_count) + ' ' + _('speakers have been removed from the list of speakers.')
|
message = (
|
||||||
|
str(deleted_speaker_count)
|
||||||
|
+ " "
|
||||||
|
+ _("speakers have been removed from the list of speakers.")
|
||||||
|
)
|
||||||
elif deleted_speaker_count == 1:
|
elif deleted_speaker_count == 1:
|
||||||
message = _('User %s has been removed from the list of speakers.') % deleted_speaker_name
|
message = (
|
||||||
|
_("User %s has been removed from the list of speakers.")
|
||||||
|
% deleted_speaker_name
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
message = _('No speakers have been removed from the list of speakers.')
|
message = _(
|
||||||
|
"No speakers have been removed from the list of speakers."
|
||||||
|
)
|
||||||
# Initiate response.
|
# Initiate response.
|
||||||
return Response({'detail': message})
|
return Response({"detail": message})
|
||||||
|
|
||||||
@detail_route(methods=['PUT', 'DELETE'])
|
@detail_route(methods=["PUT", "DELETE"])
|
||||||
def speak(self, request, pk=None):
|
def speak(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
Special view endpoint to begin and end speech of speakers. Send PUT
|
Special view endpoint to begin and end speech of speakers. Send PUT
|
||||||
|
@ -227,20 +257,22 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
# Retrieve item.
|
# Retrieve item.
|
||||||
item = self.get_object()
|
item = self.get_object()
|
||||||
|
|
||||||
if request.method == 'PUT':
|
if request.method == "PUT":
|
||||||
# Retrieve speaker_id
|
# Retrieve speaker_id
|
||||||
speaker_id = request.data.get('speaker')
|
speaker_id = request.data.get("speaker")
|
||||||
if speaker_id is None:
|
if speaker_id is None:
|
||||||
speaker = item.get_next_speaker()
|
speaker = item.get_next_speaker()
|
||||||
if speaker is None:
|
if speaker is None:
|
||||||
raise ValidationError({'detail': _('The list of speakers is empty.')})
|
raise ValidationError(
|
||||||
|
{"detail": _("The list of speakers is empty.")}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
speaker = Speaker.objects.get(pk=int(speaker_id))
|
speaker = Speaker.objects.get(pk=int(speaker_id))
|
||||||
except (ValueError, Speaker.DoesNotExist):
|
except (ValueError, Speaker.DoesNotExist):
|
||||||
raise ValidationError({'detail': _('Speaker does not exist.')})
|
raise ValidationError({"detail": _("Speaker does not exist.")})
|
||||||
speaker.begin_speech()
|
speaker.begin_speech()
|
||||||
message = _('User is now speaking.')
|
message = _("User is now speaking.")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# request.method == 'DELETE'
|
# request.method == 'DELETE'
|
||||||
|
@ -248,17 +280,27 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
# We assume that there aren't multiple entries because this
|
# We assume that there aren't multiple entries because this
|
||||||
# is forbidden by the Model's begin_speech method. We assume that
|
# is forbidden by the Model's begin_speech method. We assume that
|
||||||
# there is only one speaker instance or none.
|
# there is only one speaker instance or none.
|
||||||
current_speaker = Speaker.objects.filter(item=item, end_time=None).exclude(begin_time=None).get()
|
current_speaker = (
|
||||||
|
Speaker.objects.filter(item=item, end_time=None)
|
||||||
|
.exclude(begin_time=None)
|
||||||
|
.get()
|
||||||
|
)
|
||||||
except Speaker.DoesNotExist:
|
except Speaker.DoesNotExist:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{'detail': _('There is no one speaking at the moment according to %(item)s.') % {'item': item}})
|
{
|
||||||
|
"detail": _(
|
||||||
|
"There is no one speaking at the moment according to %(item)s."
|
||||||
|
)
|
||||||
|
% {"item": item}
|
||||||
|
}
|
||||||
|
)
|
||||||
current_speaker.end_speech()
|
current_speaker.end_speech()
|
||||||
message = _('The speech is finished now.')
|
message = _("The speech is finished now.")
|
||||||
|
|
||||||
# Initiate response.
|
# Initiate response.
|
||||||
return Response({'detail': message})
|
return Response({"detail": message})
|
||||||
|
|
||||||
@detail_route(methods=['POST'])
|
@detail_route(methods=["POST"])
|
||||||
def sort_speakers(self, request, pk=None):
|
def sort_speakers(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
Special view endpoint to sort the list of speakers.
|
Special view endpoint to sort the list of speakers.
|
||||||
|
@ -269,10 +311,9 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
item = self.get_object()
|
item = self.get_object()
|
||||||
|
|
||||||
# Check data
|
# Check data
|
||||||
speaker_ids = request.data.get('speakers')
|
speaker_ids = request.data.get("speakers")
|
||||||
if not isinstance(speaker_ids, list):
|
if not isinstance(speaker_ids, list):
|
||||||
raise ValidationError(
|
raise ValidationError({"detail": _("Invalid data.")})
|
||||||
{'detail': _('Invalid data.')})
|
|
||||||
|
|
||||||
# Get all speakers
|
# Get all speakers
|
||||||
speakers = {}
|
speakers = {}
|
||||||
|
@ -283,8 +324,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
valid_speakers = []
|
valid_speakers = []
|
||||||
for speaker_id in speaker_ids:
|
for speaker_id in speaker_ids:
|
||||||
if not isinstance(speaker_id, int) or speakers.get(speaker_id) is None:
|
if not isinstance(speaker_id, int) or speakers.get(speaker_id) is None:
|
||||||
raise ValidationError(
|
raise ValidationError({"detail": _("Invalid data.")})
|
||||||
{'detail': _('Invalid data.')})
|
|
||||||
valid_speakers.append(speakers[speaker_id])
|
valid_speakers.append(speakers[speaker_id])
|
||||||
weight = 0
|
weight = 0
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
@ -297,50 +337,57 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
inform_changed_data(item)
|
inform_changed_data(item)
|
||||||
|
|
||||||
# Initiate response.
|
# Initiate response.
|
||||||
return Response({'detail': _('List of speakers successfully sorted.')})
|
return Response({"detail": _("List of speakers successfully sorted.")})
|
||||||
|
|
||||||
@list_route(methods=['post'])
|
@list_route(methods=["post"])
|
||||||
def numbering(self, request):
|
def numbering(self, request):
|
||||||
"""
|
"""
|
||||||
Auto numbering of the agenda according to the config. Manually added
|
Auto numbering of the agenda according to the config. Manually added
|
||||||
item numbers will be overwritten.
|
item numbers will be overwritten.
|
||||||
"""
|
"""
|
||||||
if not config['agenda_enable_numbering']:
|
if not config["agenda_enable_numbering"]:
|
||||||
raise ValidationError({'detail': _('Numbering of agenda items is deactivated.')})
|
raise ValidationError(
|
||||||
|
{"detail": _("Numbering of agenda items is deactivated.")}
|
||||||
|
)
|
||||||
|
|
||||||
Item.objects.number_all(numeral_system=config['agenda_numeral_system'])
|
Item.objects.number_all(numeral_system=config["agenda_numeral_system"])
|
||||||
return Response({'detail': _('The agenda has been numbered.')})
|
return Response({"detail": _("The agenda has been numbered.")})
|
||||||
|
|
||||||
@list_route(methods=['post'])
|
@list_route(methods=["post"])
|
||||||
def sort(self, request):
|
def sort(self, request):
|
||||||
"""
|
"""
|
||||||
Sort agenda items. Also checks parent field to prevent hierarchical
|
Sort agenda items. Also checks parent field to prevent hierarchical
|
||||||
loops.
|
loops.
|
||||||
"""
|
"""
|
||||||
nodes = request.data.get('nodes', [])
|
nodes = request.data.get("nodes", [])
|
||||||
parent_id = request.data.get('parent_id')
|
parent_id = request.data.get("parent_id")
|
||||||
items = []
|
items = []
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for index, node in enumerate(nodes):
|
for index, node in enumerate(nodes):
|
||||||
item = Item.objects.get(pk=node['id'])
|
item = Item.objects.get(pk=node["id"])
|
||||||
item.parent_id = parent_id
|
item.parent_id = parent_id
|
||||||
item.weight = index
|
item.weight = index
|
||||||
item.save(skip_autoupdate=True)
|
item.save(skip_autoupdate=True)
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
|
||||||
# Now check consistency. TODO: Try to use less DB queries.
|
# Now check consistency. TODO: Try to use less DB queries.
|
||||||
item = Item.objects.get(pk=node['id'])
|
item = Item.objects.get(pk=node["id"])
|
||||||
ancestor = item.parent
|
ancestor = item.parent
|
||||||
while ancestor is not None:
|
while ancestor is not None:
|
||||||
if ancestor == item:
|
if ancestor == item:
|
||||||
raise ValidationError({'detail': _(
|
raise ValidationError(
|
||||||
'There must not be a hierarchical loop. Please reload the page.')})
|
{
|
||||||
|
"detail": _(
|
||||||
|
"There must not be a hierarchical loop. Please reload the page."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
ancestor = ancestor.parent
|
ancestor = ancestor.parent
|
||||||
|
|
||||||
inform_changed_data(items)
|
inform_changed_data(items)
|
||||||
return Response({'detail': _('The agenda has been sorted.')})
|
return Response({"detail": _("The agenda has been sorted.")})
|
||||||
|
|
||||||
@list_route(methods=['post'])
|
@list_route(methods=["post"])
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def assign(self, request):
|
def assign(self, request):
|
||||||
"""
|
"""
|
||||||
|
@ -359,9 +406,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
"items": {
|
"items": {
|
||||||
"description": "An array of agenda item ids where the items should be assigned to the new parent id.",
|
"description": "An array of agenda item ids where the items should be assigned to the new parent id.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {"type": "integer"},
|
||||||
"type": "integer",
|
|
||||||
},
|
|
||||||
"minItems": 1,
|
"minItems": 1,
|
||||||
"uniqueItems": True,
|
"uniqueItems": True,
|
||||||
},
|
},
|
||||||
|
@ -377,13 +422,19 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
try:
|
try:
|
||||||
jsonschema.validate(request.data, schema)
|
jsonschema.validate(request.data, schema)
|
||||||
except jsonschema.ValidationError as err:
|
except jsonschema.ValidationError as err:
|
||||||
raise ValidationError({'detail': str(err)})
|
raise ValidationError({"detail": str(err)})
|
||||||
|
|
||||||
# Check parent item
|
# Check parent item
|
||||||
try:
|
try:
|
||||||
parent = Item.objects.get(pk=request.data['parent_id'])
|
parent = Item.objects.get(pk=request.data["parent_id"])
|
||||||
except Item.DoesNotExist:
|
except Item.DoesNotExist:
|
||||||
raise ValidationError({'detail': 'Parent item {} does not exist'.format(request.data['parent_id'])})
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"detail": "Parent item {} does not exist".format(
|
||||||
|
request.data["parent_id"]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Collect ancestors
|
# Collect ancestors
|
||||||
ancestors = [parent.pk]
|
ancestors = [parent.pk]
|
||||||
|
@ -394,16 +445,24 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
|
|
||||||
# First validate all items before changeing them.
|
# First validate all items before changeing them.
|
||||||
items = []
|
items = []
|
||||||
for item_id in request.data['items']:
|
for item_id in request.data["items"]:
|
||||||
# Prevent hierarchical loops.
|
# Prevent hierarchical loops.
|
||||||
if item_id in ancestors:
|
if item_id in ancestors:
|
||||||
raise ValidationError({'detail': 'Assigning item {} to one of its children is not possible.'.format(item_id)})
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"detail": "Assigning item {} to one of its children is not possible.".format(
|
||||||
|
item_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Check every item
|
# Check every item
|
||||||
try:
|
try:
|
||||||
items.append(Item.objects.get(pk=item_id))
|
items.append(Item.objects.get(pk=item_id))
|
||||||
except Item.DoesNotExist:
|
except Item.DoesNotExist:
|
||||||
raise ValidationError({'detail': 'Item {} does not exist'.format(item_id)})
|
raise ValidationError(
|
||||||
|
{"detail": "Item {} does not exist".format(item_id)}
|
||||||
|
)
|
||||||
|
|
||||||
# OK, assign new parents.
|
# OK, assign new parents.
|
||||||
for item in items:
|
for item in items:
|
||||||
|
@ -415,6 +474,10 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
|
||||||
inform_changed_data(items)
|
inform_changed_data(items)
|
||||||
|
|
||||||
# Send response.
|
# Send response.
|
||||||
return Response({
|
return Response(
|
||||||
'detail': _('{number} items successfully assigned.').format(number=len(items)),
|
{
|
||||||
})
|
"detail": _("{number} items successfully assigned.").format(
|
||||||
|
number=len(items)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -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.
|
Access permissions container for Assignment and AssignmentViewSet.
|
||||||
"""
|
"""
|
||||||
base_permission = 'assignments.can_see'
|
|
||||||
|
base_permission = "assignments.can_see"
|
||||||
|
|
||||||
async def get_restricted_data(
|
async def get_restricted_data(
|
||||||
self,
|
self, full_data: List[Dict[str, Any]], user_id: int
|
||||||
full_data: List[Dict[str, Any]],
|
) -> List[Dict[str, Any]]:
|
||||||
user_id: int) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the user. Removes unpublished polls for non admins so that they
|
for the user. Removes unpublished polls for non admins so that they
|
||||||
only get a result like the AssignmentShortSerializer would give them.
|
only get a result like the AssignmentShortSerializer would give them.
|
||||||
"""
|
"""
|
||||||
# Parse data.
|
# Parse data.
|
||||||
if await async_has_perm(user_id, 'assignments.can_see') and await async_has_perm(user_id, 'assignments.can_manage'):
|
if await async_has_perm(
|
||||||
|
user_id, "assignments.can_see"
|
||||||
|
) and await async_has_perm(user_id, "assignments.can_manage"):
|
||||||
data = full_data
|
data = full_data
|
||||||
elif await async_has_perm(user_id, 'assignments.can_see'):
|
elif await async_has_perm(user_id, "assignments.can_see"):
|
||||||
# Exclude unpublished poll votes.
|
# Exclude unpublished poll votes.
|
||||||
data = []
|
data = []
|
||||||
for full in full_data:
|
for full in full_data:
|
||||||
full_copy = full.copy()
|
full_copy = full.copy()
|
||||||
polls = full_copy['polls']
|
polls = full_copy["polls"]
|
||||||
for poll in polls:
|
for poll in polls:
|
||||||
if not poll['published']:
|
if not poll["published"]:
|
||||||
for option in poll['options']:
|
for option in poll["options"]:
|
||||||
option['votes'] = [] # clear votes for not published polls
|
option["votes"] = [] # clear votes for not published polls
|
||||||
poll['has_votes'] = False # A user should see, if there are votes.
|
poll[
|
||||||
|
"has_votes"
|
||||||
|
] = False # A user should see, if there are votes.
|
||||||
data.append(full_copy)
|
data.append(full_copy)
|
||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
|
|
|
@ -7,8 +7,8 @@ from ..utils.projector import register_projector_elements
|
||||||
|
|
||||||
|
|
||||||
class AssignmentsAppConfig(AppConfig):
|
class AssignmentsAppConfig(AppConfig):
|
||||||
name = 'openslides.assignments'
|
name = "openslides.assignments"
|
||||||
verbose_name = 'OpenSlides Assignments'
|
verbose_name = "OpenSlides Assignments"
|
||||||
angular_site_module = True
|
angular_site_module = True
|
||||||
angular_projector_module = True
|
angular_projector_module = True
|
||||||
|
|
||||||
|
@ -28,17 +28,23 @@ class AssignmentsAppConfig(AppConfig):
|
||||||
# Connect signals.
|
# Connect signals.
|
||||||
permission_change.connect(
|
permission_change.connect(
|
||||||
get_permission_change_data,
|
get_permission_change_data,
|
||||||
dispatch_uid='assignments_get_permission_change_data')
|
dispatch_uid="assignments_get_permission_change_data",
|
||||||
|
)
|
||||||
|
|
||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register(self.get_model('Assignment').get_collection_string(), AssignmentViewSet)
|
router.register(
|
||||||
router.register('assignments/poll', AssignmentPollViewSet)
|
self.get_model("Assignment").get_collection_string(), AssignmentViewSet
|
||||||
|
)
|
||||||
|
router.register("assignments/poll", AssignmentPollViewSet)
|
||||||
|
|
||||||
# Register required_users
|
# Register required_users
|
||||||
required_user.add_collection_string(self.get_model('Assignment').get_collection_string(), required_users)
|
required_user.add_collection_string(
|
||||||
|
self.get_model("Assignment").get_collection_string(), required_users
|
||||||
|
)
|
||||||
|
|
||||||
def get_config_variables(self):
|
def get_config_variables(self):
|
||||||
from .config_variables import get_config_variables
|
from .config_variables import get_config_variables
|
||||||
|
|
||||||
return get_config_variables()
|
return get_config_variables()
|
||||||
|
|
||||||
def get_startup_elements(self):
|
def get_startup_elements(self):
|
||||||
|
@ -46,18 +52,15 @@ class AssignmentsAppConfig(AppConfig):
|
||||||
Yields all Cachables required on startup i. e. opening the websocket
|
Yields all Cachables required on startup i. e. opening the websocket
|
||||||
connection.
|
connection.
|
||||||
"""
|
"""
|
||||||
yield self.get_model('Assignment')
|
yield self.get_model("Assignment")
|
||||||
|
|
||||||
def get_angular_constants(self):
|
def get_angular_constants(self):
|
||||||
assignment = self.get_model('Assignment')
|
assignment = self.get_model("Assignment")
|
||||||
Item = TypedDict('Item', {'value': int, 'display_name': str})
|
Item = TypedDict("Item", {"value": int, "display_name": str})
|
||||||
phases: List[Item] = []
|
phases: List[Item] = []
|
||||||
for phase in assignment.PHASES:
|
for phase in assignment.PHASES:
|
||||||
phases.append({
|
phases.append({"value": phase[0], "display_name": phase[1]})
|
||||||
'value': phase[0],
|
return {"AssignmentPhases": phases}
|
||||||
'display_name': phase[1],
|
|
||||||
})
|
|
||||||
return {'AssignmentPhases': phases}
|
|
||||||
|
|
||||||
|
|
||||||
def required_users(element: Dict[str, Any]) -> Set[int]:
|
def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||||
|
@ -65,7 +68,9 @@ def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||||
Returns all user ids that are displayed as candidates (including poll
|
Returns all user ids that are displayed as candidates (including poll
|
||||||
options) in the assignment element.
|
options) in the assignment element.
|
||||||
"""
|
"""
|
||||||
candidates = set(related_user['user_id'] for related_user in element['assignment_related_users'])
|
candidates = set(
|
||||||
for poll in element['polls']:
|
related_user["user_id"] for related_user in element["assignment_related_users"]
|
||||||
candidates.update(option['candidate_id'] for option in poll['options'])
|
)
|
||||||
|
for poll in element["polls"]:
|
||||||
|
candidates.update(option["candidate_id"] for option in poll["options"])
|
||||||
return candidates
|
return candidates
|
||||||
|
|
|
@ -13,96 +13,118 @@ def get_config_variables():
|
||||||
"""
|
"""
|
||||||
# Ballot and ballot papers
|
# Ballot and ballot papers
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='assignments_poll_vote_values',
|
name="assignments_poll_vote_values",
|
||||||
default_value='auto',
|
default_value="auto",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='Election method',
|
label="Election method",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': 'auto', 'display_name': 'Automatic assign of method'},
|
{"value": "auto", "display_name": "Automatic assign of method"},
|
||||||
{'value': 'votes', 'display_name': 'Always one option per candidate'},
|
{"value": "votes", "display_name": "Always one option per candidate"},
|
||||||
{'value': 'yesnoabstain', 'display_name': 'Always Yes-No-Abstain per candidate'},
|
{
|
||||||
{'value': 'yesno', 'display_name': 'Always Yes/No per candidate'}),
|
"value": "yesnoabstain",
|
||||||
|
"display_name": "Always Yes-No-Abstain per candidate",
|
||||||
|
},
|
||||||
|
{"value": "yesno", "display_name": "Always Yes/No per candidate"},
|
||||||
|
),
|
||||||
weight=410,
|
weight=410,
|
||||||
group='Elections',
|
group="Elections",
|
||||||
subgroup='Ballot and ballot papers')
|
subgroup="Ballot and ballot papers",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='assignments_poll_100_percent_base',
|
name="assignments_poll_100_percent_base",
|
||||||
default_value='YES_NO_ABSTAIN',
|
default_value="YES_NO_ABSTAIN",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='The 100-%-base of an election result consists of',
|
label="The 100-%-base of an election result consists of",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': 'YES_NO_ABSTAIN', 'display_name': 'Yes/No/Abstain per candidate'},
|
{"value": "YES_NO_ABSTAIN", "display_name": "Yes/No/Abstain per candidate"},
|
||||||
{'value': 'YES_NO', 'display_name': 'Yes/No per candidate'},
|
{"value": "YES_NO", "display_name": "Yes/No per candidate"},
|
||||||
{'value': 'VALID', 'display_name': 'All valid ballots'},
|
{"value": "VALID", "display_name": "All valid ballots"},
|
||||||
{'value': 'CAST', 'display_name': 'All casted ballots'},
|
{"value": "CAST", "display_name": "All casted ballots"},
|
||||||
{'value': 'DISABLED', 'display_name': 'Disabled (no percents)'}),
|
{"value": "DISABLED", "display_name": "Disabled (no percents)"},
|
||||||
help_text=('For Yes/No/Abstain per candidate and Yes/No per candidate the 100-%-base '
|
),
|
||||||
'depends on the election method: If there is only one option per candidate, '
|
help_text=(
|
||||||
'the sum of all votes of all candidates is 100 %. Otherwise for each '
|
"For Yes/No/Abstain per candidate and Yes/No per candidate the 100-%-base "
|
||||||
'candidate the sum of all votes is 100 %.'),
|
"depends on the election method: If there is only one option per candidate, "
|
||||||
|
"the sum of all votes of all candidates is 100 %. Otherwise for each "
|
||||||
|
"candidate the sum of all votes is 100 %."
|
||||||
|
),
|
||||||
weight=420,
|
weight=420,
|
||||||
group='Elections',
|
group="Elections",
|
||||||
subgroup='Ballot and ballot papers')
|
subgroup="Ballot and ballot papers",
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: Add server side validation of the choices.
|
# TODO: Add server side validation of the choices.
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='assignments_poll_default_majority_method',
|
name="assignments_poll_default_majority_method",
|
||||||
default_value=majorityMethods[0]['value'],
|
default_value=majorityMethods[0]["value"],
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
choices=majorityMethods,
|
choices=majorityMethods,
|
||||||
label='Required majority',
|
label="Required majority",
|
||||||
help_text='Default method to check whether a candidate has reached the required majority.',
|
help_text="Default method to check whether a candidate has reached the required majority.",
|
||||||
weight=425,
|
weight=425,
|
||||||
group='Elections',
|
group="Elections",
|
||||||
subgroup='Ballot and ballot papers')
|
subgroup="Ballot and ballot papers",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='assignments_add_candidates_to_list_of_speakers',
|
name="assignments_add_candidates_to_list_of_speakers",
|
||||||
default_value=True,
|
default_value=True,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Put all candidates on the list of speakers',
|
label="Put all candidates on the list of speakers",
|
||||||
weight=428,
|
weight=428,
|
||||||
group='Elections',
|
group="Elections",
|
||||||
subgroup='Ballot and ballot papers')
|
subgroup="Ballot and ballot papers",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='assignments_pdf_ballot_papers_selection',
|
name="assignments_pdf_ballot_papers_selection",
|
||||||
default_value='CUSTOM_NUMBER',
|
default_value="CUSTOM_NUMBER",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='Number of ballot papers (selection)',
|
label="Number of ballot papers (selection)",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': 'NUMBER_OF_DELEGATES', 'display_name': 'Number of all delegates'},
|
{"value": "NUMBER_OF_DELEGATES", "display_name": "Number of all delegates"},
|
||||||
{'value': 'NUMBER_OF_ALL_PARTICIPANTS', 'display_name': 'Number of all participants'},
|
{
|
||||||
{'value': 'CUSTOM_NUMBER', 'display_name': 'Use the following custom number'}),
|
"value": "NUMBER_OF_ALL_PARTICIPANTS",
|
||||||
|
"display_name": "Number of all participants",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "CUSTOM_NUMBER",
|
||||||
|
"display_name": "Use the following custom number",
|
||||||
|
},
|
||||||
|
),
|
||||||
weight=430,
|
weight=430,
|
||||||
group='Elections',
|
group="Elections",
|
||||||
subgroup='Ballot and ballot papers')
|
subgroup="Ballot and ballot papers",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='assignments_pdf_ballot_papers_number',
|
name="assignments_pdf_ballot_papers_number",
|
||||||
default_value=8,
|
default_value=8,
|
||||||
input_type='integer',
|
input_type="integer",
|
||||||
label='Custom number of ballot papers',
|
label="Custom number of ballot papers",
|
||||||
weight=440,
|
weight=440,
|
||||||
group='Elections',
|
group="Elections",
|
||||||
subgroup='Ballot and ballot papers',
|
subgroup="Ballot and ballot papers",
|
||||||
validators=(MinValueValidator(1),))
|
validators=(MinValueValidator(1),),
|
||||||
|
)
|
||||||
|
|
||||||
# PDF
|
# PDF
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='assignments_pdf_title',
|
name="assignments_pdf_title",
|
||||||
default_value='Elections',
|
default_value="Elections",
|
||||||
label='Title for PDF document (all elections)',
|
label="Title for PDF document (all elections)",
|
||||||
weight=460,
|
weight=460,
|
||||||
group='Elections',
|
group="Elections",
|
||||||
subgroup='PDF')
|
subgroup="PDF",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='assignments_pdf_preamble',
|
name="assignments_pdf_preamble",
|
||||||
default_value='',
|
default_value="",
|
||||||
label='Preamble text for PDF document (all elections)',
|
label="Preamble text for PDF document (all elections)",
|
||||||
weight=470,
|
weight=470,
|
||||||
group='Elections',
|
group="Elections",
|
||||||
subgroup='PDF')
|
subgroup="PDF",
|
||||||
|
)
|
||||||
|
|
|
@ -15,104 +15,196 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('core', '0001_initial'),
|
("core", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Assignment',
|
name="Assignment",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('title', models.CharField(max_length=100)),
|
"id",
|
||||||
('description', models.TextField(blank=True)),
|
models.AutoField(
|
||||||
('open_posts', models.PositiveSmallIntegerField()),
|
auto_created=True,
|
||||||
('poll_description_default', models.CharField(blank=True, max_length=79)),
|
primary_key=True,
|
||||||
('phase', models.IntegerField(choices=[(0, 'Searching for candidates'), (1, 'Voting'), (2, 'Finished')], default=0)),
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=100)),
|
||||||
|
("description", models.TextField(blank=True)),
|
||||||
|
("open_posts", models.PositiveSmallIntegerField()),
|
||||||
|
(
|
||||||
|
"poll_description_default",
|
||||||
|
models.CharField(blank=True, max_length=79),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"phase",
|
||||||
|
models.IntegerField(
|
||||||
|
choices=[
|
||||||
|
(0, "Searching for candidates"),
|
||||||
|
(1, "Voting"),
|
||||||
|
(2, "Finished"),
|
||||||
|
],
|
||||||
|
default=0,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Election',
|
"verbose_name": "Election",
|
||||||
'default_permissions': (),
|
"default_permissions": (),
|
||||||
'permissions': (
|
"permissions": (
|
||||||
('can_see', 'Can see elections'),
|
("can_see", "Can see elections"),
|
||||||
('can_nominate_other', 'Can nominate another participant'),
|
("can_nominate_other", "Can nominate another participant"),
|
||||||
('can_nominate_self', 'Can nominate oneself'),
|
("can_nominate_self", "Can nominate oneself"),
|
||||||
('can_manage', 'Can manage elections')),
|
("can_manage", "Can manage elections"),
|
||||||
'ordering': ('title',),
|
),
|
||||||
|
"ordering": ("title",),
|
||||||
},
|
},
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='AssignmentOption',
|
name="AssignmentOption",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"candidate",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='AssignmentPoll',
|
name="AssignmentPoll",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('votesvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)),
|
"id",
|
||||||
('votesinvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)),
|
models.AutoField(
|
||||||
('votescast', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)),
|
auto_created=True,
|
||||||
('published', models.BooleanField(default=False)),
|
primary_key=True,
|
||||||
('yesnoabstain', models.BooleanField(default=False)),
|
serialize=False,
|
||||||
('description', models.CharField(blank=True, max_length=79)),
|
verbose_name="ID",
|
||||||
('assignment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polls', to='assignments.Assignment')),
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"votesvalid",
|
||||||
|
openslides.utils.models.MinMaxIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"votesinvalid",
|
||||||
|
openslides.utils.models.MinMaxIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"votescast",
|
||||||
|
openslides.utils.models.MinMaxIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
("published", models.BooleanField(default=False)),
|
||||||
|
("yesnoabstain", models.BooleanField(default=False)),
|
||||||
|
("description", models.CharField(blank=True, max_length=79)),
|
||||||
|
(
|
||||||
|
"assignment",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="polls",
|
||||||
|
to="assignments.Assignment",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='AssignmentRelatedUser',
|
name="AssignmentRelatedUser",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('elected', models.BooleanField(default=False)),
|
"id",
|
||||||
('assignment', models.ForeignKey(
|
models.AutoField(
|
||||||
on_delete=django.db.models.deletion.CASCADE, related_name='assignment_related_users', to='assignments.Assignment')),
|
auto_created=True,
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("elected", models.BooleanField(default=False)),
|
||||||
|
(
|
||||||
|
"assignment",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="assignment_related_users",
|
||||||
|
to="assignments.Assignment",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='AssignmentVote',
|
name="AssignmentVote",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('weight', models.IntegerField(default=1, null=True)),
|
"id",
|
||||||
('value', models.CharField(max_length=255, null=True)),
|
models.AutoField(
|
||||||
('option', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='assignments.AssignmentOption')),
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("weight", models.IntegerField(default=1, null=True)),
|
||||||
|
("value", models.CharField(max_length=255, null=True)),
|
||||||
|
(
|
||||||
|
"option",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="votes",
|
||||||
|
to="assignments.AssignmentOption",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='assignmentoption',
|
model_name="assignmentoption",
|
||||||
name='poll',
|
name="poll",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='assignments.AssignmentPoll'),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="options",
|
||||||
|
to="assignments.AssignmentPoll",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='assignment',
|
model_name="assignment",
|
||||||
name='related_users',
|
name="related_users",
|
||||||
field=models.ManyToManyField(through='assignments.AssignmentRelatedUser', to=settings.AUTH_USER_MODEL),
|
field=models.ManyToManyField(
|
||||||
|
through="assignments.AssignmentRelatedUser", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='assignment',
|
model_name="assignment",
|
||||||
name='tags',
|
name="tags",
|
||||||
field=models.ManyToManyField(blank=True, to='core.Tag'),
|
field=models.ManyToManyField(blank=True, to="core.Tag"),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='assignmentrelateduser',
|
name="assignmentrelateduser", unique_together=set([("assignment", "user")])
|
||||||
unique_together=set([('assignment', 'user')]),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,18 +7,13 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("assignments", "0001_initial")]
|
||||||
('assignments', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(model_name="assignmentpoll", name="yesnoabstain"),
|
||||||
model_name='assignmentpoll',
|
|
||||||
name='yesnoabstain',
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='assignmentpoll',
|
model_name="assignmentpoll",
|
||||||
name='pollmethod',
|
name="pollmethod",
|
||||||
field=models.CharField(default='yna', max_length=5),
|
field=models.CharField(default="yna", max_length=5),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,19 +7,17 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("assignments", "0002_assignmentpoll_pollmethod")]
|
||||||
('assignments', '0002_assignmentpoll_pollmethod'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='assignmentrelateduser',
|
model_name="assignmentrelateduser",
|
||||||
name='weight',
|
name="weight",
|
||||||
field=models.IntegerField(default=0),
|
field=models.IntegerField(default=0),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='assignmentoption',
|
model_name="assignmentoption",
|
||||||
name='weight',
|
name="weight",
|
||||||
field=models.IntegerField(default=0),
|
field=models.IntegerField(default=0),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,19 +9,17 @@ from openslides.utils.models import MinMaxIntegerField
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("assignments", "0003_candidate_weight")]
|
||||||
('assignments', '0003_candidate_weight'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='assignmentpoll',
|
model_name="assignmentpoll",
|
||||||
name='votesabstain',
|
name="votesabstain",
|
||||||
field=MinMaxIntegerField(null=True, blank=True, min_value=-2),
|
field=MinMaxIntegerField(null=True, blank=True, min_value=-2),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='assignmentpoll',
|
model_name="assignmentpoll",
|
||||||
name='votesno',
|
name="votesno",
|
||||||
field=MinMaxIntegerField(null=True, blank=True, min_value=-2),
|
field=MinMaxIntegerField(null=True, blank=True, min_value=-2),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,69 +8,73 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("assignments", "0004_auto_20180703_1523")]
|
||||||
('assignments', '0004_auto_20180703_1523'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='assignmentpoll',
|
model_name="assignmentpoll",
|
||||||
name='votescast',
|
name="votescast",
|
||||||
field=models.DecimalField(
|
field=models.DecimalField(
|
||||||
blank=True,
|
blank=True,
|
||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
max_digits=15,
|
max_digits=15,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='assignmentpoll',
|
model_name="assignmentpoll",
|
||||||
name='votesinvalid',
|
name="votesinvalid",
|
||||||
field=models.DecimalField(
|
field=models.DecimalField(
|
||||||
blank=True,
|
blank=True,
|
||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
max_digits=15,
|
max_digits=15,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='assignmentpoll',
|
model_name="assignmentpoll",
|
||||||
name='votesvalid',
|
name="votesvalid",
|
||||||
field=models.DecimalField(
|
field=models.DecimalField(
|
||||||
blank=True,
|
blank=True,
|
||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
max_digits=15,
|
max_digits=15,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='assignmentvote',
|
model_name="assignmentvote",
|
||||||
name='weight',
|
name="weight",
|
||||||
field=models.DecimalField(
|
field=models.DecimalField(
|
||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
default=Decimal('1'),
|
default=Decimal("1"),
|
||||||
max_digits=15,
|
max_digits=15,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='assignmentpoll',
|
model_name="assignmentpoll",
|
||||||
name='votesabstain',
|
name="votesabstain",
|
||||||
field=models.DecimalField(
|
field=models.DecimalField(
|
||||||
blank=True,
|
blank=True,
|
||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
max_digits=15,
|
max_digits=15,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='assignmentpoll',
|
model_name="assignmentpoll",
|
||||||
name='votesno',
|
name="votesno",
|
||||||
field=models.DecimalField(
|
field=models.DecimalField(
|
||||||
blank=True,
|
blank=True,
|
||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
max_digits=15,
|
max_digits=15,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -31,16 +31,13 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
assignment = models.ForeignKey(
|
assignment = models.ForeignKey(
|
||||||
'Assignment',
|
"Assignment", on_delete=models.CASCADE, related_name="assignment_related_users"
|
||||||
on_delete=models.CASCADE,
|
)
|
||||||
related_name='assignment_related_users')
|
|
||||||
"""
|
"""
|
||||||
ForeinKey to the assignment.
|
ForeinKey to the assignment.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
"""
|
"""
|
||||||
ForeinKey to the user who is related to the assignment.
|
ForeinKey to the user who is related to the assignment.
|
||||||
"""
|
"""
|
||||||
|
@ -57,7 +54,7 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
unique_together = ('assignment', 'user')
|
unique_together = ("assignment", "user")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s <-> %s" % (self.assignment, self.user)
|
return "%s <-> %s" % (self.assignment, self.user)
|
||||||
|
@ -73,6 +70,7 @@ class AssignmentManager(models.Manager):
|
||||||
"""
|
"""
|
||||||
Customized model manager to support our get_full_queryset method.
|
Customized model manager to support our get_full_queryset method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_full_queryset(self):
|
def get_full_queryset(self):
|
||||||
"""
|
"""
|
||||||
Returns the normal queryset with all assignments. In the background
|
Returns the normal queryset with all assignments. In the background
|
||||||
|
@ -80,18 +78,17 @@ class AssignmentManager(models.Manager):
|
||||||
polls are prefetched from the database.
|
polls are prefetched from the database.
|
||||||
"""
|
"""
|
||||||
return self.get_queryset().prefetch_related(
|
return self.get_queryset().prefetch_related(
|
||||||
'related_users',
|
"related_users", "agenda_items", "polls", "tags"
|
||||||
'agenda_items',
|
)
|
||||||
'polls',
|
|
||||||
'tags')
|
|
||||||
|
|
||||||
|
|
||||||
class Assignment(RESTModelMixin, models.Model):
|
class Assignment(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Model for assignments.
|
Model for assignments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = AssignmentAccessPermissions()
|
access_permissions = AssignmentAccessPermissions()
|
||||||
can_see_permission = 'assignments.can_see'
|
can_see_permission = "assignments.can_see"
|
||||||
|
|
||||||
objects = AssignmentManager()
|
objects = AssignmentManager()
|
||||||
|
|
||||||
|
@ -100,19 +97,17 @@ class Assignment(RESTModelMixin, models.Model):
|
||||||
PHASE_FINISHED = 2
|
PHASE_FINISHED = 2
|
||||||
|
|
||||||
PHASES = (
|
PHASES = (
|
||||||
(PHASE_SEARCH, 'Searching for candidates'),
|
(PHASE_SEARCH, "Searching for candidates"),
|
||||||
(PHASE_VOTING, 'Voting'),
|
(PHASE_VOTING, "Voting"),
|
||||||
(PHASE_FINISHED, 'Finished'),
|
(PHASE_FINISHED, "Finished"),
|
||||||
)
|
)
|
||||||
|
|
||||||
title = models.CharField(
|
title = models.CharField(max_length=100)
|
||||||
max_length=100)
|
|
||||||
"""
|
"""
|
||||||
Title of the assignment.
|
Title of the assignment.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
description = models.TextField(
|
description = models.TextField(blank=True)
|
||||||
blank=True)
|
|
||||||
"""
|
"""
|
||||||
Text to describe the assignment.
|
Text to describe the assignment.
|
||||||
"""
|
"""
|
||||||
|
@ -122,23 +117,19 @@ class Assignment(RESTModelMixin, models.Model):
|
||||||
The number of members to be elected.
|
The number of members to be elected.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
poll_description_default = models.CharField(
|
poll_description_default = models.CharField(max_length=79, blank=True)
|
||||||
max_length=79,
|
|
||||||
blank=True)
|
|
||||||
"""
|
"""
|
||||||
Default text for the poll description.
|
Default text for the poll description.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
phase = models.IntegerField(
|
phase = models.IntegerField(choices=PHASES, default=PHASE_SEARCH)
|
||||||
choices=PHASES,
|
|
||||||
default=PHASE_SEARCH)
|
|
||||||
"""
|
"""
|
||||||
Phase in which the assignment is.
|
Phase in which the assignment is.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
related_users = models.ManyToManyField(
|
related_users = models.ManyToManyField(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL, through="AssignmentRelatedUser"
|
||||||
through='AssignmentRelatedUser')
|
)
|
||||||
"""
|
"""
|
||||||
Users that are candidates or elected.
|
Users that are candidates or elected.
|
||||||
|
|
||||||
|
@ -152,18 +143,18 @@ class Assignment(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
# In theory there could be one then more agenda_item. But we support only
|
# In theory there could be one then more agenda_item. But we support only
|
||||||
# one. See the property agenda_item.
|
# one. See the property agenda_item.
|
||||||
agenda_items = GenericRelation(Item, related_name='assignments')
|
agenda_items = GenericRelation(Item, related_name="assignments")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
permissions = (
|
permissions = (
|
||||||
('can_see', 'Can see elections'),
|
("can_see", "Can see elections"),
|
||||||
('can_nominate_other', 'Can nominate another participant'),
|
("can_nominate_other", "Can nominate another participant"),
|
||||||
('can_nominate_self', 'Can nominate oneself'),
|
("can_nominate_self", "Can nominate oneself"),
|
||||||
('can_manage', 'Can manage elections'),
|
("can_manage", "Can manage elections"),
|
||||||
)
|
)
|
||||||
ordering = ('title', )
|
ordering = ("title",)
|
||||||
verbose_name = ugettext_noop('Election')
|
verbose_name = ugettext_noop("Election")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
@ -174,26 +165,25 @@ class Assignment(RESTModelMixin, models.Model):
|
||||||
assignment projector element is disabled.
|
assignment projector element is disabled.
|
||||||
"""
|
"""
|
||||||
Projector.remove_any(
|
Projector.remove_any(
|
||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate, name="assignments/assignment", id=self.pk
|
||||||
name='assignments/assignment',
|
)
|
||||||
id=self.pk)
|
return super().delete( # type: ignore
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore # TODO fix typing
|
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def candidates(self):
|
def candidates(self):
|
||||||
"""
|
"""
|
||||||
Queryset that represents the candidates for the assignment.
|
Queryset that represents the candidates for the assignment.
|
||||||
"""
|
"""
|
||||||
return self.related_users.filter(
|
return self.related_users.filter(assignmentrelateduser__elected=False)
|
||||||
assignmentrelateduser__elected=False)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def elected(self):
|
def elected(self):
|
||||||
"""
|
"""
|
||||||
Queryset that represents all elected users for the assignment.
|
Queryset that represents all elected users for the assignment.
|
||||||
"""
|
"""
|
||||||
return self.related_users.filter(
|
return self.related_users.filter(assignmentrelateduser__elected=True)
|
||||||
assignmentrelateduser__elected=True)
|
|
||||||
|
|
||||||
def is_candidate(self, user):
|
def is_candidate(self, user):
|
||||||
"""
|
"""
|
||||||
|
@ -215,22 +205,22 @@ class Assignment(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Adds the user as candidate.
|
Adds the user as candidate.
|
||||||
"""
|
"""
|
||||||
weight = self.assignment_related_users.aggregate(
|
weight = (
|
||||||
models.Max('weight'))['weight__max'] or 0
|
self.assignment_related_users.aggregate(models.Max("weight"))["weight__max"]
|
||||||
defaults = {
|
or 0
|
||||||
'elected': False,
|
)
|
||||||
'weight': weight + 1}
|
defaults = {"elected": False, "weight": weight + 1}
|
||||||
related_user, __ = self.assignment_related_users.update_or_create(
|
related_user, __ = self.assignment_related_users.update_or_create(
|
||||||
user=user,
|
user=user, defaults=defaults
|
||||||
defaults=defaults)
|
)
|
||||||
|
|
||||||
def set_elected(self, user):
|
def set_elected(self, user):
|
||||||
"""
|
"""
|
||||||
Makes user an elected user for this assignment.
|
Makes user an elected user for this assignment.
|
||||||
"""
|
"""
|
||||||
related_user, __ = self.assignment_related_users.update_or_create(
|
related_user, __ = self.assignment_related_users.update_or_create(
|
||||||
user=user,
|
user=user, defaults={"elected": True}
|
||||||
defaults={'elected': True})
|
)
|
||||||
|
|
||||||
def delete_related_user(self, user):
|
def delete_related_user(self, user):
|
||||||
"""
|
"""
|
||||||
|
@ -258,39 +248,43 @@ class Assignment(RESTModelMixin, models.Model):
|
||||||
candidates = self.candidates.all()
|
candidates = self.candidates.all()
|
||||||
|
|
||||||
# Find out the method of the election
|
# Find out the method of the election
|
||||||
if config['assignments_poll_vote_values'] == 'votes':
|
if config["assignments_poll_vote_values"] == "votes":
|
||||||
pollmethod = 'votes'
|
pollmethod = "votes"
|
||||||
elif config['assignments_poll_vote_values'] == 'yesnoabstain':
|
elif config["assignments_poll_vote_values"] == "yesnoabstain":
|
||||||
pollmethod = 'yna'
|
pollmethod = "yna"
|
||||||
elif config['assignments_poll_vote_values'] == 'yesno':
|
elif config["assignments_poll_vote_values"] == "yesno":
|
||||||
pollmethod = 'yn'
|
pollmethod = "yn"
|
||||||
else:
|
else:
|
||||||
# config['assignments_poll_vote_values'] == 'auto'
|
# config['assignments_poll_vote_values'] == 'auto'
|
||||||
# candidates <= available posts -> yes/no/abstain
|
# candidates <= available posts -> yes/no/abstain
|
||||||
if len(candidates) <= (self.open_posts - self.elected.count()):
|
if len(candidates) <= (self.open_posts - self.elected.count()):
|
||||||
pollmethod = 'yna'
|
pollmethod = "yna"
|
||||||
else:
|
else:
|
||||||
pollmethod = 'votes'
|
pollmethod = "votes"
|
||||||
|
|
||||||
# Create the poll with the candidates.
|
# Create the poll with the candidates.
|
||||||
poll = self.polls.create(
|
poll = self.polls.create(
|
||||||
description=self.poll_description_default,
|
description=self.poll_description_default, pollmethod=pollmethod
|
||||||
pollmethod=pollmethod)
|
)
|
||||||
options = []
|
options = []
|
||||||
related_users = AssignmentRelatedUser.objects.filter(assignment__id=self.id).exclude(elected=True)
|
related_users = AssignmentRelatedUser.objects.filter(
|
||||||
|
assignment__id=self.id
|
||||||
|
).exclude(elected=True)
|
||||||
for related_user in related_users:
|
for related_user in related_users:
|
||||||
options.append({
|
options.append(
|
||||||
'candidate': related_user.user,
|
{"candidate": related_user.user, "weight": related_user.weight}
|
||||||
'weight': related_user.weight})
|
)
|
||||||
poll.set_options(options, skip_autoupdate=True)
|
poll.set_options(options, skip_autoupdate=True)
|
||||||
inform_changed_data(self)
|
inform_changed_data(self)
|
||||||
|
|
||||||
# Add all candidates to list of speakers of related agenda item
|
# Add all candidates to list of speakers of related agenda item
|
||||||
# TODO: Try to do this in a bulk create
|
# TODO: Try to do this in a bulk create
|
||||||
if config['assignments_add_candidates_to_list_of_speakers']:
|
if config["assignments_add_candidates_to_list_of_speakers"]:
|
||||||
for candidate in self.candidates:
|
for candidate in self.candidates:
|
||||||
try:
|
try:
|
||||||
Speaker.objects.add(candidate, self.agenda_item, skip_autoupdate=True)
|
Speaker.objects.add(
|
||||||
|
candidate, self.agenda_item, skip_autoupdate=True
|
||||||
|
)
|
||||||
except OpenSlidesError:
|
except OpenSlidesError:
|
||||||
# The Speaker is already on the list. Do nothing.
|
# The Speaker is already on the list. Do nothing.
|
||||||
# TODO: Find a smart way not to catch the error concerning AnonymousUser.
|
# TODO: Find a smart way not to catch the error concerning AnonymousUser.
|
||||||
|
@ -349,7 +343,7 @@ class Assignment(RESTModelMixin, models.Model):
|
||||||
Return a title for the agenda with the appended assignment verbose name.
|
Return a title for the agenda with the appended assignment verbose name.
|
||||||
Note: It has to be the same return value like in JavaScript.
|
Note: It has to be the same return value like in JavaScript.
|
||||||
"""
|
"""
|
||||||
return '%s (%s)' % (self.get_agenda_title(), _(self._meta.verbose_name))
|
return "%s (%s)" % (self.get_agenda_title(), _(self._meta.verbose_name))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def agenda_item(self):
|
def agenda_item(self):
|
||||||
|
@ -370,9 +364,8 @@ class Assignment(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
class AssignmentVote(RESTModelMixin, BaseVote):
|
class AssignmentVote(RESTModelMixin, BaseVote):
|
||||||
option = models.ForeignKey(
|
option = models.ForeignKey(
|
||||||
'AssignmentOption',
|
"AssignmentOption", on_delete=models.CASCADE, related_name="votes"
|
||||||
on_delete=models.CASCADE,
|
)
|
||||||
related_name='votes')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
@ -386,12 +379,9 @@ class AssignmentVote(RESTModelMixin, BaseVote):
|
||||||
|
|
||||||
class AssignmentOption(RESTModelMixin, BaseOption):
|
class AssignmentOption(RESTModelMixin, BaseOption):
|
||||||
poll = models.ForeignKey(
|
poll = models.ForeignKey(
|
||||||
'AssignmentPoll',
|
"AssignmentPoll", on_delete=models.CASCADE, related_name="options"
|
||||||
on_delete=models.CASCADE,
|
)
|
||||||
related_name='options')
|
candidate = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
candidate = models.ForeignKey(
|
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
weight = models.IntegerField(default=0)
|
weight = models.IntegerField(default=0)
|
||||||
|
|
||||||
vote_class = AssignmentVote
|
vote_class = AssignmentVote
|
||||||
|
@ -411,26 +401,32 @@ class AssignmentOption(RESTModelMixin, BaseOption):
|
||||||
|
|
||||||
# TODO: remove the type-ignoring in the next line, after this is solved:
|
# TODO: remove the type-ignoring in the next line, after this is solved:
|
||||||
# https://github.com/python/mypy/issues/3855
|
# https://github.com/python/mypy/issues/3855
|
||||||
class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore
|
class AssignmentPoll( # type: ignore
|
||||||
PublishPollMixin, BasePoll):
|
RESTModelMixin, CollectDefaultVotesMixin, PublishPollMixin, BasePoll
|
||||||
|
):
|
||||||
option_class = AssignmentOption
|
option_class = AssignmentOption
|
||||||
|
|
||||||
assignment = models.ForeignKey(
|
assignment = models.ForeignKey(
|
||||||
Assignment,
|
Assignment, on_delete=models.CASCADE, related_name="polls"
|
||||||
on_delete=models.CASCADE,
|
)
|
||||||
related_name='polls')
|
pollmethod = models.CharField(max_length=5, default="yna")
|
||||||
pollmethod = models.CharField(
|
description = models.CharField(max_length=79, blank=True)
|
||||||
max_length=5,
|
|
||||||
default='yna')
|
|
||||||
description = models.CharField(
|
|
||||||
max_length=79,
|
|
||||||
blank=True)
|
|
||||||
|
|
||||||
votesabstain = models.DecimalField(null=True, blank=True, validators=[
|
votesabstain = models.DecimalField(
|
||||||
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
validators=[MinValueValidator(Decimal("-2"))],
|
||||||
|
max_digits=15,
|
||||||
|
decimal_places=6,
|
||||||
|
)
|
||||||
""" General abstain votes, used for pollmethod 'votes' """
|
""" General abstain votes, used for pollmethod 'votes' """
|
||||||
votesno = models.DecimalField(null=True, blank=True, validators=[
|
votesno = models.DecimalField(
|
||||||
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
validators=[MinValueValidator(Decimal("-2"))],
|
||||||
|
max_digits=15,
|
||||||
|
decimal_places=6,
|
||||||
|
)
|
||||||
""" General no votes, used for pollmethod 'votes' """
|
""" General no votes, used for pollmethod 'votes' """
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -443,27 +439,30 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore
|
||||||
"""
|
"""
|
||||||
Projector.remove_any(
|
Projector.remove_any(
|
||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate,
|
||||||
name='assignments/assignment',
|
name="assignments/assignment",
|
||||||
id=self.assignment.pk,
|
id=self.assignment.pk,
|
||||||
poll=self.pk)
|
poll=self.pk,
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore # TODO: fix typing
|
)
|
||||||
|
return super().delete( # type: ignore
|
||||||
|
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
def get_assignment(self):
|
def get_assignment(self):
|
||||||
return self.assignment
|
return self.assignment
|
||||||
|
|
||||||
def get_vote_values(self):
|
def get_vote_values(self):
|
||||||
if self.pollmethod == 'yna':
|
if self.pollmethod == "yna":
|
||||||
return ['Yes', 'No', 'Abstain']
|
return ["Yes", "No", "Abstain"]
|
||||||
elif self.pollmethod == 'yn':
|
elif self.pollmethod == "yn":
|
||||||
return ['Yes', 'No']
|
return ["Yes", "No"]
|
||||||
else:
|
else:
|
||||||
return ['Votes']
|
return ["Votes"]
|
||||||
|
|
||||||
def get_ballot(self):
|
def get_ballot(self):
|
||||||
return self.assignment.polls.filter(id__lte=self.pk).count()
|
return self.assignment.polls.filter(id__lte=self.pk).count()
|
||||||
|
|
||||||
def get_percent_base_choice(self):
|
def get_percent_base_choice(self):
|
||||||
return config['assignments_poll_100_percent_base']
|
return config["assignments_poll_100_percent_base"]
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
def get_root_rest_element(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -11,30 +11,33 @@ class AssignmentSlide(ProjectorElement):
|
||||||
|
|
||||||
You can send a poll id to get a poll slide.
|
You can send a poll id to get a poll slide.
|
||||||
"""
|
"""
|
||||||
name = 'assignments/assignment'
|
|
||||||
|
name = "assignments/assignment"
|
||||||
|
|
||||||
def check_data(self):
|
def check_data(self):
|
||||||
if not Assignment.objects.filter(pk=self.config_entry.get('id')).exists():
|
if not Assignment.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||||
raise ProjectorException('Election does not exist.')
|
raise ProjectorException("Election does not exist.")
|
||||||
poll_id = self.config_entry.get('poll')
|
poll_id = self.config_entry.get("poll")
|
||||||
if poll_id:
|
if poll_id:
|
||||||
# Poll slide.
|
# Poll slide.
|
||||||
try:
|
try:
|
||||||
poll = AssignmentPoll.objects.get(pk=poll_id)
|
poll = AssignmentPoll.objects.get(pk=poll_id)
|
||||||
except AssignmentPoll.DoesNotExist:
|
except AssignmentPoll.DoesNotExist:
|
||||||
raise ProjectorException('Poll does not exist.')
|
raise ProjectorException("Poll does not exist.")
|
||||||
if poll.assignment_id != self.config_entry.get('id'):
|
if poll.assignment_id != self.config_entry.get("id"):
|
||||||
raise ProjectorException('Assignment id and poll do not belong together.')
|
raise ProjectorException(
|
||||||
|
"Assignment id and poll do not belong together."
|
||||||
|
)
|
||||||
|
|
||||||
def update_data(self):
|
def update_data(self):
|
||||||
data = None
|
data = None
|
||||||
try:
|
try:
|
||||||
assignment = Assignment.objects.get(pk=self.config_entry.get('id'))
|
assignment = Assignment.objects.get(pk=self.config_entry.get("id"))
|
||||||
except Assignment.DoesNotExist:
|
except Assignment.DoesNotExist:
|
||||||
# Assignment does not exist, so just do nothing.
|
# Assignment does not exist, so just do nothing.
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
data = {'agenda_item_id': assignment.agenda_item_id}
|
data = {"agenda_item_id": assignment.agenda_item_id}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,10 @@ def posts_validator(data):
|
||||||
"""
|
"""
|
||||||
Validator for open posts. It checks that the values for the open posts are greater than 0.
|
Validator for open posts. It checks that the values for the open posts are greater than 0.
|
||||||
"""
|
"""
|
||||||
if (data['open_posts'] and data['open_posts'] is not None and data['open_posts'] < 1):
|
if data["open_posts"] and data["open_posts"] is not None and data["open_posts"] < 1:
|
||||||
raise ValidationError({'detail': _('Value for {} must be greater than 0').format('open_posts')})
|
raise ValidationError(
|
||||||
|
{"detail": _("Value for {} must be greater than 0").format("open_posts")}
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,35 +39,39 @@ class AssignmentRelatedUserSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for assignment.models.AssignmentRelatedUser objects.
|
Serializer for assignment.models.AssignmentRelatedUser objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AssignmentRelatedUser
|
model = AssignmentRelatedUser
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
"id",
|
||||||
'user',
|
"user",
|
||||||
'elected',
|
"elected",
|
||||||
'assignment',
|
"assignment",
|
||||||
'weight') # js-data needs the assignment-id in the nested object to define relations.
|
"weight",
|
||||||
|
) # js-data needs the assignment-id in the nested object to define relations.
|
||||||
|
|
||||||
|
|
||||||
class AssignmentVoteSerializer(ModelSerializer):
|
class AssignmentVoteSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for assignment.models.AssignmentVote objects.
|
Serializer for assignment.models.AssignmentVote objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AssignmentVote
|
model = AssignmentVote
|
||||||
fields = ('weight', 'value',)
|
fields = ("weight", "value")
|
||||||
|
|
||||||
|
|
||||||
class AssignmentOptionSerializer(ModelSerializer):
|
class AssignmentOptionSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for assignment.models.AssignmentOption objects.
|
Serializer for assignment.models.AssignmentOption objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
votes = AssignmentVoteSerializer(many=True, read_only=True)
|
votes = AssignmentVoteSerializer(many=True, read_only=True)
|
||||||
is_elected = SerializerMethodField()
|
is_elected = SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AssignmentOption
|
model = AssignmentOption
|
||||||
fields = ('id', 'candidate', 'is_elected', 'votes', 'poll', 'weight')
|
fields = ("id", "candidate", "is_elected", "votes", "poll", "weight")
|
||||||
|
|
||||||
def get_is_elected(self, obj):
|
def get_is_elected(self, obj):
|
||||||
"""
|
"""
|
||||||
|
@ -78,6 +84,7 @@ class FilterPollListSerializer(ListSerializer):
|
||||||
"""
|
"""
|
||||||
Customized serializer to filter polls (exclude unpublished).
|
Customized serializer to filter polls (exclude unpublished).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def to_representation(self, data):
|
def to_representation(self, data):
|
||||||
"""
|
"""
|
||||||
List of object instances -> List of dicts of primitive datatypes.
|
List of object instances -> List of dicts of primitive datatypes.
|
||||||
|
@ -86,7 +93,9 @@ class FilterPollListSerializer(ListSerializer):
|
||||||
"""
|
"""
|
||||||
# Dealing with nested relationships, data can be a Manager,
|
# Dealing with nested relationships, data can be a Manager,
|
||||||
# so, first get a queryset from the Manager if needed
|
# so, first get a queryset from the Manager if needed
|
||||||
iterable = data.filter(published=True) if isinstance(data, models.Manager) else data
|
iterable = (
|
||||||
|
data.filter(published=True) if isinstance(data, models.Manager) else data
|
||||||
|
)
|
||||||
return [self.child.to_representation(item) for item in iterable]
|
return [self.child.to_representation(item) for item in iterable]
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,31 +105,35 @@ class AssignmentAllPollSerializer(ModelSerializer):
|
||||||
|
|
||||||
Serializes all polls.
|
Serializes all polls.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
options = AssignmentOptionSerializer(many=True, read_only=True)
|
options = AssignmentOptionSerializer(many=True, read_only=True)
|
||||||
votes = ListField(
|
votes = ListField(
|
||||||
child=DictField(
|
child=DictField(
|
||||||
child=DecimalField(max_digits=15, decimal_places=6, min_value=-2)),
|
child=DecimalField(max_digits=15, decimal_places=6, min_value=-2)
|
||||||
|
),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False)
|
required=False,
|
||||||
|
)
|
||||||
has_votes = SerializerMethodField()
|
has_votes = SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AssignmentPoll
|
model = AssignmentPoll
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
"id",
|
||||||
'pollmethod',
|
"pollmethod",
|
||||||
'description',
|
"description",
|
||||||
'published',
|
"published",
|
||||||
'options',
|
"options",
|
||||||
'votesabstain',
|
"votesabstain",
|
||||||
'votesno',
|
"votesno",
|
||||||
'votesvalid',
|
"votesvalid",
|
||||||
'votesinvalid',
|
"votesinvalid",
|
||||||
'votescast',
|
"votescast",
|
||||||
'votes',
|
"votes",
|
||||||
'has_votes',
|
"has_votes",
|
||||||
'assignment') # js-data needs the assignment-id in the nested object to define relations.
|
"assignment",
|
||||||
read_only_fields = ('pollmethod',)
|
) # js-data needs the assignment-id in the nested object to define relations.
|
||||||
|
read_only_fields = ("pollmethod",)
|
||||||
validators = (default_votes_validator,)
|
validators = (default_votes_validator,)
|
||||||
|
|
||||||
def get_has_votes(self, obj):
|
def get_has_votes(self, obj):
|
||||||
|
@ -144,30 +157,45 @@ class AssignmentAllPollSerializer(ModelSerializer):
|
||||||
"votes": [{"Votes": 10}, {"Votes": 0}]
|
"votes": [{"Votes": 10}, {"Votes": 0}]
|
||||||
"""
|
"""
|
||||||
# Update votes.
|
# Update votes.
|
||||||
votes = validated_data.get('votes')
|
votes = validated_data.get("votes")
|
||||||
if votes:
|
if votes:
|
||||||
options = list(instance.get_options())
|
options = list(instance.get_options())
|
||||||
if len(votes) != len(options):
|
if len(votes) != len(options):
|
||||||
raise ValidationError({
|
raise ValidationError(
|
||||||
'detail': _('You have to submit data for %d candidates.') % len(options)})
|
{
|
||||||
|
"detail": _("You have to submit data for %d candidates.")
|
||||||
|
% len(options)
|
||||||
|
}
|
||||||
|
)
|
||||||
for index, option in enumerate(options):
|
for index, option in enumerate(options):
|
||||||
if len(votes[index]) != len(instance.get_vote_values()):
|
if len(votes[index]) != len(instance.get_vote_values()):
|
||||||
raise ValidationError({
|
raise ValidationError(
|
||||||
'detail': _('You have to submit data for %d vote values.') % len(instance.get_vote_values())})
|
{
|
||||||
|
"detail": _("You have to submit data for %d vote values.")
|
||||||
|
% len(instance.get_vote_values())
|
||||||
|
}
|
||||||
|
)
|
||||||
for vote_value, vote_weight in votes[index].items():
|
for vote_value, vote_weight in votes[index].items():
|
||||||
if vote_value not in instance.get_vote_values():
|
if vote_value not in instance.get_vote_values():
|
||||||
raise ValidationError({
|
raise ValidationError(
|
||||||
'detail': _('Vote value %s is invalid.') % vote_value})
|
{"detail": _("Vote value %s is invalid.") % vote_value}
|
||||||
instance.set_vote_objects_with_values(option, votes[index], skip_autoupdate=True)
|
)
|
||||||
|
instance.set_vote_objects_with_values(
|
||||||
|
option, votes[index], skip_autoupdate=True
|
||||||
|
)
|
||||||
|
|
||||||
# Update remaining writeable fields.
|
# Update remaining writeable fields.
|
||||||
instance.description = validated_data.get('description', instance.description)
|
instance.description = validated_data.get("description", instance.description)
|
||||||
instance.published = validated_data.get('published', instance.published)
|
instance.published = validated_data.get("published", instance.published)
|
||||||
instance.votesabstain = validated_data.get('votesabstain', instance.votesabstain)
|
instance.votesabstain = validated_data.get(
|
||||||
instance.votesno = validated_data.get('votesno', instance.votesno)
|
"votesabstain", instance.votesabstain
|
||||||
instance.votesvalid = validated_data.get('votesvalid', instance.votesvalid)
|
)
|
||||||
instance.votesinvalid = validated_data.get('votesinvalid', instance.votesinvalid)
|
instance.votesno = validated_data.get("votesno", instance.votesno)
|
||||||
instance.votescast = validated_data.get('votescast', instance.votescast)
|
instance.votesvalid = validated_data.get("votesvalid", instance.votesvalid)
|
||||||
|
instance.votesinvalid = validated_data.get(
|
||||||
|
"votesinvalid", instance.votesinvalid
|
||||||
|
)
|
||||||
|
instance.votescast = validated_data.get("votescast", instance.votescast)
|
||||||
instance.save()
|
instance.save()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
@ -178,52 +206,60 @@ class AssignmentShortPollSerializer(AssignmentAllPollSerializer):
|
||||||
|
|
||||||
Serializes only short polls (excluded unpublished polls).
|
Serializes only short polls (excluded unpublished polls).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
list_serializer_class = FilterPollListSerializer
|
list_serializer_class = FilterPollListSerializer
|
||||||
model = AssignmentPoll
|
model = AssignmentPoll
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
"id",
|
||||||
'pollmethod',
|
"pollmethod",
|
||||||
'description',
|
"description",
|
||||||
'published',
|
"published",
|
||||||
'options',
|
"options",
|
||||||
'votesabstain',
|
"votesabstain",
|
||||||
'votesno',
|
"votesno",
|
||||||
'votesvalid',
|
"votesvalid",
|
||||||
'votesinvalid',
|
"votesinvalid",
|
||||||
'votescast',
|
"votescast",
|
||||||
'has_votes',)
|
"has_votes",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AssignmentFullSerializer(ModelSerializer):
|
class AssignmentFullSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for assignment.models.Assignment objects. With all polls.
|
Serializer for assignment.models.Assignment objects. With all polls.
|
||||||
"""
|
"""
|
||||||
assignment_related_users = AssignmentRelatedUserSerializer(many=True, read_only=True)
|
|
||||||
|
assignment_related_users = AssignmentRelatedUserSerializer(
|
||||||
|
many=True, read_only=True
|
||||||
|
)
|
||||||
polls = AssignmentAllPollSerializer(many=True, read_only=True)
|
polls = AssignmentAllPollSerializer(many=True, read_only=True)
|
||||||
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3)
|
agenda_type = IntegerField(
|
||||||
|
write_only=True, required=False, min_value=1, max_value=3
|
||||||
|
)
|
||||||
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
|
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Assignment
|
model = Assignment
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
"id",
|
||||||
'title',
|
"title",
|
||||||
'description',
|
"description",
|
||||||
'open_posts',
|
"open_posts",
|
||||||
'phase',
|
"phase",
|
||||||
'assignment_related_users',
|
"assignment_related_users",
|
||||||
'poll_description_default',
|
"poll_description_default",
|
||||||
'polls',
|
"polls",
|
||||||
'agenda_item_id',
|
"agenda_item_id",
|
||||||
'agenda_type',
|
"agenda_type",
|
||||||
'agenda_parent_id',
|
"agenda_parent_id",
|
||||||
'tags',)
|
"tags",
|
||||||
|
)
|
||||||
validators = (posts_validator,)
|
validators = (posts_validator,)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if 'description' in data:
|
if "description" in data:
|
||||||
data['description'] = validate_html(data['description'])
|
data["description"] = validate_html(data["description"])
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
|
@ -231,10 +267,10 @@ class AssignmentFullSerializer(ModelSerializer):
|
||||||
Customized create method. Set information about related agenda item
|
Customized create method. Set information about related agenda item
|
||||||
into agenda_item_update_information container.
|
into agenda_item_update_information container.
|
||||||
"""
|
"""
|
||||||
agenda_type = validated_data.pop('agenda_type', None)
|
agenda_type = validated_data.pop("agenda_type", None)
|
||||||
agenda_parent_id = validated_data.pop('agenda_parent_id', None)
|
agenda_parent_id = validated_data.pop("agenda_parent_id", None)
|
||||||
assignment = Assignment(**validated_data)
|
assignment = Assignment(**validated_data)
|
||||||
assignment.agenda_item_update_information['type'] = agenda_type
|
assignment.agenda_item_update_information["type"] = agenda_type
|
||||||
assignment.agenda_item_update_information['parent_id'] = agenda_parent_id
|
assignment.agenda_item_update_information["parent_id"] = agenda_parent_id
|
||||||
assignment.save()
|
assignment.save()
|
||||||
return assignment
|
return assignment
|
||||||
|
|
|
@ -5,8 +5,11 @@ def get_permission_change_data(sender, permissions=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Yields all necessary collections if 'assignments.can_see' permission changes.
|
Yields all necessary collections if 'assignments.can_see' permission changes.
|
||||||
"""
|
"""
|
||||||
assignments_app = apps.get_app_config(app_label='assignments')
|
assignments_app = apps.get_app_config(app_label="assignments")
|
||||||
for permission in permissions:
|
for permission in permissions:
|
||||||
# There could be only one 'assignment.can_see' and then we want to return data.
|
# There could be only one 'assignment.can_see' and then we want to return data.
|
||||||
if permission.content_type.app_label == assignments_app.label and permission.codename == 'can_see':
|
if (
|
||||||
|
permission.content_type.app_label == assignments_app.label
|
||||||
|
and permission.codename == "can_see"
|
||||||
|
):
|
||||||
yield from assignments_app.get_startup_elements()
|
yield from assignments_app.get_startup_elements()
|
||||||
|
|
|
@ -21,6 +21,7 @@ from .serializers import AssignmentAllPollSerializer
|
||||||
|
|
||||||
# Viewsets for the REST API
|
# Viewsets for the REST API
|
||||||
|
|
||||||
|
|
||||||
class AssignmentViewSet(ModelViewSet):
|
class AssignmentViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint for assignments.
|
API endpoint for assignments.
|
||||||
|
@ -29,6 +30,7 @@ class AssignmentViewSet(ModelViewSet):
|
||||||
partial_update, update, destroy, candidature_self, candidature_other,
|
partial_update, update, destroy, candidature_self, candidature_other,
|
||||||
mark_elected and create_poll.
|
mark_elected and create_poll.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = AssignmentAccessPermissions()
|
access_permissions = AssignmentAccessPermissions()
|
||||||
queryset = Assignment.objects.all()
|
queryset = Assignment.objects.all()
|
||||||
|
|
||||||
|
@ -36,26 +38,36 @@ class AssignmentViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ('list', 'retrieve'):
|
if self.action in ("list", "retrieve"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action == 'metadata':
|
elif self.action == "metadata":
|
||||||
# Everybody is allowed to see the metadata.
|
# Everybody is allowed to see the metadata.
|
||||||
result = True
|
result = True
|
||||||
elif self.action in ('create', 'partial_update', 'update', 'destroy',
|
elif self.action in (
|
||||||
'mark_elected', 'create_poll', 'sort_related_users'):
|
"create",
|
||||||
result = (has_perm(self.request.user, 'assignments.can_see') and
|
"partial_update",
|
||||||
has_perm(self.request.user, 'assignments.can_manage'))
|
"update",
|
||||||
elif self.action == 'candidature_self':
|
"destroy",
|
||||||
result = (has_perm(self.request.user, 'assignments.can_see') and
|
"mark_elected",
|
||||||
has_perm(self.request.user, 'assignments.can_nominate_self'))
|
"create_poll",
|
||||||
elif self.action == 'candidature_other':
|
"sort_related_users",
|
||||||
result = (has_perm(self.request.user, 'assignments.can_see') and
|
):
|
||||||
has_perm(self.request.user, 'assignments.can_nominate_other'))
|
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
||||||
|
self.request.user, "assignments.can_manage"
|
||||||
|
)
|
||||||
|
elif self.action == "candidature_self":
|
||||||
|
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
||||||
|
self.request.user, "assignments.can_nominate_self"
|
||||||
|
)
|
||||||
|
elif self.action == "candidature_other":
|
||||||
|
result = has_perm(self.request.user, "assignments.can_see") and has_perm(
|
||||||
|
self.request.user, "assignments.can_nominate_other"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@detail_route(methods=['post', 'delete'])
|
@detail_route(methods=["post", "delete"])
|
||||||
def candidature_self(self, request, pk=None):
|
def candidature_self(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
View to nominate self as candidate (POST) or withdraw own
|
View to nominate self as candidate (POST) or withdraw own
|
||||||
|
@ -63,18 +75,26 @@ class AssignmentViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
assignment = self.get_object()
|
assignment = self.get_object()
|
||||||
if assignment.is_elected(request.user):
|
if assignment.is_elected(request.user):
|
||||||
raise ValidationError({'detail': _('You are already elected.')})
|
raise ValidationError({"detail": _("You are already elected.")})
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
message = self.nominate_self(request, assignment)
|
message = self.nominate_self(request, assignment)
|
||||||
else:
|
else:
|
||||||
# request.method == 'DELETE'
|
# request.method == 'DELETE'
|
||||||
message = self.withdraw_self(request, assignment)
|
message = self.withdraw_self(request, assignment)
|
||||||
return Response({'detail': message})
|
return Response({"detail": message})
|
||||||
|
|
||||||
def nominate_self(self, request, assignment):
|
def nominate_self(self, request, assignment):
|
||||||
if assignment.phase == assignment.PHASE_FINISHED:
|
if assignment.phase == assignment.PHASE_FINISHED:
|
||||||
raise ValidationError({'detail': _('You can not candidate to this election because it is finished.')})
|
raise ValidationError(
|
||||||
if assignment.phase == assignment.PHASE_VOTING and not has_perm(request.user, 'assignments.can_manage'):
|
{
|
||||||
|
"detail": _(
|
||||||
|
"You can not candidate to this election because it is finished."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if assignment.phase == assignment.PHASE_VOTING and not has_perm(
|
||||||
|
request.user, "assignments.can_manage"
|
||||||
|
):
|
||||||
# To nominate self during voting you have to be a manager.
|
# To nominate self during voting you have to be a manager.
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
# If the request.user is already a candidate he can nominate himself nevertheless.
|
# If the request.user is already a candidate he can nominate himself nevertheless.
|
||||||
|
@ -82,19 +102,29 @@ class AssignmentViewSet(ModelViewSet):
|
||||||
# Send new candidate via autoupdate because users without permission
|
# Send new candidate via autoupdate because users without permission
|
||||||
# to see users may not have it but can get it now.
|
# to see users may not have it but can get it now.
|
||||||
inform_changed_data([request.user])
|
inform_changed_data([request.user])
|
||||||
return _('You were nominated successfully.')
|
return _("You were nominated successfully.")
|
||||||
|
|
||||||
def withdraw_self(self, request, assignment):
|
def withdraw_self(self, request, assignment):
|
||||||
# Withdraw candidature.
|
# Withdraw candidature.
|
||||||
if assignment.phase == assignment.PHASE_FINISHED:
|
if assignment.phase == assignment.PHASE_FINISHED:
|
||||||
raise ValidationError({'detail': _('You can not withdraw your candidature to this election because it is finished.')})
|
raise ValidationError(
|
||||||
if assignment.phase == assignment.PHASE_VOTING and not has_perm(request.user, 'assignments.can_manage'):
|
{
|
||||||
|
"detail": _(
|
||||||
|
"You can not withdraw your candidature to this election because it is finished."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if assignment.phase == assignment.PHASE_VOTING and not has_perm(
|
||||||
|
request.user, "assignments.can_manage"
|
||||||
|
):
|
||||||
# To withdraw self during voting you have to be a manager.
|
# To withdraw self during voting you have to be a manager.
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
if not assignment.is_candidate(request.user):
|
if not assignment.is_candidate(request.user):
|
||||||
raise ValidationError({'detail': _('You are not a candidate of this election.')})
|
raise ValidationError(
|
||||||
|
{"detail": _("You are not a candidate of this election.")}
|
||||||
|
)
|
||||||
assignment.delete_related_user(request.user)
|
assignment.delete_related_user(request.user)
|
||||||
return _('You have withdrawn your candidature successfully.')
|
return _("You have withdrawn your candidature successfully.")
|
||||||
|
|
||||||
def get_user_from_request_data(self, request):
|
def get_user_from_request_data(self, request):
|
||||||
"""
|
"""
|
||||||
|
@ -103,20 +133,26 @@ class AssignmentViewSet(ModelViewSet):
|
||||||
self.mark_elected can play with it.
|
self.mark_elected can play with it.
|
||||||
"""
|
"""
|
||||||
if not isinstance(request.data, dict):
|
if not isinstance(request.data, dict):
|
||||||
detail = _('Invalid data. Expected dictionary, got %s.') % type(request.data)
|
detail = _("Invalid data. Expected dictionary, got %s.") % type(
|
||||||
raise ValidationError({'detail': detail})
|
request.data
|
||||||
user_str = request.data.get('user', '')
|
)
|
||||||
|
raise ValidationError({"detail": detail})
|
||||||
|
user_str = request.data.get("user", "")
|
||||||
try:
|
try:
|
||||||
user_pk = int(user_str)
|
user_pk = int(user_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValidationError({'detail': _('Invalid data. Expected something like {"user": <id>}.')})
|
raise ValidationError(
|
||||||
|
{"detail": _('Invalid data. Expected something like {"user": <id>}.')}
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
user = get_user_model().objects.get(pk=user_pk)
|
user = get_user_model().objects.get(pk=user_pk)
|
||||||
except get_user_model().DoesNotExist:
|
except get_user_model().DoesNotExist:
|
||||||
raise ValidationError({'detail': _('Invalid data. User %d does not exist.') % user_pk})
|
raise ValidationError(
|
||||||
|
{"detail": _("Invalid data. User %d does not exist.") % user_pk}
|
||||||
|
)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@detail_route(methods=['post', 'delete'])
|
@detail_route(methods=["post", "delete"])
|
||||||
def candidature_other(self, request, pk=None):
|
def candidature_other(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
View to nominate other users (POST) or delete their candidature
|
View to nominate other users (POST) or delete their candidature
|
||||||
|
@ -124,43 +160,51 @@ class AssignmentViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
user = self.get_user_from_request_data(request)
|
user = self.get_user_from_request_data(request)
|
||||||
assignment = self.get_object()
|
assignment = self.get_object()
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
message = self.nominate_other(request, user, assignment)
|
message = self.nominate_other(request, user, assignment)
|
||||||
else:
|
else:
|
||||||
# request.method == 'DELETE'
|
# request.method == 'DELETE'
|
||||||
message = self.delete_other(request, user, assignment)
|
message = self.delete_other(request, user, assignment)
|
||||||
return Response({'detail': message})
|
return Response({"detail": message})
|
||||||
|
|
||||||
def nominate_other(self, request, user, assignment):
|
def nominate_other(self, request, user, assignment):
|
||||||
if assignment.is_elected(user):
|
if assignment.is_elected(user):
|
||||||
raise ValidationError({'detail': _('User %s is already elected.') % user})
|
raise ValidationError({"detail": _("User %s is already elected.") % user})
|
||||||
if assignment.phase == assignment.PHASE_FINISHED:
|
if assignment.phase == assignment.PHASE_FINISHED:
|
||||||
detail = _('You can not nominate someone to this election because it is finished.')
|
detail = _(
|
||||||
raise ValidationError({'detail': detail})
|
"You can not nominate someone to this election because it is finished."
|
||||||
if assignment.phase == assignment.PHASE_VOTING and not has_perm(request.user, 'assignments.can_manage'):
|
)
|
||||||
|
raise ValidationError({"detail": detail})
|
||||||
|
if assignment.phase == assignment.PHASE_VOTING and not has_perm(
|
||||||
|
request.user, "assignments.can_manage"
|
||||||
|
):
|
||||||
# To nominate another user during voting you have to be a manager.
|
# To nominate another user during voting you have to be a manager.
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
if assignment.is_candidate(user):
|
if assignment.is_candidate(user):
|
||||||
raise ValidationError({'detail': _('User %s is already nominated.') % user})
|
raise ValidationError({"detail": _("User %s is already nominated.") % user})
|
||||||
assignment.set_candidate(user)
|
assignment.set_candidate(user)
|
||||||
# Send new candidate via autoupdate because users without permission
|
# Send new candidate via autoupdate because users without permission
|
||||||
# to see users may not have it but can get it now.
|
# to see users may not have it but can get it now.
|
||||||
inform_changed_data(user)
|
inform_changed_data(user)
|
||||||
return _('User %s was nominated successfully.') % user
|
return _("User %s was nominated successfully.") % user
|
||||||
|
|
||||||
def delete_other(self, request, user, assignment):
|
def delete_other(self, request, user, assignment):
|
||||||
# To delete candidature status you have to be a manager.
|
# To delete candidature status you have to be a manager.
|
||||||
if not has_perm(request.user, 'assignments.can_manage'):
|
if not has_perm(request.user, "assignments.can_manage"):
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
if assignment.phase == assignment.PHASE_FINISHED:
|
if assignment.phase == assignment.PHASE_FINISHED:
|
||||||
detail = _("You can not delete someone's candidature to this election because it is finished.")
|
detail = _(
|
||||||
raise ValidationError({'detail': detail})
|
"You can not delete someone's candidature to this election because it is finished."
|
||||||
|
)
|
||||||
|
raise ValidationError({"detail": detail})
|
||||||
if not assignment.is_candidate(user) and not assignment.is_elected(user):
|
if not assignment.is_candidate(user) and not assignment.is_elected(user):
|
||||||
raise ValidationError({'detail': _('User %s has no status in this election.') % user})
|
raise ValidationError(
|
||||||
|
{"detail": _("User %s has no status in this election.") % user}
|
||||||
|
)
|
||||||
assignment.delete_related_user(user)
|
assignment.delete_related_user(user)
|
||||||
return _('Candidate %s was withdrawn successfully.') % user
|
return _("Candidate %s was withdrawn successfully.") % user
|
||||||
|
|
||||||
@detail_route(methods=['post', 'delete'])
|
@detail_route(methods=["post", "delete"])
|
||||||
def mark_elected(self, request, pk=None):
|
def mark_elected(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
View to mark other users as elected (POST) or undo this (DELETE).
|
View to mark other users as elected (POST) or undo this (DELETE).
|
||||||
|
@ -168,35 +212,41 @@ class AssignmentViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
user = self.get_user_from_request_data(request)
|
user = self.get_user_from_request_data(request)
|
||||||
assignment = self.get_object()
|
assignment = self.get_object()
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
if not assignment.is_candidate(user):
|
if not assignment.is_candidate(user):
|
||||||
raise ValidationError({'detail': _('User %s is not a candidate of this election.') % user})
|
raise ValidationError(
|
||||||
|
{"detail": _("User %s is not a candidate of this election.") % user}
|
||||||
|
)
|
||||||
assignment.set_elected(user)
|
assignment.set_elected(user)
|
||||||
message = _('User %s was successfully elected.') % user
|
message = _("User %s was successfully elected.") % user
|
||||||
else:
|
else:
|
||||||
# request.method == 'DELETE'
|
# request.method == 'DELETE'
|
||||||
if not assignment.is_elected(user):
|
if not assignment.is_elected(user):
|
||||||
detail = _('User %s is not an elected candidate of this election.') % user
|
detail = (
|
||||||
raise ValidationError({'detail': detail})
|
_("User %s is not an elected candidate of this election.") % user
|
||||||
|
)
|
||||||
|
raise ValidationError({"detail": detail})
|
||||||
assignment.set_candidate(user)
|
assignment.set_candidate(user)
|
||||||
message = _('User %s was successfully unelected.') % user
|
message = _("User %s was successfully unelected.") % user
|
||||||
return Response({'detail': message})
|
return Response({"detail": message})
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=["post"])
|
||||||
def create_poll(self, request, pk=None):
|
def create_poll(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
View to create a poll. It is a POST request without any data.
|
View to create a poll. It is a POST request without any data.
|
||||||
"""
|
"""
|
||||||
assignment = self.get_object()
|
assignment = self.get_object()
|
||||||
if not assignment.candidates.exists():
|
if not assignment.candidates.exists():
|
||||||
raise ValidationError({'detail': _('Can not create ballot because there are no candidates.')})
|
raise ValidationError(
|
||||||
|
{"detail": _("Can not create ballot because there are no candidates.")}
|
||||||
|
)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
poll = assignment.create_poll()
|
poll = assignment.create_poll()
|
||||||
return Response({
|
return Response(
|
||||||
'detail': _('Ballot created successfully.'),
|
{"detail": _("Ballot created successfully."), "createdPollId": poll.pk}
|
||||||
'createdPollId': poll.pk})
|
)
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=["post"])
|
||||||
def sort_related_users(self, request, pk=None):
|
def sort_related_users(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
Special view endpoint to sort the assignment related users.
|
Special view endpoint to sort the assignment related users.
|
||||||
|
@ -206,22 +256,25 @@ class AssignmentViewSet(ModelViewSet):
|
||||||
assignment = self.get_object()
|
assignment = self.get_object()
|
||||||
|
|
||||||
# Check data
|
# Check data
|
||||||
related_user_ids = request.data.get('related_users')
|
related_user_ids = request.data.get("related_users")
|
||||||
if not isinstance(related_user_ids, list):
|
if not isinstance(related_user_ids, list):
|
||||||
raise ValidationError(
|
raise ValidationError({"detail": _("users has to be a list of IDs.")})
|
||||||
{'detail': _('users has to be a list of IDs.')})
|
|
||||||
|
|
||||||
# Get all related users from AssignmentRelatedUser.
|
# Get all related users from AssignmentRelatedUser.
|
||||||
related_users = {}
|
related_users = {}
|
||||||
for related_user in AssignmentRelatedUser.objects.filter(assignment__id=assignment.id):
|
for related_user in AssignmentRelatedUser.objects.filter(
|
||||||
|
assignment__id=assignment.id
|
||||||
|
):
|
||||||
related_users[related_user.pk] = related_user
|
related_users[related_user.pk] = related_user
|
||||||
|
|
||||||
# Check all given candidates from the request
|
# Check all given candidates from the request
|
||||||
valid_related_users = []
|
valid_related_users = []
|
||||||
for related_user_id in related_user_ids:
|
for related_user_id in related_user_ids:
|
||||||
if not isinstance(related_user_id, int) or related_users.get(related_user_id) is None:
|
if (
|
||||||
raise ValidationError(
|
not isinstance(related_user_id, int)
|
||||||
{'detail': _('Invalid data.')})
|
or related_users.get(related_user_id) is None
|
||||||
|
):
|
||||||
|
raise ValidationError({"detail": _("Invalid data.")})
|
||||||
valid_related_users.append(related_users[related_user_id])
|
valid_related_users.append(related_users[related_user_id])
|
||||||
|
|
||||||
# Sort the related users
|
# Sort the related users
|
||||||
|
@ -236,7 +289,7 @@ class AssignmentViewSet(ModelViewSet):
|
||||||
inform_changed_data(assignment)
|
inform_changed_data(assignment)
|
||||||
|
|
||||||
# Initiate response.
|
# Initiate response.
|
||||||
return Response({'detail': _('Assignment related users successfully sorted.')})
|
return Response({"detail": _("Assignment related users successfully sorted.")})
|
||||||
|
|
||||||
|
|
||||||
class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
||||||
|
@ -245,6 +298,7 @@ class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet)
|
||||||
|
|
||||||
There are the following views: update, partial_update and destroy.
|
There are the following views: update, partial_update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = AssignmentPoll.objects.all()
|
queryset = AssignmentPoll.objects.all()
|
||||||
serializer_class = AssignmentAllPollSerializer
|
serializer_class = AssignmentAllPollSerializer
|
||||||
|
|
||||||
|
@ -252,5 +306,6 @@ class AssignmentPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet)
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
return (has_perm(self.request.user, 'assignments.can_see') and
|
return has_perm(self.request.user, "assignments.can_see") and has_perm(
|
||||||
has_perm(self.request.user, 'assignments.can_manage'))
|
self.request.user, "assignments.can_manage"
|
||||||
|
)
|
||||||
|
|
|
@ -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.
|
Access permissions container for Projector and ProjectorViewSet.
|
||||||
"""
|
"""
|
||||||
base_permission = 'core.can_see_projector'
|
|
||||||
|
base_permission = "core.can_see_projector"
|
||||||
|
|
||||||
|
|
||||||
class TagAccessPermissions(BaseAccessPermissions):
|
class TagAccessPermissions(BaseAccessPermissions):
|
||||||
|
@ -19,21 +20,24 @@ class ChatMessageAccessPermissions(BaseAccessPermissions):
|
||||||
"""
|
"""
|
||||||
Access permissions container for ChatMessage and ChatMessageViewSet.
|
Access permissions container for ChatMessage and ChatMessageViewSet.
|
||||||
"""
|
"""
|
||||||
base_permission = 'core.can_use_chat'
|
|
||||||
|
base_permission = "core.can_use_chat"
|
||||||
|
|
||||||
|
|
||||||
class ProjectorMessageAccessPermissions(BaseAccessPermissions):
|
class ProjectorMessageAccessPermissions(BaseAccessPermissions):
|
||||||
"""
|
"""
|
||||||
Access permissions for ProjectorMessage.
|
Access permissions for ProjectorMessage.
|
||||||
"""
|
"""
|
||||||
base_permission = 'core.can_see_projector'
|
|
||||||
|
base_permission = "core.can_see_projector"
|
||||||
|
|
||||||
|
|
||||||
class CountdownAccessPermissions(BaseAccessPermissions):
|
class CountdownAccessPermissions(BaseAccessPermissions):
|
||||||
"""
|
"""
|
||||||
Access permissions for Countdown.
|
Access permissions for Countdown.
|
||||||
"""
|
"""
|
||||||
base_permission = 'core.can_see_projector'
|
|
||||||
|
base_permission = "core.can_see_projector"
|
||||||
|
|
||||||
|
|
||||||
class ConfigAccessPermissions(BaseAccessPermissions):
|
class ConfigAccessPermissions(BaseAccessPermissions):
|
||||||
|
|
|
@ -11,8 +11,8 @@ from ..utils.projector import register_projector_elements
|
||||||
|
|
||||||
|
|
||||||
class CoreAppConfig(AppConfig):
|
class CoreAppConfig(AppConfig):
|
||||||
name = 'openslides.core'
|
name = "openslides.core"
|
||||||
verbose_name = 'OpenSlides Core'
|
verbose_name = "OpenSlides Core"
|
||||||
angular_site_module = True
|
angular_site_module = True
|
||||||
angular_projector_module = True
|
angular_projector_module = True
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class CoreAppConfig(AppConfig):
|
||||||
# Skip all database related accesses during migrations.
|
# Skip all database related accesses during migrations.
|
||||||
is_normal_server_start = False
|
is_normal_server_start = False
|
||||||
for sys_part in sys.argv:
|
for sys_part in sys.argv:
|
||||||
for entry in ('runserver', 'gunicorn', 'daphne', 'create-example-data'):
|
for entry in ("runserver", "gunicorn", "daphne", "create-example-data"):
|
||||||
if sys_part.endswith(entry):
|
if sys_part.endswith(entry):
|
||||||
is_normal_server_start = True
|
is_normal_server_start = True
|
||||||
break
|
break
|
||||||
|
@ -68,27 +68,46 @@ class CoreAppConfig(AppConfig):
|
||||||
|
|
||||||
# Connect signals.
|
# Connect signals.
|
||||||
post_permission_creation.connect(
|
post_permission_creation.connect(
|
||||||
delete_django_app_permissions,
|
delete_django_app_permissions, dispatch_uid="delete_django_app_permissions"
|
||||||
dispatch_uid='delete_django_app_permissions')
|
)
|
||||||
permission_change.connect(
|
permission_change.connect(
|
||||||
get_permission_change_data,
|
get_permission_change_data, dispatch_uid="core_get_permission_change_data"
|
||||||
dispatch_uid='core_get_permission_change_data')
|
)
|
||||||
|
|
||||||
post_migrate.connect(call_save_default_values, sender=self, dispatch_uid='core_save_config_default_values')
|
post_migrate.connect(
|
||||||
|
call_save_default_values,
|
||||||
|
sender=self,
|
||||||
|
dispatch_uid="core_save_config_default_values",
|
||||||
|
)
|
||||||
|
|
||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet)
|
router.register(
|
||||||
router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet)
|
self.get_model("Projector").get_collection_string(), ProjectorViewSet
|
||||||
router.register(self.get_model('Tag').get_collection_string(), TagViewSet)
|
)
|
||||||
router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config')
|
router.register(
|
||||||
router.register(self.get_model('ProjectorMessage').get_collection_string(), ProjectorMessageViewSet)
|
self.get_model("ChatMessage").get_collection_string(), ChatMessageViewSet
|
||||||
router.register(self.get_model('Countdown').get_collection_string(), CountdownViewSet)
|
)
|
||||||
router.register(self.get_model('History').get_collection_string(), HistoryViewSet)
|
router.register(self.get_model("Tag").get_collection_string(), TagViewSet)
|
||||||
|
router.register(
|
||||||
|
self.get_model("ConfigStore").get_collection_string(),
|
||||||
|
ConfigViewSet,
|
||||||
|
"config",
|
||||||
|
)
|
||||||
|
router.register(
|
||||||
|
self.get_model("ProjectorMessage").get_collection_string(),
|
||||||
|
ProjectorMessageViewSet,
|
||||||
|
)
|
||||||
|
router.register(
|
||||||
|
self.get_model("Countdown").get_collection_string(), CountdownViewSet
|
||||||
|
)
|
||||||
|
router.register(
|
||||||
|
self.get_model("History").get_collection_string(), HistoryViewSet
|
||||||
|
)
|
||||||
|
|
||||||
# Sets the cache and builds the startup history
|
# Sets the cache and builds the startup history
|
||||||
if is_normal_server_start:
|
if is_normal_server_start:
|
||||||
element_cache.ensure_cache()
|
element_cache.ensure_cache()
|
||||||
self.get_model('History').objects.build_history()
|
self.get_model("History").objects.build_history()
|
||||||
|
|
||||||
# Register client messages
|
# Register client messages
|
||||||
register_client_message(NotifyWebsocketClientMessage())
|
register_client_message(NotifyWebsocketClientMessage())
|
||||||
|
@ -97,10 +116,13 @@ class CoreAppConfig(AppConfig):
|
||||||
register_client_message(AutoupdateWebsocketClientMessage())
|
register_client_message(AutoupdateWebsocketClientMessage())
|
||||||
|
|
||||||
# register required_users
|
# register required_users
|
||||||
required_user.add_collection_string(self.get_model('ChatMessage').get_collection_string(), required_users)
|
required_user.add_collection_string(
|
||||||
|
self.get_model("ChatMessage").get_collection_string(), required_users
|
||||||
|
)
|
||||||
|
|
||||||
def get_config_variables(self):
|
def get_config_variables(self):
|
||||||
from .config_variables import get_config_variables
|
from .config_variables import get_config_variables
|
||||||
|
|
||||||
return get_config_variables()
|
return get_config_variables()
|
||||||
|
|
||||||
def get_startup_elements(self):
|
def get_startup_elements(self):
|
||||||
|
@ -108,7 +130,15 @@ class CoreAppConfig(AppConfig):
|
||||||
Yields all Cachables required on startup i. e. opening the websocket
|
Yields all Cachables required on startup i. e. opening the websocket
|
||||||
connection.
|
connection.
|
||||||
"""
|
"""
|
||||||
for model_name in ('Projector', 'ChatMessage', 'Tag', 'ProjectorMessage', 'Countdown', 'ConfigStore', 'History'):
|
for model_name in (
|
||||||
|
"Projector",
|
||||||
|
"ChatMessage",
|
||||||
|
"Tag",
|
||||||
|
"ProjectorMessage",
|
||||||
|
"Countdown",
|
||||||
|
"ConfigStore",
|
||||||
|
"History",
|
||||||
|
):
|
||||||
yield self.get_model(model_name)
|
yield self.get_model(model_name)
|
||||||
|
|
||||||
def get_angular_constants(self):
|
def get_angular_constants(self):
|
||||||
|
@ -118,9 +148,9 @@ class CoreAppConfig(AppConfig):
|
||||||
|
|
||||||
# Client settings
|
# Client settings
|
||||||
client_settings_keys = [
|
client_settings_keys = [
|
||||||
'MOTION_IDENTIFIER_MIN_DIGITS',
|
"MOTION_IDENTIFIER_MIN_DIGITS",
|
||||||
'MOTION_IDENTIFIER_WITHOUT_BLANKS',
|
"MOTION_IDENTIFIER_WITHOUT_BLANKS",
|
||||||
'MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS'
|
"MOTIONS_ALLOW_AMENDMENTS_OF_AMENDMENTS",
|
||||||
]
|
]
|
||||||
client_settings_dict = {}
|
client_settings_dict = {}
|
||||||
for key in client_settings_keys:
|
for key in client_settings_keys:
|
||||||
|
@ -130,33 +160,40 @@ class CoreAppConfig(AppConfig):
|
||||||
# Settings key does not exist. Do nothing. The client will
|
# Settings key does not exist. Do nothing. The client will
|
||||||
# treat this as undefined.
|
# treat this as undefined.
|
||||||
pass
|
pass
|
||||||
constants['OpenSlidesSettings'] = client_settings_dict
|
constants["OpenSlidesSettings"] = client_settings_dict
|
||||||
|
|
||||||
# Config variables
|
# Config variables
|
||||||
config_groups: List[Any] = []
|
config_groups: List[Any] = []
|
||||||
for config_variable in sorted(config.config_variables.values(), key=attrgetter('weight')):
|
for config_variable in sorted(
|
||||||
|
config.config_variables.values(), key=attrgetter("weight")
|
||||||
|
):
|
||||||
if config_variable.is_hidden():
|
if config_variable.is_hidden():
|
||||||
# Skip hidden config variables. Do not even check groups and subgroups.
|
# Skip hidden config variables. Do not even check groups and subgroups.
|
||||||
continue
|
continue
|
||||||
if not config_groups or config_groups[-1]['name'] != config_variable.group:
|
if not config_groups or config_groups[-1]["name"] != config_variable.group:
|
||||||
# Add new group.
|
# Add new group.
|
||||||
config_groups.append(OrderedDict(
|
config_groups.append(
|
||||||
name=config_variable.group,
|
OrderedDict(name=config_variable.group, subgroups=[])
|
||||||
subgroups=[]))
|
)
|
||||||
if not config_groups[-1]['subgroups'] or config_groups[-1]['subgroups'][-1]['name'] != config_variable.subgroup:
|
if (
|
||||||
|
not config_groups[-1]["subgroups"]
|
||||||
|
or config_groups[-1]["subgroups"][-1]["name"]
|
||||||
|
!= config_variable.subgroup
|
||||||
|
):
|
||||||
# Add new subgroup.
|
# Add new subgroup.
|
||||||
config_groups[-1]['subgroups'].append(OrderedDict(
|
config_groups[-1]["subgroups"].append(
|
||||||
name=config_variable.subgroup,
|
OrderedDict(name=config_variable.subgroup, items=[])
|
||||||
items=[]))
|
)
|
||||||
# Add the config variable to the current group and subgroup.
|
# Add the config variable to the current group and subgroup.
|
||||||
config_groups[-1]['subgroups'][-1]['items'].append(config_variable.data)
|
config_groups[-1]["subgroups"][-1]["items"].append(config_variable.data)
|
||||||
constants['OpenSlidesConfigVariables'] = config_groups
|
constants["OpenSlidesConfigVariables"] = config_groups
|
||||||
|
|
||||||
return constants
|
return constants
|
||||||
|
|
||||||
|
|
||||||
def call_save_default_values(**kwargs):
|
def call_save_default_values(**kwargs):
|
||||||
from .config import config
|
from .config import config
|
||||||
|
|
||||||
config.save_default_values()
|
config.save_default_values()
|
||||||
|
|
||||||
|
|
||||||
|
@ -164,4 +201,4 @@ def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||||
"""
|
"""
|
||||||
Returns all user ids that are displayed as chatters.
|
Returns all user ids that are displayed as chatters.
|
||||||
"""
|
"""
|
||||||
return set(element['user_id'])
|
return set(element["user_id"])
|
||||||
|
|
|
@ -1,13 +1,4 @@
|
||||||
from typing import (
|
from typing import Any, Callable, Dict, Iterable, Optional, TypeVar, Union, cast
|
||||||
Any,
|
|
||||||
Callable,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
|
||||||
Optional,
|
|
||||||
TypeVar,
|
|
||||||
Union,
|
|
||||||
cast,
|
|
||||||
)
|
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
@ -21,16 +12,16 @@ from .models import ConfigStore
|
||||||
|
|
||||||
|
|
||||||
INPUT_TYPE_MAPPING = {
|
INPUT_TYPE_MAPPING = {
|
||||||
'string': str,
|
"string": str,
|
||||||
'text': str,
|
"text": str,
|
||||||
'markupText': str,
|
"markupText": str,
|
||||||
'integer': int,
|
"integer": int,
|
||||||
'boolean': bool,
|
"boolean": bool,
|
||||||
'choice': str,
|
"choice": str,
|
||||||
'colorpicker': str,
|
"colorpicker": str,
|
||||||
'datetimepicker': int,
|
"datetimepicker": int,
|
||||||
'static': dict,
|
"static": dict,
|
||||||
'translations': list,
|
"translations": list,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,9 +45,11 @@ class ConfigHandler:
|
||||||
Returns the value of the config variable.
|
Returns the value of the config variable.
|
||||||
"""
|
"""
|
||||||
if not self.exists(key):
|
if not self.exists(key):
|
||||||
raise ConfigNotFound(_('The config variable {} was not found.').format(key))
|
raise ConfigNotFound(_("The config variable {} was not found.").format(key))
|
||||||
|
|
||||||
return async_to_sync(element_cache.get_element_full_data)(self.get_collection_string(), self.get_key_to_id()[key])['value']
|
return async_to_sync(element_cache.get_element_full_data)(
|
||||||
|
self.get_collection_string(), self.get_key_to_id()[key]
|
||||||
|
)["value"]
|
||||||
|
|
||||||
def get_key_to_id(self) -> Dict[str, int]:
|
def get_key_to_id(self) -> Dict[str, int]:
|
||||||
"""
|
"""
|
||||||
|
@ -80,7 +73,7 @@ class ConfigHandler:
|
||||||
all_data = await element_cache.get_all_full_data()
|
all_data = await element_cache.get_all_full_data()
|
||||||
elements = all_data[self.get_collection_string()]
|
elements = all_data[self.get_collection_string()]
|
||||||
for element in elements:
|
for element in elements:
|
||||||
self.key_to_id[element['key']] = element['id']
|
self.key_to_id[element["key"]] = element["id"]
|
||||||
|
|
||||||
def exists(self, key: str) -> bool:
|
def exists(self, key: str) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -102,7 +95,7 @@ class ConfigHandler:
|
||||||
try:
|
try:
|
||||||
config_variable = self.config_variables[key]
|
config_variable = self.config_variables[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ConfigNotFound(_('The config variable {} was not found.').format(key))
|
raise ConfigNotFound(_("The config variable {} was not found.").format(key))
|
||||||
|
|
||||||
# Validate datatype and run validators.
|
# Validate datatype and run validators.
|
||||||
expected_type = INPUT_TYPE_MAPPING[config_variable.input_type]
|
expected_type = INPUT_TYPE_MAPPING[config_variable.input_type]
|
||||||
|
@ -111,17 +104,21 @@ class ConfigHandler:
|
||||||
try:
|
try:
|
||||||
value = expected_type(value)
|
value = expected_type(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ConfigError(_('Wrong datatype. Expected %(expected_type)s, got %(got_type)s.') % {
|
raise ConfigError(
|
||||||
'expected_type': expected_type, 'got_type': type(value)})
|
_("Wrong datatype. Expected %(expected_type)s, got %(got_type)s.")
|
||||||
|
% {"expected_type": expected_type, "got_type": type(value)}
|
||||||
|
)
|
||||||
|
|
||||||
if config_variable.input_type == 'choice':
|
if config_variable.input_type == "choice":
|
||||||
# Choices can be a callable. In this case call it at this place
|
# Choices can be a callable. In this case call it at this place
|
||||||
if callable(config_variable.choices):
|
if callable(config_variable.choices):
|
||||||
choices = config_variable.choices()
|
choices = config_variable.choices()
|
||||||
else:
|
else:
|
||||||
choices = config_variable.choices
|
choices = config_variable.choices
|
||||||
if choices is None or value not in map(lambda choice: choice['value'], choices):
|
if choices is None or value not in map(
|
||||||
raise ConfigError(_('Invalid input. Choice does not match.'))
|
lambda choice: choice["value"], choices
|
||||||
|
):
|
||||||
|
raise ConfigError(_("Invalid input. Choice does not match."))
|
||||||
|
|
||||||
for validator in config_variable.validators:
|
for validator in config_variable.validators:
|
||||||
try:
|
try:
|
||||||
|
@ -129,34 +126,36 @@ class ConfigHandler:
|
||||||
except DjangoValidationError as e:
|
except DjangoValidationError as e:
|
||||||
raise ConfigError(e.messages[0])
|
raise ConfigError(e.messages[0])
|
||||||
|
|
||||||
if config_variable.input_type == 'static':
|
if config_variable.input_type == "static":
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
raise ConfigError(_('This has to be a dict.'))
|
raise ConfigError(_("This has to be a dict."))
|
||||||
whitelist = (
|
whitelist = ("path", "display_name")
|
||||||
'path',
|
|
||||||
'display_name',
|
|
||||||
)
|
|
||||||
for required_entry in whitelist:
|
for required_entry in whitelist:
|
||||||
if required_entry not in value:
|
if required_entry not in value:
|
||||||
raise ConfigError(_('{} has to be given.'.format(required_entry)))
|
raise ConfigError(_("{} has to be given.".format(required_entry)))
|
||||||
if not isinstance(value[required_entry], str):
|
if not isinstance(value[required_entry], str):
|
||||||
raise ConfigError(_('{} has to be a string.'.format(required_entry)))
|
raise ConfigError(
|
||||||
|
_("{} has to be a string.".format(required_entry))
|
||||||
|
)
|
||||||
|
|
||||||
if config_variable.input_type == 'translations':
|
if config_variable.input_type == "translations":
|
||||||
if not isinstance(value, list):
|
if not isinstance(value, list):
|
||||||
raise ConfigError(_('Translations has to be a list.'))
|
raise ConfigError(_("Translations has to be a list."))
|
||||||
for entry in value:
|
for entry in value:
|
||||||
if not isinstance(entry, dict):
|
if not isinstance(entry, dict):
|
||||||
raise ConfigError(_('Every value has to be a dict, not {}.'.format(type(entry))))
|
raise ConfigError(
|
||||||
whitelist = (
|
_("Every value has to be a dict, not {}.".format(type(entry)))
|
||||||
'original',
|
)
|
||||||
'translation',
|
whitelist = ("original", "translation")
|
||||||
)
|
|
||||||
for required_entry in whitelist:
|
for required_entry in whitelist:
|
||||||
if required_entry not in entry:
|
if required_entry not in entry:
|
||||||
raise ConfigError(_('{} has to be given.'.format(required_entry)))
|
raise ConfigError(
|
||||||
|
_("{} has to be given.".format(required_entry))
|
||||||
|
)
|
||||||
if not isinstance(entry[required_entry], str):
|
if not isinstance(entry[required_entry], str):
|
||||||
raise ConfigError(_('{} has to be a string.'.format(required_entry)))
|
raise ConfigError(
|
||||||
|
_("{} has to be a string.".format(required_entry))
|
||||||
|
)
|
||||||
|
|
||||||
# Save the new value to the database.
|
# Save the new value to the database.
|
||||||
db_value = ConfigStore.objects.get(key=key)
|
db_value = ConfigStore.objects.get(key=key)
|
||||||
|
@ -178,7 +177,7 @@ class ConfigHandler:
|
||||||
continue
|
continue
|
||||||
self.update_config_variables(get_config_variables())
|
self.update_config_variables(get_config_variables())
|
||||||
|
|
||||||
def update_config_variables(self, items: Iterable['ConfigVariable']) -> None:
|
def update_config_variables(self, items: Iterable["ConfigVariable"]) -> None:
|
||||||
"""
|
"""
|
||||||
Updates the config_variables dict.
|
Updates the config_variables dict.
|
||||||
"""
|
"""
|
||||||
|
@ -189,7 +188,9 @@ class ConfigHandler:
|
||||||
# be in already in self.config_variables
|
# be in already in self.config_variables
|
||||||
intersection = set(item_index.keys()).intersection(self.config_variables.keys())
|
intersection = set(item_index.keys()).intersection(self.config_variables.keys())
|
||||||
if intersection:
|
if intersection:
|
||||||
raise ConfigError(_('Too many values for config variables {} found.').format(intersection))
|
raise ConfigError(
|
||||||
|
_("Too many values for config variables {} found.").format(intersection)
|
||||||
|
)
|
||||||
|
|
||||||
self.config_variables.update(item_index)
|
self.config_variables.update(item_index)
|
||||||
|
|
||||||
|
@ -224,19 +225,22 @@ use x = config[...], to set it use config[...] = x.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar("T")
|
||||||
ChoiceType = Optional[Iterable[Dict[str, str]]]
|
ChoiceType = Optional[Iterable[Dict[str, str]]]
|
||||||
ChoiceCallableType = Union[ChoiceType, Callable[[], ChoiceType]]
|
ChoiceCallableType = Union[ChoiceType, Callable[[], ChoiceType]]
|
||||||
ValidatorsType = Iterable[Callable[[T], None]]
|
ValidatorsType = Iterable[Callable[[T], None]]
|
||||||
OnChangeType = Callable[[], None]
|
OnChangeType = Callable[[], None]
|
||||||
ConfigVariableDict = TypedDict('ConfigVariableDict', {
|
ConfigVariableDict = TypedDict(
|
||||||
'key': str,
|
"ConfigVariableDict",
|
||||||
'default_value': Any,
|
{
|
||||||
'input_type': str,
|
"key": str,
|
||||||
'label': str,
|
"default_value": Any,
|
||||||
'help_text': str,
|
"input_type": str,
|
||||||
'choices': ChoiceType,
|
"label": str,
|
||||||
})
|
"help_text": str,
|
||||||
|
"choices": ChoiceType,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigVariable:
|
class ConfigVariable:
|
||||||
|
@ -265,27 +269,47 @@ class ConfigVariable:
|
||||||
the value during setup of the database if the admin uses the respective
|
the value during setup of the database if the admin uses the respective
|
||||||
command line option.
|
command line option.
|
||||||
"""
|
"""
|
||||||
def __init__(self, name: str, default_value: T, input_type: str = 'string',
|
|
||||||
label: str = None, help_text: str = None, choices: ChoiceCallableType = None,
|
def __init__(
|
||||||
hidden: bool = False, weight: int = 0, group: str = None, subgroup: str = None,
|
self,
|
||||||
validators: ValidatorsType = None, on_change: OnChangeType = None) -> None:
|
name: str,
|
||||||
|
default_value: T,
|
||||||
|
input_type: str = "string",
|
||||||
|
label: str = None,
|
||||||
|
help_text: str = None,
|
||||||
|
choices: ChoiceCallableType = None,
|
||||||
|
hidden: bool = False,
|
||||||
|
weight: int = 0,
|
||||||
|
group: str = None,
|
||||||
|
subgroup: str = None,
|
||||||
|
validators: ValidatorsType = None,
|
||||||
|
on_change: OnChangeType = None,
|
||||||
|
) -> None:
|
||||||
if input_type not in INPUT_TYPE_MAPPING:
|
if input_type not in INPUT_TYPE_MAPPING:
|
||||||
raise ValueError(_('Invalid value for config attribute input_type.'))
|
raise ValueError(_("Invalid value for config attribute input_type."))
|
||||||
if input_type == 'choice' and choices is None:
|
if input_type == "choice" and choices is None:
|
||||||
raise ConfigError(_("Either config attribute 'choices' must not be None or "
|
raise ConfigError(
|
||||||
"'input_type' must not be 'choice'."))
|
_(
|
||||||
elif input_type != 'choice' and choices is not None:
|
"Either config attribute 'choices' must not be None or "
|
||||||
raise ConfigError(_("Either config attribute 'choices' must be None or "
|
"'input_type' must not be 'choice'."
|
||||||
"'input_type' must be 'choice'."))
|
)
|
||||||
|
)
|
||||||
|
elif input_type != "choice" and choices is not None:
|
||||||
|
raise ConfigError(
|
||||||
|
_(
|
||||||
|
"Either config attribute 'choices' must be None or "
|
||||||
|
"'input_type' must be 'choice'."
|
||||||
|
)
|
||||||
|
)
|
||||||
self.name = name
|
self.name = name
|
||||||
self.default_value = default_value
|
self.default_value = default_value
|
||||||
self.input_type = input_type
|
self.input_type = input_type
|
||||||
self.label = label or name
|
self.label = label or name
|
||||||
self.help_text = help_text or ''
|
self.help_text = help_text or ""
|
||||||
self.choices = choices
|
self.choices = choices
|
||||||
self.hidden = hidden
|
self.hidden = hidden
|
||||||
self.weight = weight
|
self.weight = weight
|
||||||
self.group = group or _('General')
|
self.group = group or _("General")
|
||||||
self.subgroup = subgroup
|
self.subgroup = subgroup
|
||||||
self.validators = validators or ()
|
self.validators = validators or ()
|
||||||
self.on_change = on_change
|
self.on_change = on_change
|
||||||
|
@ -301,7 +325,7 @@ class ConfigVariable:
|
||||||
input_type=self.input_type,
|
input_type=self.input_type,
|
||||||
label=self.label,
|
label=self.label,
|
||||||
help_text=self.help_text,
|
help_text=self.help_text,
|
||||||
choices=self.choices() if callable(self.choices) else self.choices
|
choices=self.choices() if callable(self.choices) else self.choices,
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_hidden(self) -> bool:
|
def is_hidden(self) -> bool:
|
||||||
|
|
|
@ -12,401 +12,429 @@ def get_config_variables():
|
||||||
(see apps.py).
|
(see apps.py).
|
||||||
"""
|
"""
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='general_event_name',
|
name="general_event_name",
|
||||||
default_value='OpenSlides',
|
default_value="OpenSlides",
|
||||||
label='Event name',
|
label="Event name",
|
||||||
weight=110,
|
weight=110,
|
||||||
group='General',
|
group="General",
|
||||||
subgroup='Event',
|
subgroup="Event",
|
||||||
validators=(MaxLengthValidator(100),))
|
validators=(MaxLengthValidator(100),),
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='general_event_description',
|
name="general_event_description",
|
||||||
default_value='Presentation and assembly system',
|
default_value="Presentation and assembly system",
|
||||||
label='Short description of event',
|
label="Short description of event",
|
||||||
weight=115,
|
weight=115,
|
||||||
group='General',
|
group="General",
|
||||||
subgroup='Event',
|
subgroup="Event",
|
||||||
validators=(MaxLengthValidator(100),))
|
validators=(MaxLengthValidator(100),),
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='general_event_date',
|
name="general_event_date",
|
||||||
default_value='',
|
default_value="",
|
||||||
label='Event date',
|
label="Event date",
|
||||||
weight=120,
|
weight=120,
|
||||||
group='General',
|
group="General",
|
||||||
subgroup='Event')
|
subgroup="Event",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='general_event_location',
|
name="general_event_location",
|
||||||
default_value='',
|
default_value="",
|
||||||
label='Event location',
|
label="Event location",
|
||||||
weight=125,
|
weight=125,
|
||||||
group='General',
|
group="General",
|
||||||
subgroup='Event')
|
subgroup="Event",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='general_event_legal_notice',
|
name="general_event_legal_notice",
|
||||||
default_value='<a href="http://www.openslides.org">OpenSlides</a> is a '
|
default_value='<a href="http://www.openslides.org">OpenSlides</a> is a '
|
||||||
'free web based presentation and assembly system for '
|
"free web based presentation and assembly system for "
|
||||||
'visualizing and controlling agenda, motions and '
|
"visualizing and controlling agenda, motions and "
|
||||||
'elections of an assembly.',
|
"elections of an assembly.",
|
||||||
input_type='markupText',
|
input_type="markupText",
|
||||||
label='Legal notice',
|
label="Legal notice",
|
||||||
weight=132,
|
weight=132,
|
||||||
group='General',
|
group="General",
|
||||||
subgroup='Event')
|
subgroup="Event",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='general_event_privacy_policy',
|
name="general_event_privacy_policy",
|
||||||
default_value='',
|
default_value="",
|
||||||
input_type='markupText',
|
input_type="markupText",
|
||||||
label='Privacy policy',
|
label="Privacy policy",
|
||||||
weight=132,
|
weight=132,
|
||||||
group='General',
|
group="General",
|
||||||
subgroup='Event')
|
subgroup="Event",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='general_event_welcome_title',
|
name="general_event_welcome_title",
|
||||||
default_value='Welcome to OpenSlides',
|
default_value="Welcome to OpenSlides",
|
||||||
label='Front page title',
|
label="Front page title",
|
||||||
weight=134,
|
weight=134,
|
||||||
group='General',
|
group="General",
|
||||||
subgroup='Event')
|
subgroup="Event",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='general_event_welcome_text',
|
name="general_event_welcome_text",
|
||||||
default_value='[Space for your welcome text.]',
|
default_value="[Space for your welcome text.]",
|
||||||
input_type='markupText',
|
input_type="markupText",
|
||||||
label='Front page text',
|
label="Front page text",
|
||||||
weight=136,
|
weight=136,
|
||||||
group='General',
|
group="General",
|
||||||
subgroup='Event')
|
subgroup="Event",
|
||||||
|
)
|
||||||
|
|
||||||
# General System
|
# General System
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='general_system_enable_anonymous',
|
name="general_system_enable_anonymous",
|
||||||
default_value=False,
|
default_value=False,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Allow access for anonymous guest users',
|
label="Allow access for anonymous guest users",
|
||||||
weight=138,
|
weight=138,
|
||||||
group='General',
|
group="General",
|
||||||
subgroup='System')
|
subgroup="System",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='general_login_info_text',
|
name="general_login_info_text",
|
||||||
default_value='',
|
default_value="",
|
||||||
label='Show this text on the login page',
|
label="Show this text on the login page",
|
||||||
weight=140,
|
weight=140,
|
||||||
group='General',
|
group="General",
|
||||||
subgroup='System')
|
subgroup="System",
|
||||||
|
)
|
||||||
|
|
||||||
# General export settings
|
# General export settings
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='general_csv_separator',
|
name="general_csv_separator",
|
||||||
default_value=',',
|
default_value=",",
|
||||||
label='Separator used for all csv exports and examples',
|
label="Separator used for all csv exports and examples",
|
||||||
weight=142,
|
weight=142,
|
||||||
group='General',
|
group="General",
|
||||||
subgroup='Export')
|
subgroup="Export",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='general_export_pdf_pagenumber_alignment',
|
name="general_export_pdf_pagenumber_alignment",
|
||||||
default_value='center',
|
default_value="center",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='Page number alignment in PDF',
|
label="Page number alignment in PDF",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': 'left', 'display_name': 'Left'},
|
{"value": "left", "display_name": "Left"},
|
||||||
{'value': 'center', 'display_name': 'Center'},
|
{"value": "center", "display_name": "Center"},
|
||||||
{'value': 'right', 'display_name': 'Right'}),
|
{"value": "right", "display_name": "Right"},
|
||||||
|
),
|
||||||
weight=144,
|
weight=144,
|
||||||
group='General',
|
group="General",
|
||||||
subgroup='Export')
|
subgroup="Export",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='general_export_pdf_fontsize',
|
name="general_export_pdf_fontsize",
|
||||||
default_value='10',
|
default_value="10",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='Standard font size in PDF',
|
label="Standard font size in PDF",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': '10', 'display_name': '10'},
|
{"value": "10", "display_name": "10"},
|
||||||
{'value': '11', 'display_name': '11'},
|
{"value": "11", "display_name": "11"},
|
||||||
{'value': '12', 'display_name': '12'}),
|
{"value": "12", "display_name": "12"},
|
||||||
|
),
|
||||||
weight=146,
|
weight=146,
|
||||||
group='General',
|
group="General",
|
||||||
subgroup='Export')
|
subgroup="Export",
|
||||||
|
)
|
||||||
|
|
||||||
# Projector
|
# Projector
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='projector_language',
|
name="projector_language",
|
||||||
default_value='browser',
|
default_value="browser",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='Projector language',
|
label="Projector language",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': 'browser', 'display_name': 'Current browser language'},
|
{"value": "browser", "display_name": "Current browser language"},
|
||||||
{'value': 'en', 'display_name': 'English'},
|
{"value": "en", "display_name": "English"},
|
||||||
{'value': 'de', 'display_name': 'Deutsch'},
|
{"value": "de", "display_name": "Deutsch"},
|
||||||
{'value': 'fr', 'display_name': 'Français'},
|
{"value": "fr", "display_name": "Français"},
|
||||||
{'value': 'es', 'display_name': 'Español'},
|
{"value": "es", "display_name": "Español"},
|
||||||
{'value': 'pt', 'display_name': 'Português'},
|
{"value": "pt", "display_name": "Português"},
|
||||||
{'value': 'cs', 'display_name': 'Čeština'},
|
{"value": "cs", "display_name": "Čeština"},
|
||||||
{'value': 'ru', 'display_name': 'русский'}),
|
{"value": "ru", "display_name": "русский"},
|
||||||
|
),
|
||||||
weight=150,
|
weight=150,
|
||||||
group='Projector')
|
group="Projector",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='projector_enable_logo',
|
name="projector_enable_logo",
|
||||||
default_value=True,
|
default_value=True,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Show logo on projector',
|
label="Show logo on projector",
|
||||||
help_text='You can replace the logo by uploading an image and set it as '
|
help_text="You can replace the logo by uploading an image and set it as "
|
||||||
'the "Projector logo" in "files".',
|
'the "Projector logo" in "files".',
|
||||||
weight=152,
|
weight=152,
|
||||||
group='Projector')
|
group="Projector",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='projector_enable_clock',
|
name="projector_enable_clock",
|
||||||
default_value=True,
|
default_value=True,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Show the clock on projector',
|
label="Show the clock on projector",
|
||||||
weight=154,
|
weight=154,
|
||||||
group='Projector')
|
group="Projector",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='projector_enable_title',
|
name="projector_enable_title",
|
||||||
default_value=True,
|
default_value=True,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Show title and description of event on projector',
|
label="Show title and description of event on projector",
|
||||||
weight=155,
|
weight=155,
|
||||||
group='Projector')
|
group="Projector",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='projector_enable_header_footer',
|
name="projector_enable_header_footer",
|
||||||
default_value=True,
|
default_value=True,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Display header and footer',
|
label="Display header and footer",
|
||||||
weight=157,
|
weight=157,
|
||||||
group='Projector')
|
group="Projector",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='projector_header_backgroundcolor',
|
name="projector_header_backgroundcolor",
|
||||||
default_value='#317796',
|
default_value="#317796",
|
||||||
input_type='colorpicker',
|
input_type="colorpicker",
|
||||||
label='Background color of projector header and footer',
|
label="Background color of projector header and footer",
|
||||||
weight=160,
|
weight=160,
|
||||||
group='Projector')
|
group="Projector",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='projector_header_fontcolor',
|
name="projector_header_fontcolor",
|
||||||
default_value='#F5F5F5',
|
default_value="#F5F5F5",
|
||||||
input_type='colorpicker',
|
input_type="colorpicker",
|
||||||
label='Font color of projector header and footer',
|
label="Font color of projector header and footer",
|
||||||
weight=165,
|
weight=165,
|
||||||
group='Projector')
|
group="Projector",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='projector_h1_fontcolor',
|
name="projector_h1_fontcolor",
|
||||||
default_value='#317796',
|
default_value="#317796",
|
||||||
input_type='colorpicker',
|
input_type="colorpicker",
|
||||||
label='Font color of projector headline',
|
label="Font color of projector headline",
|
||||||
weight=170,
|
weight=170,
|
||||||
group='Projector')
|
group="Projector",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='projector_default_countdown',
|
name="projector_default_countdown",
|
||||||
default_value=60,
|
default_value=60,
|
||||||
input_type='integer',
|
input_type="integer",
|
||||||
label='Predefined seconds of new countdowns',
|
label="Predefined seconds of new countdowns",
|
||||||
weight=185,
|
weight=185,
|
||||||
group='Projector')
|
group="Projector",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='projector_blank_color',
|
name="projector_blank_color",
|
||||||
default_value='#FFFFFF',
|
default_value="#FFFFFF",
|
||||||
input_type='colorpicker',
|
input_type="colorpicker",
|
||||||
label='Color for blanked projector',
|
label="Color for blanked projector",
|
||||||
weight=190,
|
weight=190,
|
||||||
group='Projector')
|
group="Projector",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='projector_broadcast',
|
name="projector_broadcast",
|
||||||
default_value=0,
|
default_value=0,
|
||||||
input_type='integer',
|
input_type="integer",
|
||||||
label='Projector which is broadcasted',
|
label="Projector which is broadcasted",
|
||||||
weight=200,
|
weight=200,
|
||||||
group='Projector',
|
group="Projector",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='projector_currentListOfSpeakers_reference',
|
name="projector_currentListOfSpeakers_reference",
|
||||||
default_value=1,
|
default_value=1,
|
||||||
input_type='integer',
|
input_type="integer",
|
||||||
label='Projector reference for list of speakers',
|
label="Projector reference for list of speakers",
|
||||||
weight=201,
|
weight=201,
|
||||||
group='Projector',
|
group="Projector",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
# Logos.
|
# Logos.
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='logos_available',
|
name="logos_available",
|
||||||
default_value=[
|
default_value=[
|
||||||
'logo_projector_main',
|
"logo_projector_main",
|
||||||
'logo_projector_header',
|
"logo_projector_header",
|
||||||
'logo_web_header',
|
"logo_web_header",
|
||||||
'logo_pdf_header_L',
|
"logo_pdf_header_L",
|
||||||
'logo_pdf_header_R',
|
"logo_pdf_header_R",
|
||||||
'logo_pdf_footer_L',
|
"logo_pdf_footer_L",
|
||||||
'logo_pdf_footer_R',
|
"logo_pdf_footer_R",
|
||||||
'logo_pdf_ballot_paper'],
|
"logo_pdf_ballot_paper",
|
||||||
|
],
|
||||||
weight=300,
|
weight=300,
|
||||||
group='Logo',
|
group="Logo",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='logo_projector_main',
|
name="logo_projector_main",
|
||||||
default_value={
|
default_value={"display_name": "Projector logo", "path": ""},
|
||||||
'display_name': 'Projector logo',
|
input_type="static",
|
||||||
'path': ''},
|
|
||||||
input_type='static',
|
|
||||||
weight=301,
|
weight=301,
|
||||||
group='Logo',
|
group="Logo",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='logo_projector_header',
|
name="logo_projector_header",
|
||||||
default_value={
|
default_value={"display_name": "Projector header image", "path": ""},
|
||||||
'display_name': 'Projector header image',
|
input_type="static",
|
||||||
'path': ''},
|
|
||||||
input_type='static',
|
|
||||||
weight=302,
|
weight=302,
|
||||||
group='Logo',
|
group="Logo",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='logo_web_header',
|
name="logo_web_header",
|
||||||
default_value={
|
default_value={"display_name": "Web interface header logo", "path": ""},
|
||||||
'display_name': 'Web interface header logo',
|
input_type="static",
|
||||||
'path': ''},
|
|
||||||
input_type='static',
|
|
||||||
weight=303,
|
weight=303,
|
||||||
group='Logo',
|
group="Logo",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
# PDF logos
|
# PDF logos
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='logo_pdf_header_L',
|
name="logo_pdf_header_L",
|
||||||
default_value={
|
default_value={"display_name": "PDF header logo (left)", "path": ""},
|
||||||
'display_name': 'PDF header logo (left)',
|
input_type="static",
|
||||||
'path': ''},
|
|
||||||
input_type='static',
|
|
||||||
weight=310,
|
weight=310,
|
||||||
group='Logo',
|
group="Logo",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='logo_pdf_header_R',
|
name="logo_pdf_header_R",
|
||||||
default_value={
|
default_value={"display_name": "PDF header logo (right)", "path": ""},
|
||||||
'display_name': 'PDF header logo (right)',
|
input_type="static",
|
||||||
'path': ''},
|
|
||||||
input_type='static',
|
|
||||||
weight=311,
|
weight=311,
|
||||||
group='Logo',
|
group="Logo",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='logo_pdf_footer_L',
|
name="logo_pdf_footer_L",
|
||||||
default_value={
|
default_value={"display_name": "PDF footer logo (left)", "path": ""},
|
||||||
'display_name': 'PDF footer logo (left)',
|
input_type="static",
|
||||||
'path': ''},
|
|
||||||
input_type='static',
|
|
||||||
weight=312,
|
weight=312,
|
||||||
group='Logo',
|
group="Logo",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='logo_pdf_footer_R',
|
name="logo_pdf_footer_R",
|
||||||
default_value={
|
default_value={"display_name": "PDF footer logo (right)", "path": ""},
|
||||||
'display_name': 'PDF footer logo (right)',
|
input_type="static",
|
||||||
'path': ''},
|
|
||||||
input_type='static',
|
|
||||||
weight=313,
|
weight=313,
|
||||||
group='Logo',
|
group="Logo",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='logo_pdf_ballot_paper',
|
name="logo_pdf_ballot_paper",
|
||||||
default_value={
|
default_value={"display_name": "PDF ballot paper logo", "path": ""},
|
||||||
'display_name': 'PDF ballot paper logo',
|
input_type="static",
|
||||||
'path': ''},
|
|
||||||
input_type='static',
|
|
||||||
weight=314,
|
weight=314,
|
||||||
group='Logo',
|
group="Logo",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
# Fonts
|
# Fonts
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='fonts_available',
|
name="fonts_available",
|
||||||
default_value=[
|
default_value=["font_regular", "font_italic", "font_bold", "font_bold_italic"],
|
||||||
'font_regular',
|
|
||||||
'font_italic',
|
|
||||||
'font_bold',
|
|
||||||
'font_bold_italic'],
|
|
||||||
weight=320,
|
weight=320,
|
||||||
group='Font',
|
group="Font",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='font_regular',
|
name="font_regular",
|
||||||
default_value={
|
default_value={
|
||||||
'display_name': 'Font regular',
|
"display_name": "Font regular",
|
||||||
'default': 'static/fonts/Roboto-Regular.woff',
|
"default": "static/fonts/Roboto-Regular.woff",
|
||||||
'path': ''},
|
"path": "",
|
||||||
input_type='static',
|
},
|
||||||
|
input_type="static",
|
||||||
weight=321,
|
weight=321,
|
||||||
group='Font',
|
group="Font",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='font_italic',
|
name="font_italic",
|
||||||
default_value={
|
default_value={
|
||||||
'display_name': 'Font italic',
|
"display_name": "Font italic",
|
||||||
'default': 'static/fonts/Roboto-Medium.woff',
|
"default": "static/fonts/Roboto-Medium.woff",
|
||||||
'path': ''},
|
"path": "",
|
||||||
input_type='static',
|
},
|
||||||
|
input_type="static",
|
||||||
weight=321,
|
weight=321,
|
||||||
group='Font',
|
group="Font",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='font_bold',
|
name="font_bold",
|
||||||
default_value={
|
default_value={
|
||||||
'display_name': 'Font bold',
|
"display_name": "Font bold",
|
||||||
'default': 'static/fonts/Roboto-Condensed-Regular.woff',
|
"default": "static/fonts/Roboto-Condensed-Regular.woff",
|
||||||
'path': ''},
|
"path": "",
|
||||||
input_type='static',
|
},
|
||||||
|
input_type="static",
|
||||||
weight=321,
|
weight=321,
|
||||||
group='Font',
|
group="Font",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='font_bold_italic',
|
name="font_bold_italic",
|
||||||
default_value={
|
default_value={
|
||||||
'display_name': 'Font bold italic',
|
"display_name": "Font bold italic",
|
||||||
'default': 'static/fonts/Roboto-Condensed-Light.woff',
|
"default": "static/fonts/Roboto-Condensed-Light.woff",
|
||||||
'path': ''},
|
"path": "",
|
||||||
input_type='static',
|
},
|
||||||
|
input_type="static",
|
||||||
weight=321,
|
weight=321,
|
||||||
group='Font',
|
group="Font",
|
||||||
hidden=True)
|
hidden=True,
|
||||||
|
)
|
||||||
|
|
||||||
# Custom translations
|
# Custom translations
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='translations',
|
name="translations",
|
||||||
label='Custom translations',
|
label="Custom translations",
|
||||||
default_value=[],
|
default_value=[],
|
||||||
input_type='translations',
|
input_type="translations",
|
||||||
weight=1000,
|
weight=1000,
|
||||||
group='Custom translations')
|
group="Custom translations",
|
||||||
|
)
|
||||||
|
|
|
@ -10,17 +10,18 @@ class Command(BaseCommand):
|
||||||
"""
|
"""
|
||||||
Command to backup the SQLite3 database.
|
Command to backup the SQLite3 database.
|
||||||
"""
|
"""
|
||||||
help = 'Backups the SQLite3 database.'
|
|
||||||
|
help = "Backups the SQLite3 database."
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--path',
|
"--path",
|
||||||
default='database_backup.sqlite',
|
default="database_backup.sqlite",
|
||||||
help='Path for the backup file (Default: database_backup.sqlite).'
|
help="Path for the backup file (Default: database_backup.sqlite).",
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
path = options.get('path')
|
path = options.get("path")
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def do_backup(src_path, dest_path):
|
def do_backup(src_path, dest_path):
|
||||||
|
@ -39,8 +40,11 @@ class Command(BaseCommand):
|
||||||
database_path = get_database_path_from_settings()
|
database_path = get_database_path_from_settings()
|
||||||
if database_path:
|
if database_path:
|
||||||
do_backup(database_path, path)
|
do_backup(database_path, path)
|
||||||
self.stdout.write('Database %s successfully stored at %s.' % (database_path, path))
|
self.stdout.write(
|
||||||
|
"Database %s successfully stored at %s." % (database_path, path)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise CommandError(
|
raise CommandError(
|
||||||
'Default database is not SQLite3. Only SQLite3 databases'
|
"Default database is not SQLite3. Only SQLite3 databases"
|
||||||
'can currently be backuped.')
|
"can currently be backuped."
|
||||||
|
)
|
||||||
|
|
|
@ -8,25 +8,28 @@ class Command(BaseCommand):
|
||||||
"""
|
"""
|
||||||
Command to change OpenSlides config values.
|
Command to change OpenSlides config values.
|
||||||
"""
|
"""
|
||||||
help = 'Changes OpenSlides config values.'
|
|
||||||
|
help = "Changes OpenSlides config values."
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'key',
|
"key", help="Config key. See config_variables.py in every app."
|
||||||
help='Config key. See config_variables.py in every app.'
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'value',
|
"value", help='New config value. For a falsy boolean use "False".'
|
||||||
help='New config value. For a falsy boolean use "False".'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
if options['value'].lower() == 'false':
|
if options["value"].lower() == "false":
|
||||||
options['value'] = False
|
options["value"] = False
|
||||||
try:
|
try:
|
||||||
config[options['key']] = options['value']
|
config[options["key"]] = options["value"]
|
||||||
except (ConfigError, ConfigNotFound) as e:
|
except (ConfigError, ConfigNotFound) as e:
|
||||||
raise CommandError(str(e))
|
raise CommandError(str(e))
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
self.style.SUCCESS('Config {key} successfully changed to {value}.'.format(
|
self.style.SUCCESS(
|
||||||
key=options['key'], value=config[options['key']])))
|
"Config {key} successfully changed to {value}.".format(
|
||||||
|
key=options["key"], value=config[options["key"]]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -7,19 +7,16 @@ class Command(BaseCommand):
|
||||||
"""
|
"""
|
||||||
Command to change a user's password.
|
Command to change a user's password.
|
||||||
"""
|
"""
|
||||||
help = 'Changes user password.'
|
|
||||||
|
help = "Changes user password."
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'username',
|
"username", help="The name of the user to set the password for"
|
||||||
help='The name of the user to set the password for'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'password',
|
|
||||||
help='The new password of the user'
|
|
||||||
)
|
)
|
||||||
|
parser.add_argument("password", help="The new password of the user")
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
user = User.objects.get(username=options['username'])
|
user = User.objects.get(username=options["username"])
|
||||||
user.set_password(options['password'])
|
user.set_password(options["password"])
|
||||||
user.save()
|
user.save()
|
||||||
|
|
|
@ -10,10 +10,12 @@ class Command(_Command):
|
||||||
Migration command that does nearly the same as Django's migration command
|
Migration command that does nearly the same as Django's migration command
|
||||||
but also calls the post_permission_creation signal.
|
but also calls the post_permission_creation signal.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
# Creates the folder for a SQLite3 database if necessary.
|
# Creates the folder for a SQLite3 database if necessary.
|
||||||
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3':
|
if settings.DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3":
|
||||||
try:
|
try:
|
||||||
os.makedirs(settings.OPENSLIDES_USER_DATA_PATH)
|
os.makedirs(settings.OPENSLIDES_USER_DATA_PATH)
|
||||||
except (FileExistsError, AttributeError):
|
except (FileExistsError, AttributeError):
|
||||||
|
|
|
@ -18,11 +18,9 @@ def add_default_projector(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
# We get the model from the versioned app registry;
|
# We get the model from the versioned app registry;
|
||||||
# if we directly import it, it will be the wrong version.
|
# if we directly import it, it will be the wrong version.
|
||||||
Projector = apps.get_model('core', 'Projector')
|
Projector = apps.get_model("core", "Projector")
|
||||||
projector_config = {}
|
projector_config = {}
|
||||||
projector_config[uuid.uuid4().hex] = {
|
projector_config[uuid.uuid4().hex] = {"name": "core/clock", "stable": True}
|
||||||
'name': 'core/clock',
|
|
||||||
'stable': True}
|
|
||||||
# We use bulk_create here because we do not want model's save() method
|
# We use bulk_create here because we do not want model's save() method
|
||||||
# to be called because we do not want our autoupdate signals to be
|
# to be called because we do not want our autoupdate signals to be
|
||||||
# triggered.
|
# triggered.
|
||||||
|
@ -34,79 +32,126 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mediafiles', '0001_initial'),
|
("mediafiles", "0001_initial"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ChatMessage',
|
name="ChatMessage",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('message', models.TextField()),
|
"id",
|
||||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
models.AutoField(
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("message", models.TextField()),
|
||||||
|
("timestamp", models.DateTimeField(auto_now_add=True)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'default_permissions': (),
|
"default_permissions": (),
|
||||||
'permissions': (('can_use_chat', 'Can use the chat'),),
|
"permissions": (("can_use_chat", "Can use the chat"),),
|
||||||
},
|
},
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ConfigStore',
|
name="ConfigStore",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('key', models.CharField(db_index=True, max_length=255, unique=True)),
|
"id",
|
||||||
('value', jsonfield.fields.JSONField()),
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("key", models.CharField(db_index=True, max_length=255, unique=True)),
|
||||||
|
("value", jsonfield.fields.JSONField()),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'default_permissions': (),
|
"default_permissions": (),
|
||||||
'permissions': (('can_manage_config', 'Can manage configuration'),),
|
"permissions": (("can_manage_config", "Can manage configuration"),),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='CustomSlide',
|
name="CustomSlide",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('title', models.CharField(max_length=256)),
|
"id",
|
||||||
('text', models.TextField(blank=True)),
|
models.AutoField(
|
||||||
('weight', models.IntegerField(default=0)),
|
auto_created=True,
|
||||||
('attachments', models.ManyToManyField(blank=True, to='mediafiles.Mediafile')),
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=256)),
|
||||||
|
("text", models.TextField(blank=True)),
|
||||||
|
("weight", models.IntegerField(default=0)),
|
||||||
|
(
|
||||||
|
"attachments",
|
||||||
|
models.ManyToManyField(blank=True, to="mediafiles.Mediafile"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"default_permissions": (), "ordering": ("weight", "title")},
|
||||||
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Projector",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("config", jsonfield.fields.JSONField()),
|
||||||
|
("scale", models.IntegerField(default=0)),
|
||||||
|
("scroll", models.IntegerField(default=0)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'default_permissions': (),
|
"default_permissions": (),
|
||||||
'ordering': ('weight', 'title'),
|
"permissions": (
|
||||||
|
("can_see_projector", "Can see the projector"),
|
||||||
|
("can_manage_projector", "Can manage the projector"),
|
||||||
|
("can_see_frontpage", "Can see the front page"),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Projector',
|
name="Tag",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('config', jsonfield.fields.JSONField()),
|
"id",
|
||||||
('scale', models.IntegerField(default=0)),
|
models.AutoField(
|
||||||
('scroll', models.IntegerField(default=0)),
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=255, unique=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'default_permissions': (),
|
"default_permissions": (),
|
||||||
'permissions': (
|
"permissions": (("can_manage_tags", "Can manage tags"),),
|
||||||
('can_see_projector', 'Can see the projector'),
|
"ordering": ("name",),
|
||||||
('can_manage_projector', 'Can manage the projector'),
|
|
||||||
('can_see_frontpage', 'Can see the front page')),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Tag',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=255, unique=True)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'default_permissions': (),
|
|
||||||
'permissions': (('can_manage_tags', 'Can manage tags'),),
|
|
||||||
'ordering': ('name',),
|
|
||||||
},
|
},
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
|
|
|
@ -15,10 +15,10 @@ def move_custom_slides_to_topics(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
# We get the model from the versioned app registry;
|
# We get the model from the versioned app registry;
|
||||||
# if we directly import it, it will be the wrong version.
|
# if we directly import it, it will be the wrong version.
|
||||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||||
CustomSlide = apps.get_model('core', 'CustomSlide')
|
CustomSlide = apps.get_model("core", "CustomSlide")
|
||||||
Item = apps.get_model('agenda', 'Item')
|
Item = apps.get_model("agenda", "Item")
|
||||||
Topic = apps.get_model('topics', 'Topic')
|
Topic = apps.get_model("topics", "Topic")
|
||||||
|
|
||||||
# Copy data.
|
# Copy data.
|
||||||
content_type_custom_slide = ContentType.objects.get_for_model(CustomSlide)
|
content_type_custom_slide = ContentType.objects.get_for_model(CustomSlide)
|
||||||
|
@ -28,7 +28,9 @@ def move_custom_slides_to_topics(apps, schema_editor):
|
||||||
# no method 'get_agenda_title()'. See agenda/signals.py.
|
# no method 'get_agenda_title()'. See agenda/signals.py.
|
||||||
topic = Topic.objects.create(title=custom_slide.title, text=custom_slide.text)
|
topic = Topic.objects.create(title=custom_slide.title, text=custom_slide.text)
|
||||||
topic.attachments.add(*custom_slide.attachments.all())
|
topic.attachments.add(*custom_slide.attachments.all())
|
||||||
item = Item.objects.get(object_id=custom_slide.pk, content_type=content_type_custom_slide)
|
item = Item.objects.get(
|
||||||
|
object_id=custom_slide.pk, content_type=content_type_custom_slide
|
||||||
|
)
|
||||||
item.object_id = topic.pk
|
item.object_id = topic.pk
|
||||||
item.content_type = content_type_topic
|
item.content_type = content_type_topic
|
||||||
item.save(skip_autoupdate=True)
|
item.save(skip_autoupdate=True)
|
||||||
|
@ -42,20 +44,20 @@ def name_default_projector(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
Set the name of the default projector to 'Defaultprojector'
|
Set the name of the default projector to 'Defaultprojector'
|
||||||
"""
|
"""
|
||||||
Projector = apps.get_model('core', 'Projector')
|
Projector = apps.get_model("core", "Projector")
|
||||||
Projector.objects.filter(pk=1).update(name='Default projector')
|
Projector.objects.filter(pk=1).update(name="Default projector")
|
||||||
|
|
||||||
|
|
||||||
def remove_old_countdowns_messages(apps, schema_editor):
|
def remove_old_countdowns_messages(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
Remove old countdowns and messages created by 2.0 from projector elements which are unusable in 2.1.
|
Remove old countdowns and messages created by 2.0 from projector elements which are unusable in 2.1.
|
||||||
"""
|
"""
|
||||||
Projector = apps.get_model('core', 'Projector')
|
Projector = apps.get_model("core", "Projector")
|
||||||
projector = Projector.objects.get(pk=1)
|
projector = Projector.objects.get(pk=1)
|
||||||
|
|
||||||
projector_config = projector.config
|
projector_config = projector.config
|
||||||
for key, value in list(projector.config.items()):
|
for key, value in list(projector.config.items()):
|
||||||
if value.get('name') in ('core/countdown', 'core/message'):
|
if value.get("name") in ("core/countdown", "core/message"):
|
||||||
del projector_config[key]
|
del projector_config[key]
|
||||||
projector.config = projector_config
|
projector.config = projector_config
|
||||||
projector.save(skip_autoupdate=True)
|
projector.save(skip_autoupdate=True)
|
||||||
|
@ -65,57 +67,74 @@ def add_projection_defaults(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
Adds projectiondefaults for messages and countdowns.
|
Adds projectiondefaults for messages and countdowns.
|
||||||
"""
|
"""
|
||||||
Projector = apps.get_model('core', 'Projector')
|
Projector = apps.get_model("core", "Projector")
|
||||||
ProjectionDefault = apps.get_model('core', 'ProjectionDefault')
|
ProjectionDefault = apps.get_model("core", "ProjectionDefault")
|
||||||
# The default projector (pk=1) is always available.
|
# The default projector (pk=1) is always available.
|
||||||
default_projector = Projector.objects.get(pk=1)
|
default_projector = Projector.objects.get(pk=1)
|
||||||
|
|
||||||
projectiondefaults = []
|
projectiondefaults = []
|
||||||
|
|
||||||
projectiondefaults.append(ProjectionDefault(
|
projectiondefaults.append(
|
||||||
name='agenda_all_items',
|
ProjectionDefault(
|
||||||
display_name='Agenda',
|
name="agenda_all_items", display_name="Agenda", projector=default_projector
|
||||||
projector=default_projector))
|
)
|
||||||
projectiondefaults.append(ProjectionDefault(
|
)
|
||||||
name='topics',
|
projectiondefaults.append(
|
||||||
display_name='Topics',
|
ProjectionDefault(
|
||||||
projector=default_projector))
|
name="topics", display_name="Topics", projector=default_projector
|
||||||
projectiondefaults.append(ProjectionDefault(
|
)
|
||||||
name='agenda_list_of_speakers',
|
)
|
||||||
display_name='List of speakers',
|
projectiondefaults.append(
|
||||||
projector=default_projector))
|
ProjectionDefault(
|
||||||
projectiondefaults.append(ProjectionDefault(
|
name="agenda_list_of_speakers",
|
||||||
name='agenda_current_list_of_speakers',
|
display_name="List of speakers",
|
||||||
display_name='Current list of speakers',
|
projector=default_projector,
|
||||||
projector=default_projector))
|
)
|
||||||
projectiondefaults.append(ProjectionDefault(
|
)
|
||||||
name='motions',
|
projectiondefaults.append(
|
||||||
display_name='Motions',
|
ProjectionDefault(
|
||||||
projector=default_projector))
|
name="agenda_current_list_of_speakers",
|
||||||
projectiondefaults.append(ProjectionDefault(
|
display_name="Current list of speakers",
|
||||||
name='motionBlocks',
|
projector=default_projector,
|
||||||
display_name='Motion blocks',
|
)
|
||||||
projector=default_projector))
|
)
|
||||||
projectiondefaults.append(ProjectionDefault(
|
projectiondefaults.append(
|
||||||
name='assignments',
|
ProjectionDefault(
|
||||||
display_name='Elections',
|
name="motions", display_name="Motions", projector=default_projector
|
||||||
projector=default_projector))
|
)
|
||||||
projectiondefaults.append(ProjectionDefault(
|
)
|
||||||
name='users',
|
projectiondefaults.append(
|
||||||
display_name='Participants',
|
ProjectionDefault(
|
||||||
projector=default_projector))
|
name="motionBlocks",
|
||||||
projectiondefaults.append(ProjectionDefault(
|
display_name="Motion blocks",
|
||||||
name='mediafiles',
|
projector=default_projector,
|
||||||
display_name='Files',
|
)
|
||||||
projector=default_projector))
|
)
|
||||||
projectiondefaults.append(ProjectionDefault(
|
projectiondefaults.append(
|
||||||
name='messages',
|
ProjectionDefault(
|
||||||
display_name='Messages',
|
name="assignments", display_name="Elections", projector=default_projector
|
||||||
projector=default_projector))
|
)
|
||||||
projectiondefaults.append(ProjectionDefault(
|
)
|
||||||
name='countdowns',
|
projectiondefaults.append(
|
||||||
display_name='Countdowns',
|
ProjectionDefault(
|
||||||
projector=default_projector))
|
name="users", display_name="Participants", projector=default_projector
|
||||||
|
)
|
||||||
|
)
|
||||||
|
projectiondefaults.append(
|
||||||
|
ProjectionDefault(
|
||||||
|
name="mediafiles", display_name="Files", projector=default_projector
|
||||||
|
)
|
||||||
|
)
|
||||||
|
projectiondefaults.append(
|
||||||
|
ProjectionDefault(
|
||||||
|
name="messages", display_name="Messages", projector=default_projector
|
||||||
|
)
|
||||||
|
)
|
||||||
|
projectiondefaults.append(
|
||||||
|
ProjectionDefault(
|
||||||
|
name="countdowns", display_name="Countdowns", projector=default_projector
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Create all new projectiondefaults
|
# Create all new projectiondefaults
|
||||||
ProjectionDefault.objects.bulk_create(projectiondefaults)
|
ProjectionDefault.objects.bulk_create(projectiondefaults)
|
||||||
|
@ -125,114 +144,141 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('sessions', '0001_initial'),
|
("sessions", "0001_initial"),
|
||||||
('contenttypes', '0002_remove_content_type_name'),
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
('core', '0001_initial'),
|
("core", "0001_initial"),
|
||||||
('agenda', '0001_initial'), # ('agenda', '0002_item_duration') is not required but would be also ok.
|
(
|
||||||
('topics', '0001_initial'),
|
"agenda",
|
||||||
|
"0001_initial",
|
||||||
|
), # ('agenda', '0002_item_duration') is not required but would be also ok.
|
||||||
|
("topics", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Countdown',
|
name="Countdown",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('description', models.CharField(blank=True, max_length=256)),
|
"id",
|
||||||
('running', models.BooleanField(default=False)),
|
models.AutoField(
|
||||||
('default_time', models.PositiveIntegerField(default=60)),
|
auto_created=True,
|
||||||
('countdown_time', models.FloatField(default=60)),
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("description", models.CharField(blank=True, max_length=256)),
|
||||||
|
("running", models.BooleanField(default=False)),
|
||||||
|
("default_time", models.PositiveIntegerField(default=60)),
|
||||||
|
("countdown_time", models.FloatField(default=60)),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ProjectionDefault',
|
name="ProjectionDefault",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('name', models.CharField(max_length=256)),
|
"id",
|
||||||
('display_name', models.CharField(max_length=256)),
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=256)),
|
||||||
|
("display_name", models.CharField(max_length=256)),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ProjectorMessage',
|
name="ProjectorMessage",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('message', models.TextField(blank=True)),
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("message", models.TextField(blank=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Session',
|
name="Session",
|
||||||
fields=[
|
fields=[
|
||||||
('session_ptr', models.OneToOneField(
|
(
|
||||||
auto_created=True,
|
"session_ptr",
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
models.OneToOneField(
|
||||||
parent_link=True,
|
auto_created=True,
|
||||||
primary_key=True,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
serialize=False,
|
parent_link=True,
|
||||||
to='sessions.Session')),
|
primary_key=True,
|
||||||
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
serialize=False,
|
||||||
|
to="sessions.Session",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
bases=("sessions.session",),
|
||||||
},
|
|
||||||
bases=('sessions.session',),
|
|
||||||
),
|
|
||||||
migrations.RunPython(
|
|
||||||
move_custom_slides_to_topics
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='customslide',
|
|
||||||
name='attachments',
|
|
||||||
),
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='CustomSlide',
|
|
||||||
),
|
),
|
||||||
|
migrations.RunPython(move_custom_slides_to_topics),
|
||||||
|
migrations.RemoveField(model_name="customslide", name="attachments"),
|
||||||
|
migrations.DeleteModel(name="CustomSlide"),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='chatmessage',
|
name="chatmessage",
|
||||||
options={'default_permissions': (), 'permissions': (('can_use_chat', 'Can use the chat'), ('can_manage_chat', 'Can manage the chat'))},
|
options={
|
||||||
|
"default_permissions": (),
|
||||||
|
"permissions": (
|
||||||
|
("can_use_chat", "Can use the chat"),
|
||||||
|
("can_manage_chat", "Can manage the chat"),
|
||||||
|
),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='projector',
|
model_name="projector",
|
||||||
name='blank',
|
name="blank",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='projector',
|
model_name="projector",
|
||||||
name='height',
|
name="height",
|
||||||
field=models.PositiveIntegerField(default=915),
|
field=models.PositiveIntegerField(default=915),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='projector',
|
model_name="projector",
|
||||||
name='name',
|
name="name",
|
||||||
field=models.CharField(blank=True, max_length=255, unique=True),
|
field=models.CharField(blank=True, max_length=255, unique=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='projector',
|
model_name="projector",
|
||||||
name='width',
|
name="width",
|
||||||
field=models.PositiveIntegerField(default=1220),
|
field=models.PositiveIntegerField(default=1220),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='projectiondefault',
|
model_name="projectiondefault",
|
||||||
name='projector',
|
name="projector",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projectiondefaults', to='core.Projector'),
|
field=models.ForeignKey(
|
||||||
),
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
migrations.RunPython(
|
related_name="projectiondefaults",
|
||||||
name_default_projector
|
to="core.Projector",
|
||||||
),
|
),
|
||||||
migrations.RunPython(
|
|
||||||
remove_old_countdowns_messages
|
|
||||||
),
|
|
||||||
migrations.RunPython(
|
|
||||||
add_projection_defaults
|
|
||||||
),
|
),
|
||||||
|
migrations.RunPython(name_default_projector),
|
||||||
|
migrations.RunPython(remove_old_countdowns_messages),
|
||||||
|
migrations.RunPython(add_projection_defaults),
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,22 +11,16 @@ def remove_session_content_type(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
# We get the model from the versioned app registry;
|
# We get the model from the versioned app registry;
|
||||||
# if we directly import it, it will be the wrong version.
|
# if we directly import it, it will be the wrong version.
|
||||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||||
Session = apps.get_model('core', 'Session')
|
Session = apps.get_model("core", "Session")
|
||||||
ContentType.objects.get_for_model(Session).delete()
|
ContentType.objects.get_for_model(Session).delete()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("core", "0002_misc_features")]
|
||||||
('core', '0002_misc_features'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RunPython(
|
migrations.RunPython(remove_session_content_type),
|
||||||
remove_session_content_type
|
migrations.DeleteModel(name="Session"),
|
||||||
),
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='Session',
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -13,25 +13,19 @@ def rename_projector_message_slides(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
# We get the model from the versioned app registry;
|
# We get the model from the versioned app registry;
|
||||||
# if we directly import it, it will be the wrong version.
|
# if we directly import it, it will be the wrong version.
|
||||||
Projector = apps.get_model('core', 'Projector')
|
Projector = apps.get_model("core", "Projector")
|
||||||
for projector in Projector.objects.all():
|
for projector in Projector.objects.all():
|
||||||
new_config = {}
|
new_config = {}
|
||||||
for key, value in projector.config.items():
|
for key, value in projector.config.items():
|
||||||
new_config[key] = value
|
new_config[key] = value
|
||||||
if value['name'] == 'core/projectormessage':
|
if value["name"] == "core/projectormessage":
|
||||||
new_config[key]['name'] = 'core/projector-message'
|
new_config[key]["name"] = "core/projector-message"
|
||||||
projector.config = new_config
|
projector.config = new_config
|
||||||
projector.save(skip_autoupdate=True)
|
projector.save(skip_autoupdate=True)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("core", "0003_auto_20161217_1158")]
|
||||||
('core', '0003_auto_20161217_1158'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [migrations.RunPython(rename_projector_message_slides)]
|
||||||
migrations.RunPython(
|
|
||||||
rename_projector_message_slides
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
|
@ -11,22 +11,26 @@ from openslides.utils.migrations import (
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("core", "0004_auto_20170215_1624")]
|
||||||
('core', '0004_auto_20170215_1624'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='configstore',
|
name="configstore",
|
||||||
options={
|
options={
|
||||||
'default_permissions': (),
|
"default_permissions": (),
|
||||||
'permissions': (
|
"permissions": (
|
||||||
('can_manage_config', 'Can manage configuration'),
|
("can_manage_config", "Can manage configuration"),
|
||||||
('can_manage_logos', 'Can manage logos')
|
("can_manage_logos", "Can manage logos"),
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.RunPython(add_permission_to_groups_based_on_existing_permission(
|
migrations.RunPython(
|
||||||
'can_manage_config', 'configstore', 'core', 'can_manage_logos', 'Can manage logos'
|
add_permission_to_groups_based_on_existing_permission(
|
||||||
)),
|
"can_manage_config",
|
||||||
|
"configstore",
|
||||||
|
"core",
|
||||||
|
"can_manage_logos",
|
||||||
|
"Can manage logos",
|
||||||
|
)
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,19 +7,17 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("core", "0005_auto_20170412_1258")]
|
||||||
('core', '0005_auto_20170412_1258'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='projector',
|
model_name="projector",
|
||||||
name='height',
|
name="height",
|
||||||
field=models.PositiveIntegerField(default=768),
|
field=models.PositiveIntegerField(default=768),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='projector',
|
model_name="projector",
|
||||||
name='width',
|
name="width",
|
||||||
field=models.PositiveIntegerField(default=1024),
|
field=models.PositiveIntegerField(default=1024),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -15,7 +15,7 @@ def delete_old_logo_permission(apps, schema_editor):
|
||||||
If this is an old database, the new permission will be created and the old
|
If this is an old database, the new permission will be created and the old
|
||||||
one deleted. Also it will be assigned to the groups, which had the old permission.
|
one deleted. Also it will be assigned to the groups, which had the old permission.
|
||||||
"""
|
"""
|
||||||
perm = Permission.objects.filter(codename='can_manage_logos')
|
perm = Permission.objects.filter(codename="can_manage_logos")
|
||||||
|
|
||||||
if len(perm):
|
if len(perm):
|
||||||
perm = perm.get()
|
perm = perm.get()
|
||||||
|
@ -30,9 +30,10 @@ def delete_old_logo_permission(apps, schema_editor):
|
||||||
|
|
||||||
# Create new permission
|
# Create new permission
|
||||||
perm = Permission.objects.create(
|
perm = Permission.objects.create(
|
||||||
codename='can_manage_logos_and_fonts',
|
codename="can_manage_logos_and_fonts",
|
||||||
name='Can manage logos and fonts',
|
name="Can manage logos and fonts",
|
||||||
content_type=content_type)
|
content_type=content_type,
|
||||||
|
)
|
||||||
|
|
||||||
for group in groups:
|
for group in groups:
|
||||||
group.permissions.add(perm)
|
group.permissions.add(perm)
|
||||||
|
@ -41,22 +42,18 @@ def delete_old_logo_permission(apps, schema_editor):
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("core", "0006_auto_20180123_0903")]
|
||||||
('core', '0006_auto_20180123_0903'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='configstore',
|
name="configstore",
|
||||||
options={
|
options={
|
||||||
'default_permissions': (),
|
"default_permissions": (),
|
||||||
'permissions': (
|
"permissions": (
|
||||||
('can_manage_config', 'Can manage configuration'),
|
("can_manage_config", "Can manage configuration"),
|
||||||
('can_manage_logos_and_fonts', 'Can manage logos and fonts')
|
("can_manage_logos_and_fonts", "Can manage logos and fonts"),
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.RunPython(
|
migrations.RunPython(delete_old_logo_permission),
|
||||||
delete_old_logo_permission
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,14 +11,14 @@ def logos_available_default_to_database(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
Writes the new default value of the 'logos_available' into the database.
|
Writes the new default value of the 'logos_available' into the database.
|
||||||
"""
|
"""
|
||||||
ConfigStore = apps.get_model('core', 'ConfigStore')
|
ConfigStore = apps.get_model("core", "ConfigStore")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logos_available = ConfigStore.objects.get(key='logos_available')
|
logos_available = ConfigStore.objects.get(key="logos_available")
|
||||||
except ConfigStore.DoesNotExist:
|
except ConfigStore.DoesNotExist:
|
||||||
return # The key is not in the database, nothing to change here
|
return # The key is not in the database, nothing to change here
|
||||||
|
|
||||||
default_value = config.config_variables['logos_available'].default_value
|
default_value = config.config_variables["logos_available"].default_value
|
||||||
logos_available.value = default_value
|
logos_available.value = default_value
|
||||||
logos_available.save()
|
logos_available.save()
|
||||||
|
|
||||||
|
@ -28,16 +28,16 @@ def move_old_logo_settings(apps, schema_editor):
|
||||||
moves the value of 'logo_pdf_header' to 'logo_pdf_header_L' and the same
|
moves the value of 'logo_pdf_header' to 'logo_pdf_header_L' and the same
|
||||||
for the footer. The old ones are deleted.
|
for the footer. The old ones are deleted.
|
||||||
"""
|
"""
|
||||||
ConfigStore = apps.get_model('core', 'ConfigStore')
|
ConfigStore = apps.get_model("core", "ConfigStore")
|
||||||
|
|
||||||
for place in ('header', 'footer'):
|
for place in ("header", "footer"):
|
||||||
try:
|
try:
|
||||||
logo_pdf = ConfigStore.objects.get(key='logo_pdf_{}'.format(place))
|
logo_pdf = ConfigStore.objects.get(key="logo_pdf_{}".format(place))
|
||||||
except ConfigStore.DoesNotExist:
|
except ConfigStore.DoesNotExist:
|
||||||
continue # The old entry is not in the database, nothing to change here
|
continue # The old entry is not in the database, nothing to change here
|
||||||
|
|
||||||
# The key of the new entry
|
# The key of the new entry
|
||||||
new_value_key = 'logo_pdf_{}_L'.format(place)
|
new_value_key = "logo_pdf_{}_L".format(place)
|
||||||
try:
|
try:
|
||||||
logo_pdf_L = ConfigStore.objects.get(key=new_value_key)
|
logo_pdf_L = ConfigStore.objects.get(key=new_value_key)
|
||||||
except ConfigStore.DoesNotExist:
|
except ConfigStore.DoesNotExist:
|
||||||
|
@ -45,7 +45,7 @@ def move_old_logo_settings(apps, schema_editor):
|
||||||
logo_pdf_L.value = {}
|
logo_pdf_L.value = {}
|
||||||
|
|
||||||
# Move the path to the new configentry
|
# Move the path to the new configentry
|
||||||
logo_pdf_L.value['path'] = logo_pdf.value.get('path', '')
|
logo_pdf_L.value["path"] = logo_pdf.value.get("path", "")
|
||||||
# Save the new one, delete the old
|
# Save the new one, delete the old
|
||||||
logo_pdf_L.save()
|
logo_pdf_L.save()
|
||||||
logo_pdf.delete()
|
logo_pdf.delete()
|
||||||
|
@ -53,15 +53,9 @@ def move_old_logo_settings(apps, schema_editor):
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("core", "0007_auto_20180130_1400")]
|
||||||
('core', '0007_auto_20180130_1400'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RunPython(
|
migrations.RunPython(logos_available_default_to_database),
|
||||||
logos_available_default_to_database
|
migrations.RunPython(move_old_logo_settings),
|
||||||
),
|
|
||||||
migrations.RunPython(
|
|
||||||
move_old_logo_settings
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -13,42 +13,68 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('core', '0008_changed_logo_fields'),
|
("core", "0008_changed_logo_fields"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='History',
|
name="History",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('element_id', models.CharField(max_length=255)),
|
"id",
|
||||||
('now', models.DateTimeField(auto_now_add=True)),
|
models.AutoField(
|
||||||
('information', models.CharField(max_length=255)),
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("element_id", models.CharField(max_length=255)),
|
||||||
|
("now", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("information", models.CharField(max_length=255)),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='HistoryData',
|
name="HistoryData",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('full_data', jsonfield.fields.JSONField(
|
"id",
|
||||||
dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={})),
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"full_data",
|
||||||
|
jsonfield.fields.JSONField(
|
||||||
|
dump_kwargs={
|
||||||
|
"cls": jsonfield.encoder.JSONEncoder,
|
||||||
|
"separators": (",", ":"),
|
||||||
|
},
|
||||||
|
load_kwargs={},
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='history',
|
model_name="history",
|
||||||
name='full_data',
|
name="full_data",
|
||||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='core.HistoryData'),
|
field=models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="core.HistoryData"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='history',
|
model_name="history",
|
||||||
name='user',
|
name="user",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -24,13 +24,13 @@ class ProjectorManager(models.Manager):
|
||||||
"""
|
"""
|
||||||
Customized model manager to support our get_full_queryset method.
|
Customized model manager to support our get_full_queryset method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_full_queryset(self):
|
def get_full_queryset(self):
|
||||||
"""
|
"""
|
||||||
Returns the normal queryset with all projectors. In the background
|
Returns the normal queryset with all projectors. In the background
|
||||||
projector defaults are prefetched from the database.
|
projector defaults are prefetched from the database.
|
||||||
"""
|
"""
|
||||||
return self.get_queryset().prefetch_related(
|
return self.get_queryset().prefetch_related("projectiondefaults")
|
||||||
'projectiondefaults')
|
|
||||||
|
|
||||||
|
|
||||||
class Projector(RESTModelMixin, models.Model):
|
class Projector(RESTModelMixin, models.Model):
|
||||||
|
@ -72,6 +72,7 @@ class Projector(RESTModelMixin, models.Model):
|
||||||
The projector can be controlled using the REST API with POST requests
|
The projector can be controlled using the REST API with POST requests
|
||||||
on e. g. the URL /rest/core/projector/1/activate_elements/.
|
on e. g. the URL /rest/core/projector/1/activate_elements/.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ProjectorAccessPermissions()
|
access_permissions = ProjectorAccessPermissions()
|
||||||
|
|
||||||
objects = ProjectorManager()
|
objects = ProjectorManager()
|
||||||
|
@ -86,24 +87,21 @@ class Projector(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
height = models.PositiveIntegerField(default=768)
|
height = models.PositiveIntegerField(default=768)
|
||||||
|
|
||||||
name = models.CharField(
|
name = models.CharField(max_length=255, unique=True, blank=True)
|
||||||
max_length=255,
|
|
||||||
unique=True,
|
|
||||||
blank=True)
|
|
||||||
|
|
||||||
blank = models.BooleanField(
|
blank = models.BooleanField(blank=False, default=False)
|
||||||
blank=False,
|
|
||||||
default=False)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""
|
"""
|
||||||
Contains general permissions that can not be placed in a specific app.
|
Contains general permissions that can not be placed in a specific app.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
permissions = (
|
permissions = (
|
||||||
('can_see_projector', 'Can see the projector'),
|
("can_see_projector", "Can see the projector"),
|
||||||
('can_manage_projector', 'Can manage the projector'),
|
("can_manage_projector", "Can manage the projector"),
|
||||||
('can_see_frontpage', 'Can see the front page'),)
|
("can_see_frontpage", "Can see the front page"),
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def elements(self):
|
def elements(self):
|
||||||
|
@ -120,17 +118,19 @@ class Projector(RESTModelMixin, models.Model):
|
||||||
for key, value in self.config.items():
|
for key, value in self.config.items():
|
||||||
# Use a copy here not to change the origin value in the config field.
|
# Use a copy here not to change the origin value in the config field.
|
||||||
result[key] = value.copy()
|
result[key] = value.copy()
|
||||||
result[key]['uuid'] = key
|
result[key]["uuid"] = key
|
||||||
element = elements.get(value['name'])
|
element = elements.get(value["name"])
|
||||||
if element is None:
|
if element is None:
|
||||||
result[key]['error'] = 'Projector element does not exist.'
|
result[key]["error"] = "Projector element does not exist."
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
result[key].update(element.check_and_update_data(
|
result[key].update(
|
||||||
projector_object=self,
|
element.check_and_update_data(
|
||||||
config_entry=value))
|
projector_object=self, config_entry=value
|
||||||
|
)
|
||||||
|
)
|
||||||
except ProjectorException as e:
|
except ProjectorException as e:
|
||||||
result[key]['error'] = str(e)
|
result[key]["error"] = str(e)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -173,14 +173,14 @@ class ProjectionDefault(RESTModelMixin, models.Model):
|
||||||
special name like 'list_of_speakers'. The display_name is the shown
|
special name like 'list_of_speakers'. The display_name is the shown
|
||||||
name on the front end for the user.
|
name on the front end for the user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = models.CharField(max_length=256)
|
name = models.CharField(max_length=256)
|
||||||
|
|
||||||
display_name = models.CharField(max_length=256)
|
display_name = models.CharField(max_length=256)
|
||||||
|
|
||||||
projector = models.ForeignKey(
|
projector = models.ForeignKey(
|
||||||
Projector,
|
Projector, on_delete=models.CASCADE, related_name="projectiondefaults"
|
||||||
on_delete=models.CASCADE,
|
)
|
||||||
related_name='projectiondefaults')
|
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
def get_root_rest_element(self):
|
||||||
return self.projector
|
return self.projector
|
||||||
|
@ -197,17 +197,15 @@ class Tag(RESTModelMixin, models.Model):
|
||||||
Model for tags. This tags can be used for other models like agenda items,
|
Model for tags. This tags can be used for other models like agenda items,
|
||||||
motions or assignments.
|
motions or assignments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = TagAccessPermissions()
|
access_permissions = TagAccessPermissions()
|
||||||
|
|
||||||
name = models.CharField(
|
name = models.CharField(max_length=255, unique=True)
|
||||||
max_length=255,
|
|
||||||
unique=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('name',)
|
ordering = ("name",)
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
permissions = (
|
permissions = (("can_manage_tags", "Can manage tags"),)
|
||||||
('can_manage_tags', 'Can manage tags'),)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -217,6 +215,7 @@ class ConfigStore(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
A model class to store all config variables in the database.
|
A model class to store all config variables in the database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ConfigAccessPermissions()
|
access_permissions = ConfigAccessPermissions()
|
||||||
|
|
||||||
key = models.CharField(max_length=255, unique=True, db_index=True)
|
key = models.CharField(max_length=255, unique=True, db_index=True)
|
||||||
|
@ -228,12 +227,13 @@ class ConfigStore(RESTModelMixin, models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
permissions = (
|
permissions = (
|
||||||
('can_manage_config', 'Can manage configuration'),
|
("can_manage_config", "Can manage configuration"),
|
||||||
('can_manage_logos_and_fonts', 'Can manage logos and fonts'))
|
("can_manage_logos_and_fonts", "Can manage logos and fonts"),
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_collection_string(cls):
|
def get_collection_string(cls):
|
||||||
return 'core/config'
|
return "core/config"
|
||||||
|
|
||||||
|
|
||||||
class ChatMessage(RESTModelMixin, models.Model):
|
class ChatMessage(RESTModelMixin, models.Model):
|
||||||
|
@ -242,31 +242,32 @@ class ChatMessage(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
At the moment we only have one global chat room for managers.
|
At the moment we only have one global chat room for managers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ChatMessageAccessPermissions()
|
access_permissions = ChatMessageAccessPermissions()
|
||||||
can_see_permission = 'core.can_use_chat'
|
can_see_permission = "core.can_use_chat"
|
||||||
|
|
||||||
message = models.TextField()
|
message = models.TextField()
|
||||||
|
|
||||||
timestamp = models.DateTimeField(auto_now_add=True)
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
permissions = (
|
permissions = (
|
||||||
('can_use_chat', 'Can use the chat'),
|
("can_use_chat", "Can use the chat"),
|
||||||
('can_manage_chat', 'Can manage the chat'),)
|
("can_manage_chat", "Can manage the chat"),
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'Message {}'.format(self.timestamp)
|
return "Message {}".format(self.timestamp)
|
||||||
|
|
||||||
|
|
||||||
class ProjectorMessage(RESTModelMixin, models.Model):
|
class ProjectorMessage(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Model for ProjectorMessages.
|
Model for ProjectorMessages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ProjectorMessageAccessPermissions()
|
access_permissions = ProjectorMessageAccessPermissions()
|
||||||
|
|
||||||
message = models.TextField(blank=True)
|
message = models.TextField(blank=True)
|
||||||
|
@ -280,16 +281,18 @@ class ProjectorMessage(RESTModelMixin, models.Model):
|
||||||
projector message projector element is disabled.
|
projector message projector element is disabled.
|
||||||
"""
|
"""
|
||||||
Projector.remove_any(
|
Projector.remove_any(
|
||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate, name="core/projector-message", id=self.pk
|
||||||
name='core/projector-message',
|
)
|
||||||
id=self.pk)
|
return super().delete( # type: ignore
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Countdown(RESTModelMixin, models.Model):
|
class Countdown(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Model for countdowns.
|
Model for countdowns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = CountdownAccessPermissions()
|
access_permissions = CountdownAccessPermissions()
|
||||||
|
|
||||||
description = models.CharField(max_length=256, blank=True)
|
description = models.CharField(max_length=256, blank=True)
|
||||||
|
@ -309,19 +312,22 @@ class Countdown(RESTModelMixin, models.Model):
|
||||||
countdown projector element is disabled.
|
countdown projector element is disabled.
|
||||||
"""
|
"""
|
||||||
Projector.remove_any(
|
Projector.remove_any(
|
||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate, name="core/countdown", id=self.pk
|
||||||
name='core/countdown',
|
)
|
||||||
id=self.pk)
|
return super().delete( # type: ignore
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
def control(self, action, skip_autoupdate=False):
|
def control(self, action, skip_autoupdate=False):
|
||||||
if action not in ('start', 'stop', 'reset'):
|
if action not in ("start", "stop", "reset"):
|
||||||
raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action))
|
raise ValueError(
|
||||||
|
"Action must be 'start', 'stop' or 'reset', not {}.".format(action)
|
||||||
|
)
|
||||||
|
|
||||||
if action == 'start':
|
if action == "start":
|
||||||
self.running = True
|
self.running = True
|
||||||
self.countdown_time = now().timestamp() + self.default_time
|
self.countdown_time = now().timestamp() + self.default_time
|
||||||
elif action == 'stop' and self.running:
|
elif action == "stop" and self.running:
|
||||||
self.running = False
|
self.running = False
|
||||||
self.countdown_time = self.countdown_time - now().timestamp()
|
self.countdown_time = self.countdown_time - now().timestamp()
|
||||||
else: # reset
|
else: # reset
|
||||||
|
@ -337,6 +343,7 @@ class HistoryData(models.Model):
|
||||||
This is not a RESTModel. It is not cachable and can only be reached by a
|
This is not a RESTModel. It is not cachable and can only be reached by a
|
||||||
special viewset.
|
special viewset.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
full_data = JSONField()
|
full_data = JSONField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -347,6 +354,7 @@ class HistoryManager(models.Manager):
|
||||||
"""
|
"""
|
||||||
Customized model manager for the history model.
|
Customized model manager for the history model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def add_elements(self, elements):
|
def add_elements(self, elements):
|
||||||
"""
|
"""
|
||||||
Method to add elements to the history. This does not trigger autoupdate.
|
Method to add elements to the history. This does not trigger autoupdate.
|
||||||
|
@ -354,18 +362,26 @@ class HistoryManager(models.Manager):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
instances = []
|
instances = []
|
||||||
for element in elements:
|
for element in elements:
|
||||||
if element['disable_history'] or element['collection_string'] == self.model.get_collection_string():
|
if (
|
||||||
|
element["disable_history"]
|
||||||
|
or element["collection_string"]
|
||||||
|
== self.model.get_collection_string()
|
||||||
|
):
|
||||||
# Do not update history for history elements itself or if history is disabled.
|
# Do not update history for history elements itself or if history is disabled.
|
||||||
continue
|
continue
|
||||||
# HistoryData is not a root rest element so there is no autoupdate and not history saving here.
|
# HistoryData is not a root rest element so there is no autoupdate and not history saving here.
|
||||||
data = HistoryData.objects.create(full_data=element['full_data'])
|
data = HistoryData.objects.create(full_data=element["full_data"])
|
||||||
instance = self.model(
|
instance = self.model(
|
||||||
element_id=get_element_id(element['collection_string'], element['id']),
|
element_id=get_element_id(
|
||||||
information=element['information'],
|
element["collection_string"], element["id"]
|
||||||
user_id=element['user_id'],
|
),
|
||||||
|
information=element["information"],
|
||||||
|
user_id=element["user_id"],
|
||||||
full_data=data,
|
full_data=data,
|
||||||
)
|
)
|
||||||
instance.save(skip_autoupdate=True) # Skip autoupdate and of course history saving.
|
instance.save(
|
||||||
|
skip_autoupdate=True
|
||||||
|
) # Skip autoupdate and of course history saving.
|
||||||
instances.append(instance)
|
instances.append(instance)
|
||||||
return instances
|
return instances
|
||||||
|
|
||||||
|
@ -380,14 +396,16 @@ class HistoryManager(models.Manager):
|
||||||
all_full_data = async_to_sync(element_cache.get_all_full_data)()
|
all_full_data = async_to_sync(element_cache.get_all_full_data)()
|
||||||
for collection_string, data in all_full_data.items():
|
for collection_string, data in all_full_data.items():
|
||||||
for full_data in data:
|
for full_data in data:
|
||||||
elements.append(Element(
|
elements.append(
|
||||||
id=full_data['id'],
|
Element(
|
||||||
collection_string=collection_string,
|
id=full_data["id"],
|
||||||
full_data=full_data,
|
collection_string=collection_string,
|
||||||
information='',
|
full_data=full_data,
|
||||||
user_id=None,
|
information="",
|
||||||
disable_history=False,
|
user_id=None,
|
||||||
))
|
disable_history=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
instances = self.add_elements(elements)
|
instances = self.add_elements(elements)
|
||||||
return instances
|
return instances
|
||||||
|
|
||||||
|
@ -399,29 +417,22 @@ class History(RESTModelMixin, models.Model):
|
||||||
This model itself is not part of the history. This means that if you
|
This model itself is not part of the history. This means that if you
|
||||||
delete a user you may lose the information of the user field here.
|
delete a user you may lose the information of the user field here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = HistoryAccessPermissions()
|
access_permissions = HistoryAccessPermissions()
|
||||||
|
|
||||||
objects = HistoryManager()
|
objects = HistoryManager()
|
||||||
|
|
||||||
element_id = models.CharField(
|
element_id = models.CharField(max_length=255)
|
||||||
max_length=255,
|
|
||||||
)
|
|
||||||
|
|
||||||
now = models.DateTimeField(auto_now_add=True)
|
now = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
information = models.CharField(
|
information = models.CharField(max_length=255)
|
||||||
max_length=255,
|
|
||||||
)
|
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
|
||||||
null=True,
|
|
||||||
on_delete=models.SET_NULL)
|
|
||||||
|
|
||||||
full_data = models.OneToOneField(
|
|
||||||
HistoryData,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
full_data = models.OneToOneField(HistoryData, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
|
|
@ -9,29 +9,32 @@ class Clock(ProjectorElement):
|
||||||
"""
|
"""
|
||||||
Clock on the projector.
|
Clock on the projector.
|
||||||
"""
|
"""
|
||||||
name = 'core/clock'
|
|
||||||
|
name = "core/clock"
|
||||||
|
|
||||||
|
|
||||||
class CountdownElement(ProjectorElement):
|
class CountdownElement(ProjectorElement):
|
||||||
"""
|
"""
|
||||||
Countdown slide for the projector.
|
Countdown slide for the projector.
|
||||||
"""
|
"""
|
||||||
name = 'core/countdown'
|
|
||||||
|
name = "core/countdown"
|
||||||
|
|
||||||
def check_data(self):
|
def check_data(self):
|
||||||
if not Countdown.objects.filter(pk=self.config_entry.get('id')).exists():
|
if not Countdown.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||||
raise ProjectorException('Countdown does not exists.')
|
raise ProjectorException("Countdown does not exists.")
|
||||||
|
|
||||||
|
|
||||||
class ProjectorMessageElement(ProjectorElement):
|
class ProjectorMessageElement(ProjectorElement):
|
||||||
"""
|
"""
|
||||||
Short message on the projector. Rendered as overlay.
|
Short message on the projector. Rendered as overlay.
|
||||||
"""
|
"""
|
||||||
name = 'core/projector-message'
|
|
||||||
|
name = "core/projector-message"
|
||||||
|
|
||||||
def check_data(self):
|
def check_data(self):
|
||||||
if not ProjectorMessage.objects.filter(pk=self.config_entry.get('id')).exists():
|
if not ProjectorMessage.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||||
raise ProjectorException('Message does not exists.')
|
raise ProjectorException("Message does not exists.")
|
||||||
|
|
||||||
|
|
||||||
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
||||||
|
|
|
@ -17,18 +17,21 @@ class JSONSerializerField(Field):
|
||||||
"""
|
"""
|
||||||
Serializer for projector's and config JSONField.
|
Serializer for projector's and config JSONField.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
"""
|
"""
|
||||||
Checks that data is a dictionary. The key is a hex UUID and the
|
Checks that data is a dictionary. The key is a hex UUID and the
|
||||||
value is a dictionary with must have a key 'name'.
|
value is a dictionary with must have a key 'name'.
|
||||||
"""
|
"""
|
||||||
if type(data) is not dict:
|
if type(data) is not dict:
|
||||||
raise ValidationError({'detail': 'Data must be a dictionary.'})
|
raise ValidationError({"detail": "Data must be a dictionary."})
|
||||||
for element in data.values():
|
for element in data.values():
|
||||||
if type(element) is not dict:
|
if type(element) is not dict:
|
||||||
raise ValidationError({'detail': 'Data must be a dictionary.'})
|
raise ValidationError({"detail": "Data must be a dictionary."})
|
||||||
elif element.get('name') is None:
|
elif element.get("name") is None:
|
||||||
raise ValidationError({'detail': "Every dictionary must have a key 'name'."})
|
raise ValidationError(
|
||||||
|
{"detail": "Every dictionary must have a key 'name'."}
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
|
@ -42,65 +45,82 @@ class ProjectionDefaultSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for core.models.ProjectionDefault objects.
|
Serializer for core.models.ProjectionDefault objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProjectionDefault
|
model = ProjectionDefault
|
||||||
fields = ('id', 'name', 'display_name', 'projector', )
|
fields = ("id", "name", "display_name", "projector")
|
||||||
|
|
||||||
|
|
||||||
class ProjectorSerializer(ModelSerializer):
|
class ProjectorSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for core.models.Projector objects.
|
Serializer for core.models.Projector objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config = JSONSerializerField(write_only=True)
|
config = JSONSerializerField(write_only=True)
|
||||||
projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True)
|
projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Projector
|
model = Projector
|
||||||
fields = ('id', 'config', 'elements', 'scale', 'scroll', 'name', 'blank', 'width', 'height', 'projectiondefaults', )
|
fields = (
|
||||||
read_only_fields = ('scale', 'scroll', 'blank', 'width', 'height', )
|
"id",
|
||||||
|
"config",
|
||||||
|
"elements",
|
||||||
|
"scale",
|
||||||
|
"scroll",
|
||||||
|
"name",
|
||||||
|
"blank",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"projectiondefaults",
|
||||||
|
)
|
||||||
|
read_only_fields = ("scale", "scroll", "blank", "width", "height")
|
||||||
|
|
||||||
|
|
||||||
class TagSerializer(ModelSerializer):
|
class TagSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for core.models.Tag objects.
|
Serializer for core.models.Tag objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
fields = ('id', 'name', )
|
fields = ("id", "name")
|
||||||
|
|
||||||
|
|
||||||
class ConfigSerializer(ModelSerializer):
|
class ConfigSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for core.models.Tag objects.
|
Serializer for core.models.Tag objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
value = JSONSerializerField()
|
value = JSONSerializerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConfigStore
|
model = ConfigStore
|
||||||
fields = ('id', 'key', 'value')
|
fields = ("id", "key", "value")
|
||||||
|
|
||||||
|
|
||||||
class ChatMessageSerializer(ModelSerializer):
|
class ChatMessageSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for core.models.ChatMessage objects.
|
Serializer for core.models.ChatMessage objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ChatMessage
|
model = ChatMessage
|
||||||
fields = ('id', 'message', 'timestamp', 'user', )
|
fields = ("id", "message", "timestamp", "user")
|
||||||
read_only_fields = ('user', )
|
read_only_fields = ("user",)
|
||||||
|
|
||||||
|
|
||||||
class ProjectorMessageSerializer(ModelSerializer):
|
class ProjectorMessageSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for core.models.ProjectorMessage objects.
|
Serializer for core.models.ProjectorMessage objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProjectorMessage
|
model = ProjectorMessage
|
||||||
fields = ('id', 'message', )
|
fields = ("id", "message")
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if 'message' in data:
|
if "message" in data:
|
||||||
data['message'] = validate_html(data['message'])
|
data["message"] = validate_html(data["message"])
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,9 +128,10 @@ class CountdownSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for core.models.Countdown objects.
|
Serializer for core.models.Countdown objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Countdown
|
model = Countdown
|
||||||
fields = ('id', 'description', 'default_time', 'countdown_time', 'running', )
|
fields = ("id", "description", "default_time", "countdown_time", "running")
|
||||||
|
|
||||||
|
|
||||||
class HistorySerializer(ModelSerializer):
|
class HistorySerializer(ModelSerializer):
|
||||||
|
@ -119,6 +140,7 @@ class HistorySerializer(ModelSerializer):
|
||||||
|
|
||||||
Does not contain full data of history object.
|
Does not contain full data of history object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = History
|
model = History
|
||||||
fields = ('id', 'element_id', 'now', 'information', 'user', )
|
fields = ("id", "element_id", "now", "information", "user")
|
||||||
|
|
|
@ -21,9 +21,8 @@ def delete_django_app_permissions(sender, **kwargs):
|
||||||
for auth, contenttypes and sessions.
|
for auth, contenttypes and sessions.
|
||||||
"""
|
"""
|
||||||
contenttypes = ContentType.objects.filter(
|
contenttypes = ContentType.objects.filter(
|
||||||
Q(app_label='auth') |
|
Q(app_label="auth") | Q(app_label="contenttypes") | Q(app_label="sessions")
|
||||||
Q(app_label='contenttypes') |
|
)
|
||||||
Q(app_label='sessions'))
|
|
||||||
Permission.objects.filter(content_type__in=contenttypes).delete()
|
Permission.objects.filter(content_type__in=contenttypes).delete()
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,13 +30,13 @@ def get_permission_change_data(sender, permissions, **kwargs):
|
||||||
"""
|
"""
|
||||||
Yields all necessary Cachables if the respective permissions change.
|
Yields all necessary Cachables if the respective permissions change.
|
||||||
"""
|
"""
|
||||||
core_app = apps.get_app_config(app_label='core')
|
core_app = apps.get_app_config(app_label="core")
|
||||||
for permission in permissions:
|
for permission in permissions:
|
||||||
if permission.content_type.app_label == core_app.label:
|
if permission.content_type.app_label == core_app.label:
|
||||||
if permission.codename == 'can_see_projector':
|
if permission.codename == "can_see_projector":
|
||||||
yield core_app.get_model('Projector')
|
yield core_app.get_model("Projector")
|
||||||
elif permission.codename == 'can_manage_projector':
|
elif permission.codename == "can_manage_projector":
|
||||||
yield core_app.get_model('ProjectorMessage')
|
yield core_app.get_model("ProjectorMessage")
|
||||||
yield core_app.get_model('Countdown')
|
yield core_app.get_model("Countdown")
|
||||||
elif permission.codename == 'can_use_chat':
|
elif permission.codename == "can_use_chat":
|
||||||
yield core_app.get_model('ChatMessage')
|
yield core_app.get_model("ChatMessage")
|
||||||
|
|
|
@ -4,15 +4,7 @@ from . import views
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^servertime/$',
|
url(r"^servertime/$", views.ServerTime.as_view(), name="core_servertime"),
|
||||||
views.ServerTime.as_view(),
|
url(r"^version/$", views.VersionView.as_view(), name="core_version"),
|
||||||
name='core_servertime'),
|
url(r"^history/$", views.HistoryView.as_view(), name="core_history"),
|
||||||
|
|
||||||
url(r'^version/$',
|
|
||||||
views.VersionView.as_view(),
|
|
||||||
name='core_version'),
|
|
||||||
|
|
||||||
url(r'^history/$',
|
|
||||||
views.HistoryView.as_view(),
|
|
||||||
name='core_history'),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -17,12 +17,7 @@ from mypy_extensions import TypedDict
|
||||||
from .. import __license__ as license, __url__ as url, __version__ as version
|
from .. import __license__ as license, __url__ as url, __version__ as version
|
||||||
from ..utils import views as utils_views
|
from ..utils import views as utils_views
|
||||||
from ..utils.arguments import arguments
|
from ..utils.arguments import arguments
|
||||||
from ..utils.auth import (
|
from ..utils.auth import GROUP_ADMIN_PK, anonymous_is_enabled, has_perm, in_some_groups
|
||||||
GROUP_ADMIN_PK,
|
|
||||||
anonymous_is_enabled,
|
|
||||||
has_perm,
|
|
||||||
in_some_groups,
|
|
||||||
)
|
|
||||||
from ..utils.autoupdate import inform_changed_data, inform_deleted_data
|
from ..utils.autoupdate import inform_changed_data, inform_deleted_data
|
||||||
from ..utils.plugins import (
|
from ..utils.plugins import (
|
||||||
get_plugin_description,
|
get_plugin_description,
|
||||||
|
@ -67,6 +62,7 @@ from .models import (
|
||||||
|
|
||||||
# Special Django views
|
# Special Django views
|
||||||
|
|
||||||
|
|
||||||
class IndexView(View):
|
class IndexView(View):
|
||||||
"""
|
"""
|
||||||
The primary view for the OpenSlides client. Serves static files. If a file
|
The primary view for the OpenSlides client. Serves static files. If a file
|
||||||
|
@ -83,11 +79,11 @@ class IndexView(View):
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
no_caching = arguments.get('no_template_caching', False)
|
no_caching = arguments.get("no_template_caching", False)
|
||||||
if 'index' not in self.cache or no_caching:
|
if "index" not in self.cache or no_caching:
|
||||||
self.cache['index'] = finders.find('index.html')
|
self.cache["index"] = finders.find("index.html")
|
||||||
|
|
||||||
self.index_document_root, self.index_path = os.path.split(self.cache['index'])
|
self.index_document_root, self.index_path = os.path.split(self.cache["index"])
|
||||||
|
|
||||||
def get(self, request, path, **kwargs) -> HttpResponse:
|
def get(self, request, path, **kwargs) -> HttpResponse:
|
||||||
"""
|
"""
|
||||||
|
@ -97,18 +93,25 @@ class IndexView(View):
|
||||||
try:
|
try:
|
||||||
response = serve(request, path, **kwargs)
|
response = serve(request, path, **kwargs)
|
||||||
except Http404:
|
except Http404:
|
||||||
response = static.serve(request, self.index_path, document_root=self.index_document_root, **kwargs)
|
response = static.serve(
|
||||||
|
request,
|
||||||
|
self.index_path,
|
||||||
|
document_root=self.index_document_root,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
# Viewsets for the REST API
|
# Viewsets for the REST API
|
||||||
|
|
||||||
|
|
||||||
class ProjectorViewSet(ModelViewSet):
|
class ProjectorViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint for the projector slide info.
|
API endpoint for the projector slide info.
|
||||||
|
|
||||||
There are the following views: See strings in check_view_permissions().
|
There are the following views: See strings in check_view_permissions().
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ProjectorAccessPermissions()
|
access_permissions = ProjectorAccessPermissions()
|
||||||
queryset = Projector.objects.all()
|
queryset = Projector.objects.all()
|
||||||
|
|
||||||
|
@ -116,18 +119,31 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ('list', 'retrieve'):
|
if self.action in ("list", "retrieve"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action == 'metadata':
|
elif self.action == "metadata":
|
||||||
result = has_perm(self.request.user, 'core.can_see_projector')
|
result = has_perm(self.request.user, "core.can_see_projector")
|
||||||
elif self.action in (
|
elif self.action in (
|
||||||
'create', 'update', 'partial_update', 'destroy',
|
"create",
|
||||||
'activate_elements', 'prune_elements', 'update_elements', 'deactivate_elements', 'clear_elements',
|
"update",
|
||||||
'project', 'control_view', 'set_resolution', 'set_scroll', 'control_blank',
|
"partial_update",
|
||||||
'broadcast', 'set_projectiondefault',
|
"destroy",
|
||||||
|
"activate_elements",
|
||||||
|
"prune_elements",
|
||||||
|
"update_elements",
|
||||||
|
"deactivate_elements",
|
||||||
|
"clear_elements",
|
||||||
|
"project",
|
||||||
|
"control_view",
|
||||||
|
"set_resolution",
|
||||||
|
"set_scroll",
|
||||||
|
"control_blank",
|
||||||
|
"broadcast",
|
||||||
|
"set_projectiondefault",
|
||||||
):
|
):
|
||||||
result = (has_perm(self.request.user, 'core.can_see_projector') and
|
result = has_perm(self.request.user, "core.can_see_projector") and has_perm(
|
||||||
has_perm(self.request.user, 'core.can_manage_projector'))
|
self.request.user, "core.can_manage_projector"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
@ -144,11 +160,11 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
if projection_default.projector.id == projector_instance.id:
|
if projection_default.projector.id == projector_instance.id:
|
||||||
projection_default.projector_id = 1
|
projection_default.projector_id = 1
|
||||||
projection_default.save()
|
projection_default.save()
|
||||||
if config['projector_broadcast'] == projector_instance.pk:
|
if config["projector_broadcast"] == projector_instance.pk:
|
||||||
config['projector_broadcast'] = 0
|
config["projector_broadcast"] = 0
|
||||||
return super(ProjectorViewSet, self).destroy(*args, **kwargs)
|
return super(ProjectorViewSet, self).destroy(*args, **kwargs)
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=["post"])
|
||||||
def activate_elements(self, request, pk):
|
def activate_elements(self, request, pk):
|
||||||
"""
|
"""
|
||||||
REST API operation to activate projector elements. It expects a POST
|
REST API operation to activate projector elements. It expects a POST
|
||||||
|
@ -156,21 +172,25 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
of dictionaries to be appended to the projector config entry.
|
of dictionaries to be appended to the projector config entry.
|
||||||
"""
|
"""
|
||||||
if not isinstance(request.data, list):
|
if not isinstance(request.data, list):
|
||||||
raise ValidationError({'detail': 'Data must be a list.'})
|
raise ValidationError({"detail": "Data must be a list."})
|
||||||
|
|
||||||
projector_instance = self.get_object()
|
projector_instance = self.get_object()
|
||||||
projector_config = projector_instance.config
|
projector_config = projector_instance.config
|
||||||
for element in request.data:
|
for element in request.data:
|
||||||
if element.get('name') is None:
|
if element.get("name") is None:
|
||||||
raise ValidationError({'detail': 'Invalid projector element. Name is missing.'})
|
raise ValidationError(
|
||||||
|
{"detail": "Invalid projector element. Name is missing."}
|
||||||
|
)
|
||||||
projector_config[uuid.uuid4().hex] = element
|
projector_config[uuid.uuid4().hex] = element
|
||||||
|
|
||||||
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
serializer = self.get_serializer(
|
||||||
|
projector_instance, data={"config": projector_config}, partial=False
|
||||||
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=["post"])
|
||||||
def prune_elements(self, request, pk):
|
def prune_elements(self, request, pk):
|
||||||
"""
|
"""
|
||||||
REST API operation to activate projector elements. It expects a POST
|
REST API operation to activate projector elements. It expects a POST
|
||||||
|
@ -179,17 +199,19 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
entries are deleted but not entries with stable == True.
|
entries are deleted but not entries with stable == True.
|
||||||
"""
|
"""
|
||||||
if not isinstance(request.data, list):
|
if not isinstance(request.data, list):
|
||||||
raise ValidationError({'detail': 'Data must be a list.'})
|
raise ValidationError({"detail": "Data must be a list."})
|
||||||
|
|
||||||
projector = self.get_object()
|
projector = self.get_object()
|
||||||
elements = request.data
|
elements = request.data
|
||||||
if not isinstance(elements, list):
|
if not isinstance(elements, list):
|
||||||
raise ValidationError({'detail': _('The data has to be a list.')})
|
raise ValidationError({"detail": _("The data has to be a list.")})
|
||||||
for element in elements:
|
for element in elements:
|
||||||
if not isinstance(element, dict):
|
if not isinstance(element, dict):
|
||||||
raise ValidationError({'detail': _('All elements have to be dicts.')})
|
raise ValidationError({"detail": _("All elements have to be dicts.")})
|
||||||
if element.get('name') is None:
|
if element.get("name") is None:
|
||||||
raise ValidationError({'detail': 'Invalid projector element. Name is missing.'})
|
raise ValidationError(
|
||||||
|
{"detail": "Invalid projector element. Name is missing."}
|
||||||
|
)
|
||||||
return Response(self.prune(projector, elements))
|
return Response(self.prune(projector, elements))
|
||||||
|
|
||||||
def prune(self, projector, elements):
|
def prune(self, projector, elements):
|
||||||
|
@ -201,21 +223,23 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
projector_config = {}
|
projector_config = {}
|
||||||
for key, value in projector.config.items():
|
for key, value in projector.config.items():
|
||||||
if value.get('stable'):
|
if value.get("stable"):
|
||||||
projector_config[key] = value
|
projector_config[key] = value
|
||||||
for element in elements:
|
for element in elements:
|
||||||
projector_config[uuid.uuid4().hex] = element
|
projector_config[uuid.uuid4().hex] = element
|
||||||
|
|
||||||
serializer = self.get_serializer(projector, data={'config': projector_config}, partial=False)
|
serializer = self.get_serializer(
|
||||||
|
projector, data={"config": projector_config}, partial=False
|
||||||
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
# reset scroll level
|
# reset scroll level
|
||||||
if (projector.scroll != 0):
|
if projector.scroll != 0:
|
||||||
projector.scroll = 0
|
projector.scroll = 0
|
||||||
projector.save()
|
projector.save()
|
||||||
return serializer.data
|
return serializer.data
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=["post"])
|
||||||
def update_elements(self, request, pk):
|
def update_elements(self, request, pk):
|
||||||
"""
|
"""
|
||||||
REST API operation to update projector elements. It expects a POST
|
REST API operation to update projector elements. It expects a POST
|
||||||
|
@ -237,8 +261,10 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
if not isinstance(request.data, dict):
|
if not isinstance(request.data, dict):
|
||||||
raise ValidationError({'detail': 'Data must be a dictionary.'})
|
raise ValidationError({"detail": "Data must be a dictionary."})
|
||||||
error = {'detail': 'Data must be a dictionary with UUIDs as keys and dictionaries as values.'}
|
error = {
|
||||||
|
"detail": "Data must be a dictionary with UUIDs as keys and dictionaries as values."
|
||||||
|
}
|
||||||
for key, value in request.data.items():
|
for key, value in request.data.items():
|
||||||
try:
|
try:
|
||||||
uuid.UUID(hex=str(key))
|
uuid.UUID(hex=str(key))
|
||||||
|
@ -251,15 +277,19 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
projector_config = projector_instance.config
|
projector_config = projector_instance.config
|
||||||
for key, value in request.data.items():
|
for key, value in request.data.items():
|
||||||
if key not in projector_config:
|
if key not in projector_config:
|
||||||
raise ValidationError({'detail': 'Invalid projector element. Wrong UUID.'})
|
raise ValidationError(
|
||||||
|
{"detail": "Invalid projector element. Wrong UUID."}
|
||||||
|
)
|
||||||
projector_config[key].update(request.data[key])
|
projector_config[key].update(request.data[key])
|
||||||
|
|
||||||
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
serializer = self.get_serializer(
|
||||||
|
projector_instance, data={"config": projector_config}, partial=False
|
||||||
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=["post"])
|
||||||
def deactivate_elements(self, request, pk):
|
def deactivate_elements(self, request, pk):
|
||||||
"""
|
"""
|
||||||
REST API operation to deactivate projector elements. It expects a
|
REST API operation to deactivate projector elements. It expects a
|
||||||
|
@ -268,12 +298,12 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
that should be deleted.
|
that should be deleted.
|
||||||
"""
|
"""
|
||||||
if not isinstance(request.data, list):
|
if not isinstance(request.data, list):
|
||||||
raise ValidationError({'detail': 'Data must be a list of hex UUIDs.'})
|
raise ValidationError({"detail": "Data must be a list of hex UUIDs."})
|
||||||
for item in request.data:
|
for item in request.data:
|
||||||
try:
|
try:
|
||||||
uuid.UUID(hex=str(item))
|
uuid.UUID(hex=str(item))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValidationError({'detail': 'Data must be a list of hex UUIDs.'})
|
raise ValidationError({"detail": "Data must be a list of hex UUIDs."})
|
||||||
|
|
||||||
projector_instance = self.get_object()
|
projector_instance = self.get_object()
|
||||||
projector_config = projector_instance.config
|
projector_config = projector_instance.config
|
||||||
|
@ -281,14 +311,16 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
try:
|
try:
|
||||||
del projector_config[key]
|
del projector_config[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValidationError({'detail': 'Invalid UUID.'})
|
raise ValidationError({"detail": "Invalid UUID."})
|
||||||
|
|
||||||
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
serializer = self.get_serializer(
|
||||||
|
projector_instance, data={"config": projector_config}, partial=False
|
||||||
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=["post"])
|
||||||
def clear_elements(self, request, pk):
|
def clear_elements(self, request, pk):
|
||||||
"""
|
"""
|
||||||
REST API operation to deactivate all projector elements but not
|
REST API operation to deactivate all projector elements but not
|
||||||
|
@ -301,15 +333,17 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
def clear(self, projector):
|
def clear(self, projector):
|
||||||
projector_config = {}
|
projector_config = {}
|
||||||
for key, value in projector.config.items():
|
for key, value in projector.config.items():
|
||||||
if value.get('stable'):
|
if value.get("stable"):
|
||||||
projector_config[key] = value
|
projector_config[key] = value
|
||||||
|
|
||||||
serializer = self.get_serializer(projector, data={'config': projector_config}, partial=False)
|
serializer = self.get_serializer(
|
||||||
|
projector, data={"config": projector_config}, partial=False
|
||||||
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return serializer.data
|
return serializer.data
|
||||||
|
|
||||||
@list_route(methods=['post'])
|
@list_route(methods=["post"])
|
||||||
def project(self, request, *args, **kwargs):
|
def project(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
REST API operation. Does a combination of clear_elements and prune_elements:
|
REST API operation. Does a combination of clear_elements and prune_elements:
|
||||||
|
@ -327,33 +361,46 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
# The data has to be a dict.
|
# The data has to be a dict.
|
||||||
if not isinstance(request.data, dict):
|
if not isinstance(request.data, dict):
|
||||||
raise ValidationError({'detail': _('The data has to be a dict.')})
|
raise ValidationError({"detail": _("The data has to be a dict.")})
|
||||||
|
|
||||||
# Get projector ids to clear
|
# Get projector ids to clear
|
||||||
clear_projector_ids = request.data.get('clear_ids', [])
|
clear_projector_ids = request.data.get("clear_ids", [])
|
||||||
for id in clear_projector_ids:
|
for id in clear_projector_ids:
|
||||||
if not isinstance(id, int):
|
if not isinstance(id, int):
|
||||||
raise ValidationError({'detail': _('The id "{}" has to be int.').format(id)})
|
raise ValidationError(
|
||||||
|
{"detail": _('The id "{}" has to be int.').format(id)}
|
||||||
|
)
|
||||||
|
|
||||||
# Get the projector id and validate element to prune. This is optional.
|
# Get the projector id and validate element to prune. This is optional.
|
||||||
prune = request.data.get('prune')
|
prune = request.data.get("prune")
|
||||||
if prune is not None:
|
if prune is not None:
|
||||||
if not isinstance(prune, dict):
|
if not isinstance(prune, dict):
|
||||||
raise ValidationError({'detail': _('Prune has to be an object.')})
|
raise ValidationError({"detail": _("Prune has to be an object.")})
|
||||||
prune_projector_id = prune.get('id')
|
prune_projector_id = prune.get("id")
|
||||||
if not isinstance(prune_projector_id, int):
|
if not isinstance(prune_projector_id, int):
|
||||||
raise ValidationError({'detail': _('The prune projector id has to be int.')})
|
raise ValidationError(
|
||||||
|
{"detail": _("The prune projector id has to be int.")}
|
||||||
|
)
|
||||||
|
|
||||||
# Get the projector after all clear operations, but check, if it exist.
|
# Get the projector after all clear operations, but check, if it exist.
|
||||||
if not Projector.objects.filter(pk=prune_projector_id).exists():
|
if not Projector.objects.filter(pk=prune_projector_id).exists():
|
||||||
raise ValidationError({
|
raise ValidationError(
|
||||||
'detail': _('The projector with id "{}" does not exist').format(prune_projector_id)})
|
{
|
||||||
|
"detail": _('The projector with id "{}" does not exist').format(
|
||||||
|
prune_projector_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
prune_element = prune.get('element', {})
|
prune_element = prune.get("element", {})
|
||||||
if not isinstance(prune_element, dict):
|
if not isinstance(prune_element, dict):
|
||||||
raise ValidationError({'detail': _('Prune element has to be a dict or not given.')})
|
raise ValidationError(
|
||||||
if prune_element.get('name') is None:
|
{"detail": _("Prune element has to be a dict or not given.")}
|
||||||
raise ValidationError({'detail': 'Invalid projector element. Name is missing.'})
|
)
|
||||||
|
if prune_element.get("name") is None:
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "Invalid projector element. Name is missing."}
|
||||||
|
)
|
||||||
|
|
||||||
# First step: Clear all given projectors
|
# First step: Clear all given projectors
|
||||||
for projector in Projector.objects.filter(pk__in=clear_projector_ids):
|
for projector in Projector.objects.filter(pk__in=clear_projector_ids):
|
||||||
|
@ -367,7 +414,7 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
|
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=["post"])
|
||||||
def set_resolution(self, request, pk):
|
def set_resolution(self, request, pk):
|
||||||
"""
|
"""
|
||||||
REST API operation to set the resolution.
|
REST API operation to set the resolution.
|
||||||
|
@ -388,26 +435,34 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
if not isinstance(request.data, dict):
|
if not isinstance(request.data, dict):
|
||||||
raise ValidationError({'detail': 'Data must be a dictionary.'})
|
raise ValidationError({"detail": "Data must be a dictionary."})
|
||||||
if request.data.get('width') is None or request.data.get('height') is None:
|
if request.data.get("width") is None or request.data.get("height") is None:
|
||||||
raise ValidationError({'detail': 'A width and a height have to be given.'})
|
raise ValidationError({"detail": "A width and a height have to be given."})
|
||||||
if not isinstance(request.data['width'], int) or not isinstance(request.data['height'], int):
|
if not isinstance(request.data["width"], int) or not isinstance(
|
||||||
raise ValidationError({'detail': 'Data has to be integers.'})
|
request.data["height"], int
|
||||||
if (request.data['width'] < 800 or request.data['width'] > 3840 or
|
):
|
||||||
request.data['height'] < 340 or request.data['height'] > 2880):
|
raise ValidationError({"detail": "Data has to be integers."})
|
||||||
raise ValidationError({'detail': 'The Resolution have to be between 800x340 and 3840x2880.'})
|
if (
|
||||||
|
request.data["width"] < 800
|
||||||
|
or request.data["width"] > 3840
|
||||||
|
or request.data["height"] < 340
|
||||||
|
or request.data["height"] > 2880
|
||||||
|
):
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "The Resolution have to be between 800x340 and 3840x2880."}
|
||||||
|
)
|
||||||
|
|
||||||
projector_instance = self.get_object()
|
projector_instance = self.get_object()
|
||||||
projector_instance.width = request.data['width']
|
projector_instance.width = request.data["width"]
|
||||||
projector_instance.height = request.data['height']
|
projector_instance.height = request.data["height"]
|
||||||
projector_instance.save()
|
projector_instance.save()
|
||||||
|
|
||||||
message = 'Changing resolution to {width}x{height} was successful.'.format(
|
message = "Changing resolution to {width}x{height} was successful.".format(
|
||||||
width=request.data['width'],
|
width=request.data["width"], height=request.data["height"]
|
||||||
height=request.data['height'])
|
)
|
||||||
return Response({'detail': message})
|
return Response({"detail": message})
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=["post"])
|
||||||
def control_view(self, request, pk):
|
def control_view(self, request, pk):
|
||||||
"""
|
"""
|
||||||
REST API operation to control the projector view, i. e. scale and
|
REST API operation to control the projector view, i. e. scale and
|
||||||
|
@ -426,27 +481,32 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
if not isinstance(request.data, dict):
|
if not isinstance(request.data, dict):
|
||||||
raise ValidationError({'detail': 'Data must be a dictionary.'})
|
raise ValidationError({"detail": "Data must be a dictionary."})
|
||||||
if (request.data.get('action') not in ('scale', 'scroll') or
|
if request.data.get("action") not in ("scale", "scroll") or request.data.get(
|
||||||
request.data.get('direction') not in ('up', 'down', 'reset')):
|
"direction"
|
||||||
raise ValidationError({'detail': "Data must be a dictionary with an action ('scale' or 'scroll') "
|
) not in ("up", "down", "reset"):
|
||||||
"and a direction ('up', 'down' or 'reset')."})
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"detail": "Data must be a dictionary with an action ('scale' or 'scroll') "
|
||||||
|
"and a direction ('up', 'down' or 'reset')."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
projector_instance = self.get_object()
|
projector_instance = self.get_object()
|
||||||
if request.data['action'] == 'scale':
|
if request.data["action"] == "scale":
|
||||||
if request.data['direction'] == 'up':
|
if request.data["direction"] == "up":
|
||||||
projector_instance.scale = F('scale') + 1
|
projector_instance.scale = F("scale") + 1
|
||||||
elif request.data['direction'] == 'down':
|
elif request.data["direction"] == "down":
|
||||||
projector_instance.scale = F('scale') - 1
|
projector_instance.scale = F("scale") - 1
|
||||||
else:
|
else:
|
||||||
# request.data['direction'] == 'reset'
|
# request.data['direction'] == 'reset'
|
||||||
projector_instance.scale = 0
|
projector_instance.scale = 0
|
||||||
else:
|
else:
|
||||||
# request.data['action'] == 'scroll'
|
# request.data['action'] == 'scroll'
|
||||||
if request.data['direction'] == 'up':
|
if request.data["direction"] == "up":
|
||||||
projector_instance.scroll = F('scroll') + 1
|
projector_instance.scroll = F("scroll") + 1
|
||||||
elif request.data['direction'] == 'down':
|
elif request.data["direction"] == "down":
|
||||||
projector_instance.scroll = F('scroll') - 1
|
projector_instance.scroll = F("scroll") - 1
|
||||||
else:
|
else:
|
||||||
# request.data['direction'] == 'reset'
|
# request.data['direction'] == 'reset'
|
||||||
projector_instance.scroll = 0
|
projector_instance.scroll = 0
|
||||||
|
@ -454,12 +514,13 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
projector_instance.save(skip_autoupdate=True)
|
projector_instance.save(skip_autoupdate=True)
|
||||||
projector_instance.refresh_from_db()
|
projector_instance.refresh_from_db()
|
||||||
inform_changed_data(projector_instance)
|
inform_changed_data(projector_instance)
|
||||||
message = '{action} {direction} was successful.'.format(
|
message = "{action} {direction} was successful.".format(
|
||||||
action=request.data['action'].capitalize(),
|
action=request.data["action"].capitalize(),
|
||||||
direction=request.data['direction'])
|
direction=request.data["direction"],
|
||||||
return Response({'detail': message})
|
)
|
||||||
|
return Response({"detail": message})
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=["post"])
|
||||||
def set_scroll(self, request, pk):
|
def set_scroll(self, request, pk):
|
||||||
"""
|
"""
|
||||||
REST API operation to scroll the projector.
|
REST API operation to scroll the projector.
|
||||||
|
@ -468,17 +529,18 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
/rest/core/projector/<pk>/set_scroll/ with a new value for scroll.
|
/rest/core/projector/<pk>/set_scroll/ with a new value for scroll.
|
||||||
"""
|
"""
|
||||||
if not isinstance(request.data, int):
|
if not isinstance(request.data, int):
|
||||||
raise ValidationError({'detail': 'Data must be an int.'})
|
raise ValidationError({"detail": "Data must be an int."})
|
||||||
|
|
||||||
projector_instance = self.get_object()
|
projector_instance = self.get_object()
|
||||||
projector_instance.scroll = request.data
|
projector_instance.scroll = request.data
|
||||||
|
|
||||||
projector_instance.save()
|
projector_instance.save()
|
||||||
message = 'Setting scroll to {scroll} was successful.'.format(
|
message = "Setting scroll to {scroll} was successful.".format(
|
||||||
scroll=request.data)
|
scroll=request.data
|
||||||
return Response({'detail': message})
|
)
|
||||||
|
return Response({"detail": message})
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=["post"])
|
||||||
def control_blank(self, request, pk):
|
def control_blank(self, request, pk):
|
||||||
"""
|
"""
|
||||||
REST API operation to blank the projector.
|
REST API operation to blank the projector.
|
||||||
|
@ -487,16 +549,17 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
/rest/core/projector/<pk>/control_blank/ with a value for blank.
|
/rest/core/projector/<pk>/control_blank/ with a value for blank.
|
||||||
"""
|
"""
|
||||||
if not isinstance(request.data, bool):
|
if not isinstance(request.data, bool):
|
||||||
raise ValidationError({'detail': 'Data must be a bool.'})
|
raise ValidationError({"detail": "Data must be a bool."})
|
||||||
|
|
||||||
projector_instance = self.get_object()
|
projector_instance = self.get_object()
|
||||||
projector_instance.blank = request.data
|
projector_instance.blank = request.data
|
||||||
projector_instance.save()
|
projector_instance.save()
|
||||||
message = "Setting 'blank' to {blank} was successful.".format(
|
message = "Setting 'blank' to {blank} was successful.".format(
|
||||||
blank=request.data)
|
blank=request.data
|
||||||
return Response({'detail': message})
|
)
|
||||||
|
return Response({"detail": message})
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=["post"])
|
||||||
def broadcast(self, request, pk):
|
def broadcast(self, request, pk):
|
||||||
"""
|
"""
|
||||||
REST API operation to (un-)broadcast the given projector.
|
REST API operation to (un-)broadcast the given projector.
|
||||||
|
@ -505,16 +568,17 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
It expects a POST request to
|
It expects a POST request to
|
||||||
/rest/core/projector/<pk>/broadcast/ without an argument
|
/rest/core/projector/<pk>/broadcast/ without an argument
|
||||||
"""
|
"""
|
||||||
if config['projector_broadcast'] == 0:
|
if config["projector_broadcast"] == 0:
|
||||||
config['projector_broadcast'] = pk
|
config["projector_broadcast"] = pk
|
||||||
message = "Setting projector {id} as broadcast projector was successful.".format(
|
message = "Setting projector {id} as broadcast projector was successful.".format(
|
||||||
id=pk)
|
id=pk
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
config['projector_broadcast'] = 0
|
config["projector_broadcast"] = 0
|
||||||
message = "Disabling broadcast was successful."
|
message = "Disabling broadcast was successful."
|
||||||
return Response({'detail': message})
|
return Response({"detail": message})
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=["post"])
|
||||||
def set_projectiondefault(self, request, pk):
|
def set_projectiondefault(self, request, pk):
|
||||||
"""
|
"""
|
||||||
REST API operation to set a projectiondefault to the requested projector. The argument
|
REST API operation to set a projectiondefault to the requested projector. The argument
|
||||||
|
@ -524,21 +588,28 @@ class ProjectorViewSet(ModelViewSet):
|
||||||
/rest/core/projector/<pk>/set_projectiondefault/ with the projectiondefault id as the argument
|
/rest/core/projector/<pk>/set_projectiondefault/ with the projectiondefault id as the argument
|
||||||
"""
|
"""
|
||||||
if not isinstance(request.data, int):
|
if not isinstance(request.data, int):
|
||||||
raise ValidationError({'detail': 'Data must be an int.'})
|
raise ValidationError({"detail": "Data must be an int."})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
projectiondefault = ProjectionDefault.objects.get(pk=request.data)
|
projectiondefault = ProjectionDefault.objects.get(pk=request.data)
|
||||||
except ProjectionDefault.DoesNotExist:
|
except ProjectionDefault.DoesNotExist:
|
||||||
raise ValidationError({'detail': 'The projectiondefault with pk={pk} was not found.'.format(
|
raise ValidationError(
|
||||||
pk=request.data)})
|
{
|
||||||
|
"detail": "The projectiondefault with pk={pk} was not found.".format(
|
||||||
|
pk=request.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
projector_instance = self.get_object()
|
projector_instance = self.get_object()
|
||||||
projectiondefault.projector = projector_instance
|
projectiondefault.projector = projector_instance
|
||||||
projectiondefault.save()
|
projectiondefault.save()
|
||||||
|
|
||||||
return Response('Setting projectiondefault "{name}" to projector {projector_id} was successful.'.format(
|
return Response(
|
||||||
name=projectiondefault.display_name,
|
'Setting projectiondefault "{name}" to projector {projector_id} was successful.'.format(
|
||||||
projector_id=projector_instance.pk))
|
name=projectiondefault.display_name, projector_id=projector_instance.pk
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TagViewSet(ModelViewSet):
|
class TagViewSet(ModelViewSet):
|
||||||
|
@ -548,6 +619,7 @@ class TagViewSet(ModelViewSet):
|
||||||
There are the following views: metadata, list, retrieve, create,
|
There are the following views: metadata, list, retrieve, create,
|
||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = TagAccessPermissions()
|
access_permissions = TagAccessPermissions()
|
||||||
queryset = Tag.objects.all()
|
queryset = Tag.objects.all()
|
||||||
|
|
||||||
|
@ -555,14 +627,14 @@ class TagViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ('list', 'retrieve'):
|
if self.action in ("list", "retrieve"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action == 'metadata':
|
elif self.action == "metadata":
|
||||||
# Every authenticated user can see the metadata.
|
# Every authenticated user can see the metadata.
|
||||||
# Anonymous users can do so if they are enabled.
|
# Anonymous users can do so if they are enabled.
|
||||||
result = self.request.user.is_authenticated or anonymous_is_enabled()
|
result = self.request.user.is_authenticated or anonymous_is_enabled()
|
||||||
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
elif self.action in ("create", "partial_update", "update", "destroy"):
|
||||||
result = has_perm(self.request.user, 'core.can_manage_tags')
|
result = has_perm(self.request.user, "core.can_manage_tags")
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
@ -575,6 +647,7 @@ class ConfigViewSet(ModelViewSet):
|
||||||
There are the following views: metadata, list, retrieve, update and
|
There are the following views: metadata, list, retrieve, update and
|
||||||
partial_update.
|
partial_update.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ConfigAccessPermissions()
|
access_permissions = ConfigAccessPermissions()
|
||||||
queryset = ConfigStore.objects.all()
|
queryset = ConfigStore.objects.all()
|
||||||
|
|
||||||
|
@ -582,22 +655,22 @@ class ConfigViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ('list', 'retrieve'):
|
if self.action in ("list", "retrieve"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action == 'metadata':
|
elif self.action == "metadata":
|
||||||
# Every authenticated user can see the metadata and list or
|
# Every authenticated user can see the metadata and list or
|
||||||
# retrieve the config. Anonymous users can do so if they are
|
# retrieve the config. Anonymous users can do so if they are
|
||||||
# enabled.
|
# enabled.
|
||||||
result = self.request.user.is_authenticated or anonymous_is_enabled()
|
result = self.request.user.is_authenticated or anonymous_is_enabled()
|
||||||
elif self.action in ('partial_update', 'update'):
|
elif self.action in ("partial_update", "update"):
|
||||||
# The user needs 'core.can_manage_logos_and_fonts' for all config values
|
# The user needs 'core.can_manage_logos_and_fonts' for all config values
|
||||||
# starting with 'logo' and 'font'. For all other config values th euser needs
|
# starting with 'logo' and 'font'. For all other config values th euser needs
|
||||||
# the default permissions 'core.can_manage_config'.
|
# the default permissions 'core.can_manage_config'.
|
||||||
pk = self.kwargs['pk']
|
pk = self.kwargs["pk"]
|
||||||
if pk.startswith('logo') or pk.startswith('font'):
|
if pk.startswith("logo") or pk.startswith("font"):
|
||||||
result = has_perm(self.request.user, 'core.can_manage_logos_and_fonts')
|
result = has_perm(self.request.user, "core.can_manage_logos_and_fonts")
|
||||||
else:
|
else:
|
||||||
result = has_perm(self.request.user, 'core.can_manage_config')
|
result = has_perm(self.request.user, "core.can_manage_config")
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
@ -608,10 +681,10 @@ class ConfigViewSet(ModelViewSet):
|
||||||
|
|
||||||
Example: {"value": 42}
|
Example: {"value": 42}
|
||||||
"""
|
"""
|
||||||
key = kwargs['pk']
|
key = kwargs["pk"]
|
||||||
value = request.data.get('value')
|
value = request.data.get("value")
|
||||||
if value is None:
|
if value is None:
|
||||||
raise ValidationError({'detail': 'Invalid input. Config value is missing.'})
|
raise ValidationError({"detail": "Invalid input. Config value is missing."})
|
||||||
|
|
||||||
# Validate and change value.
|
# Validate and change value.
|
||||||
try:
|
try:
|
||||||
|
@ -619,10 +692,10 @@ class ConfigViewSet(ModelViewSet):
|
||||||
except ConfigNotFound:
|
except ConfigNotFound:
|
||||||
raise Http404
|
raise Http404
|
||||||
except ConfigError as e:
|
except ConfigError as e:
|
||||||
raise ValidationError({'detail': str(e)})
|
raise ValidationError({"detail": str(e)})
|
||||||
|
|
||||||
# Return response.
|
# Return response.
|
||||||
return Response({'key': key, 'value': value})
|
return Response({"key": key, "value": value})
|
||||||
|
|
||||||
|
|
||||||
class ChatMessageViewSet(ModelViewSet):
|
class ChatMessageViewSet(ModelViewSet):
|
||||||
|
@ -632,6 +705,7 @@ class ChatMessageViewSet(ModelViewSet):
|
||||||
There are the following views: metadata, list, retrieve and create.
|
There are the following views: metadata, list, retrieve and create.
|
||||||
The views partial_update, update and destroy are disabled.
|
The views partial_update, update and destroy are disabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ChatMessageAccessPermissions()
|
access_permissions = ChatMessageAccessPermissions()
|
||||||
queryset = ChatMessage.objects.all()
|
queryset = ChatMessage.objects.all()
|
||||||
|
|
||||||
|
@ -639,18 +713,18 @@ class ChatMessageViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ('list', 'retrieve'):
|
if self.action in ("list", "retrieve"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action in ('metadata', 'create'):
|
elif self.action in ("metadata", "create"):
|
||||||
# We do not want anonymous users to use the chat even the anonymous
|
# We do not want anonymous users to use the chat even the anonymous
|
||||||
# group has the permission core.can_use_chat.
|
# group has the permission core.can_use_chat.
|
||||||
result = (
|
result = self.request.user.is_authenticated and has_perm(
|
||||||
self.request.user.is_authenticated and
|
self.request.user, "core.can_use_chat"
|
||||||
has_perm(self.request.user, 'core.can_use_chat'))
|
)
|
||||||
elif self.action == 'clear':
|
elif self.action == "clear":
|
||||||
result = (
|
result = has_perm(self.request.user, "core.can_use_chat") and has_perm(
|
||||||
has_perm(self.request.user, 'core.can_use_chat') and
|
self.request.user, "core.can_manage_chat"
|
||||||
has_perm(self.request.user, 'core.can_manage_chat'))
|
)
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
@ -665,7 +739,7 @@ class ChatMessageViewSet(ModelViewSet):
|
||||||
# to see users may not have it but can get it now.
|
# to see users may not have it but can get it now.
|
||||||
inform_changed_data([self.request.user])
|
inform_changed_data([self.request.user])
|
||||||
|
|
||||||
@list_route(methods=['post'])
|
@list_route(methods=["post"])
|
||||||
def clear(self, request):
|
def clear(self, request):
|
||||||
"""
|
"""
|
||||||
Deletes all chat messages.
|
Deletes all chat messages.
|
||||||
|
@ -679,7 +753,7 @@ class ChatMessageViewSet(ModelViewSet):
|
||||||
# Trigger autoupdate and setup response.
|
# Trigger autoupdate and setup response.
|
||||||
if len(args) > 0:
|
if len(args) > 0:
|
||||||
inform_deleted_data(args)
|
inform_deleted_data(args)
|
||||||
return Response({'detail': _('All chat messages deleted successfully.')})
|
return Response({"detail": _("All chat messages deleted successfully.")})
|
||||||
|
|
||||||
|
|
||||||
class ProjectorMessageViewSet(ModelViewSet):
|
class ProjectorMessageViewSet(ModelViewSet):
|
||||||
|
@ -689,6 +763,7 @@ class ProjectorMessageViewSet(ModelViewSet):
|
||||||
There are the following views: list, retrieve, create, update,
|
There are the following views: list, retrieve, create, update,
|
||||||
partial_update and destroy.
|
partial_update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ProjectorMessageAccessPermissions()
|
access_permissions = ProjectorMessageAccessPermissions()
|
||||||
queryset = ProjectorMessage.objects.all()
|
queryset = ProjectorMessage.objects.all()
|
||||||
|
|
||||||
|
@ -696,10 +771,10 @@ class ProjectorMessageViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ('list', 'retrieve'):
|
if self.action in ("list", "retrieve"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
elif self.action in ("create", "partial_update", "update", "destroy"):
|
||||||
result = has_perm(self.request.user, 'core.can_manage_projector')
|
result = has_perm(self.request.user, "core.can_manage_projector")
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
@ -712,6 +787,7 @@ class CountdownViewSet(ModelViewSet):
|
||||||
There are the following views: list, retrieve, create, update,
|
There are the following views: list, retrieve, create, update,
|
||||||
partial_update and destroy.
|
partial_update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = CountdownAccessPermissions()
|
access_permissions = CountdownAccessPermissions()
|
||||||
queryset = Countdown.objects.all()
|
queryset = Countdown.objects.all()
|
||||||
|
|
||||||
|
@ -719,10 +795,10 @@ class CountdownViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ('list', 'retrieve'):
|
if self.action in ("list", "retrieve"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
elif self.action in ("create", "partial_update", "update", "destroy"):
|
||||||
result = has_perm(self.request.user, 'core.can_manage_projector')
|
result = has_perm(self.request.user, "core.can_manage_projector")
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
@ -734,6 +810,7 @@ class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||||
|
|
||||||
There are the following views: list, retrieve, clear_history.
|
There are the following views: list, retrieve, clear_history.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = HistoryAccessPermissions()
|
access_permissions = HistoryAccessPermissions()
|
||||||
queryset = History.objects.all()
|
queryset = History.objects.all()
|
||||||
|
|
||||||
|
@ -741,13 +818,13 @@ class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ('list', 'retrieve', 'clear_history'):
|
if self.action in ("list", "retrieve", "clear_history"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@list_route(methods=['post'])
|
@list_route(methods=["post"])
|
||||||
def clear_history(self, request):
|
def clear_history(self, request):
|
||||||
"""
|
"""
|
||||||
Deletes and rebuilds the history.
|
Deletes and rebuilds the history.
|
||||||
|
@ -769,16 +846,18 @@ class HistoryViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||||
inform_changed_data(history_instances)
|
inform_changed_data(history_instances)
|
||||||
|
|
||||||
# Setup response.
|
# Setup response.
|
||||||
return Response({'detail': _('History was deleted successfully.')})
|
return Response({"detail": _("History was deleted successfully.")})
|
||||||
|
|
||||||
|
|
||||||
# Special API views
|
# Special API views
|
||||||
|
|
||||||
|
|
||||||
class ServerTime(utils_views.APIView):
|
class ServerTime(utils_views.APIView):
|
||||||
"""
|
"""
|
||||||
Returns the server time as UNIX timestamp.
|
Returns the server time as UNIX timestamp.
|
||||||
"""
|
"""
|
||||||
http_method_names = ['get']
|
|
||||||
|
http_method_names = ["get"]
|
||||||
|
|
||||||
def get_context_data(self, **context):
|
def get_context_data(self, **context):
|
||||||
return now().timestamp()
|
return now().timestamp()
|
||||||
|
@ -789,27 +868,36 @@ class VersionView(utils_views.APIView):
|
||||||
Returns a dictionary with the OpenSlides version and the version of all
|
Returns a dictionary with the OpenSlides version and the version of all
|
||||||
plugins.
|
plugins.
|
||||||
"""
|
"""
|
||||||
http_method_names = ['get']
|
|
||||||
|
http_method_names = ["get"]
|
||||||
|
|
||||||
def get_context_data(self, **context):
|
def get_context_data(self, **context):
|
||||||
Result = TypedDict('Result', {
|
Result = TypedDict(
|
||||||
'openslides_version': str,
|
"Result",
|
||||||
'openslides_license': str,
|
{
|
||||||
'openslides_url': str,
|
"openslides_version": str,
|
||||||
'plugins': List[Dict[str, str]]})
|
"openslides_license": str,
|
||||||
|
"openslides_url": str,
|
||||||
|
"plugins": List[Dict[str, str]],
|
||||||
|
},
|
||||||
|
)
|
||||||
result: Result = dict(
|
result: Result = dict(
|
||||||
openslides_version=version,
|
openslides_version=version,
|
||||||
openslides_license=license,
|
openslides_license=license,
|
||||||
openslides_url=url,
|
openslides_url=url,
|
||||||
plugins=[])
|
plugins=[],
|
||||||
|
)
|
||||||
# Versions of plugins.
|
# Versions of plugins.
|
||||||
for plugin in settings.INSTALLED_PLUGINS:
|
for plugin in settings.INSTALLED_PLUGINS:
|
||||||
result['plugins'].append({
|
result["plugins"].append(
|
||||||
'verbose_name': get_plugin_verbose_name(plugin),
|
{
|
||||||
'description': get_plugin_description(plugin),
|
"verbose_name": get_plugin_verbose_name(plugin),
|
||||||
'version': get_plugin_version(plugin),
|
"description": get_plugin_description(plugin),
|
||||||
'license': get_plugin_license(plugin),
|
"version": get_plugin_version(plugin),
|
||||||
'url': get_plugin_url(plugin)})
|
"license": get_plugin_license(plugin),
|
||||||
|
"url": get_plugin_url(plugin),
|
||||||
|
}
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@ -820,7 +908,8 @@ class HistoryView(utils_views.APIView):
|
||||||
Use query paramter timestamp (UNIX timestamp) to get all elements from begin
|
Use query paramter timestamp (UNIX timestamp) to get all elements from begin
|
||||||
until (including) this timestamp.
|
until (including) this timestamp.
|
||||||
"""
|
"""
|
||||||
http_method_names = ['get']
|
|
||||||
|
http_method_names = ["get"]
|
||||||
|
|
||||||
def get_context_data(self, **context):
|
def get_context_data(self, **context):
|
||||||
"""
|
"""
|
||||||
|
@ -830,19 +919,25 @@ class HistoryView(utils_views.APIView):
|
||||||
if not in_some_groups(self.request.user.pk or 0, [GROUP_ADMIN_PK]):
|
if not in_some_groups(self.request.user.pk or 0, [GROUP_ADMIN_PK]):
|
||||||
self.permission_denied(self.request)
|
self.permission_denied(self.request)
|
||||||
try:
|
try:
|
||||||
timestamp = int(self.request.query_params.get('timestamp', 0))
|
timestamp = int(self.request.query_params.get("timestamp", 0))
|
||||||
except (ValueError):
|
except (ValueError):
|
||||||
raise ValidationError({'detail': 'Invalid input. Timestamp should be an integer.'})
|
raise ValidationError(
|
||||||
|
{"detail": "Invalid input. Timestamp should be an integer."}
|
||||||
|
)
|
||||||
data = []
|
data = []
|
||||||
queryset = History.objects.select_related('full_data')
|
queryset = History.objects.select_related("full_data")
|
||||||
if timestamp:
|
if timestamp:
|
||||||
queryset = queryset.filter(now__lte=datetime.datetime.fromtimestamp(timestamp))
|
queryset = queryset.filter(
|
||||||
|
now__lte=datetime.datetime.fromtimestamp(timestamp)
|
||||||
|
)
|
||||||
for instance in queryset:
|
for instance in queryset:
|
||||||
data.append({
|
data.append(
|
||||||
'full_data': instance.full_data.full_data,
|
{
|
||||||
'element_id': instance.element_id,
|
"full_data": instance.full_data.full_data,
|
||||||
'timestamp': instance.now.timestamp(),
|
"element_id": instance.element_id,
|
||||||
'information': instance.information,
|
"timestamp": instance.now.timestamp(),
|
||||||
'user_id': instance.user.pk if instance.user else None,
|
"information": instance.information,
|
||||||
})
|
"user_id": instance.user.pk if instance.user else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -12,7 +12,8 @@ class NotifyWebsocketClientMessage(BaseWebsocketClientMessage):
|
||||||
"""
|
"""
|
||||||
Websocket message from a client to send a message to other clients.
|
Websocket message from a client to send a message to other clients.
|
||||||
"""
|
"""
|
||||||
identifier = 'notify'
|
|
||||||
|
identifier = "notify"
|
||||||
schema = {
|
schema = {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"title": "Notify elements.",
|
"title": "Notify elements.",
|
||||||
|
@ -21,31 +22,24 @@ class NotifyWebsocketClientMessage(BaseWebsocketClientMessage):
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"projectors": {
|
"projectors": {"type": "array", "items": {"type": "integer"}},
|
||||||
"type": "array",
|
"reply_channels": {"type": "array", "items": {"type": "string"}},
|
||||||
"items": {"type": "integer"},
|
"users": {"type": "array", "items": {"type": "integer"}},
|
||||||
},
|
},
|
||||||
"reply_channels": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "string"},
|
|
||||||
},
|
|
||||||
"users": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "integer"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"minItems": 1,
|
"minItems": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str) -> None:
|
async def receive_content(
|
||||||
|
self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str
|
||||||
|
) -> None:
|
||||||
await consumer.channel_layer.group_send(
|
await consumer.channel_layer.group_send(
|
||||||
"site",
|
"site",
|
||||||
{
|
{
|
||||||
"type": "send_notify",
|
"type": "send_notify",
|
||||||
"incomming": content,
|
"incomming": content,
|
||||||
"senderReplyChannelName": consumer.channel_name,
|
"senderReplyChannelName": consumer.channel_name,
|
||||||
"senderUserId": consumer.scope['user']['id'],
|
"senderUserId": consumer.scope["user"]["id"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,19 +48,25 @@ class ConstantsWebsocketClientMessage(BaseWebsocketClientMessage):
|
||||||
"""
|
"""
|
||||||
The Client requests the constants.
|
The Client requests the constants.
|
||||||
"""
|
"""
|
||||||
identifier = 'constants'
|
|
||||||
|
identifier = "constants"
|
||||||
content_required = False
|
content_required = False
|
||||||
|
|
||||||
async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str) -> None:
|
async def receive_content(
|
||||||
|
self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str
|
||||||
|
) -> None:
|
||||||
# Return all constants to the client.
|
# Return all constants to the client.
|
||||||
await consumer.send_json(type='constants', content=get_constants(), in_response=id)
|
await consumer.send_json(
|
||||||
|
type="constants", content=get_constants(), in_response=id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GetElementsWebsocketClientMessage(BaseWebsocketClientMessage):
|
class GetElementsWebsocketClientMessage(BaseWebsocketClientMessage):
|
||||||
"""
|
"""
|
||||||
The Client request database elements.
|
The Client request database elements.
|
||||||
"""
|
"""
|
||||||
identifier = 'getElements'
|
|
||||||
|
identifier = "getElements"
|
||||||
schema = {
|
schema = {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"titel": "getElement request",
|
"titel": "getElement request",
|
||||||
|
@ -74,31 +74,40 @@ class GetElementsWebsocketClientMessage(BaseWebsocketClientMessage):
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
# change_id is not required
|
# change_id is not required
|
||||||
"change_id": {
|
"change_id": {"type": "integer"}
|
||||||
"type": "integer",
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str) -> None:
|
async def receive_content(
|
||||||
requested_change_id = content.get('change_id', 0)
|
self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str
|
||||||
|
) -> None:
|
||||||
|
requested_change_id = content.get("change_id", 0)
|
||||||
try:
|
try:
|
||||||
element_data = await get_element_data(consumer.scope['user']['id'], requested_change_id)
|
element_data = await get_element_data(
|
||||||
|
consumer.scope["user"]["id"], requested_change_id
|
||||||
|
)
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
await consumer.send_json(type='error', content=str(error), in_response=id)
|
await consumer.send_json(type="error", content=str(error), in_response=id)
|
||||||
else:
|
else:
|
||||||
await consumer.send_json(type='autoupdate', content=element_data, in_response=id)
|
await consumer.send_json(
|
||||||
|
type="autoupdate", content=element_data, in_response=id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AutoupdateWebsocketClientMessage(BaseWebsocketClientMessage):
|
class AutoupdateWebsocketClientMessage(BaseWebsocketClientMessage):
|
||||||
"""
|
"""
|
||||||
The Client turns autoupdate on or off.
|
The Client turns autoupdate on or off.
|
||||||
"""
|
"""
|
||||||
identifier = 'autoupdate'
|
|
||||||
|
|
||||||
async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str) -> None:
|
identifier = "autoupdate"
|
||||||
|
|
||||||
|
async def receive_content(
|
||||||
|
self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str
|
||||||
|
) -> None:
|
||||||
# Turn on or off the autoupdate for the client
|
# Turn on or off the autoupdate for the client
|
||||||
if content: # accept any value, that can be interpreted as bool
|
if content: # accept any value, that can be interpreted as bool
|
||||||
await consumer.channel_layer.group_add('autoupdate', consumer.channel_name)
|
await consumer.channel_layer.group_add("autoupdate", consumer.channel_name)
|
||||||
else:
|
else:
|
||||||
await consumer.channel_layer.group_discard('autoupdate', consumer.channel_name)
|
await consumer.channel_layer.group_discard(
|
||||||
|
"autoupdate", consumer.channel_name
|
||||||
|
)
|
||||||
|
|
|
@ -9,68 +9,70 @@ MODULE_DIR = os.path.realpath(os.path.dirname(os.path.abspath(__file__)))
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'openslides.core',
|
"openslides.core",
|
||||||
'openslides.users',
|
"openslides.users",
|
||||||
'django.contrib.auth',
|
"django.contrib.auth",
|
||||||
'django.contrib.contenttypes',
|
"django.contrib.contenttypes",
|
||||||
'django.contrib.sessions',
|
"django.contrib.sessions",
|
||||||
'django.contrib.staticfiles',
|
"django.contrib.staticfiles",
|
||||||
'rest_framework',
|
"rest_framework",
|
||||||
'channels',
|
"channels",
|
||||||
'openslides.agenda',
|
"openslides.agenda",
|
||||||
'openslides.topics',
|
"openslides.topics",
|
||||||
'openslides.motions',
|
"openslides.motions",
|
||||||
'openslides.assignments',
|
"openslides.assignments",
|
||||||
'openslides.mediafiles',
|
"openslides.mediafiles",
|
||||||
]
|
]
|
||||||
|
|
||||||
INSTALLED_PLUGINS = collect_plugins() # Adds all automaticly collected plugins
|
INSTALLED_PLUGINS = collect_plugins() # Adds all automaticly collected plugins
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
"django.middleware.security.SecurityMiddleware",
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
"django.middleware.locale.LocaleMiddleware",
|
||||||
'django.middleware.common.CommonMiddleware',
|
"django.middleware.common.CommonMiddleware",
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
'openslides.utils.autoupdate.AutoupdateBundleMiddleware',
|
"openslides.utils.autoupdate.AutoupdateBundleMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'openslides.urls'
|
ROOT_URLCONF = "openslides.urls"
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
'DIRS': [],
|
"DIRS": [],
|
||||||
'APP_DIRS': True,
|
"APP_DIRS": True,
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
# https://docs.djangoproject.com/en/1.10/topics/email/
|
# https://docs.djangoproject.com/en/1.10/topics/email/
|
||||||
|
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||||
EMAIL_TIMEOUT = 5 # Timeout in seconds for blocking operations like the connection attempt
|
EMAIL_TIMEOUT = (
|
||||||
|
5
|
||||||
|
) # Timeout in seconds for blocking operations like the connection attempt
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.10/topics/i18n/
|
# https://docs.djangoproject.com/en/1.10/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en'
|
LANGUAGE_CODE = "en"
|
||||||
|
|
||||||
LANGUAGES = (
|
LANGUAGES = (
|
||||||
('en', 'English'),
|
("en", "English"),
|
||||||
('de', 'Deutsch'),
|
("de", "Deutsch"),
|
||||||
('fr', 'Français'),
|
("fr", "Français"),
|
||||||
('es', 'Español'),
|
("es", "Español"),
|
||||||
('pt', 'Português'),
|
("pt", "Português"),
|
||||||
('cs', 'Český'),
|
("cs", "Český"),
|
||||||
('ru', 'русский'),
|
("ru", "русский"),
|
||||||
)
|
)
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = "UTC"
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
|
@ -78,62 +80,54 @@ USE_L10N = True
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
LOCALE_PATHS = [
|
LOCALE_PATHS = [os.path.join(MODULE_DIR, "locale")]
|
||||||
os.path.join(MODULE_DIR, 'locale'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = "/static/"
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [os.path.join(MODULE_DIR, "static")]
|
||||||
os.path.join(MODULE_DIR, 'static'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Sessions and user authentication
|
# Sessions and user authentication
|
||||||
# https://docs.djangoproject.com/en/1.10/topics/http/sessions/
|
# https://docs.djangoproject.com/en/1.10/topics/http/sessions/
|
||||||
# https://docs.djangoproject.com/en/1.10/topics/auth/
|
# https://docs.djangoproject.com/en/1.10/topics/auth/
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'users.User'
|
AUTH_USER_MODEL = "users.User"
|
||||||
|
|
||||||
AUTH_GROUP_MODEL = 'users.Group'
|
AUTH_GROUP_MODEL = "users.Group"
|
||||||
|
|
||||||
SESSION_COOKIE_NAME = 'OpenSlidesSessionID'
|
SESSION_COOKIE_NAME = "OpenSlidesSessionID"
|
||||||
|
|
||||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||||
|
|
||||||
CSRF_COOKIE_NAME = 'OpenSlidesCsrfToken'
|
CSRF_COOKIE_NAME = "OpenSlidesCsrfToken"
|
||||||
|
|
||||||
CSRF_COOKIE_AGE = None
|
CSRF_COOKIE_AGE = None
|
||||||
|
|
||||||
PASSWORD_HASHERS = [
|
PASSWORD_HASHERS = [
|
||||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
|
||||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
||||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
|
||||||
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
"django.contrib.auth.hashers.BCryptPasswordHasher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Files
|
# Files
|
||||||
# https://docs.djangoproject.com/en/1.10/topics/files/
|
# https://docs.djangoproject.com/en/1.10/topics/files/
|
||||||
|
|
||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = "/media/"
|
||||||
|
|
||||||
|
|
||||||
# Django Channels
|
# Django Channels
|
||||||
# http://channels.readthedocs.io/en/latest/
|
# http://channels.readthedocs.io/en/latest/
|
||||||
|
|
||||||
ASGI_APPLICATION = 'openslides.routing.application'
|
ASGI_APPLICATION = "openslides.routing.application"
|
||||||
|
|
||||||
CHANNEL_LAYERS = {
|
CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
|
||||||
'default': {
|
|
||||||
'BACKEND': 'channels.layers.InMemoryChannelLayer',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Enable updating the last_login field for users on every login.
|
# Enable updating the last_login field for users on every login.
|
||||||
|
|
|
@ -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.
|
Access permissions container for Mediafile and MediafileViewSet.
|
||||||
"""
|
"""
|
||||||
base_permission = 'mediafiles.can_see'
|
|
||||||
|
base_permission = "mediafiles.can_see"
|
||||||
|
|
||||||
async def get_restricted_data(
|
async def get_restricted_data(
|
||||||
self,
|
self, full_data: List[Dict[str, Any]], user_id: int
|
||||||
full_data: List[Dict[str, Any]],
|
) -> List[Dict[str, Any]]:
|
||||||
user_id: int) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the user. Removes hidden mediafiles for some users.
|
for the user. Removes hidden mediafiles for some users.
|
||||||
"""
|
"""
|
||||||
# Parse data.
|
# Parse data.
|
||||||
if await async_has_perm(user_id, 'mediafiles.can_see') and await async_has_perm(user_id, 'mediafiles.can_see_hidden'):
|
if await async_has_perm(user_id, "mediafiles.can_see") and await async_has_perm(
|
||||||
|
user_id, "mediafiles.can_see_hidden"
|
||||||
|
):
|
||||||
data = full_data
|
data = full_data
|
||||||
elif await async_has_perm(user_id, 'mediafiles.can_see'):
|
elif await async_has_perm(user_id, "mediafiles.can_see"):
|
||||||
# Exclude hidden mediafiles.
|
# Exclude hidden mediafiles.
|
||||||
data = [full for full in full_data if not full['hidden']]
|
data = [full for full in full_data if not full["hidden"]]
|
||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ from ..utils.projector import register_projector_elements
|
||||||
|
|
||||||
|
|
||||||
class MediafilesAppConfig(AppConfig):
|
class MediafilesAppConfig(AppConfig):
|
||||||
name = 'openslides.mediafiles'
|
name = "openslides.mediafiles"
|
||||||
verbose_name = 'OpenSlides Mediafiles'
|
verbose_name = "OpenSlides Mediafiles"
|
||||||
angular_site_module = True
|
angular_site_module = True
|
||||||
angular_projector_module = True
|
angular_projector_module = True
|
||||||
|
|
||||||
|
@ -27,20 +27,25 @@ class MediafilesAppConfig(AppConfig):
|
||||||
# Connect signals.
|
# Connect signals.
|
||||||
permission_change.connect(
|
permission_change.connect(
|
||||||
get_permission_change_data,
|
get_permission_change_data,
|
||||||
dispatch_uid='mediafiles_get_permission_change_data')
|
dispatch_uid="mediafiles_get_permission_change_data",
|
||||||
|
)
|
||||||
|
|
||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register(self.get_model('Mediafile').get_collection_string(), MediafileViewSet)
|
router.register(
|
||||||
|
self.get_model("Mediafile").get_collection_string(), MediafileViewSet
|
||||||
|
)
|
||||||
|
|
||||||
# register required_users
|
# register required_users
|
||||||
required_user.add_collection_string(self.get_model('Mediafile').get_collection_string(), required_users)
|
required_user.add_collection_string(
|
||||||
|
self.get_model("Mediafile").get_collection_string(), required_users
|
||||||
|
)
|
||||||
|
|
||||||
def get_startup_elements(self):
|
def get_startup_elements(self):
|
||||||
"""
|
"""
|
||||||
Yields all Cachables required on startup i. e. opening the websocket
|
Yields all Cachables required on startup i. e. opening the websocket
|
||||||
connection.
|
connection.
|
||||||
"""
|
"""
|
||||||
yield self.get_model('Mediafile')
|
yield self.get_model("Mediafile")
|
||||||
|
|
||||||
|
|
||||||
def required_users(element: Dict[str, Any]) -> Set[int]:
|
def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||||
|
@ -49,4 +54,4 @@ def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||||
if request_user can see mediafiles. This function may return an empty
|
if request_user can see mediafiles. This function may return an empty
|
||||||
set.
|
set.
|
||||||
"""
|
"""
|
||||||
return set(element['uploader_id'])
|
return set(element["uploader_id"])
|
||||||
|
|
|
@ -13,25 +13,43 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Mediafile',
|
name="Mediafile",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('mediafile', models.FileField(upload_to='file')),
|
"id",
|
||||||
('title', models.CharField(max_length=255, unique=True)),
|
models.AutoField(
|
||||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
auto_created=True,
|
||||||
('uploader', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("mediafile", models.FileField(upload_to="file")),
|
||||||
|
("title", models.CharField(max_length=255, unique=True)),
|
||||||
|
("timestamp", models.DateTimeField(auto_now_add=True)),
|
||||||
|
(
|
||||||
|
"uploader",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'permissions': (('can_see', 'Can see the list of files'), ('can_upload', 'Can upload files'), ('can_manage', 'Can manage files')),
|
"permissions": (
|
||||||
'default_permissions': (),
|
("can_see", "Can see the list of files"),
|
||||||
'ordering': ['title'],
|
("can_upload", "Can upload files"),
|
||||||
|
("can_manage", "Can manage files"),
|
||||||
|
),
|
||||||
|
"default_permissions": (),
|
||||||
|
"ordering": ["title"],
|
||||||
},
|
},
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
)
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,22 +7,25 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("mediafiles", "0001_initial")]
|
||||||
('mediafiles', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='mediafile',
|
name="mediafile",
|
||||||
options={'default_permissions': (), 'ordering': ['title'], 'permissions': (
|
options={
|
||||||
('can_see', 'Can see the list of files'),
|
"default_permissions": (),
|
||||||
('can_see_hidden', 'Can see hidden files'),
|
"ordering": ["title"],
|
||||||
('can_upload', 'Can upload files'),
|
"permissions": (
|
||||||
('can_manage', 'Can manage files'))},
|
("can_see", "Can see the list of files"),
|
||||||
|
("can_see_hidden", "Can see hidden files"),
|
||||||
|
("can_upload", "Can upload files"),
|
||||||
|
("can_manage", "Can manage files"),
|
||||||
|
),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='mediafile',
|
model_name="mediafile",
|
||||||
name='hidden',
|
name="hidden",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -13,10 +13,11 @@ class Mediafile(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Class for uploaded files which can be delivered under a certain url.
|
Class for uploaded files which can be delivered under a certain url.
|
||||||
"""
|
"""
|
||||||
access_permissions = MediafileAccessPermissions()
|
|
||||||
can_see_permission = 'mediafiles.can_see'
|
|
||||||
|
|
||||||
mediafile = models.FileField(upload_to='file')
|
access_permissions = MediafileAccessPermissions()
|
||||||
|
can_see_permission = "mediafiles.can_see"
|
||||||
|
|
||||||
|
mediafile = models.FileField(upload_to="file")
|
||||||
"""
|
"""
|
||||||
See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield
|
See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield
|
||||||
for more information.
|
for more information.
|
||||||
|
@ -26,10 +27,8 @@ class Mediafile(RESTModelMixin, models.Model):
|
||||||
"""A string representing the title of the file."""
|
"""A string representing the title of the file."""
|
||||||
|
|
||||||
uploader = models.ForeignKey(
|
uploader = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True
|
||||||
on_delete=models.SET_NULL,
|
)
|
||||||
null=True,
|
|
||||||
blank=True)
|
|
||||||
"""A user – the uploader of a file."""
|
"""A user – the uploader of a file."""
|
||||||
|
|
||||||
hidden = models.BooleanField(default=False)
|
hidden = models.BooleanField(default=False)
|
||||||
|
@ -42,13 +41,15 @@ class Mediafile(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Meta class for the mediafile model.
|
Meta class for the mediafile model.
|
||||||
"""
|
"""
|
||||||
ordering = ['title']
|
|
||||||
|
ordering = ["title"]
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
permissions = (
|
permissions = (
|
||||||
('can_see', 'Can see the list of files'),
|
("can_see", "Can see the list of files"),
|
||||||
('can_see_hidden', 'Can see hidden files'),
|
("can_see_hidden", "Can see hidden files"),
|
||||||
('can_upload', 'Can upload files'),
|
("can_upload", "Can upload files"),
|
||||||
('can_manage', 'Can manage files'))
|
("can_manage", "Can manage files"),
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""
|
"""
|
||||||
|
@ -72,10 +73,11 @@ class Mediafile(RESTModelMixin, models.Model):
|
||||||
mediafile projector element is disabled.
|
mediafile projector element is disabled.
|
||||||
"""
|
"""
|
||||||
Projector.remove_any(
|
Projector.remove_any(
|
||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate, name="mediafiles/mediafile", id=self.pk
|
||||||
name='mediafiles/mediafile',
|
)
|
||||||
id=self.pk)
|
return super().delete( # type: ignore
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
def get_filesize(self):
|
def get_filesize(self):
|
||||||
"""
|
"""
|
||||||
|
@ -85,26 +87,26 @@ class Mediafile(RESTModelMixin, models.Model):
|
||||||
try:
|
try:
|
||||||
size = self.mediafile.size
|
size = self.mediafile.size
|
||||||
except OSError:
|
except OSError:
|
||||||
size_string = _('unknown')
|
size_string = _("unknown")
|
||||||
else:
|
else:
|
||||||
if size < 1024:
|
if size < 1024:
|
||||||
size_string = '< 1 kB'
|
size_string = "< 1 kB"
|
||||||
elif size >= 1024 * 1024:
|
elif size >= 1024 * 1024:
|
||||||
mB = size / 1024 / 1024
|
mB = size / 1024 / 1024
|
||||||
size_string = '%d MB' % mB
|
size_string = "%d MB" % mB
|
||||||
else:
|
else:
|
||||||
kB = size / 1024
|
kB = size / 1024
|
||||||
size_string = '%d kB' % kB
|
size_string = "%d kB" % kB
|
||||||
return size_string
|
return size_string
|
||||||
|
|
||||||
def is_logo(self):
|
def is_logo(self):
|
||||||
for key in config['logos_available']:
|
for key in config["logos_available"]:
|
||||||
if config[key]['path'] == self.mediafile.url:
|
if config[key]["path"] == self.mediafile.url:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_font(self):
|
def is_font(self):
|
||||||
for key in config['fonts_available']:
|
for key in config["fonts_available"]:
|
||||||
if config[key]['path'] == self.mediafile.url:
|
if config[key]["path"] == self.mediafile.url:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -9,11 +9,12 @@ class MediafileSlide(ProjectorElement):
|
||||||
"""
|
"""
|
||||||
Slide definitions for Mediafile model.
|
Slide definitions for Mediafile model.
|
||||||
"""
|
"""
|
||||||
name = 'mediafiles/mediafile'
|
|
||||||
|
name = "mediafiles/mediafile"
|
||||||
|
|
||||||
def check_data(self):
|
def check_data(self):
|
||||||
if not Mediafile.objects.filter(pk=self.config_entry.get('id')).exists():
|
if not Mediafile.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||||
raise ProjectorException('File does not exist.')
|
raise ProjectorException("File does not exist.")
|
||||||
|
|
||||||
|
|
||||||
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
def get_projector_elements() -> Generator[Type[ProjectorElement], None, None]:
|
||||||
|
|
|
@ -10,9 +10,8 @@ from .models import Mediafile
|
||||||
|
|
||||||
|
|
||||||
class AngularCompatibleFileField(FileField):
|
class AngularCompatibleFileField(FileField):
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
if data == '':
|
if data == "":
|
||||||
return None
|
return None
|
||||||
return super(AngularCompatibleFileField, self).to_internal_value(data)
|
return super(AngularCompatibleFileField, self).to_internal_value(data)
|
||||||
|
|
||||||
|
@ -20,20 +19,17 @@ class AngularCompatibleFileField(FileField):
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
filetype = mimetypes.guess_type(value.path)[0]
|
filetype = mimetypes.guess_type(value.path)[0]
|
||||||
result = {
|
result = {"name": value.name, "type": filetype}
|
||||||
'name': value.name,
|
if filetype == "application/pdf":
|
||||||
'type': filetype
|
|
||||||
}
|
|
||||||
if filetype == 'application/pdf':
|
|
||||||
try:
|
try:
|
||||||
result['pages'] = PdfFileReader(open(value.path, 'rb')).getNumPages()
|
result["pages"] = PdfFileReader(open(value.path, "rb")).getNumPages()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# File was deleted from server. Set 'pages' to 0.
|
# File was deleted from server. Set 'pages' to 0.
|
||||||
result['pages'] = 0
|
result["pages"] = 0
|
||||||
except PdfReadError:
|
except PdfReadError:
|
||||||
# File could be encrypted but not be detected by PyPDF.
|
# File could be encrypted but not be detected by PyPDF.
|
||||||
result['pages'] = 0
|
result["pages"] = 0
|
||||||
result['encrypted'] = True
|
result["encrypted"] = True
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,6 +37,7 @@ class MediafileSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for mediafile.models.Mediafile objects.
|
Serializer for mediafile.models.Mediafile objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
media_url_prefix = SerializerMethodField()
|
media_url_prefix = SerializerMethodField()
|
||||||
filesize = SerializerMethodField()
|
filesize = SerializerMethodField()
|
||||||
|
|
||||||
|
@ -52,19 +49,20 @@ class MediafileSerializer(ModelSerializer):
|
||||||
super(MediafileSerializer, self).__init__(*args, **kwargs)
|
super(MediafileSerializer, self).__init__(*args, **kwargs)
|
||||||
self.serializer_field_mapping[dbmodels.FileField] = AngularCompatibleFileField
|
self.serializer_field_mapping[dbmodels.FileField] = AngularCompatibleFileField
|
||||||
if self.instance is not None:
|
if self.instance is not None:
|
||||||
self.fields['mediafile'].read_only = True
|
self.fields["mediafile"].read_only = True
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Mediafile
|
model = Mediafile
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
"id",
|
||||||
'title',
|
"title",
|
||||||
'mediafile',
|
"mediafile",
|
||||||
'media_url_prefix',
|
"media_url_prefix",
|
||||||
'uploader',
|
"uploader",
|
||||||
'filesize',
|
"filesize",
|
||||||
'hidden',
|
"hidden",
|
||||||
'timestamp',)
|
"timestamp",
|
||||||
|
)
|
||||||
|
|
||||||
def get_filesize(self, mediafile):
|
def get_filesize(self, mediafile):
|
||||||
return mediafile.get_filesize()
|
return mediafile.get_filesize()
|
||||||
|
|
|
@ -5,8 +5,11 @@ def get_permission_change_data(sender, permissions=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Yields all necessary collections if 'mediafiles.can_see' permission changes.
|
Yields all necessary collections if 'mediafiles.can_see' permission changes.
|
||||||
"""
|
"""
|
||||||
mediafiles_app = apps.get_app_config(app_label='mediafiles')
|
mediafiles_app = apps.get_app_config(app_label="mediafiles")
|
||||||
for permission in permissions:
|
for permission in permissions:
|
||||||
# There could be only one 'mediafiles.can_see' and then we want to return data.
|
# There could be only one 'mediafiles.can_see' and then we want to return data.
|
||||||
if permission.content_type.app_label == mediafiles_app.label and permission.codename == 'can_see':
|
if (
|
||||||
|
permission.content_type.app_label == mediafiles_app.label
|
||||||
|
and permission.codename == "can_see"
|
||||||
|
):
|
||||||
yield from mediafiles_app.get_startup_elements()
|
yield from mediafiles_app.get_startup_elements()
|
||||||
|
|
|
@ -9,6 +9,7 @@ from .models import Mediafile
|
||||||
|
|
||||||
# Viewsets for the REST API
|
# Viewsets for the REST API
|
||||||
|
|
||||||
|
|
||||||
class MediafileViewSet(ModelViewSet):
|
class MediafileViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint for mediafile objects.
|
API endpoint for mediafile objects.
|
||||||
|
@ -16,6 +17,7 @@ class MediafileViewSet(ModelViewSet):
|
||||||
There are the following views: metadata, list, retrieve, create,
|
There are the following views: metadata, list, retrieve, create,
|
||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = MediafileAccessPermissions()
|
access_permissions = MediafileAccessPermissions()
|
||||||
queryset = Mediafile.objects.all()
|
queryset = Mediafile.objects.all()
|
||||||
|
|
||||||
|
@ -23,20 +25,24 @@ class MediafileViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ('list', 'retrieve'):
|
if self.action in ("list", "retrieve"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action == 'metadata':
|
elif self.action == "metadata":
|
||||||
result = has_perm(self.request.user, 'mediafiles.can_see')
|
result = has_perm(self.request.user, "mediafiles.can_see")
|
||||||
elif self.action == 'create':
|
elif self.action == "create":
|
||||||
result = (has_perm(self.request.user, 'mediafiles.can_see') and
|
result = has_perm(self.request.user, "mediafiles.can_see") and has_perm(
|
||||||
has_perm(self.request.user, 'mediafiles.can_upload'))
|
self.request.user, "mediafiles.can_upload"
|
||||||
elif self.action in ('partial_update', 'update'):
|
)
|
||||||
result = (has_perm(self.request.user, 'mediafiles.can_see') and
|
elif self.action in ("partial_update", "update"):
|
||||||
has_perm(self.request.user, 'mediafiles.can_upload') and
|
result = (
|
||||||
has_perm(self.request.user, 'mediafiles.can_manage'))
|
has_perm(self.request.user, "mediafiles.can_see")
|
||||||
elif self.action == 'destroy':
|
and has_perm(self.request.user, "mediafiles.can_upload")
|
||||||
result = (has_perm(self.request.user, 'mediafiles.can_see') and
|
and has_perm(self.request.user, "mediafiles.can_manage")
|
||||||
has_perm(self.request.user, 'mediafiles.can_manage'))
|
)
|
||||||
|
elif self.action == "destroy":
|
||||||
|
result = has_perm(self.request.user, "mediafiles.can_see") and has_perm(
|
||||||
|
self.request.user, "mediafiles.can_manage"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
@ -46,13 +52,15 @@ class MediafileViewSet(ModelViewSet):
|
||||||
Customized view endpoint to upload a new file.
|
Customized view endpoint to upload a new file.
|
||||||
"""
|
"""
|
||||||
# Check permission to check if the uploader has to be changed.
|
# Check permission to check if the uploader has to be changed.
|
||||||
uploader_id = self.request.data.get('uploader_id')
|
uploader_id = self.request.data.get("uploader_id")
|
||||||
if (uploader_id and
|
if (
|
||||||
not has_perm(request.user, 'mediafiles.can_manage') and
|
uploader_id
|
||||||
str(self.request.user.pk) != str(uploader_id)):
|
and not has_perm(request.user, "mediafiles.can_manage")
|
||||||
|
and str(self.request.user.pk) != str(uploader_id)
|
||||||
|
):
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
if not self.request.data.get('mediafile'):
|
if not self.request.data.get("mediafile"):
|
||||||
raise ValidationError({'detail': 'You forgot to provide a file.'})
|
raise ValidationError({"detail": "You forgot to provide a file."})
|
||||||
return super().create(request, *args, **kwargs)
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
@ -77,9 +85,11 @@ def protected_serve(request, path, document_root=None, show_indexes=False):
|
||||||
except Mediafile.DoesNotExist:
|
except Mediafile.DoesNotExist:
|
||||||
return HttpResponseNotFound(content="Not found.")
|
return HttpResponseNotFound(content="Not found.")
|
||||||
|
|
||||||
can_see = has_perm(request.user, 'mediafiles.can_see')
|
can_see = has_perm(request.user, "mediafiles.can_see")
|
||||||
is_special_file = mediafile.is_logo() or mediafile.is_font()
|
is_special_file = mediafile.is_logo() or mediafile.is_font()
|
||||||
is_hidden_but_no_perms = mediafile.hidden and not has_perm(request.user, 'mediafiles.can_see_hidden')
|
is_hidden_but_no_perms = mediafile.hidden and not has_perm(
|
||||||
|
request.user, "mediafiles.can_see_hidden"
|
||||||
|
)
|
||||||
|
|
||||||
if not is_special_file and (not can_see or is_hidden_but_no_perms):
|
if not is_special_file and (not can_see or is_hidden_but_no_perms):
|
||||||
return HttpResponseForbidden(content="Forbidden.")
|
return HttpResponseForbidden(content="Forbidden.")
|
||||||
|
|
|
@ -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.
|
Access permissions container for Motion and MotionViewSet.
|
||||||
"""
|
"""
|
||||||
base_permission = 'motions.can_see'
|
|
||||||
|
base_permission = "motions.can_see"
|
||||||
|
|
||||||
async def get_restricted_data(
|
async def get_restricted_data(
|
||||||
self,
|
self, full_data: List[Dict[str, Any]], user_id: int
|
||||||
full_data: List[Dict[str, Any]],
|
) -> List[Dict[str, Any]]:
|
||||||
user_id: int) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared for
|
Returns the restricted serialized data for the instance prepared for
|
||||||
the user. Removes motion if the user has not the permission to see
|
the user. Removes motion if the user has not the permission to see
|
||||||
|
@ -23,33 +23,37 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
||||||
personal notes.
|
personal notes.
|
||||||
"""
|
"""
|
||||||
# Parse data.
|
# Parse data.
|
||||||
if await async_has_perm(user_id, 'motions.can_see'):
|
if await async_has_perm(user_id, "motions.can_see"):
|
||||||
# TODO: Refactor this after personal_notes system is refactored.
|
# TODO: Refactor this after personal_notes system is refactored.
|
||||||
data = []
|
data = []
|
||||||
for full in full_data:
|
for full in full_data:
|
||||||
# Check if user is submitter of this motion.
|
# Check if user is submitter of this motion.
|
||||||
if user_id:
|
if user_id:
|
||||||
is_submitter = user_id in [
|
is_submitter = user_id in [
|
||||||
submitter['user_id'] for submitter in full.get('submitters', [])]
|
submitter["user_id"] for submitter in full.get("submitters", [])
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
# Anonymous users can not be submitters.
|
# Anonymous users can not be submitters.
|
||||||
is_submitter = False
|
is_submitter = False
|
||||||
|
|
||||||
# Check see permission for this motion.
|
# Check see permission for this motion.
|
||||||
required_permission_to_see = full['state_required_permission_to_see']
|
required_permission_to_see = full["state_required_permission_to_see"]
|
||||||
permission = (
|
permission = (
|
||||||
not required_permission_to_see or
|
not required_permission_to_see
|
||||||
await async_has_perm(user_id, required_permission_to_see) or
|
or await async_has_perm(user_id, required_permission_to_see)
|
||||||
await async_has_perm(user_id, 'motions.can_manage') or
|
or await async_has_perm(user_id, "motions.can_manage")
|
||||||
is_submitter)
|
or is_submitter
|
||||||
|
)
|
||||||
|
|
||||||
# Parse single motion.
|
# Parse single motion.
|
||||||
if permission:
|
if permission:
|
||||||
full_copy = deepcopy(full)
|
full_copy = deepcopy(full)
|
||||||
full_copy['comments'] = []
|
full_copy["comments"] = []
|
||||||
for comment in full['comments']:
|
for comment in full["comments"]:
|
||||||
if await async_in_some_groups(user_id, comment['read_groups_id']):
|
if await async_in_some_groups(
|
||||||
full_copy['comments'].append(comment)
|
user_id, comment["read_groups_id"]
|
||||||
|
):
|
||||||
|
full_copy["comments"].append(comment)
|
||||||
data.append(full_copy)
|
data.append(full_copy)
|
||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
|
@ -61,23 +65,23 @@ class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
|
||||||
"""
|
"""
|
||||||
Access permissions container for MotionChangeRecommendation and MotionChangeRecommendationViewSet.
|
Access permissions container for MotionChangeRecommendation and MotionChangeRecommendationViewSet.
|
||||||
"""
|
"""
|
||||||
base_permission = 'motions.can_see'
|
|
||||||
|
base_permission = "motions.can_see"
|
||||||
|
|
||||||
async def get_restricted_data(
|
async def get_restricted_data(
|
||||||
self,
|
self, full_data: List[Dict[str, Any]], user_id: int
|
||||||
full_data: List[Dict[str, Any]],
|
) -> List[Dict[str, Any]]:
|
||||||
user_id: int) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
"""
|
||||||
Removes change recommendations if they are internal and the user has
|
Removes change recommendations if they are internal and the user has
|
||||||
not the can_manage permission. To see change recommendation the user needs
|
not the can_manage permission. To see change recommendation the user needs
|
||||||
the can_see permission.
|
the can_see permission.
|
||||||
"""
|
"""
|
||||||
# Parse data.
|
# Parse data.
|
||||||
if await async_has_perm(user_id, 'motions.can_see'):
|
if await async_has_perm(user_id, "motions.can_see"):
|
||||||
has_manage_perms = await async_has_perm(user_id, 'motion.can_manage')
|
has_manage_perms = await async_has_perm(user_id, "motion.can_manage")
|
||||||
data = []
|
data = []
|
||||||
for full in full_data:
|
for full in full_data:
|
||||||
if not full['internal'] or has_manage_perms:
|
if not full["internal"] or has_manage_perms:
|
||||||
data.append(full)
|
data.append(full)
|
||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
|
@ -89,22 +93,22 @@ class MotionCommentSectionAccessPermissions(BaseAccessPermissions):
|
||||||
"""
|
"""
|
||||||
Access permissions container for MotionCommentSection and MotionCommentSectionViewSet.
|
Access permissions container for MotionCommentSection and MotionCommentSectionViewSet.
|
||||||
"""
|
"""
|
||||||
base_permission = 'motions.can_see'
|
|
||||||
|
base_permission = "motions.can_see"
|
||||||
|
|
||||||
async def get_restricted_data(
|
async def get_restricted_data(
|
||||||
self,
|
self, full_data: List[Dict[str, Any]], user_id: int
|
||||||
full_data: List[Dict[str, Any]],
|
) -> List[Dict[str, Any]]:
|
||||||
user_id: int) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
"""
|
||||||
If the user has manage rights, he can see all sections. If not all sections
|
If the user has manage rights, he can see all sections. If not all sections
|
||||||
will be removed, when the user is not in at least one of the read_groups.
|
will be removed, when the user is not in at least one of the read_groups.
|
||||||
"""
|
"""
|
||||||
data: List[Dict[str, Any]] = []
|
data: List[Dict[str, Any]] = []
|
||||||
if await async_has_perm(user_id, 'motions.can_manage'):
|
if await async_has_perm(user_id, "motions.can_manage"):
|
||||||
data = full_data
|
data = full_data
|
||||||
else:
|
else:
|
||||||
for full in full_data:
|
for full in full_data:
|
||||||
read_groups = full.get('read_groups_id', [])
|
read_groups = full.get("read_groups_id", [])
|
||||||
if await async_in_some_groups(user_id, read_groups):
|
if await async_in_some_groups(user_id, read_groups):
|
||||||
data.append(full)
|
data.append(full)
|
||||||
return data
|
return data
|
||||||
|
@ -114,25 +118,29 @@ class StatuteParagraphAccessPermissions(BaseAccessPermissions):
|
||||||
"""
|
"""
|
||||||
Access permissions container for StatuteParagraph and StatuteParagraphViewSet.
|
Access permissions container for StatuteParagraph and StatuteParagraphViewSet.
|
||||||
"""
|
"""
|
||||||
base_permission = 'motions.can_see'
|
|
||||||
|
base_permission = "motions.can_see"
|
||||||
|
|
||||||
|
|
||||||
class CategoryAccessPermissions(BaseAccessPermissions):
|
class CategoryAccessPermissions(BaseAccessPermissions):
|
||||||
"""
|
"""
|
||||||
Access permissions container for Category and CategoryViewSet.
|
Access permissions container for Category and CategoryViewSet.
|
||||||
"""
|
"""
|
||||||
base_permission = 'motions.can_see'
|
|
||||||
|
base_permission = "motions.can_see"
|
||||||
|
|
||||||
|
|
||||||
class MotionBlockAccessPermissions(BaseAccessPermissions):
|
class MotionBlockAccessPermissions(BaseAccessPermissions):
|
||||||
"""
|
"""
|
||||||
Access permissions container for Category and CategoryViewSet.
|
Access permissions container for Category and CategoryViewSet.
|
||||||
"""
|
"""
|
||||||
base_permission = 'motions.can_see'
|
|
||||||
|
base_permission = "motions.can_see"
|
||||||
|
|
||||||
|
|
||||||
class WorkflowAccessPermissions(BaseAccessPermissions):
|
class WorkflowAccessPermissions(BaseAccessPermissions):
|
||||||
"""
|
"""
|
||||||
Access permissions container for Workflow and WorkflowViewSet.
|
Access permissions container for Workflow and WorkflowViewSet.
|
||||||
"""
|
"""
|
||||||
base_permission = 'motions.can_see'
|
|
||||||
|
base_permission = "motions.can_see"
|
||||||
|
|
|
@ -7,8 +7,8 @@ from ..utils.projector import register_projector_elements
|
||||||
|
|
||||||
|
|
||||||
class MotionsAppConfig(AppConfig):
|
class MotionsAppConfig(AppConfig):
|
||||||
name = 'openslides.motions'
|
name = "openslides.motions"
|
||||||
verbose_name = 'OpenSlides Motion'
|
verbose_name = "OpenSlides Motion"
|
||||||
angular_site_module = True
|
angular_site_module = True
|
||||||
angular_projector_module = True
|
angular_projector_module = True
|
||||||
|
|
||||||
|
@ -17,10 +17,7 @@ class MotionsAppConfig(AppConfig):
|
||||||
from openslides.core.signals import permission_change
|
from openslides.core.signals import permission_change
|
||||||
from openslides.utils.rest_api import router
|
from openslides.utils.rest_api import router
|
||||||
from .projector import get_projector_elements
|
from .projector import get_projector_elements
|
||||||
from .signals import (
|
from .signals import create_builtin_workflows, get_permission_change_data
|
||||||
create_builtin_workflows,
|
|
||||||
get_permission_change_data,
|
|
||||||
)
|
|
||||||
from . import serializers # noqa
|
from . import serializers # noqa
|
||||||
from .views import (
|
from .views import (
|
||||||
CategoryViewSet,
|
CategoryViewSet,
|
||||||
|
@ -40,29 +37,49 @@ class MotionsAppConfig(AppConfig):
|
||||||
|
|
||||||
# Connect signals.
|
# Connect signals.
|
||||||
post_migrate.connect(
|
post_migrate.connect(
|
||||||
create_builtin_workflows,
|
create_builtin_workflows, dispatch_uid="motion_create_builtin_workflows"
|
||||||
dispatch_uid='motion_create_builtin_workflows')
|
)
|
||||||
permission_change.connect(
|
permission_change.connect(
|
||||||
get_permission_change_data,
|
get_permission_change_data,
|
||||||
dispatch_uid='motions_get_permission_change_data')
|
dispatch_uid="motions_get_permission_change_data",
|
||||||
|
)
|
||||||
|
|
||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register(self.get_model('Category').get_collection_string(), CategoryViewSet)
|
router.register(
|
||||||
router.register(self.get_model('StatuteParagraph').get_collection_string(), StatuteParagraphViewSet)
|
self.get_model("Category").get_collection_string(), CategoryViewSet
|
||||||
router.register(self.get_model('Motion').get_collection_string(), MotionViewSet)
|
)
|
||||||
router.register(self.get_model('MotionBlock').get_collection_string(), MotionBlockViewSet)
|
router.register(
|
||||||
router.register(self.get_model('MotionCommentSection').get_collection_string(), MotionCommentSectionViewSet)
|
self.get_model("StatuteParagraph").get_collection_string(),
|
||||||
router.register(self.get_model('Workflow').get_collection_string(), WorkflowViewSet)
|
StatuteParagraphViewSet,
|
||||||
router.register(self.get_model('MotionChangeRecommendation').get_collection_string(),
|
)
|
||||||
MotionChangeRecommendationViewSet)
|
router.register(self.get_model("Motion").get_collection_string(), MotionViewSet)
|
||||||
router.register(self.get_model('MotionPoll').get_collection_string(), MotionPollViewSet)
|
router.register(
|
||||||
router.register(self.get_model('State').get_collection_string(), StateViewSet)
|
self.get_model("MotionBlock").get_collection_string(), MotionBlockViewSet
|
||||||
|
)
|
||||||
|
router.register(
|
||||||
|
self.get_model("MotionCommentSection").get_collection_string(),
|
||||||
|
MotionCommentSectionViewSet,
|
||||||
|
)
|
||||||
|
router.register(
|
||||||
|
self.get_model("Workflow").get_collection_string(), WorkflowViewSet
|
||||||
|
)
|
||||||
|
router.register(
|
||||||
|
self.get_model("MotionChangeRecommendation").get_collection_string(),
|
||||||
|
MotionChangeRecommendationViewSet,
|
||||||
|
)
|
||||||
|
router.register(
|
||||||
|
self.get_model("MotionPoll").get_collection_string(), MotionPollViewSet
|
||||||
|
)
|
||||||
|
router.register(self.get_model("State").get_collection_string(), StateViewSet)
|
||||||
|
|
||||||
# Register required_users
|
# Register required_users
|
||||||
required_user.add_collection_string(self.get_model('Motion').get_collection_string(), required_users)
|
required_user.add_collection_string(
|
||||||
|
self.get_model("Motion").get_collection_string(), required_users
|
||||||
|
)
|
||||||
|
|
||||||
def get_config_variables(self):
|
def get_config_variables(self):
|
||||||
from .config_variables import get_config_variables
|
from .config_variables import get_config_variables
|
||||||
|
|
||||||
return get_config_variables()
|
return get_config_variables()
|
||||||
|
|
||||||
def get_startup_elements(self):
|
def get_startup_elements(self):
|
||||||
|
@ -70,8 +87,15 @@ class MotionsAppConfig(AppConfig):
|
||||||
Yields all Cachables required on startup i. e. opening the websocket
|
Yields all Cachables required on startup i. e. opening the websocket
|
||||||
connection.
|
connection.
|
||||||
"""
|
"""
|
||||||
for model_name in ('Category', 'StatuteParagraph', 'Motion', 'MotionBlock',
|
for model_name in (
|
||||||
'Workflow', 'MotionChangeRecommendation', 'MotionCommentSection'):
|
"Category",
|
||||||
|
"StatuteParagraph",
|
||||||
|
"Motion",
|
||||||
|
"MotionBlock",
|
||||||
|
"Workflow",
|
||||||
|
"MotionChangeRecommendation",
|
||||||
|
"MotionCommentSection",
|
||||||
|
):
|
||||||
yield self.get_model(model_name)
|
yield self.get_model(model_name)
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,6 +105,8 @@ def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||||
any motion if request_user can see motions. This function may return an
|
any motion if request_user can see motions. This function may return an
|
||||||
empty set.
|
empty set.
|
||||||
"""
|
"""
|
||||||
submitters_supporters = set([submitter['user_id'] for submitter in element['submitters']])
|
submitters_supporters = set(
|
||||||
submitters_supporters.update(element['supporters_id'])
|
[submitter["user_id"] for submitter in element["submitters"]]
|
||||||
|
)
|
||||||
|
submitters_supporters.update(element["supporters_id"])
|
||||||
return submitters_supporters
|
return submitters_supporters
|
||||||
|
|
|
@ -11,8 +11,10 @@ def get_workflow_choices():
|
||||||
Returns a list of all workflows to be used as choices for the config variable
|
Returns a list of all workflows to be used as choices for the config variable
|
||||||
'motions_workflow'. Each list item contains the pk and the display name.
|
'motions_workflow'. Each list item contains the pk and the display name.
|
||||||
"""
|
"""
|
||||||
return [{'value': str(workflow.pk), 'display_name': workflow.name}
|
return [
|
||||||
for workflow in Workflow.objects.all()]
|
{"value": str(workflow.pk), "display_name": workflow.name}
|
||||||
|
for workflow in Workflow.objects.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_config_variables():
|
def get_config_variables():
|
||||||
|
@ -26,300 +28,339 @@ def get_config_variables():
|
||||||
|
|
||||||
# General
|
# General
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_workflow',
|
name="motions_workflow",
|
||||||
default_value='1',
|
default_value="1",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='Workflow of new motions',
|
label="Workflow of new motions",
|
||||||
choices=get_workflow_choices,
|
choices=get_workflow_choices,
|
||||||
weight=310,
|
weight=310,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_statute_amendments_workflow',
|
name="motions_statute_amendments_workflow",
|
||||||
default_value='1',
|
default_value="1",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='Workflow of new statute amendments',
|
label="Workflow of new statute amendments",
|
||||||
choices=get_workflow_choices,
|
choices=get_workflow_choices,
|
||||||
weight=312,
|
weight=312,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_identifier',
|
name="motions_identifier",
|
||||||
default_value='per_category',
|
default_value="per_category",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='Identifier',
|
label="Identifier",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': 'per_category', 'display_name': 'Numbered per category'},
|
{"value": "per_category", "display_name": "Numbered per category"},
|
||||||
{'value': 'serially_numbered', 'display_name': 'Serially numbered'},
|
{"value": "serially_numbered", "display_name": "Serially numbered"},
|
||||||
{'value': 'manually', 'display_name': 'Set it manually'}),
|
{"value": "manually", "display_name": "Set it manually"},
|
||||||
|
),
|
||||||
weight=315,
|
weight=315,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_preamble',
|
name="motions_preamble",
|
||||||
default_value='The assembly may decide:',
|
default_value="The assembly may decide:",
|
||||||
label='Motion preamble',
|
label="Motion preamble",
|
||||||
weight=320,
|
weight=320,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_default_line_numbering',
|
name="motions_default_line_numbering",
|
||||||
default_value='none',
|
default_value="none",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='Default line numbering',
|
label="Default line numbering",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': 'outside', 'display_name': 'outside'},
|
{"value": "outside", "display_name": "outside"},
|
||||||
{'value': 'inline', 'display_name': 'inline'},
|
{"value": "inline", "display_name": "inline"},
|
||||||
{'value': 'none', 'display_name': 'Disabled'}),
|
{"value": "none", "display_name": "Disabled"},
|
||||||
|
),
|
||||||
weight=322,
|
weight=322,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_line_length',
|
name="motions_line_length",
|
||||||
default_value=90,
|
default_value=90,
|
||||||
input_type='integer',
|
input_type="integer",
|
||||||
label='Line length',
|
label="Line length",
|
||||||
help_text='The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40',
|
help_text="The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40",
|
||||||
weight=323,
|
weight=323,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='General',
|
subgroup="General",
|
||||||
validators=(MinValueValidator(40),))
|
validators=(MinValueValidator(40),),
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_disable_reason_on_projector',
|
name="motions_disable_reason_on_projector",
|
||||||
default_value=False,
|
default_value=False,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Hide reason on projector',
|
label="Hide reason on projector",
|
||||||
weight=325,
|
weight=325,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_disable_sidebox_on_projector',
|
name="motions_disable_sidebox_on_projector",
|
||||||
default_value=False,
|
default_value=False,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Hide meta information box on projector',
|
label="Hide meta information box on projector",
|
||||||
weight=326,
|
weight=326,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_disable_recommendation_on_projector',
|
name="motions_disable_recommendation_on_projector",
|
||||||
default_value=False,
|
default_value=False,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Hide recommendation on projector',
|
label="Hide recommendation on projector",
|
||||||
weight=327,
|
weight=327,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_stop_submitting',
|
name="motions_stop_submitting",
|
||||||
default_value=False,
|
default_value=False,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Stop submitting new motions by non-staff users',
|
label="Stop submitting new motions by non-staff users",
|
||||||
weight=331,
|
weight=331,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_recommendations_by',
|
name="motions_recommendations_by",
|
||||||
default_value='',
|
default_value="",
|
||||||
label='Name of recommender',
|
label="Name of recommender",
|
||||||
help_text='Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.',
|
help_text="Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.",
|
||||||
weight=332,
|
weight=332,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_statute_recommendations_by',
|
name="motions_statute_recommendations_by",
|
||||||
default_value='',
|
default_value="",
|
||||||
label='Name of recommender for statute amendments',
|
label="Name of recommender for statute amendments",
|
||||||
help_text='Will be displayed as label before selected recommendation in statute amendments. ' +
|
help_text="Will be displayed as label before selected recommendation in statute amendments. "
|
||||||
'Use an empty value to disable the recommendation system for statute amendments.',
|
+ "Use an empty value to disable the recommendation system for statute amendments.",
|
||||||
weight=333,
|
weight=333,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_recommendation_text_mode',
|
name="motions_recommendation_text_mode",
|
||||||
default_value='original',
|
default_value="original",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='Default text version for change recommendations',
|
label="Default text version for change recommendations",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': 'original', 'display_name': 'Original version'},
|
{"value": "original", "display_name": "Original version"},
|
||||||
{'value': 'changed', 'display_name': 'Changed version'},
|
{"value": "changed", "display_name": "Changed version"},
|
||||||
{'value': 'diff', 'display_name': 'Diff version'},
|
{"value": "diff", "display_name": "Diff version"},
|
||||||
{'value': 'agreed', 'display_name': 'Final version'}),
|
{"value": "agreed", "display_name": "Final version"},
|
||||||
|
),
|
||||||
weight=334,
|
weight=334,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='General')
|
subgroup="General",
|
||||||
|
)
|
||||||
|
|
||||||
# Amendments
|
# Amendments
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_statutes_enabled',
|
name="motions_statutes_enabled",
|
||||||
default_value=False,
|
default_value=False,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Activate statute amendments',
|
label="Activate statute amendments",
|
||||||
weight=335,
|
weight=335,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Amendments')
|
subgroup="Amendments",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_amendments_enabled',
|
name="motions_amendments_enabled",
|
||||||
default_value=False,
|
default_value=False,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Activate amendments',
|
label="Activate amendments",
|
||||||
weight=336,
|
weight=336,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Amendments')
|
subgroup="Amendments",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_amendments_main_table',
|
name="motions_amendments_main_table",
|
||||||
default_value=False,
|
default_value=False,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Show amendments together with motions',
|
label="Show amendments together with motions",
|
||||||
weight=337,
|
weight=337,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Amendments')
|
subgroup="Amendments",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_amendments_prefix',
|
name="motions_amendments_prefix",
|
||||||
default_value='-',
|
default_value="-",
|
||||||
label='Prefix for the identifier for amendments',
|
label="Prefix for the identifier for amendments",
|
||||||
weight=340,
|
weight=340,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Amendments')
|
subgroup="Amendments",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_amendments_text_mode',
|
name="motions_amendments_text_mode",
|
||||||
default_value='freestyle',
|
default_value="freestyle",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='How to create new amendments',
|
label="How to create new amendments",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': 'freestyle', 'display_name': 'Empty text field'},
|
{"value": "freestyle", "display_name": "Empty text field"},
|
||||||
{'value': 'fulltext', 'display_name': 'Edit the whole motion text'},
|
{"value": "fulltext", "display_name": "Edit the whole motion text"},
|
||||||
{'value': 'paragraph', 'display_name': 'Paragraph-based, Diff-enabled'},
|
{"value": "paragraph", "display_name": "Paragraph-based, Diff-enabled"},
|
||||||
),
|
),
|
||||||
weight=342,
|
weight=342,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Amendments')
|
subgroup="Amendments",
|
||||||
|
)
|
||||||
|
|
||||||
# Supporters
|
# Supporters
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_min_supporters',
|
name="motions_min_supporters",
|
||||||
default_value=0,
|
default_value=0,
|
||||||
input_type='integer',
|
input_type="integer",
|
||||||
label='Number of (minimum) required supporters for a motion',
|
label="Number of (minimum) required supporters for a motion",
|
||||||
help_text='Choose 0 to disable the supporting system.',
|
help_text="Choose 0 to disable the supporting system.",
|
||||||
weight=345,
|
weight=345,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Supporters',
|
subgroup="Supporters",
|
||||||
validators=(MinValueValidator(0),))
|
validators=(MinValueValidator(0),),
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_remove_supporters',
|
name="motions_remove_supporters",
|
||||||
default_value=False,
|
default_value=False,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Remove all supporters of a motion if a submitter edits his motion in early state',
|
label="Remove all supporters of a motion if a submitter edits his motion in early state",
|
||||||
weight=350,
|
weight=350,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Supporters')
|
subgroup="Supporters",
|
||||||
|
)
|
||||||
|
|
||||||
# Voting and ballot papers
|
# Voting and ballot papers
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_poll_100_percent_base',
|
name="motions_poll_100_percent_base",
|
||||||
default_value='YES_NO_ABSTAIN',
|
default_value="YES_NO_ABSTAIN",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='The 100 % base of a voting result consists of',
|
label="The 100 % base of a voting result consists of",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': 'YES_NO_ABSTAIN', 'display_name': 'Yes/No/Abstain'},
|
{"value": "YES_NO_ABSTAIN", "display_name": "Yes/No/Abstain"},
|
||||||
{'value': 'YES_NO', 'display_name': 'Yes/No'},
|
{"value": "YES_NO", "display_name": "Yes/No"},
|
||||||
{'value': 'VALID', 'display_name': 'All valid ballots'},
|
{"value": "VALID", "display_name": "All valid ballots"},
|
||||||
{'value': 'CAST', 'display_name': 'All casted ballots'},
|
{"value": "CAST", "display_name": "All casted ballots"},
|
||||||
{'value': 'DISABLED', 'display_name': 'Disabled (no percents)'}
|
{"value": "DISABLED", "display_name": "Disabled (no percents)"},
|
||||||
),
|
),
|
||||||
weight=355,
|
weight=355,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Voting and ballot papers')
|
subgroup="Voting and ballot papers",
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: Add server side validation of the choices.
|
# TODO: Add server side validation of the choices.
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_poll_default_majority_method',
|
name="motions_poll_default_majority_method",
|
||||||
default_value=majorityMethods[0]['value'],
|
default_value=majorityMethods[0]["value"],
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
choices=majorityMethods,
|
choices=majorityMethods,
|
||||||
label='Required majority',
|
label="Required majority",
|
||||||
help_text='Default method to check whether a motion has reached the required majority.',
|
help_text="Default method to check whether a motion has reached the required majority.",
|
||||||
weight=357,
|
weight=357,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Voting and ballot papers')
|
subgroup="Voting and ballot papers",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_pdf_ballot_papers_selection',
|
name="motions_pdf_ballot_papers_selection",
|
||||||
default_value='CUSTOM_NUMBER',
|
default_value="CUSTOM_NUMBER",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='Number of ballot papers (selection)',
|
label="Number of ballot papers (selection)",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': 'NUMBER_OF_DELEGATES', 'display_name': 'Number of all delegates'},
|
{"value": "NUMBER_OF_DELEGATES", "display_name": "Number of all delegates"},
|
||||||
{'value': 'NUMBER_OF_ALL_PARTICIPANTS', 'display_name': 'Number of all participants'},
|
{
|
||||||
{'value': 'CUSTOM_NUMBER', 'display_name': 'Use the following custom number'}),
|
"value": "NUMBER_OF_ALL_PARTICIPANTS",
|
||||||
|
"display_name": "Number of all participants",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "CUSTOM_NUMBER",
|
||||||
|
"display_name": "Use the following custom number",
|
||||||
|
},
|
||||||
|
),
|
||||||
weight=360,
|
weight=360,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Voting and ballot papers')
|
subgroup="Voting and ballot papers",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_pdf_ballot_papers_number',
|
name="motions_pdf_ballot_papers_number",
|
||||||
default_value=8,
|
default_value=8,
|
||||||
input_type='integer',
|
input_type="integer",
|
||||||
label='Custom number of ballot papers',
|
label="Custom number of ballot papers",
|
||||||
weight=365,
|
weight=365,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Voting and ballot papers',
|
subgroup="Voting and ballot papers",
|
||||||
validators=(MinValueValidator(1),))
|
validators=(MinValueValidator(1),),
|
||||||
|
)
|
||||||
|
|
||||||
# PDF and DOCX export
|
# PDF and DOCX export
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_export_title',
|
name="motions_export_title",
|
||||||
default_value='Motions',
|
default_value="Motions",
|
||||||
label='Title for PDF and DOCX documents (all motions)',
|
label="Title for PDF and DOCX documents (all motions)",
|
||||||
weight=370,
|
weight=370,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Export')
|
subgroup="Export",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_export_preamble',
|
name="motions_export_preamble",
|
||||||
default_value='',
|
default_value="",
|
||||||
label='Preamble text for PDF and DOCX documents (all motions)',
|
label="Preamble text for PDF and DOCX documents (all motions)",
|
||||||
weight=375,
|
weight=375,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Export')
|
subgroup="Export",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_export_category_sorting',
|
name="motions_export_category_sorting",
|
||||||
default_value='prefix',
|
default_value="prefix",
|
||||||
input_type='choice',
|
input_type="choice",
|
||||||
label='Sort categories by',
|
label="Sort categories by",
|
||||||
choices=(
|
choices=(
|
||||||
{'value': 'prefix', 'display_name': 'Prefix'},
|
{"value": "prefix", "display_name": "Prefix"},
|
||||||
{'value': 'name', 'display_name': 'Name'}),
|
{"value": "name", "display_name": "Name"},
|
||||||
|
),
|
||||||
weight=380,
|
weight=380,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Export')
|
subgroup="Export",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name='motions_export_sequential_number',
|
name="motions_export_sequential_number",
|
||||||
default_value=True,
|
default_value=True,
|
||||||
input_type='boolean',
|
input_type="boolean",
|
||||||
label='Include the sequential number in PDF and DOCX',
|
label="Include the sequential number in PDF and DOCX",
|
||||||
weight=385,
|
weight=385,
|
||||||
group='Motions',
|
group="Motions",
|
||||||
subgroup='Export')
|
subgroup="Export",
|
||||||
|
)
|
||||||
|
|
|
@ -3,4 +3,5 @@ from openslides.utils.exceptions import OpenSlidesError
|
||||||
|
|
||||||
class WorkflowError(OpenSlidesError):
|
class WorkflowError(OpenSlidesError):
|
||||||
"""Exception raised when errors in a workflow or state accure."""
|
"""Exception raised when errors in a workflow or state accure."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -15,200 +15,344 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mediafiles', '0001_initial'),
|
("mediafiles", "0001_initial"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('core', '0001_initial'),
|
("core", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Category',
|
name="Category",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('name', models.CharField(max_length=255)),
|
"id",
|
||||||
('prefix', models.CharField(blank=True, max_length=32)),
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=255)),
|
||||||
|
("prefix", models.CharField(blank=True, max_length=32)),
|
||||||
|
],
|
||||||
|
options={"ordering": ["prefix"], "default_permissions": ()},
|
||||||
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Motion",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"identifier",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=255, null=True, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("identifier_number", models.IntegerField(null=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'ordering': ['prefix'],
|
"verbose_name": "Motion",
|
||||||
'default_permissions': (),
|
"permissions": (
|
||||||
|
("can_see", "Can see motions"),
|
||||||
|
("can_create", "Can create motions"),
|
||||||
|
("can_support", "Can support motions"),
|
||||||
|
("can_manage", "Can manage motions"),
|
||||||
|
),
|
||||||
|
"ordering": ("identifier",),
|
||||||
|
"default_permissions": (),
|
||||||
},
|
},
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Motion',
|
name="MotionLog",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('identifier', models.CharField(blank=True, max_length=255, null=True, unique=True)),
|
"id",
|
||||||
('identifier_number', models.IntegerField(null=True)),
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("message_list", jsonfield.fields.JSONField()),
|
||||||
|
("time", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"motion",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="log_messages",
|
||||||
|
to="motions.Motion",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"person",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={"ordering": ["-time"], "default_permissions": ()},
|
||||||
'verbose_name': 'Motion',
|
|
||||||
'permissions': (
|
|
||||||
('can_see', 'Can see motions'),
|
|
||||||
('can_create', 'Can create motions'),
|
|
||||||
('can_support', 'Can support motions'),
|
|
||||||
('can_manage', 'Can manage motions')),
|
|
||||||
'ordering': ('identifier',),
|
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='MotionLog',
|
name="MotionOption",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('message_list', jsonfield.fields.JSONField()),
|
"id",
|
||||||
('time', models.DateTimeField(auto_now=True)),
|
models.AutoField(
|
||||||
('motion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='log_messages', to='motions.Motion')),
|
auto_created=True,
|
||||||
('person', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'ordering': ['-time'],
|
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='MotionOption',
|
name="MotionPoll",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"votesvalid",
|
||||||
|
openslides.utils.models.MinMaxIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"votesinvalid",
|
||||||
|
openslides.utils.models.MinMaxIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"votescast",
|
||||||
|
openslides.utils.models.MinMaxIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"motion",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="polls",
|
||||||
|
to="motions.Motion",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='MotionPoll',
|
name="MotionVersion",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('votesvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)),
|
"id",
|
||||||
('votesinvalid', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)),
|
models.AutoField(
|
||||||
('votescast', openslides.utils.models.MinMaxIntegerField(blank=True, null=True)),
|
auto_created=True,
|
||||||
('motion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polls', to='motions.Motion')),
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("version_number", models.PositiveIntegerField(default=1)),
|
||||||
|
("title", models.CharField(max_length=255)),
|
||||||
|
("text", models.TextField()),
|
||||||
|
("reason", models.TextField(blank=True, null=True)),
|
||||||
|
("creation_time", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"motion",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="versions",
|
||||||
|
to="motions.Motion",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='MotionVersion',
|
name="MotionVote",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('version_number', models.PositiveIntegerField(default=1)),
|
"id",
|
||||||
('title', models.CharField(max_length=255)),
|
models.AutoField(
|
||||||
('text', models.TextField()),
|
auto_created=True,
|
||||||
('reason', models.TextField(blank=True, null=True)),
|
primary_key=True,
|
||||||
('creation_time', models.DateTimeField(auto_now=True)),
|
serialize=False,
|
||||||
('motion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='motions.Motion')),
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("weight", models.IntegerField(default=1, null=True)),
|
||||||
|
("value", models.CharField(max_length=255, null=True)),
|
||||||
|
(
|
||||||
|
"option",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="motions.MotionOption",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='MotionVote',
|
name="State",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('weight', models.IntegerField(default=1, null=True)),
|
"id",
|
||||||
('value', models.CharField(max_length=255, null=True)),
|
models.AutoField(
|
||||||
('option', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='motions.MotionOption')),
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=255)),
|
||||||
|
("action_word", models.CharField(max_length=255)),
|
||||||
|
("css_class", models.CharField(default="primary", max_length=255)),
|
||||||
|
(
|
||||||
|
"required_permission_to_see",
|
||||||
|
models.CharField(blank=True, max_length=255),
|
||||||
|
),
|
||||||
|
("allow_support", models.BooleanField(default=False)),
|
||||||
|
("allow_create_poll", models.BooleanField(default=False)),
|
||||||
|
("allow_submitter_edit", models.BooleanField(default=False)),
|
||||||
|
("versioning", models.BooleanField(default=False)),
|
||||||
|
("leave_old_version_active", models.BooleanField(default=False)),
|
||||||
|
("dont_set_identifier", models.BooleanField(default=False)),
|
||||||
|
("next_states", models.ManyToManyField(to="motions.State")),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='State',
|
name="Workflow",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('name', models.CharField(max_length=255)),
|
"id",
|
||||||
('action_word', models.CharField(max_length=255)),
|
models.AutoField(
|
||||||
('css_class', models.CharField(default='primary', max_length=255)),
|
auto_created=True,
|
||||||
('required_permission_to_see', models.CharField(blank=True, max_length=255)),
|
primary_key=True,
|
||||||
('allow_support', models.BooleanField(default=False)),
|
serialize=False,
|
||||||
('allow_create_poll', models.BooleanField(default=False)),
|
verbose_name="ID",
|
||||||
('allow_submitter_edit', models.BooleanField(default=False)),
|
),
|
||||||
('versioning', models.BooleanField(default=False)),
|
),
|
||||||
('leave_old_version_active', models.BooleanField(default=False)),
|
("name", models.CharField(max_length=255)),
|
||||||
('dont_set_identifier', models.BooleanField(default=False)),
|
(
|
||||||
('next_states', models.ManyToManyField(to='motions.State')),
|
"first_state",
|
||||||
|
models.OneToOneField(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
|
to="motions.State",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Workflow',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=255)),
|
|
||||||
('first_state', models.OneToOneField(
|
|
||||||
null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='motions.State')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='state',
|
model_name="state",
|
||||||
name='workflow',
|
name="workflow",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='states', to='motions.Workflow'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='motionoption',
|
|
||||||
name='poll',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='motions.MotionPoll'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='motion',
|
|
||||||
name='active_version',
|
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='active_version', to='motions.MotionVersion'),
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="states",
|
||||||
|
to="motions.Workflow",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motionoption",
|
||||||
name='attachments',
|
name="poll",
|
||||||
field=models.ManyToManyField(blank=True, to='mediafiles.Mediafile'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='motion',
|
|
||||||
name='category',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='motions.Category'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='motion',
|
|
||||||
name='parent',
|
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='amendments', to='motions.Motion'),
|
on_delete=django.db.models.deletion.CASCADE, to="motions.MotionPoll"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='state',
|
name="active_version",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='motions.State'),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="active_version",
|
||||||
|
to="motions.MotionVersion",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='submitters',
|
name="attachments",
|
||||||
field=models.ManyToManyField(blank=True, related_name='motion_submitters', to=settings.AUTH_USER_MODEL),
|
field=models.ManyToManyField(blank=True, to="mediafiles.Mediafile"),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='supporters',
|
name="category",
|
||||||
field=models.ManyToManyField(blank=True, related_name='motion_supporters', to=settings.AUTH_USER_MODEL),
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="motions.Category",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='tags',
|
name="parent",
|
||||||
field=models.ManyToManyField(blank=True, to='core.Tag'),
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="amendments",
|
||||||
|
to="motions.Motion",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motion",
|
||||||
|
name="state",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="motions.State",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motion",
|
||||||
|
name="submitters",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="motion_submitters",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motion",
|
||||||
|
name="supporters",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="motion_supporters",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="motion",
|
||||||
|
name="tags",
|
||||||
|
field=models.ManyToManyField(blank=True, to="core.Tag"),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='motionversion',
|
name="motionversion", unique_together=set([("motion", "version_number")])
|
||||||
unique_together=set([('motion', 'version_number')]),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -16,16 +16,16 @@ def change_label_of_state(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
# We get the model from the versioned app registry;
|
# We get the model from the versioned app registry;
|
||||||
# if we directly import it, it will be the wrong version.
|
# if we directly import it, it will be the wrong version.
|
||||||
State = apps.get_model('motions', 'State')
|
State = apps.get_model("motions", "State")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
state = State.objects.get(name='commited a bill')
|
state = State.objects.get(name="commited a bill")
|
||||||
except State.DoesNotExist:
|
except State.DoesNotExist:
|
||||||
# State does not exists, there is nothing to change.
|
# State does not exists, there is nothing to change.
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
state.name = 'refered to committee'
|
state.name = "refered to committee"
|
||||||
state.action_word = 'Refer to committee'
|
state.action_word = "Refer to committee"
|
||||||
state.save(skip_autoupdate=True)
|
state.save(skip_autoupdate=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,17 +35,17 @@ def add_recommendation_labels(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
# We get the model from the versioned app registry;
|
# We get the model from the versioned app registry;
|
||||||
# if we directly import it, it will be the wrong version.
|
# if we directly import it, it will be the wrong version.
|
||||||
State = apps.get_model('motions', 'State')
|
State = apps.get_model("motions", "State")
|
||||||
|
|
||||||
name_label_map = {
|
name_label_map = {
|
||||||
'accepted': 'Acceptance',
|
"accepted": "Acceptance",
|
||||||
'rejected': 'Rejection',
|
"rejected": "Rejection",
|
||||||
'not decided': 'No decision',
|
"not decided": "No decision",
|
||||||
'permitted': 'Permission',
|
"permitted": "Permission",
|
||||||
'adjourned': 'Adjournment',
|
"adjourned": "Adjournment",
|
||||||
'not concerned': 'No concernment',
|
"not concerned": "No concernment",
|
||||||
'refered to committee': 'Referral to committee',
|
"refered to committee": "Referral to committee",
|
||||||
'rejected (not authorized)': 'Rejection (not authorized)',
|
"rejected (not authorized)": "Rejection (not authorized)",
|
||||||
}
|
}
|
||||||
for state in State.objects.all():
|
for state in State.objects.all():
|
||||||
if name_label_map.get(state.name):
|
if name_label_map.get(state.name):
|
||||||
|
@ -57,101 +57,135 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('motions', '0001_initial'),
|
("motions", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='MotionBlock',
|
name="MotionBlock",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('title', models.CharField(max_length=255)),
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=255)),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='MotionChangeRecommendation',
|
name="MotionChangeRecommendation",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('rejected', models.BooleanField(default=False)),
|
"id",
|
||||||
('type', models.PositiveIntegerField(default=0)),
|
models.AutoField(
|
||||||
('line_from', models.PositiveIntegerField()),
|
auto_created=True,
|
||||||
('line_to', models.PositiveIntegerField()),
|
primary_key=True,
|
||||||
('text', models.TextField(blank=True)),
|
serialize=False,
|
||||||
('creation_time', models.DateTimeField(auto_now=True)),
|
verbose_name="ID",
|
||||||
('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
),
|
||||||
('motion_version', models.ForeignKey(
|
),
|
||||||
on_delete=django.db.models.deletion.CASCADE, related_name='change_recommendations', to='motions.MotionVersion')),
|
("rejected", models.BooleanField(default=False)),
|
||||||
|
("type", models.PositiveIntegerField(default=0)),
|
||||||
|
("line_from", models.PositiveIntegerField()),
|
||||||
|
("line_to", models.PositiveIntegerField()),
|
||||||
|
("text", models.TextField(blank=True)),
|
||||||
|
("creation_time", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"author",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"motion_version",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="change_recommendations",
|
||||||
|
to="motions.MotionVersion",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='motion',
|
name="motion",
|
||||||
options={
|
options={
|
||||||
'default_permissions': (),
|
"default_permissions": (),
|
||||||
'ordering': (
|
"ordering": ("identifier",),
|
||||||
'identifier',
|
"permissions": (
|
||||||
|
("can_see", "Can see motions"),
|
||||||
|
("can_create", "Can create motions"),
|
||||||
|
("can_support", "Can support motions"),
|
||||||
|
("can_see_and_manage_comments", "Can see and manage comments"),
|
||||||
|
("can_manage", "Can manage motions"),
|
||||||
),
|
),
|
||||||
'permissions': (
|
"verbose_name": "Motion",
|
||||||
('can_see', 'Can see motions'),
|
|
||||||
('can_create', 'Can create motions'),
|
|
||||||
('can_support', 'Can support motions'),
|
|
||||||
('can_see_and_manage_comments', 'Can see and manage comments'),
|
|
||||||
('can_manage', 'Can manage motions')
|
|
||||||
),
|
|
||||||
'verbose_name': 'Motion',
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='comments',
|
name="comments",
|
||||||
field=jsonfield.fields.JSONField(null=True),
|
field=jsonfield.fields.JSONField(null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='origin',
|
name="origin",
|
||||||
field=models.CharField(blank=True, max_length=255),
|
field=models.CharField(blank=True, max_length=255),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='recommendation',
|
name="recommendation",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='motions.State'),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
|
to="motions.State",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='state',
|
model_name="state",
|
||||||
name='recommendation_label',
|
name="recommendation_label",
|
||||||
field=models.CharField(max_length=255, null=True),
|
field=models.CharField(max_length=255, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='state',
|
model_name="state",
|
||||||
name='show_recommendation_extension_field',
|
name="show_recommendation_extension_field",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='state',
|
model_name="state",
|
||||||
name='show_state_extension_field',
|
name="show_state_extension_field",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='state',
|
name="state",
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='motions.State'),
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
|
to="motions.State",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='motion_block',
|
name="motion_block",
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='motions.MotionBlock'),
|
field=models.ForeignKey(
|
||||||
),
|
blank=True,
|
||||||
migrations.RunPython(
|
null=True,
|
||||||
change_label_of_state
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
),
|
to="motions.MotionBlock",
|
||||||
migrations.RunPython(
|
),
|
||||||
add_recommendation_labels
|
|
||||||
),
|
),
|
||||||
|
migrations.RunPython(change_label_of_state),
|
||||||
|
migrations.RunPython(add_recommendation_labels),
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,24 +12,24 @@ def change_motions_comments(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
# We get the model from the versioned app registry;
|
# We get the model from the versioned app registry;
|
||||||
# if we directly import it, it will be the wrong version.
|
# if we directly import it, it will be the wrong version.
|
||||||
ConfigStore = apps.get_model('core', 'ConfigStore')
|
ConfigStore = apps.get_model("core", "ConfigStore")
|
||||||
Motion = apps.get_model('motions', 'Motion')
|
Motion = apps.get_model("motions", "Motion")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config_comments_fields = ConfigStore.objects.get(key='motions_comments').value
|
config_comments_fields = ConfigStore.objects.get(key="motions_comments").value
|
||||||
except ConfigStore.DoesNotExist:
|
except ConfigStore.DoesNotExist:
|
||||||
config_comments_fields = [] # The old default: An empty list.
|
config_comments_fields = [] # The old default: An empty list.
|
||||||
|
|
||||||
comments_fields = {}
|
comments_fields = {}
|
||||||
for index, field in enumerate(config_comments_fields):
|
for index, field in enumerate(config_comments_fields):
|
||||||
comments_fields[index+1] = field
|
comments_fields[index + 1] = field
|
||||||
|
|
||||||
max_index = len(config_comments_fields)-1
|
max_index = len(config_comments_fields) - 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db_value = ConfigStore.objects.get(key='motions_comments')
|
db_value = ConfigStore.objects.get(key="motions_comments")
|
||||||
except ConfigStore.DoesNotExist:
|
except ConfigStore.DoesNotExist:
|
||||||
db_value = ConfigStore(key='motions_comments')
|
db_value = ConfigStore(key="motions_comments")
|
||||||
db_value.value = comments_fields
|
db_value.value = comments_fields
|
||||||
# We cannot provide skip_autoupdate=True here, becuase this object is a fake object. It does *not*
|
# We cannot provide skip_autoupdate=True here, becuase this object is a fake object. It does *not*
|
||||||
# inherit from the RESTModelMixin, so the save() methos from base_model.py (django's default)
|
# inherit from the RESTModelMixin, so the save() methos from base_model.py (django's default)
|
||||||
|
@ -42,19 +42,13 @@ def change_motions_comments(apps, schema_editor):
|
||||||
for index, comment in enumerate(motion.comments or []):
|
for index, comment in enumerate(motion.comments or []):
|
||||||
if index > max_index:
|
if index > max_index:
|
||||||
break
|
break
|
||||||
comments[index+1] = comment
|
comments[index + 1] = comment
|
||||||
motion.comments = comments
|
motion.comments = comments
|
||||||
motion.save(skip_autoupdate=True)
|
motion.save(skip_autoupdate=True)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0002_misc_features")]
|
||||||
('motions', '0002_misc_features'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [migrations.RunPython(change_motions_comments)]
|
||||||
migrations.RunPython(
|
|
||||||
change_motions_comments
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
|
@ -7,14 +7,12 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0003_motion_comments")]
|
||||||
('motions', '0003_motion_comments'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motionchangerecommendation',
|
model_name="motionchangerecommendation",
|
||||||
name='other_description',
|
name="other_description",
|
||||||
field=models.TextField(blank=True),
|
field=models.TextField(blank=True),
|
||||||
),
|
)
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,7 +11,7 @@ def delete_old_comment_permission(apps, schema_editor):
|
||||||
Deletes the old 'can_see_and_manage_comments' permission which is
|
Deletes the old 'can_see_and_manage_comments' permission which is
|
||||||
split up into two seperate permissions.
|
split up into two seperate permissions.
|
||||||
"""
|
"""
|
||||||
perm = Permission.objects.filter(codename='can_see_and_manage_comments')
|
perm = Permission.objects.filter(codename="can_see_and_manage_comments")
|
||||||
|
|
||||||
if len(perm):
|
if len(perm):
|
||||||
perm = perm.get()
|
perm = perm.get()
|
||||||
|
@ -26,13 +26,15 @@ def delete_old_comment_permission(apps, schema_editor):
|
||||||
|
|
||||||
# Create new permission
|
# Create new permission
|
||||||
perm_see = Permission.objects.create(
|
perm_see = Permission.objects.create(
|
||||||
codename='can_see_comments',
|
codename="can_see_comments",
|
||||||
name='Can see comments',
|
name="Can see comments",
|
||||||
content_type=content_type)
|
content_type=content_type,
|
||||||
|
)
|
||||||
perm_manage = Permission.objects.create(
|
perm_manage = Permission.objects.create(
|
||||||
codename='can_manage_comments',
|
codename="can_manage_comments",
|
||||||
name='Can manage comments',
|
name="Can manage comments",
|
||||||
content_type=content_type)
|
content_type=content_type,
|
||||||
|
)
|
||||||
|
|
||||||
for group in groups:
|
for group in groups:
|
||||||
group.permissions.add(perm_see)
|
group.permissions.add(perm_see)
|
||||||
|
@ -42,28 +44,24 @@ def delete_old_comment_permission(apps, schema_editor):
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0004_motionchangerecommendation_other_description")]
|
||||||
('motions', '0004_motionchangerecommendation_other_description'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='motion',
|
name="motion",
|
||||||
options={
|
options={
|
||||||
'default_permissions': (),
|
"default_permissions": (),
|
||||||
'ordering': ('identifier',),
|
"ordering": ("identifier",),
|
||||||
'permissions': (
|
"permissions": (
|
||||||
('can_see', 'Can see motions'),
|
("can_see", "Can see motions"),
|
||||||
('can_create', 'Can create motions'),
|
("can_create", "Can create motions"),
|
||||||
('can_support', 'Can support motions'),
|
("can_support", "Can support motions"),
|
||||||
('can_see_comments', 'Can see comments'),
|
("can_see_comments", "Can see comments"),
|
||||||
('can_manage_comments', 'Can manage comments'),
|
("can_manage_comments", "Can manage comments"),
|
||||||
('can_manage', 'Can manage motions')
|
("can_manage", "Can manage motions"),
|
||||||
),
|
),
|
||||||
'verbose_name': 'Motion'
|
"verbose_name": "Motion",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.RunPython(
|
migrations.RunPython(delete_old_comment_permission),
|
||||||
delete_old_comment_permission
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,8 +11,8 @@ import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
def move_submitters_to_own_model(apps, schema_editor):
|
def move_submitters_to_own_model(apps, schema_editor):
|
||||||
Motion = apps.get_model('motions', 'Motion')
|
Motion = apps.get_model("motions", "Motion")
|
||||||
Submitter = apps.get_model('motions', 'Submitter')
|
Submitter = apps.get_model("motions", "Submitter")
|
||||||
|
|
||||||
for motion in Motion.objects.all():
|
for motion in Motion.objects.all():
|
||||||
weight = 0
|
weight = 0
|
||||||
|
@ -32,41 +32,46 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
('motions', '0005_auto_20180202_1318'),
|
("motions", "0005_auto_20180202_1318"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name='motion',
|
model_name="motion", old_name="submitters", new_name="submittersOld"
|
||||||
old_name='submitters',
|
|
||||||
new_name='submittersOld',
|
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Submitter',
|
name="Submitter",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('weight', models.IntegerField(null=True)),
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("weight", models.IntegerField(null=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='submitter',
|
model_name="submitter",
|
||||||
name='motion',
|
name="motion",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submitters', to='motions.Motion'),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="submitters",
|
||||||
|
to="motions.Motion",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='submitter',
|
model_name="submitter",
|
||||||
name='user',
|
name="user",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
field=models.ForeignKey(
|
||||||
),
|
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||||
migrations.RunPython(
|
),
|
||||||
move_submitters_to_own_model
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='motion',
|
|
||||||
name='submittersOld',
|
|
||||||
),
|
),
|
||||||
|
migrations.RunPython(move_submitters_to_own_model),
|
||||||
|
migrations.RemoveField(model_name="motion", name="submittersOld"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,14 +8,12 @@ from django.db import migrations
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0006_submitter_model")]
|
||||||
('motions', '0006_submitter_model'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motionversion',
|
model_name="motionversion",
|
||||||
name='amendment_paragraphs',
|
name="amendment_paragraphs",
|
||||||
field=jsonfield.fields.JSONField(null=True),
|
field=jsonfield.fields.JSONField(null=True),
|
||||||
),
|
)
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,38 +8,38 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0007_motionversion_amendment_data")]
|
||||||
('motions', '0007_motionversion_amendment_data'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='workflow',
|
model_name="workflow",
|
||||||
name='first_state',
|
name="first_state",
|
||||||
field=models.OneToOneField(
|
field=models.OneToOneField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
related_name='+',
|
related_name="+",
|
||||||
to='motions.State'),
|
to="motions.State",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='state',
|
name="state",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
related_name='+',
|
related_name="+",
|
||||||
to='motions.State'),
|
to="motions.State",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='state',
|
model_name="state",
|
||||||
name='next_states',
|
name="next_states",
|
||||||
field=models.ManyToManyField(blank=True, to='motions.State'),
|
field=models.ManyToManyField(blank=True, to="motions.State"),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='state',
|
model_name="state",
|
||||||
name='action_word',
|
name="action_word",
|
||||||
field=models.CharField(blank=True, max_length=255),
|
field=models.CharField(blank=True, max_length=255),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,14 +7,12 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0008_auto_20180702_1128")]
|
||||||
('motions', '0008_auto_20180702_1128'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motionversion',
|
model_name="motionversion",
|
||||||
name='modified_final_version',
|
name="modified_final_version",
|
||||||
field=models.TextField(blank=True, null=True),
|
field=models.TextField(blank=True, null=True),
|
||||||
),
|
)
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,49 +8,51 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0009_motionversion_modified_final_version")]
|
||||||
('motions', '0009_motionversion_modified_final_version'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='motionpoll',
|
model_name="motionpoll",
|
||||||
name='votescast',
|
name="votescast",
|
||||||
field=models.DecimalField(
|
field=models.DecimalField(
|
||||||
blank=True,
|
blank=True,
|
||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
max_digits=15,
|
max_digits=15,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='motionpoll',
|
model_name="motionpoll",
|
||||||
name='votesinvalid',
|
name="votesinvalid",
|
||||||
field=models.DecimalField(
|
field=models.DecimalField(
|
||||||
blank=True,
|
blank=True,
|
||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
max_digits=15,
|
max_digits=15,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='motionpoll',
|
model_name="motionpoll",
|
||||||
name='votesvalid',
|
name="votesvalid",
|
||||||
field=models.DecimalField(
|
field=models.DecimalField(
|
||||||
blank=True,
|
blank=True,
|
||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
max_digits=15,
|
max_digits=15,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='motionvote',
|
model_name="motionvote",
|
||||||
name='weight',
|
name="weight",
|
||||||
field=models.DecimalField(
|
field=models.DecimalField(
|
||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
default=Decimal('1'),
|
default=Decimal("1"),
|
||||||
max_digits=15,
|
max_digits=15,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[django.core.validators.MinValueValidator(Decimal('-2'))]),
|
validators=[django.core.validators.MinValueValidator(Decimal("-2"))],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -10,7 +10,7 @@ def copy_motion_version_content_to_motion(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
Move all motion version content of the active version to the motion.
|
Move all motion version content of the active version to the motion.
|
||||||
"""
|
"""
|
||||||
Motion = apps.get_model('motions', 'Motion')
|
Motion = apps.get_model("motions", "Motion")
|
||||||
|
|
||||||
for motion in Motion.objects.all():
|
for motion in Motion.objects.all():
|
||||||
motion.title = motion.active_version.title
|
motion.title = motion.active_version.title
|
||||||
|
@ -26,7 +26,7 @@ def migrate_active_change_recommendations(apps, schema_editor):
|
||||||
Delete all change recommendation of motion versions, that are not active. For active
|
Delete all change recommendation of motion versions, that are not active. For active
|
||||||
change recommendations the motion id will be set.
|
change recommendations the motion id will be set.
|
||||||
"""
|
"""
|
||||||
MotionChangeRecommendation = apps.get_model('motions', 'MotionChangeRecommendation')
|
MotionChangeRecommendation = apps.get_model("motions", "MotionChangeRecommendation")
|
||||||
to_delete = []
|
to_delete = []
|
||||||
for cr in MotionChangeRecommendation.objects.all():
|
for cr in MotionChangeRecommendation.objects.all():
|
||||||
# chack if version id matches the active version of the motion
|
# chack if version id matches the active version of the motion
|
||||||
|
@ -43,89 +43,65 @@ def migrate_active_change_recommendations(apps, schema_editor):
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0010_auto_20180822_1042")]
|
||||||
('motions', '0010_auto_20180822_1042'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
# Create new fields. Title and Text have empty defaults, but the values
|
# Create new fields. Title and Text have empty defaults, but the values
|
||||||
# should be overwritten by copy_motion_version_content_to_motion. In the next
|
# should be overwritten by copy_motion_version_content_to_motion. In the next
|
||||||
# migration file these defaults are removed.
|
# migration file these defaults are removed.
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='title',
|
name="title",
|
||||||
field=models.CharField(max_length=255, default=''),
|
field=models.CharField(max_length=255, default=""),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion", name="text", field=models.TextField(default="")
|
||||||
name='text',
|
|
||||||
field=models.TextField(default=''),
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='reason',
|
name="reason",
|
||||||
field=models.TextField(blank=True, null=True),
|
field=models.TextField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='modified_final_version',
|
name="modified_final_version",
|
||||||
field=models.TextField(blank=True, null=True),
|
field=models.TextField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='amendment_paragraphs',
|
name="amendment_paragraphs",
|
||||||
field=jsonfield.fields.JSONField(
|
field=jsonfield.fields.JSONField(
|
||||||
dump_kwargs={
|
dump_kwargs={
|
||||||
'cls': jsonfield.encoder.JSONEncoder,
|
"cls": jsonfield.encoder.JSONEncoder,
|
||||||
'separators': (',', ':')
|
"separators": (",", ":"),
|
||||||
},
|
},
|
||||||
load_kwargs={},
|
load_kwargs={},
|
||||||
null=True),
|
null=True,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
# Copy old motion version data
|
# Copy old motion version data
|
||||||
migrations.RunPython(copy_motion_version_content_to_motion),
|
migrations.RunPython(copy_motion_version_content_to_motion),
|
||||||
|
|
||||||
# Change recommendations
|
# Change recommendations
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motionchangerecommendation',
|
model_name="motionchangerecommendation",
|
||||||
name='motion',
|
name="motion",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
null=True, # This is reverted in the next migration
|
null=True, # This is reverted in the next migration
|
||||||
related_name='change_recommendations',
|
related_name="change_recommendations",
|
||||||
to='motions.Motion'),
|
to="motions.Motion",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.RunPython(migrate_active_change_recommendations),
|
migrations.RunPython(migrate_active_change_recommendations),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='motionchangerecommendation',
|
model_name="motionchangerecommendation", name="motion_version"
|
||||||
name='motion_version',
|
|
||||||
),
|
),
|
||||||
|
|
||||||
# remove motion version references from motion and state.
|
# remove motion version references from motion and state.
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(model_name="motion", name="active_version"),
|
||||||
model_name='motion',
|
migrations.AlterUniqueTogether(name="motionversion", unique_together=set()),
|
||||||
name='active_version',
|
migrations.RemoveField(model_name="motionversion", name="motion"),
|
||||||
),
|
migrations.RemoveField(model_name="state", name="leave_old_version_active"),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.RemoveField(model_name="state", name="versioning"),
|
||||||
name='motionversion',
|
|
||||||
unique_together=set(),
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='motionversion',
|
|
||||||
name='motion',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='state',
|
|
||||||
name='leave_old_version_active',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='state',
|
|
||||||
name='versioning',
|
|
||||||
),
|
|
||||||
|
|
||||||
# Delete motion version.
|
# Delete motion version.
|
||||||
migrations.DeleteModel(
|
migrations.DeleteModel(name="MotionVersion"),
|
||||||
name='MotionVersion',
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,16 +8,18 @@ from django.db import migrations, models
|
||||||
import openslides
|
import openslides
|
||||||
|
|
||||||
|
|
||||||
def create_comment_sections_from_config_and_move_comments_to_own_model(apps, schema_editor):
|
def create_comment_sections_from_config_and_move_comments_to_own_model(
|
||||||
ConfigStore = apps.get_model('core', 'ConfigStore')
|
apps, schema_editor
|
||||||
Motion = apps.get_model('motions', 'Motion')
|
):
|
||||||
MotionComment = apps.get_model('motions', 'MotionComment')
|
ConfigStore = apps.get_model("core", "ConfigStore")
|
||||||
MotionCommentSection = apps.get_model('motions', 'MotionCommentSection')
|
Motion = apps.get_model("motions", "Motion")
|
||||||
|
MotionComment = apps.get_model("motions", "MotionComment")
|
||||||
|
MotionCommentSection = apps.get_model("motions", "MotionCommentSection")
|
||||||
Group = apps.get_model(settings.AUTH_GROUP_MODEL)
|
Group = apps.get_model(settings.AUTH_GROUP_MODEL)
|
||||||
|
|
||||||
# try to get old motions_comments config variable, where all comment fields are saved
|
# try to get old motions_comments config variable, where all comment fields are saved
|
||||||
try:
|
try:
|
||||||
motions_comments = ConfigStore.objects.get(key='motions_comments')
|
motions_comments = ConfigStore.objects.get(key="motions_comments")
|
||||||
except ConfigStore.DoesNotExist:
|
except ConfigStore.DoesNotExist:
|
||||||
return
|
return
|
||||||
comments_sections = motions_comments.value
|
comments_sections = motions_comments.value
|
||||||
|
@ -26,14 +28,14 @@ def create_comment_sections_from_config_and_move_comments_to_own_model(apps, sch
|
||||||
motions_comments.delete()
|
motions_comments.delete()
|
||||||
|
|
||||||
# Get can_see_comments and can_manage_comments permissions and the associated groups
|
# Get can_see_comments and can_manage_comments permissions and the associated groups
|
||||||
can_see_comments = Permission.objects.filter(codename='can_see_comments')
|
can_see_comments = Permission.objects.filter(codename="can_see_comments")
|
||||||
if len(can_see_comments) == 1:
|
if len(can_see_comments) == 1:
|
||||||
# Save groups. list() is necessary to evaluate the database query right now.
|
# Save groups. list() is necessary to evaluate the database query right now.
|
||||||
can_see_groups = list(can_see_comments.get().group_set.all())
|
can_see_groups = list(can_see_comments.get().group_set.all())
|
||||||
else:
|
else:
|
||||||
can_see_groups = Group.objects.all()
|
can_see_groups = Group.objects.all()
|
||||||
|
|
||||||
can_manage_comments = Permission.objects.filter(codename='can_manage_comments')
|
can_manage_comments = Permission.objects.filter(codename="can_manage_comments")
|
||||||
if len(can_manage_comments) == 1:
|
if len(can_manage_comments) == 1:
|
||||||
# Save groups. list() is necessary to evaluate the database query right now.
|
# Save groups. list() is necessary to evaluate the database query right now.
|
||||||
can_manage_groups = list(can_manage_comments.get().group_set.all())
|
can_manage_groups = list(can_manage_comments.get().group_set.all())
|
||||||
|
@ -50,12 +52,12 @@ def create_comment_sections_from_config_and_move_comments_to_own_model(apps, sch
|
||||||
for id, section in comments_sections.items():
|
for id, section in comments_sections.items():
|
||||||
if section is None:
|
if section is None:
|
||||||
continue
|
continue
|
||||||
if section.get('forState', False):
|
if section.get("forState", False):
|
||||||
forStateId = id
|
forStateId = id
|
||||||
elif section.get('forRecommendation', False):
|
elif section.get("forRecommendation", False):
|
||||||
forRecommendationId = id
|
forRecommendationId = id
|
||||||
else:
|
else:
|
||||||
comment_section = MotionCommentSection(name=section['name'])
|
comment_section = MotionCommentSection(name=section["name"])
|
||||||
comment_section.save(skip_autoupdate=True)
|
comment_section.save(skip_autoupdate=True)
|
||||||
comment_section.read_groups.add(*[group.id for group in can_see_groups])
|
comment_section.read_groups.add(*[group.id for group in can_see_groups])
|
||||||
comment_section.write_groups.add(*[group.id for group in can_manage_groups])
|
comment_section.write_groups.add(*[group.id for group in can_manage_groups])
|
||||||
|
@ -70,7 +72,7 @@ def create_comment_sections_from_config_and_move_comments_to_own_model(apps, sch
|
||||||
for section_id, comment_value in motion.comments.items():
|
for section_id, comment_value in motion.comments.items():
|
||||||
# Skip empty sections.
|
# Skip empty sections.
|
||||||
comment_value = comment_value.strip()
|
comment_value = comment_value.strip()
|
||||||
if comment_value == '':
|
if comment_value == "":
|
||||||
continue
|
continue
|
||||||
# Special comments will be moved to separate fields.
|
# Special comments will be moved to separate fields.
|
||||||
if section_id == forStateId:
|
if section_id == forStateId:
|
||||||
|
@ -83,136 +85,147 @@ def create_comment_sections_from_config_and_move_comments_to_own_model(apps, sch
|
||||||
comment = MotionComment(
|
comment = MotionComment(
|
||||||
comment=comment_value,
|
comment=comment_value,
|
||||||
motion=motion,
|
motion=motion,
|
||||||
section=old_id_mapping[section_id])
|
section=old_id_mapping[section_id],
|
||||||
|
)
|
||||||
comments.append(comment)
|
comments.append(comment)
|
||||||
MotionComment.objects.bulk_create(comments)
|
MotionComment.objects.bulk_create(comments)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("users", "0006_user_email"), ("motions", "0011_motion_version")]
|
||||||
('users', '0006_user_email'),
|
|
||||||
('motions', '0011_motion_version'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
# Cleanup from last migration. Somehow cannot be done there.
|
# Cleanup from last migration. Somehow cannot be done there.
|
||||||
migrations.AlterField( # remove default=''
|
migrations.AlterField( # remove default=''
|
||||||
model_name='motion',
|
model_name="motion", name="text", field=models.TextField()
|
||||||
name='text',
|
|
||||||
field=models.TextField(),
|
|
||||||
),
|
),
|
||||||
migrations.AlterField( # remove default=''
|
migrations.AlterField( # remove default=''
|
||||||
model_name='motion',
|
model_name="motion", name="title", field=models.CharField(max_length=255)
|
||||||
name='title',
|
|
||||||
field=models.CharField(max_length=255),
|
|
||||||
),
|
),
|
||||||
migrations.AlterField( # remove null=True
|
migrations.AlterField( # remove null=True
|
||||||
model_name='motionchangerecommendation',
|
model_name="motionchangerecommendation",
|
||||||
name='motion',
|
name="motion",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name='change_recommendations',
|
related_name="change_recommendations",
|
||||||
to='motions.Motion'),
|
to="motions.Motion",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
# Add extension fields for former "special comments". No hack anymore..
|
# Add extension fields for former "special comments". No hack anymore..
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='recommendation_extension',
|
name="recommendation_extension",
|
||||||
field=models.TextField(blank=True, null=True),
|
field=models.TextField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='state_extension',
|
name="state_extension",
|
||||||
field=models.TextField(blank=True, null=True),
|
field=models.TextField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
|
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='motion',
|
name="motion",
|
||||||
options={
|
options={
|
||||||
'default_permissions': (),
|
"default_permissions": (),
|
||||||
'ordering': ('identifier',),
|
"ordering": ("identifier",),
|
||||||
'permissions': (
|
"permissions": (
|
||||||
('can_see', 'Can see motions'),
|
("can_see", "Can see motions"),
|
||||||
('can_create', 'Can create motions'),
|
("can_create", "Can create motions"),
|
||||||
('can_support', 'Can support motions'),
|
("can_support", "Can support motions"),
|
||||||
('can_manage', 'Can manage motions')),
|
("can_manage", "Can manage motions"),
|
||||||
'verbose_name': 'Motion'},
|
),
|
||||||
|
"verbose_name": "Motion",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
# Comments and CommentsSection models
|
# Comments and CommentsSection models
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='MotionComment',
|
name="MotionComment",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
auto_created=True,
|
auto_created=True,
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
serialize=False,
|
serialize=False,
|
||||||
verbose_name='ID')),
|
verbose_name="ID",
|
||||||
('comment', models.TextField()),
|
),
|
||||||
|
),
|
||||||
|
("comment", models.TextField()),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
bases=(
|
||||||
},
|
openslides.utils.models.RESTModelMixin, # type: ignore
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model), # type: ignore
|
models.Model,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='MotionCommentSection',
|
name="MotionCommentSection",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(
|
(
|
||||||
auto_created=True,
|
"id",
|
||||||
primary_key=True,
|
models.AutoField(
|
||||||
serialize=False,
|
auto_created=True,
|
||||||
verbose_name='ID')),
|
primary_key=True,
|
||||||
('name', models.CharField(max_length=255)),
|
serialize=False,
|
||||||
('read_groups', models.ManyToManyField(
|
verbose_name="ID",
|
||||||
blank=True,
|
),
|
||||||
related_name='read_comments',
|
),
|
||||||
to=settings.AUTH_GROUP_MODEL)),
|
("name", models.CharField(max_length=255)),
|
||||||
('write_groups', models.ManyToManyField(
|
(
|
||||||
blank=True,
|
"read_groups",
|
||||||
related_name='write_comments',
|
models.ManyToManyField(
|
||||||
to=settings.AUTH_GROUP_MODEL)),
|
blank=True,
|
||||||
|
related_name="read_comments",
|
||||||
|
to=settings.AUTH_GROUP_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"write_groups",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="write_comments",
|
||||||
|
to=settings.AUTH_GROUP_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={"default_permissions": ()},
|
||||||
'default_permissions': (),
|
bases=(
|
||||||
},
|
openslides.utils.models.RESTModelMixin, # type: ignore
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model), # type: ignore
|
models.Model,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motioncomment',
|
model_name="motioncomment",
|
||||||
name='section',
|
name="section",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
related_name='comments',
|
related_name="comments",
|
||||||
to='motions.MotionCommentSection'),
|
to="motions.MotionCommentSection",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motioncomment',
|
model_name="motioncomment",
|
||||||
name='motion',
|
name="motion",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE, to="motions.Motion"
|
||||||
to='motions.Motion'),
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='motioncomment',
|
name="motioncomment", unique_together={("motion", "section")}
|
||||||
unique_together={('motion', 'section')},
|
|
||||||
),
|
),
|
||||||
|
|
||||||
# Move the comments and sections
|
# Move the comments and sections
|
||||||
migrations.RunPython(create_comment_sections_from_config_and_move_comments_to_own_model),
|
migrations.RunPython(
|
||||||
|
create_comment_sections_from_config_and_move_comments_to_own_model
|
||||||
# Remove old comment field from motion, use the new model instead
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='motion',
|
|
||||||
name='comments',
|
|
||||||
),
|
),
|
||||||
|
# Remove old comment field from motion, use the new model instead
|
||||||
|
migrations.RemoveField(model_name="motion", name="comments"),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='motioncomment',
|
model_name="motioncomment",
|
||||||
name='motion',
|
name="motion",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name='comments',
|
related_name="comments",
|
||||||
to='motions.Motion'),
|
to="motions.Motion",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,58 +8,55 @@ import openslides.utils.models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0012_motion_comments")]
|
||||||
('motions', '0012_motion_comments'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='motionblock',
|
name="motionblock",
|
||||||
options={
|
options={"default_permissions": (), "verbose_name": "Motion block"},
|
||||||
'default_permissions': (),
|
|
||||||
'verbose_name': 'Motion block'},
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='sort_parent',
|
name="sort_parent",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
related_name='children',
|
related_name="children",
|
||||||
to='motions.Motion'),
|
to="motions.Motion",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion", name="weight", field=models.IntegerField(default=10000)
|
||||||
name='weight',
|
|
||||||
field=models.IntegerField(default=10000),
|
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='StatuteParagraph',
|
name="StatuteParagraph",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(
|
(
|
||||||
auto_created=True,
|
"id",
|
||||||
primary_key=True,
|
models.AutoField(
|
||||||
serialize=False,
|
auto_created=True,
|
||||||
verbose_name='ID')),
|
primary_key=True,
|
||||||
('title', models.CharField(max_length=255)),
|
serialize=False,
|
||||||
('text', models.TextField()),
|
verbose_name="ID",
|
||||||
('weight', models.IntegerField(default=10000)),
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=255)),
|
||||||
|
("text", models.TextField()),
|
||||||
|
("weight", models.IntegerField(default=10000)),
|
||||||
],
|
],
|
||||||
options={
|
options={"ordering": ["weight", "title"], "default_permissions": ()},
|
||||||
'ordering': ['weight', 'title'],
|
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motion',
|
model_name="motion",
|
||||||
name='statute_paragraph',
|
name="statute_paragraph",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
related_name='motions',
|
related_name="motions",
|
||||||
to='motions.StatuteParagraph'),
|
to="motions.StatuteParagraph",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,14 +5,12 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0013_motion_sorting_and_statute")]
|
||||||
('motions', '0013_motion_sorting_and_statute'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='motionchangerecommendation',
|
model_name="motionchangerecommendation",
|
||||||
name='internal',
|
name="internal",
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
)
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,24 +5,22 @@ from django.db import migrations
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0014_motionchangerecommendation_internal")]
|
||||||
('motions', '0014_motionchangerecommendation_internal'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='motion',
|
name="motion",
|
||||||
options={
|
options={
|
||||||
'default_permissions': (),
|
"default_permissions": (),
|
||||||
'ordering': ('identifier',),
|
"ordering": ("identifier",),
|
||||||
'permissions': (
|
"permissions": (
|
||||||
('can_see', 'Can see motions'),
|
("can_see", "Can see motions"),
|
||||||
('can_create', 'Can create motions'),
|
("can_create", "Can create motions"),
|
||||||
('can_support', 'Can support motions'),
|
("can_support", "Can support motions"),
|
||||||
('can_manage_metadata', 'Can manage motion metadata'),
|
("can_manage_metadata", "Can manage motion metadata"),
|
||||||
('can_manage', 'Can manage motions')
|
("can_manage", "Can manage motions"),
|
||||||
),
|
),
|
||||||
'verbose_name': 'Motion'
|
"verbose_name": "Motion",
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,14 +5,12 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0015_metadata_permission")]
|
||||||
('motions', '0015_metadata_permission'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='state',
|
model_name="state",
|
||||||
name='merge_amendment_into_final',
|
name="merge_amendment_into_final",
|
||||||
field=models.SmallIntegerField(default=0),
|
field=models.SmallIntegerField(default=0),
|
||||||
),
|
)
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,13 +5,6 @@ from django.db import migrations
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [("motions", "0016_merge_amendment_into_final")]
|
||||||
('motions', '0016_merge_amendment_into_final'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [migrations.RemoveField(model_name="state", name="action_word")]
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='state',
|
|
||||||
name='action_word',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ class StatuteParagraph(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Model for parts of the statute
|
Model for parts of the statute
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = StatuteParagraphAccessPermissions()
|
access_permissions = StatuteParagraphAccessPermissions()
|
||||||
|
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
|
@ -55,7 +56,7 @@ class StatuteParagraph(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
ordering = ['weight', 'title']
|
ordering = ["weight", "title"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
@ -65,25 +66,29 @@ class MotionManager(models.Manager):
|
||||||
"""
|
"""
|
||||||
Customized model manager to support our get_full_queryset method.
|
Customized model manager to support our get_full_queryset method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_full_queryset(self):
|
def get_full_queryset(self):
|
||||||
"""
|
"""
|
||||||
Returns the normal queryset with all motions. In the background we
|
Returns the normal queryset with all motions. In the background we
|
||||||
join and prefetch all related models.
|
join and prefetch all related models.
|
||||||
"""
|
"""
|
||||||
return (self.get_queryset()
|
return (
|
||||||
.select_related('state')
|
self.get_queryset()
|
||||||
.prefetch_related(
|
.select_related("state")
|
||||||
'state__workflow',
|
.prefetch_related(
|
||||||
'comments',
|
"state__workflow",
|
||||||
'comments__section',
|
"comments",
|
||||||
'comments__section__read_groups',
|
"comments__section",
|
||||||
'agenda_items',
|
"comments__section__read_groups",
|
||||||
'log_messages',
|
"agenda_items",
|
||||||
'polls',
|
"log_messages",
|
||||||
'attachments',
|
"polls",
|
||||||
'tags',
|
"attachments",
|
||||||
'submitters',
|
"tags",
|
||||||
'supporters'))
|
"submitters",
|
||||||
|
"supporters",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Motion(RESTModelMixin, models.Model):
|
class Motion(RESTModelMixin, models.Model):
|
||||||
|
@ -92,8 +97,9 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
This class is the main entry point to all other classes related to a motion.
|
This class is the main entry point to all other classes related to a motion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = MotionAccessPermissions()
|
access_permissions = MotionAccessPermissions()
|
||||||
can_see_permission = 'motions.can_see'
|
can_see_permission = "motions.can_see"
|
||||||
|
|
||||||
objects = MotionManager()
|
objects = MotionManager()
|
||||||
|
|
||||||
|
@ -119,10 +125,11 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
"""The reason for a motion."""
|
"""The reason for a motion."""
|
||||||
|
|
||||||
state = models.ForeignKey(
|
state = models.ForeignKey(
|
||||||
'State',
|
"State",
|
||||||
related_name='+',
|
related_name="+",
|
||||||
on_delete=models.PROTECT, # Do not let the user delete states, that are used for motions
|
on_delete=models.PROTECT, # Do not let the user delete states, that are used for motions
|
||||||
null=True) # TODO: Check whether null=True is necessary.
|
null=True,
|
||||||
|
) # TODO: Check whether null=True is necessary.
|
||||||
"""
|
"""
|
||||||
The related state object.
|
The related state object.
|
||||||
|
|
||||||
|
@ -135,10 +142,8 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
recommendation = models.ForeignKey(
|
recommendation = models.ForeignKey(
|
||||||
'State',
|
"State", related_name="+", on_delete=models.SET_NULL, null=True
|
||||||
related_name='+',
|
)
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True)
|
|
||||||
"""
|
"""
|
||||||
The recommendation of a person or committee for this motion.
|
The recommendation of a person or committee for this motion.
|
||||||
"""
|
"""
|
||||||
|
@ -148,8 +153,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
A text field fo additional information about the recommendation.
|
A text field fo additional information about the recommendation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
identifier = models.CharField(max_length=255, null=True, blank=True,
|
identifier = models.CharField(max_length=255, null=True, blank=True, unique=True)
|
||||||
unique=True)
|
|
||||||
"""
|
"""
|
||||||
A string as human readable identifier for the motion.
|
A string as human readable identifier for the motion.
|
||||||
"""
|
"""
|
||||||
|
@ -167,29 +171,26 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sort_parent = models.ForeignKey(
|
sort_parent = models.ForeignKey(
|
||||||
'self',
|
"self",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name='children')
|
related_name="children",
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
A parent field for multi-depth sorting of motions.
|
A parent field for multi-depth sorting of motions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
'Category',
|
"Category", on_delete=models.SET_NULL, null=True, blank=True
|
||||||
on_delete=models.SET_NULL,
|
)
|
||||||
null=True,
|
|
||||||
blank=True)
|
|
||||||
"""
|
"""
|
||||||
ForeignKey to one category of motions.
|
ForeignKey to one category of motions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
motion_block = models.ForeignKey(
|
motion_block = models.ForeignKey(
|
||||||
'MotionBlock',
|
"MotionBlock", on_delete=models.SET_NULL, null=True, blank=True
|
||||||
on_delete=models.SET_NULL,
|
)
|
||||||
null=True,
|
|
||||||
blank=True)
|
|
||||||
"""
|
"""
|
||||||
ForeignKey to one block of motions.
|
ForeignKey to one block of motions.
|
||||||
"""
|
"""
|
||||||
|
@ -206,11 +207,12 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parent = models.ForeignKey(
|
parent = models.ForeignKey(
|
||||||
'self',
|
"self",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name='amendments')
|
related_name="amendments",
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
Field for amendments to reference to the motion that should be altered.
|
Field for amendments to reference to the motion that should be altered.
|
||||||
|
|
||||||
|
@ -222,7 +224,8 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name='motions')
|
related_name="motions",
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
Field to reference to a statute paragraph if this motion is a
|
Field to reference to a statute paragraph if this motion is a
|
||||||
statute-amendment.
|
statute-amendment.
|
||||||
|
@ -235,26 +238,28 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
Tags to categorise motions.
|
Tags to categorise motions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
supporters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='motion_supporters', blank=True)
|
supporters = models.ManyToManyField(
|
||||||
|
settings.AUTH_USER_MODEL, related_name="motion_supporters", blank=True
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
Users who support this motion.
|
Users who support this motion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# In theory there could be one then more agenda_item. But we support only
|
# In theory there could be one then more agenda_item. But we support only
|
||||||
# one. See the property agenda_item.
|
# one. See the property agenda_item.
|
||||||
agenda_items = GenericRelation(Item, related_name='motions')
|
agenda_items = GenericRelation(Item, related_name="motions")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
permissions = (
|
permissions = (
|
||||||
('can_see', 'Can see motions'),
|
("can_see", "Can see motions"),
|
||||||
('can_create', 'Can create motions'),
|
("can_create", "Can create motions"),
|
||||||
('can_support', 'Can support motions'),
|
("can_support", "Can support motions"),
|
||||||
('can_manage_metadata', 'Can manage motion metadata'),
|
("can_manage_metadata", "Can manage motion metadata"),
|
||||||
('can_manage', 'Can manage motions'),
|
("can_manage", "Can manage motions"),
|
||||||
)
|
)
|
||||||
ordering = ('identifier', )
|
ordering = ("identifier",)
|
||||||
verbose_name = ugettext_noop('Motion')
|
verbose_name = ugettext_noop("Motion")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""
|
"""
|
||||||
|
@ -284,14 +289,15 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
try:
|
try:
|
||||||
# Always skip autoupdate. Maybe we run it later in this method.
|
# Always skip autoupdate. Maybe we run it later in this method.
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
super(Motion, self).save(skip_autoupdate=True, *args, **kwargs) # type: ignore
|
super(Motion, self).save( # type: ignore
|
||||||
|
skip_autoupdate=True, *args, **kwargs
|
||||||
|
)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
# Identifier is already used.
|
# Identifier is already used.
|
||||||
if hasattr(self, '_identifier_prefix'):
|
if hasattr(self, "_identifier_prefix"):
|
||||||
# Calculate a new one and try again.
|
# Calculate a new one and try again.
|
||||||
self.identifier_number, self.identifier = self.increment_identifier_number(
|
self.identifier_number, self.identifier = self.increment_identifier_number(
|
||||||
self.identifier_number,
|
self.identifier_number, self._identifier_prefix
|
||||||
self._identifier_prefix,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Do not calculate a new one but reraise the IntegrityError.
|
# Do not calculate a new one but reraise the IntegrityError.
|
||||||
|
@ -310,10 +316,11 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
motion projector element is disabled.
|
motion projector element is disabled.
|
||||||
"""
|
"""
|
||||||
Projector.remove_any(
|
Projector.remove_any(
|
||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate, name="motions/motion", id=self.pk
|
||||||
name='motions/motion',
|
)
|
||||||
id=self.pk)
|
return super().delete( # type: ignore
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
def set_identifier(self):
|
def set_identifier(self):
|
||||||
"""
|
"""
|
||||||
|
@ -321,27 +328,36 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
it is not set yet.
|
it is not set yet.
|
||||||
"""
|
"""
|
||||||
# The identifier is already set or should be set manually.
|
# The identifier is already set or should be set manually.
|
||||||
if config['motions_identifier'] == 'manually' or self.identifier:
|
if config["motions_identifier"] == "manually" or self.identifier:
|
||||||
# Do not set an identifier.
|
# Do not set an identifier.
|
||||||
return
|
return
|
||||||
|
|
||||||
# If MOTION_IDENTIFIER_WITHOUT_BLANKS is set, don't use blanks when building identifier.
|
# If MOTION_IDENTIFIER_WITHOUT_BLANKS is set, don't use blanks when building identifier.
|
||||||
without_blank = hasattr(settings, 'MOTION_IDENTIFIER_WITHOUT_BLANKS') and settings.MOTION_IDENTIFIER_WITHOUT_BLANKS
|
without_blank = (
|
||||||
|
hasattr(settings, "MOTION_IDENTIFIER_WITHOUT_BLANKS")
|
||||||
|
and settings.MOTION_IDENTIFIER_WITHOUT_BLANKS
|
||||||
|
)
|
||||||
|
|
||||||
# Build prefix.
|
# Build prefix.
|
||||||
if self.is_amendment():
|
if self.is_amendment():
|
||||||
parent_identifier = self.parent.identifier or ''
|
parent_identifier = self.parent.identifier or ""
|
||||||
if without_blank:
|
if without_blank:
|
||||||
prefix = '%s%s' % (parent_identifier, config['motions_amendments_prefix'])
|
prefix = "%s%s" % (
|
||||||
|
parent_identifier,
|
||||||
|
config["motions_amendments_prefix"],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
prefix = '%s %s ' % (parent_identifier, config['motions_amendments_prefix'])
|
prefix = "%s %s " % (
|
||||||
|
parent_identifier,
|
||||||
|
config["motions_amendments_prefix"],
|
||||||
|
)
|
||||||
elif self.category is None or not self.category.prefix:
|
elif self.category is None or not self.category.prefix:
|
||||||
prefix = ''
|
prefix = ""
|
||||||
else:
|
else:
|
||||||
if without_blank:
|
if without_blank:
|
||||||
prefix = '%s' % self.category.prefix
|
prefix = "%s" % self.category.prefix
|
||||||
else:
|
else:
|
||||||
prefix = '%s ' % self.category.prefix
|
prefix = "%s " % self.category.prefix
|
||||||
self._identifier_prefix = prefix
|
self._identifier_prefix = prefix
|
||||||
|
|
||||||
# Use the already assigned identifier_number, if the motion has one.
|
# Use the already assigned identifier_number, if the motion has one.
|
||||||
|
@ -354,20 +370,22 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
if self.is_amendment():
|
if self.is_amendment():
|
||||||
motions = self.parent.amendments.all()
|
motions = self.parent.amendments.all()
|
||||||
# The motions should be counted per category.
|
# The motions should be counted per category.
|
||||||
elif config['motions_identifier'] == 'per_category':
|
elif config["motions_identifier"] == "per_category":
|
||||||
motions = Motion.objects.filter(category=self.category)
|
motions = Motion.objects.filter(category=self.category)
|
||||||
# The motions should be counted over all.
|
# The motions should be counted over all.
|
||||||
else:
|
else:
|
||||||
motions = Motion.objects.all()
|
motions = Motion.objects.all()
|
||||||
|
|
||||||
number = motions.aggregate(Max('identifier_number'))['identifier_number__max'] or 0
|
number = (
|
||||||
|
motions.aggregate(Max("identifier_number"))["identifier_number__max"]
|
||||||
|
or 0
|
||||||
|
)
|
||||||
initial_increment = True
|
initial_increment = True
|
||||||
|
|
||||||
# Calculate new identifier.
|
# Calculate new identifier.
|
||||||
number, identifier = self.increment_identifier_number(
|
number, identifier = self.increment_identifier_number(
|
||||||
number,
|
number, prefix, initial_increment=initial_increment
|
||||||
prefix,
|
)
|
||||||
initial_increment=initial_increment)
|
|
||||||
|
|
||||||
# Set identifier and identifier_number.
|
# Set identifier and identifier_number.
|
||||||
self.identifier = identifier
|
self.identifier = identifier
|
||||||
|
@ -380,10 +398,10 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
if initial_increment:
|
if initial_increment:
|
||||||
number += 1
|
number += 1
|
||||||
identifier = '%s%s' % (prefix, self.extend_identifier_number(number))
|
identifier = "%s%s" % (prefix, self.extend_identifier_number(number))
|
||||||
while Motion.objects.filter(identifier=identifier).exists():
|
while Motion.objects.filter(identifier=identifier).exists():
|
||||||
number += 1
|
number += 1
|
||||||
identifier = '%s%s' % (prefix, self.extend_identifier_number(number))
|
identifier = "%s%s" % (prefix, self.extend_identifier_number(number))
|
||||||
return number, identifier
|
return number, identifier
|
||||||
|
|
||||||
def extend_identifier_number(self, number):
|
def extend_identifier_number(self, number):
|
||||||
|
@ -393,10 +411,18 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
MOTION_IDENTIFIER_MIN_DIGITS.
|
MOTION_IDENTIFIER_MIN_DIGITS.
|
||||||
"""
|
"""
|
||||||
result = str(number)
|
result = str(number)
|
||||||
if hasattr(settings, 'MOTION_IDENTIFIER_MIN_DIGITS') and settings.MOTION_IDENTIFIER_MIN_DIGITS:
|
if (
|
||||||
|
hasattr(settings, "MOTION_IDENTIFIER_MIN_DIGITS")
|
||||||
|
and settings.MOTION_IDENTIFIER_MIN_DIGITS
|
||||||
|
):
|
||||||
if not isinstance(settings.MOTION_IDENTIFIER_MIN_DIGITS, int):
|
if not isinstance(settings.MOTION_IDENTIFIER_MIN_DIGITS, int):
|
||||||
raise ImproperlyConfigured('Settings value MOTION_IDENTIFIER_MIN_DIGITS must be an integer.')
|
raise ImproperlyConfigured(
|
||||||
result = '0' * (settings.MOTION_IDENTIFIER_MIN_DIGITS - len(str(number))) + result
|
"Settings value MOTION_IDENTIFIER_MIN_DIGITS must be an integer."
|
||||||
|
)
|
||||||
|
result = (
|
||||||
|
"0" * (settings.MOTION_IDENTIFIER_MIN_DIGITS - len(str(number)))
|
||||||
|
+ result
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def is_submitter(self, user):
|
def is_submitter(self, user):
|
||||||
|
@ -423,7 +449,9 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
poll.set_options(skip_autoupdate=skip_autoupdate)
|
poll.set_options(skip_autoupdate=skip_autoupdate)
|
||||||
return poll
|
return poll
|
||||||
else:
|
else:
|
||||||
raise WorkflowError('You can not create a poll in state %s.' % self.state.name)
|
raise WorkflowError(
|
||||||
|
"You can not create a poll in state %s." % self.state.name
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def workflow_id(self):
|
def workflow_id(self):
|
||||||
|
@ -464,8 +492,10 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
elif self.state:
|
elif self.state:
|
||||||
new_state = self.state.workflow.first_state
|
new_state = self.state.workflow.first_state
|
||||||
else:
|
else:
|
||||||
new_state = (Workflow.objects.get(pk=config['motions_workflow']).first_state or
|
new_state = (
|
||||||
Workflow.objects.get(pk=config['motions_workflow']).states.all()[0])
|
Workflow.objects.get(pk=config["motions_workflow"]).first_state
|
||||||
|
or Workflow.objects.get(pk=config["motions_workflow"]).states.all()[0]
|
||||||
|
)
|
||||||
self.set_state(new_state)
|
self.set_state(new_state)
|
||||||
|
|
||||||
def set_recommendation(self, recommendation):
|
def set_recommendation(self, recommendation):
|
||||||
|
@ -499,7 +529,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
Note: It has to be the same return value like in JavaScript.
|
Note: It has to be the same return value like in JavaScript.
|
||||||
"""
|
"""
|
||||||
if self.identifier:
|
if self.identifier:
|
||||||
title = '%s %s' % (_(self._meta.verbose_name), self.identifier)
|
title = "%s %s" % (_(self._meta.verbose_name), self.identifier)
|
||||||
else:
|
else:
|
||||||
title = self.title
|
title = self.title
|
||||||
return title
|
return title
|
||||||
|
@ -512,9 +542,9 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
Note: It has to be the same return value like in JavaScript.
|
Note: It has to be the same return value like in JavaScript.
|
||||||
"""
|
"""
|
||||||
if self.identifier:
|
if self.identifier:
|
||||||
title = '%s %s' % (_(self._meta.verbose_name), self.identifier)
|
title = "%s %s" % (_(self._meta.verbose_name), self.identifier)
|
||||||
else:
|
else:
|
||||||
title = '%s (%s)' % (self.title, _(self._meta.verbose_name))
|
title = "%s (%s)" % (self.title, _(self._meta.verbose_name))
|
||||||
return title
|
return title
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -552,7 +582,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
A motion is a amendment if amendments are activated in the config and
|
A motion is a amendment if amendments are activated in the config and
|
||||||
the motion has a parent.
|
the motion has a parent.
|
||||||
"""
|
"""
|
||||||
return config['motions_amendments_enabled'] and self.parent is not None
|
return config["motions_amendments_enabled"] and self.parent is not None
|
||||||
|
|
||||||
def is_paragraph_based_amendment(self):
|
def is_paragraph_based_amendment(self):
|
||||||
"""
|
"""
|
||||||
|
@ -574,7 +604,12 @@ class Motion(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Returns a list of all paragraph-based amendments to this motion
|
Returns a list of all paragraph-based amendments to this motion
|
||||||
"""
|
"""
|
||||||
return list(filter(lambda amend: amend.is_paragraph_based_amendment(), self.amendments.all()))
|
return list(
|
||||||
|
filter(
|
||||||
|
lambda amend: amend.is_paragraph_based_amendment(),
|
||||||
|
self.amendments.all(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MotionCommentSection(RESTModelMixin, models.Model):
|
class MotionCommentSection(RESTModelMixin, models.Model):
|
||||||
|
@ -582,6 +617,7 @@ class MotionCommentSection(RESTModelMixin, models.Model):
|
||||||
The model for comment sections for motions. Each comment is related to one section, so
|
The model for comment sections for motions. Each comment is related to one section, so
|
||||||
each motions has the ability to have comments from the same section.
|
each motions has the ability to have comments from the same section.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = MotionCommentSectionAccessPermissions()
|
access_permissions = MotionCommentSectionAccessPermissions()
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
@ -590,17 +626,15 @@ class MotionCommentSection(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
read_groups = models.ManyToManyField(
|
read_groups = models.ManyToManyField(
|
||||||
settings.AUTH_GROUP_MODEL,
|
settings.AUTH_GROUP_MODEL, blank=True, related_name="read_comments"
|
||||||
blank=True,
|
)
|
||||||
related_name='read_comments')
|
|
||||||
"""
|
"""
|
||||||
These groups have read-access to the section.
|
These groups have read-access to the section.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
write_groups = models.ManyToManyField(
|
write_groups = models.ManyToManyField(
|
||||||
settings.AUTH_GROUP_MODEL,
|
settings.AUTH_GROUP_MODEL, blank=True, related_name="write_comments"
|
||||||
blank=True,
|
)
|
||||||
related_name='write_comments')
|
|
||||||
"""
|
"""
|
||||||
These groups have write-access to the section.
|
These groups have write-access to the section.
|
||||||
"""
|
"""
|
||||||
|
@ -621,24 +655,22 @@ class MotionComment(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
motion = models.ForeignKey(
|
motion = models.ForeignKey(
|
||||||
Motion,
|
Motion, on_delete=models.CASCADE, related_name="comments"
|
||||||
on_delete=models.CASCADE,
|
)
|
||||||
related_name='comments')
|
|
||||||
"""
|
"""
|
||||||
The motion where this comment belongs to.
|
The motion where this comment belongs to.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
section = models.ForeignKey(
|
section = models.ForeignKey(
|
||||||
MotionCommentSection,
|
MotionCommentSection, on_delete=models.PROTECT, related_name="comments"
|
||||||
on_delete=models.PROTECT,
|
)
|
||||||
related_name='comments')
|
|
||||||
"""
|
"""
|
||||||
The section of the comment.
|
The section of the comment.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
unique_together = ('motion', 'section')
|
unique_together = ("motion", "section")
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
def get_root_rest_element(self):
|
||||||
"""
|
"""
|
||||||
|
@ -651,6 +683,7 @@ class SubmitterManager(models.Manager):
|
||||||
"""
|
"""
|
||||||
Manager for Submitter model. Provides a customized add method.
|
Manager for Submitter model. Provides a customized add method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def add(self, user, motion, skip_autoupdate=False):
|
def add(self, user, motion, skip_autoupdate=False):
|
||||||
"""
|
"""
|
||||||
Customized manager method to prevent anonymous users to be a
|
Customized manager method to prevent anonymous users to be a
|
||||||
|
@ -658,13 +691,13 @@ class SubmitterManager(models.Manager):
|
||||||
for the initial sorting of the submitters.
|
for the initial sorting of the submitters.
|
||||||
"""
|
"""
|
||||||
if self.filter(user=user, motion=motion).exists():
|
if self.filter(user=user, motion=motion).exists():
|
||||||
raise OpenSlidesError(
|
raise OpenSlidesError(_("{user} is already a submitter.").format(user=user))
|
||||||
_('{user} is already a submitter.').format(user=user))
|
|
||||||
if isinstance(user, AnonymousUser):
|
if isinstance(user, AnonymousUser):
|
||||||
raise OpenSlidesError(
|
raise OpenSlidesError(_("An anonymous user can not be a submitter."))
|
||||||
_('An anonymous user can not be a submitter.'))
|
weight = (
|
||||||
weight = (self.filter(motion=motion).aggregate(
|
self.filter(motion=motion).aggregate(models.Max("weight"))["weight__max"]
|
||||||
models.Max('weight'))['weight__max'] or 0)
|
or 0
|
||||||
|
)
|
||||||
submitter = self.model(user=user, motion=motion, weight=weight + 1)
|
submitter = self.model(user=user, motion=motion, weight=weight + 1)
|
||||||
submitter.save(force_insert=True, skip_autoupdate=skip_autoupdate)
|
submitter.save(force_insert=True, skip_autoupdate=skip_autoupdate)
|
||||||
return submitter
|
return submitter
|
||||||
|
@ -680,17 +713,14 @@ class Submitter(RESTModelMixin, models.Model):
|
||||||
Use custom Manager.
|
Use custom Manager.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
"""
|
"""
|
||||||
ForeignKey to the user who is the submitter.
|
ForeignKey to the user who is the submitter.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
motion = models.ForeignKey(
|
motion = models.ForeignKey(
|
||||||
Motion,
|
Motion, on_delete=models.CASCADE, related_name="submitters"
|
||||||
on_delete=models.CASCADE,
|
)
|
||||||
related_name='submitters')
|
|
||||||
"""
|
"""
|
||||||
ForeignKey to the motion.
|
ForeignKey to the motion.
|
||||||
"""
|
"""
|
||||||
|
@ -714,6 +744,7 @@ class MotionChangeRecommendationManager(models.Manager):
|
||||||
"""
|
"""
|
||||||
Customized model manager to support our get_full_queryset method.
|
Customized model manager to support our get_full_queryset method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_full_queryset(self):
|
def get_full_queryset(self):
|
||||||
"""
|
"""
|
||||||
Returns the normal queryset with all change recommendations. In the background we
|
Returns the normal queryset with all change recommendations. In the background we
|
||||||
|
@ -732,9 +763,8 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
||||||
objects = MotionChangeRecommendationManager()
|
objects = MotionChangeRecommendationManager()
|
||||||
|
|
||||||
motion = models.ForeignKey(
|
motion = models.ForeignKey(
|
||||||
Motion,
|
Motion, on_delete=models.CASCADE, related_name="change_recommendations"
|
||||||
on_delete=models.CASCADE,
|
)
|
||||||
related_name='change_recommendations')
|
|
||||||
"""The motion to which the change recommendation belongs."""
|
"""The motion to which the change recommendation belongs."""
|
||||||
|
|
||||||
rejected = models.BooleanField(default=False)
|
rejected = models.BooleanField(default=False)
|
||||||
|
@ -759,9 +789,8 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
||||||
"""The replacement for the section of the original text specified by motion, line_from and line_to"""
|
"""The replacement for the section of the original text specified by motion, line_from and line_to"""
|
||||||
|
|
||||||
author = models.ForeignKey(
|
author = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True
|
||||||
on_delete=models.SET_NULL,
|
)
|
||||||
null=True)
|
|
||||||
"""A user object, who created this change recommendation. Optional."""
|
"""A user object, who created this change recommendation. Optional."""
|
||||||
|
|
||||||
creation_time = models.DateTimeField(auto_now=True)
|
creation_time = models.DateTimeField(auto_now=True)
|
||||||
|
@ -769,20 +798,27 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
def collides_with_other_recommendation(self, recommendations):
|
def collides_with_other_recommendation(self, recommendations):
|
||||||
for recommendation in recommendations:
|
for recommendation in recommendations:
|
||||||
if (not (self.line_from < recommendation.line_from and self.line_to <= recommendation.line_from) and
|
if not (
|
||||||
not (self.line_from >= recommendation.line_to and self.line_to > recommendation.line_to)):
|
self.line_from < recommendation.line_from
|
||||||
|
and self.line_to <= recommendation.line_from
|
||||||
|
) and not (
|
||||||
|
self.line_from >= recommendation.line_to
|
||||||
|
and self.line_to > recommendation.line_to
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
recommendations = (MotionChangeRecommendation.objects
|
recommendations = MotionChangeRecommendation.objects.filter(
|
||||||
.filter(motion=self.motion)
|
motion=self.motion
|
||||||
.exclude(pk=self.pk))
|
).exclude(pk=self.pk)
|
||||||
|
|
||||||
if self.collides_with_other_recommendation(recommendations):
|
if self.collides_with_other_recommendation(recommendations):
|
||||||
raise ValidationError('The recommendation collides with an existing one (line %s - %s).' %
|
raise ValidationError(
|
||||||
(self.line_from, self.line_to))
|
"The recommendation collides with an existing one (line %s - %s)."
|
||||||
|
% (self.line_from, self.line_to)
|
||||||
|
)
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -791,13 +827,18 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return a string, representing this object."""
|
"""Return a string, representing this object."""
|
||||||
return "Recommendation for Motion %s, line %s - %s" % (self.motion_id, self.line_from, self.line_to)
|
return "Recommendation for Motion %s, line %s - %s" % (
|
||||||
|
self.motion_id,
|
||||||
|
self.line_from,
|
||||||
|
self.line_to,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Category(RESTModelMixin, models.Model):
|
class Category(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Model for categories of motions.
|
Model for categories of motions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = CategoryAccessPermissions()
|
access_permissions = CategoryAccessPermissions()
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
@ -811,7 +852,7 @@ class Category(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
ordering = ['prefix']
|
ordering = ["prefix"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -821,18 +862,20 @@ class MotionBlockManager(models.Manager):
|
||||||
"""
|
"""
|
||||||
Customized model manager to support our get_full_queryset method.
|
Customized model manager to support our get_full_queryset method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_full_queryset(self):
|
def get_full_queryset(self):
|
||||||
"""
|
"""
|
||||||
Returns the normal queryset with all motion blocks. In the
|
Returns the normal queryset with all motion blocks. In the
|
||||||
background the related agenda item is prefetched from the database.
|
background the related agenda item is prefetched from the database.
|
||||||
"""
|
"""
|
||||||
return self.get_queryset().prefetch_related('agenda_items')
|
return self.get_queryset().prefetch_related("agenda_items")
|
||||||
|
|
||||||
|
|
||||||
class MotionBlock(RESTModelMixin, models.Model):
|
class MotionBlock(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Model for blocks of motions.
|
Model for blocks of motions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = MotionBlockAccessPermissions()
|
access_permissions = MotionBlockAccessPermissions()
|
||||||
|
|
||||||
objects = MotionBlockManager()
|
objects = MotionBlockManager()
|
||||||
|
@ -841,10 +884,10 @@ class MotionBlock(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
# In theory there could be one then more agenda_item. But we support only
|
# In theory there could be one then more agenda_item. But we support only
|
||||||
# one. See the property agenda_item.
|
# one. See the property agenda_item.
|
||||||
agenda_items = GenericRelation(Item, related_name='topics')
|
agenda_items = GenericRelation(Item, related_name="topics")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = ugettext_noop('Motion block')
|
verbose_name = ugettext_noop("Motion block")
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -856,10 +899,11 @@ class MotionBlock(RESTModelMixin, models.Model):
|
||||||
motion block projector element is disabled.
|
motion block projector element is disabled.
|
||||||
"""
|
"""
|
||||||
Projector.remove_any(
|
Projector.remove_any(
|
||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate, name="motions/motion-block", id=self.pk
|
||||||
name='motions/motion-block',
|
)
|
||||||
id=self.pk)
|
return super().delete( # type: ignore
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
skip_autoupdate=skip_autoupdate, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Container for runtime information for agenda app (on create or update of this instance).
|
Container for runtime information for agenda app (on create or update of this instance).
|
||||||
|
@ -886,16 +930,15 @@ class MotionBlock(RESTModelMixin, models.Model):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
def get_agenda_title_with_type(self):
|
def get_agenda_title_with_type(self):
|
||||||
return '%s (%s)' % (self.get_agenda_title(), _(self._meta.verbose_name))
|
return "%s (%s)" % (self.get_agenda_title(), _(self._meta.verbose_name))
|
||||||
|
|
||||||
|
|
||||||
class MotionLog(RESTModelMixin, models.Model):
|
class MotionLog(RESTModelMixin, models.Model):
|
||||||
"""Save a logmessage for a motion."""
|
"""Save a logmessage for a motion."""
|
||||||
|
|
||||||
motion = models.ForeignKey(
|
motion = models.ForeignKey(
|
||||||
Motion,
|
Motion, on_delete=models.CASCADE, related_name="log_messages"
|
||||||
on_delete=models.CASCADE,
|
)
|
||||||
related_name='log_messages')
|
|
||||||
"""The motion to witch the object belongs."""
|
"""The motion to witch the object belongs."""
|
||||||
|
|
||||||
message_list = JSONField()
|
message_list = JSONField()
|
||||||
|
@ -904,9 +947,8 @@ class MotionLog(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
person = models.ForeignKey(
|
person = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True
|
||||||
on_delete=models.SET_NULL,
|
)
|
||||||
null=True)
|
|
||||||
"""A user object, who created the log message. Optional."""
|
"""A user object, who created the log message. Optional."""
|
||||||
|
|
||||||
time = models.DateTimeField(auto_now=True)
|
time = models.DateTimeField(auto_now=True)
|
||||||
|
@ -914,18 +956,20 @@ class MotionLog(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
ordering = ['-time']
|
ordering = ["-time"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""
|
"""
|
||||||
Return a string, representing the log message.
|
Return a string, representing the log message.
|
||||||
"""
|
"""
|
||||||
localtime = timezone.localtime(self.time)
|
localtime = timezone.localtime(self.time)
|
||||||
time = formats.date_format(localtime, 'DATETIME_FORMAT')
|
time = formats.date_format(localtime, "DATETIME_FORMAT")
|
||||||
time_and_messages = '%s ' % time + ''.join(map(_, self.message_list))
|
time_and_messages = "%s " % time + "".join(map(_, self.message_list))
|
||||||
if self.person is not None:
|
if self.person is not None:
|
||||||
return _('%(time_and_messages)s by %(person)s') % {'time_and_messages': time_and_messages,
|
return _("%(time_and_messages)s by %(person)s") % {
|
||||||
'person': self.person}
|
"time_and_messages": time_and_messages,
|
||||||
|
"person": self.person,
|
||||||
|
}
|
||||||
return time_and_messages
|
return time_and_messages
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
def get_root_rest_element(self):
|
||||||
|
@ -941,9 +985,7 @@ class MotionVote(RESTModelMixin, BaseVote):
|
||||||
There should allways be three MotionVote objects for each poll,
|
There should allways be three MotionVote objects for each poll,
|
||||||
one for 'yes', 'no', and 'abstain'."""
|
one for 'yes', 'no', and 'abstain'."""
|
||||||
|
|
||||||
option = models.ForeignKey(
|
option = models.ForeignKey("MotionOption", on_delete=models.CASCADE)
|
||||||
'MotionOption',
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
"""The option object, to witch the vote belongs."""
|
"""The option object, to witch the vote belongs."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -961,9 +1003,7 @@ class MotionOption(RESTModelMixin, BaseOption):
|
||||||
|
|
||||||
There should be one MotionOption object for each poll."""
|
There should be one MotionOption object for each poll."""
|
||||||
|
|
||||||
poll = models.ForeignKey(
|
poll = models.ForeignKey("MotionPoll", on_delete=models.CASCADE)
|
||||||
'MotionPoll',
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
"""The poll object, to witch the object belongs."""
|
"""The poll object, to witch the object belongs."""
|
||||||
|
|
||||||
vote_class = MotionVote
|
vote_class = MotionVote
|
||||||
|
@ -984,16 +1024,13 @@ class MotionOption(RESTModelMixin, BaseOption):
|
||||||
class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: ignore
|
class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: ignore
|
||||||
"""The Class to saves the vote result for a motion poll."""
|
"""The Class to saves the vote result for a motion poll."""
|
||||||
|
|
||||||
motion = models.ForeignKey(
|
motion = models.ForeignKey(Motion, on_delete=models.CASCADE, related_name="polls")
|
||||||
Motion,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='polls')
|
|
||||||
"""The motion to witch the object belongs."""
|
"""The motion to witch the object belongs."""
|
||||||
|
|
||||||
option_class = MotionOption
|
option_class = MotionOption
|
||||||
"""The option class, witch links between this object the the votes."""
|
"""The option class, witch links between this object the the votes."""
|
||||||
|
|
||||||
vote_values = ['Yes', 'No', 'Abstain']
|
vote_values = ["Yes", "No", "Abstain"]
|
||||||
"""The possible anwers for the poll. 'Yes, 'No' and 'Abstain'."""
|
"""The possible anwers for the poll. 'Yes, 'No' and 'Abstain'."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -1003,7 +1040,7 @@ class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: i
|
||||||
"""
|
"""
|
||||||
Representation method only for debugging purposes.
|
Representation method only for debugging purposes.
|
||||||
"""
|
"""
|
||||||
return 'MotionPoll for motion %s' % self.motion
|
return "MotionPoll for motion %s" % self.motion
|
||||||
|
|
||||||
def set_options(self, skip_autoupdate=False):
|
def set_options(self, skip_autoupdate=False):
|
||||||
"""Create the option class for this poll."""
|
"""Create the option class for this poll."""
|
||||||
|
@ -1012,7 +1049,7 @@ class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll): # type: i
|
||||||
self.get_option_class()(poll=self).save(skip_autoupdate=skip_autoupdate)
|
self.get_option_class()(poll=self).save(skip_autoupdate=skip_autoupdate)
|
||||||
|
|
||||||
def get_percent_base_choice(self):
|
def get_percent_base_choice(self):
|
||||||
return config['motions_poll_100_percent_base']
|
return config["motions_poll_100_percent_base"]
|
||||||
|
|
||||||
def get_slide_context(self, **context):
|
def get_slide_context(self, **context):
|
||||||
return super(MotionPoll, self).get_slide_context(poll=self)
|
return super(MotionPoll, self).get_slide_context(poll=self)
|
||||||
|
@ -1047,15 +1084,14 @@ class State(RESTModelMixin, models.Model):
|
||||||
"""A string for a recommendation to set the motion to this state."""
|
"""A string for a recommendation to set the motion to this state."""
|
||||||
|
|
||||||
workflow = models.ForeignKey(
|
workflow = models.ForeignKey(
|
||||||
'Workflow',
|
"Workflow", on_delete=models.CASCADE, related_name="states"
|
||||||
on_delete=models.CASCADE,
|
)
|
||||||
related_name='states')
|
|
||||||
"""A many-to-one relation to a workflow."""
|
"""A many-to-one relation to a workflow."""
|
||||||
|
|
||||||
next_states = models.ManyToManyField('self', symmetrical=False, blank=True)
|
next_states = models.ManyToManyField("self", symmetrical=False, blank=True)
|
||||||
"""A many-to-many relation to all states, that can be choosen from this state."""
|
"""A many-to-many relation to all states, that can be choosen from this state."""
|
||||||
|
|
||||||
css_class = models.CharField(max_length=255, default='primary')
|
css_class = models.CharField(max_length=255, default="primary")
|
||||||
"""
|
"""
|
||||||
A css class string for showing the state name in a coloured label based on bootstrap,
|
A css class string for showing the state name in a coloured label based on bootstrap,
|
||||||
e.g. 'danger' (red), 'success' (green), 'warning' (yellow), 'default' (grey).
|
e.g. 'danger' (red), 'success' (green), 'warning' (yellow), 'default' (grey).
|
||||||
|
@ -1131,9 +1167,11 @@ class State(RESTModelMixin, models.Model):
|
||||||
recommendation_label is not an empty string.
|
recommendation_label is not an empty string.
|
||||||
"""
|
"""
|
||||||
self.check_next_states()
|
self.check_next_states()
|
||||||
if self.recommendation_label == '':
|
if self.recommendation_label == "":
|
||||||
raise WorkflowError('The field recommendation_label of {} must not '
|
raise WorkflowError(
|
||||||
'be an empty string.'.format(self))
|
"The field recommendation_label of {} must not "
|
||||||
|
"be an empty string.".format(self)
|
||||||
|
)
|
||||||
super(State, self).save(**kwargs)
|
super(State, self).save(**kwargs)
|
||||||
|
|
||||||
def check_next_states(self):
|
def check_next_states(self):
|
||||||
|
@ -1143,7 +1181,10 @@ class State(RESTModelMixin, models.Model):
|
||||||
return
|
return
|
||||||
for state in self.next_states.all():
|
for state in self.next_states.all():
|
||||||
if not state.workflow == self.workflow:
|
if not state.workflow == self.workflow:
|
||||||
raise WorkflowError('%s can not be next state of %s because it does not belong to the same workflow.' % (state, self))
|
raise WorkflowError(
|
||||||
|
"%s can not be next state of %s because it does not belong to the same workflow."
|
||||||
|
% (state, self)
|
||||||
|
)
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
def get_root_rest_element(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1156,21 +1197,25 @@ class WorkflowManager(models.Manager):
|
||||||
"""
|
"""
|
||||||
Customized model manager to support our get_full_queryset method.
|
Customized model manager to support our get_full_queryset method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_full_queryset(self):
|
def get_full_queryset(self):
|
||||||
"""
|
"""
|
||||||
Returns the normal queryset with all workflows. In the background
|
Returns the normal queryset with all workflows. In the background
|
||||||
the first state is joined and all states and next states are
|
the first state is joined and all states and next states are
|
||||||
prefetched from the database.
|
prefetched from the database.
|
||||||
"""
|
"""
|
||||||
return (self.get_queryset()
|
return (
|
||||||
.select_related('first_state')
|
self.get_queryset()
|
||||||
.prefetch_related('states', 'states__next_states'))
|
.select_related("first_state")
|
||||||
|
.prefetch_related("states", "states__next_states")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Workflow(RESTModelMixin, models.Model):
|
class Workflow(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Defines a workflow for a motion.
|
Defines a workflow for a motion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = WorkflowAccessPermissions()
|
access_permissions = WorkflowAccessPermissions()
|
||||||
|
|
||||||
objects = WorkflowManager()
|
objects = WorkflowManager()
|
||||||
|
@ -1179,11 +1224,8 @@ class Workflow(RESTModelMixin, models.Model):
|
||||||
"""A string representing the workflow."""
|
"""A string representing the workflow."""
|
||||||
|
|
||||||
first_state = models.OneToOneField(
|
first_state = models.OneToOneField(
|
||||||
State,
|
State, on_delete=models.SET_NULL, related_name="+", null=True, blank=True
|
||||||
on_delete=models.SET_NULL,
|
)
|
||||||
related_name='+',
|
|
||||||
null=True,
|
|
||||||
blank=True)
|
|
||||||
"""A one-to-one relation to a state, the starting point for the workflow."""
|
"""A one-to-one relation to a state, the starting point for the workflow."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -1205,5 +1247,6 @@ class Workflow(RESTModelMixin, models.Model):
|
||||||
"""Checks whether the first_state itself belongs to the workflow."""
|
"""Checks whether the first_state itself belongs to the workflow."""
|
||||||
if self.first_state and not self.first_state.workflow == self:
|
if self.first_state and not self.first_state.workflow == self:
|
||||||
raise WorkflowError(
|
raise WorkflowError(
|
||||||
'%s can not be first state of %s because it '
|
"%s can not be first state of %s because it "
|
||||||
'does not belong to it.' % (self.first_state, self))
|
"does not belong to it." % (self.first_state, self)
|
||||||
|
)
|
||||||
|
|
|
@ -9,21 +9,22 @@ class MotionSlide(ProjectorElement):
|
||||||
"""
|
"""
|
||||||
Slide definitions for Motion model.
|
Slide definitions for Motion model.
|
||||||
"""
|
"""
|
||||||
name = 'motions/motion'
|
|
||||||
|
name = "motions/motion"
|
||||||
|
|
||||||
def check_data(self):
|
def check_data(self):
|
||||||
if not Motion.objects.filter(pk=self.config_entry.get('id')).exists():
|
if not Motion.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||||
raise ProjectorException('Motion does not exist.')
|
raise ProjectorException("Motion does not exist.")
|
||||||
|
|
||||||
def update_data(self):
|
def update_data(self):
|
||||||
data = None
|
data = None
|
||||||
try:
|
try:
|
||||||
motion = Motion.objects.get(pk=self.config_entry.get('id'))
|
motion = Motion.objects.get(pk=self.config_entry.get("id"))
|
||||||
except Motion.DoesNotExist:
|
except Motion.DoesNotExist:
|
||||||
# Motion does not exist, so just do nothing.
|
# Motion does not exist, so just do nothing.
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
data = {'agenda_item_id': motion.agenda_item_id}
|
data = {"agenda_item_id": motion.agenda_item_id}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,21 +32,22 @@ class MotionBlockSlide(ProjectorElement):
|
||||||
"""
|
"""
|
||||||
Slide definitions for a block of motions (MotionBlock model).
|
Slide definitions for a block of motions (MotionBlock model).
|
||||||
"""
|
"""
|
||||||
name = 'motions/motion-block'
|
|
||||||
|
name = "motions/motion-block"
|
||||||
|
|
||||||
def check_data(self):
|
def check_data(self):
|
||||||
if not MotionBlock.objects.filter(pk=self.config_entry.get('id')).exists():
|
if not MotionBlock.objects.filter(pk=self.config_entry.get("id")).exists():
|
||||||
raise ProjectorException('MotionBlock does not exist.')
|
raise ProjectorException("MotionBlock does not exist.")
|
||||||
|
|
||||||
def update_data(self):
|
def update_data(self):
|
||||||
data = None
|
data = None
|
||||||
try:
|
try:
|
||||||
motion_block = MotionBlock.objects.get(pk=self.config_entry.get('id'))
|
motion_block = MotionBlock.objects.get(pk=self.config_entry.get("id"))
|
||||||
except MotionBlock.DoesNotExist:
|
except MotionBlock.DoesNotExist:
|
||||||
# MotionBlock does not exist, so just do nothing.
|
# MotionBlock does not exist, so just do nothing.
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
data = {'agenda_item_id': motion_block.agenda_item_id}
|
data = {"agenda_item_id": motion_block.agenda_item_id}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -39,48 +39,55 @@ def validate_workflow_field(value):
|
||||||
Validator to ensure that the workflow with the given id exists.
|
Validator to ensure that the workflow with the given id exists.
|
||||||
"""
|
"""
|
||||||
if not Workflow.objects.filter(pk=value).exists():
|
if not Workflow.objects.filter(pk=value).exists():
|
||||||
raise ValidationError({'detail': _('Workflow %(pk)d does not exist.') % {'pk': value}})
|
raise ValidationError(
|
||||||
|
{"detail": _("Workflow %(pk)d does not exist.") % {"pk": value}}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class StatuteParagraphSerializer(ModelSerializer):
|
class StatuteParagraphSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.StatuteParagraph objects.
|
Serializer for motion.models.StatuteParagraph objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = StatuteParagraph
|
model = StatuteParagraph
|
||||||
fields = ('id', 'title', 'text', 'weight')
|
fields = ("id", "title", "text", "weight")
|
||||||
|
|
||||||
|
|
||||||
class CategorySerializer(ModelSerializer):
|
class CategorySerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.Category objects.
|
Serializer for motion.models.Category objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Category
|
model = Category
|
||||||
fields = ('id', 'name', 'prefix',)
|
fields = ("id", "name", "prefix")
|
||||||
|
|
||||||
|
|
||||||
class MotionBlockSerializer(ModelSerializer):
|
class MotionBlockSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.Category objects.
|
Serializer for motion.models.Category objects.
|
||||||
"""
|
"""
|
||||||
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3)
|
|
||||||
|
agenda_type = IntegerField(
|
||||||
|
write_only=True, required=False, min_value=1, max_value=3
|
||||||
|
)
|
||||||
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
|
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MotionBlock
|
model = MotionBlock
|
||||||
fields = ('id', 'title', 'agenda_item_id', 'agenda_type', 'agenda_parent_id',)
|
fields = ("id", "title", "agenda_item_id", "agenda_type", "agenda_parent_id")
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
"""
|
"""
|
||||||
Customized create method. Set information about related agenda item
|
Customized create method. Set information about related agenda item
|
||||||
into agenda_item_update_information container.
|
into agenda_item_update_information container.
|
||||||
"""
|
"""
|
||||||
agenda_type = validated_data.pop('agenda_type', None)
|
agenda_type = validated_data.pop("agenda_type", None)
|
||||||
agenda_parent_id = validated_data.pop('agenda_parent_id', None)
|
agenda_parent_id = validated_data.pop("agenda_parent_id", None)
|
||||||
motion_block = MotionBlock(**validated_data)
|
motion_block = MotionBlock(**validated_data)
|
||||||
motion_block.agenda_item_update_information['type'] = agenda_type
|
motion_block.agenda_item_update_information["type"] = agenda_type
|
||||||
motion_block.agenda_item_update_information['parent_id'] = agenda_parent_id
|
motion_block.agenda_item_update_information["parent_id"] = agenda_parent_id
|
||||||
motion_block.save()
|
motion_block.save()
|
||||||
return motion_block
|
return motion_block
|
||||||
|
|
||||||
|
@ -89,35 +96,38 @@ class StateSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.State objects.
|
Serializer for motion.models.State objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = State
|
model = State
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
"id",
|
||||||
'name',
|
"name",
|
||||||
'recommendation_label',
|
"recommendation_label",
|
||||||
'css_class',
|
"css_class",
|
||||||
'required_permission_to_see',
|
"required_permission_to_see",
|
||||||
'allow_support',
|
"allow_support",
|
||||||
'allow_create_poll',
|
"allow_create_poll",
|
||||||
'allow_submitter_edit',
|
"allow_submitter_edit",
|
||||||
'dont_set_identifier',
|
"dont_set_identifier",
|
||||||
'show_state_extension_field',
|
"show_state_extension_field",
|
||||||
'merge_amendment_into_final',
|
"merge_amendment_into_final",
|
||||||
'show_recommendation_extension_field',
|
"show_recommendation_extension_field",
|
||||||
'next_states',
|
"next_states",
|
||||||
'workflow')
|
"workflow",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WorkflowSerializer(ModelSerializer):
|
class WorkflowSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.Workflow objects.
|
Serializer for motion.models.Workflow objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
states = StateSerializer(many=True, read_only=True)
|
states = StateSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Workflow
|
model = Workflow
|
||||||
fields = ('id', 'name', 'states', 'first_state',)
|
fields = ("id", "name", "states", "first_state")
|
||||||
read_only_fields = ('first_state',)
|
read_only_fields = ("first_state",)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
|
@ -127,11 +137,11 @@ class WorkflowSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
workflow = super().create(validated_data)
|
workflow = super().create(validated_data)
|
||||||
first_state = State.objects.create(
|
first_state = State.objects.create(
|
||||||
name='new',
|
name="new",
|
||||||
workflow=workflow,
|
workflow=workflow,
|
||||||
allow_create_poll=True,
|
allow_create_poll=True,
|
||||||
allow_support=True,
|
allow_support=True,
|
||||||
allow_submitter_edit=True
|
allow_submitter_edit=True,
|
||||||
)
|
)
|
||||||
workflow.first_state = first_state
|
workflow.first_state = first_state
|
||||||
workflow.save()
|
workflow.save()
|
||||||
|
@ -142,6 +152,7 @@ class AmendmentParagraphsJSONSerializerField(Field):
|
||||||
"""
|
"""
|
||||||
Serializer for motions's amendment_paragraphs JSONField.
|
Serializer for motions's amendment_paragraphs JSONField.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
"""
|
"""
|
||||||
Returns the value of the field.
|
Returns the value of the field.
|
||||||
|
@ -153,10 +164,12 @@ class AmendmentParagraphsJSONSerializerField(Field):
|
||||||
Checks that data is a list of strings.
|
Checks that data is a list of strings.
|
||||||
"""
|
"""
|
||||||
if type(data) is not list:
|
if type(data) is not list:
|
||||||
raise ValidationError({'detail': 'Data must be a list.'})
|
raise ValidationError({"detail": "Data must be a list."})
|
||||||
for paragraph in data:
|
for paragraph in data:
|
||||||
if type(paragraph) is not str and paragraph is not None:
|
if type(paragraph) is not str and paragraph is not None:
|
||||||
raise ValidationError({'detail': 'Paragraph must be either a string or null/None.'})
|
raise ValidationError(
|
||||||
|
{"detail": "Paragraph must be either a string or null/None."}
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@ -164,11 +177,12 @@ class MotionLogSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.MotionLog objects.
|
Serializer for motion.models.MotionLog objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
message = SerializerMethodField()
|
message = SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MotionLog
|
model = MotionLog
|
||||||
fields = ('message_list', 'person', 'time', 'message',)
|
fields = ("message_list", "person", "time", "message")
|
||||||
|
|
||||||
def get_message(self, obj):
|
def get_message(self, obj):
|
||||||
"""
|
"""
|
||||||
|
@ -181,27 +195,32 @@ class MotionPollSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.MotionPoll objects.
|
Serializer for motion.models.MotionPoll objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
yes = SerializerMethodField()
|
yes = SerializerMethodField()
|
||||||
no = SerializerMethodField()
|
no = SerializerMethodField()
|
||||||
abstain = SerializerMethodField()
|
abstain = SerializerMethodField()
|
||||||
votes = DictField(
|
votes = DictField(
|
||||||
child=DecimalField(max_digits=15, decimal_places=6, min_value=-2, allow_null=True),
|
child=DecimalField(
|
||||||
write_only=True)
|
max_digits=15, decimal_places=6, min_value=-2, allow_null=True
|
||||||
|
),
|
||||||
|
write_only=True,
|
||||||
|
)
|
||||||
has_votes = SerializerMethodField()
|
has_votes = SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MotionPoll
|
model = MotionPoll
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
"id",
|
||||||
'motion',
|
"motion",
|
||||||
'yes',
|
"yes",
|
||||||
'no',
|
"no",
|
||||||
'abstain',
|
"abstain",
|
||||||
'votesvalid',
|
"votesvalid",
|
||||||
'votesinvalid',
|
"votesinvalid",
|
||||||
'votescast',
|
"votescast",
|
||||||
'votes',
|
"votes",
|
||||||
'has_votes')
|
"has_votes",
|
||||||
|
)
|
||||||
validators = (default_votes_validator,)
|
validators = (default_votes_validator,)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -211,21 +230,21 @@ class MotionPollSerializer(ModelSerializer):
|
||||||
|
|
||||||
def get_yes(self, obj):
|
def get_yes(self, obj):
|
||||||
try:
|
try:
|
||||||
result: Optional[str] = str(self.get_votes_dict(obj)['Yes'])
|
result: Optional[str] = str(self.get_votes_dict(obj)["Yes"])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
result = None
|
result = None
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_no(self, obj):
|
def get_no(self, obj):
|
||||||
try:
|
try:
|
||||||
result: Optional[str] = str(self.get_votes_dict(obj)['No'])
|
result: Optional[str] = str(self.get_votes_dict(obj)["No"])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
result = None
|
result = None
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_abstain(self, obj):
|
def get_abstain(self, obj):
|
||||||
try:
|
try:
|
||||||
result: Optional[str] = str(self.get_votes_dict(obj)['Abstain'])
|
result: Optional[str] = str(self.get_votes_dict(obj)["Abstain"])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
result = None
|
result = None
|
||||||
return result
|
return result
|
||||||
|
@ -256,21 +275,30 @@ class MotionPollSerializer(ModelSerializer):
|
||||||
"votes": {"Yes": 10, "No": 4, "Abstain": -2}
|
"votes": {"Yes": 10, "No": 4, "Abstain": -2}
|
||||||
"""
|
"""
|
||||||
# Update votes.
|
# Update votes.
|
||||||
votes = validated_data.get('votes')
|
votes = validated_data.get("votes")
|
||||||
if votes:
|
if votes:
|
||||||
if len(votes) != len(instance.get_vote_values()):
|
if len(votes) != len(instance.get_vote_values()):
|
||||||
raise ValidationError({
|
raise ValidationError(
|
||||||
'detail': _('You have to submit data for %d vote values.') % len(instance.get_vote_values())})
|
{
|
||||||
|
"detail": _("You have to submit data for %d vote values.")
|
||||||
|
% len(instance.get_vote_values())
|
||||||
|
}
|
||||||
|
)
|
||||||
for vote_value, vote_weight in votes.items():
|
for vote_value, vote_weight in votes.items():
|
||||||
if vote_value not in instance.get_vote_values():
|
if vote_value not in instance.get_vote_values():
|
||||||
raise ValidationError({
|
raise ValidationError(
|
||||||
'detail': _('Vote value %s is invalid.') % vote_value})
|
{"detail": _("Vote value %s is invalid.") % vote_value}
|
||||||
instance.set_vote_objects_with_values(instance.get_options().get(), votes, skip_autoupdate=True)
|
)
|
||||||
|
instance.set_vote_objects_with_values(
|
||||||
|
instance.get_options().get(), votes, skip_autoupdate=True
|
||||||
|
)
|
||||||
|
|
||||||
# Update remaining writeable fields.
|
# Update remaining writeable fields.
|
||||||
instance.votesvalid = validated_data.get('votesvalid', instance.votesvalid)
|
instance.votesvalid = validated_data.get("votesvalid", instance.votesvalid)
|
||||||
instance.votesinvalid = validated_data.get('votesinvalid', instance.votesinvalid)
|
instance.votesinvalid = validated_data.get(
|
||||||
instance.votescast = validated_data.get('votescast', instance.votescast)
|
"votesinvalid", instance.votesinvalid
|
||||||
|
)
|
||||||
|
instance.votescast = validated_data.get("votescast", instance.votescast)
|
||||||
instance.save()
|
instance.save()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
@ -279,27 +307,29 @@ class MotionChangeRecommendationSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.MotionChangeRecommendation objects.
|
Serializer for motion.models.MotionChangeRecommendation objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MotionChangeRecommendation
|
model = MotionChangeRecommendation
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
"id",
|
||||||
'motion',
|
"motion",
|
||||||
'rejected',
|
"rejected",
|
||||||
'internal',
|
"internal",
|
||||||
'type',
|
"type",
|
||||||
'other_description',
|
"other_description",
|
||||||
'line_from',
|
"line_from",
|
||||||
'line_to',
|
"line_to",
|
||||||
'text',
|
"text",
|
||||||
'creation_time',)
|
"creation_time",
|
||||||
|
)
|
||||||
|
|
||||||
def is_title_cr(self, data):
|
def is_title_cr(self, data):
|
||||||
return int(data['line_from']) == 0 and int(data['line_to']) == 0
|
return int(data["line_from"]) == 0 and int(data["line_to"]) == 0
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
# Change recommendations for titles are stored as plain-text, thus they don't need to be html-escaped
|
# Change recommendations for titles are stored as plain-text, thus they don't need to be html-escaped
|
||||||
if 'text' in data and not self.is_title_cr(data):
|
if "text" in data and not self.is_title_cr(data):
|
||||||
data['text'] = validate_html(data['text'])
|
data["text"] = validate_html(data["text"])
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@ -307,23 +337,18 @@ class MotionCommentSectionSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.MotionCommentSection objects.
|
Serializer for motion.models.MotionCommentSection objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
read_groups = IdPrimaryKeyRelatedField(
|
read_groups = IdPrimaryKeyRelatedField(
|
||||||
many=True,
|
many=True, required=False, queryset=get_group_model().objects.all()
|
||||||
required=False,
|
)
|
||||||
queryset=get_group_model().objects.all())
|
|
||||||
|
|
||||||
write_groups = IdPrimaryKeyRelatedField(
|
write_groups = IdPrimaryKeyRelatedField(
|
||||||
many=True,
|
many=True, required=False, queryset=get_group_model().objects.all()
|
||||||
required=False,
|
)
|
||||||
queryset=get_group_model().objects.all())
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MotionCommentSection
|
model = MotionCommentSection
|
||||||
fields = (
|
fields = ("id", "name", "read_groups", "write_groups")
|
||||||
'id',
|
|
||||||
'name',
|
|
||||||
'read_groups',
|
|
||||||
'write_groups',)
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
""" Call inform_changed_data on creation, so the cache includes the groups. """
|
""" Call inform_changed_data on creation, so the cache includes the groups. """
|
||||||
|
@ -336,15 +361,12 @@ class MotionCommentSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.MotionComment objects.
|
Serializer for motion.models.MotionComment objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
read_groups_id = SerializerMethodField()
|
read_groups_id = SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MotionComment
|
model = MotionComment
|
||||||
fields = (
|
fields = ("id", "comment", "section", "read_groups_id")
|
||||||
'id',
|
|
||||||
'comment',
|
|
||||||
'section',
|
|
||||||
'read_groups_id',)
|
|
||||||
|
|
||||||
def get_read_groups_id(self, comment):
|
def get_read_groups_id(self, comment):
|
||||||
return [group.id for group in comment.section.read_groups.all()]
|
return [group.id for group in comment.section.read_groups.all()]
|
||||||
|
@ -354,20 +376,17 @@ class SubmitterSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.Submitter objects.
|
Serializer for motion.models.Submitter objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Submitter
|
model = Submitter
|
||||||
fields = (
|
fields = ("id", "user", "motion", "weight")
|
||||||
'id',
|
|
||||||
'user',
|
|
||||||
'motion',
|
|
||||||
'weight',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MotionSerializer(ModelSerializer):
|
class MotionSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.Motion objects.
|
Serializer for motion.models.Motion objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
comments = MotionCommentSerializer(many=True, read_only=True)
|
comments = MotionCommentSerializer(many=True, read_only=True)
|
||||||
log_messages = MotionLogSerializer(many=True, read_only=True)
|
log_messages = MotionLogSerializer(many=True, read_only=True)
|
||||||
polls = MotionPollSerializer(many=True, read_only=True)
|
polls = MotionPollSerializer(many=True, read_only=True)
|
||||||
|
@ -378,67 +397,76 @@ class MotionSerializer(ModelSerializer):
|
||||||
title = CharField(max_length=255)
|
title = CharField(max_length=255)
|
||||||
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False)
|
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False)
|
||||||
workflow_id = IntegerField(
|
workflow_id = IntegerField(
|
||||||
min_value=1,
|
min_value=1, required=False, validators=[validate_workflow_field]
|
||||||
required=False,
|
)
|
||||||
validators=[validate_workflow_field])
|
agenda_type = IntegerField(
|
||||||
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3)
|
write_only=True, required=False, min_value=1, max_value=3
|
||||||
|
)
|
||||||
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
|
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
|
||||||
submitters = SubmitterSerializer(many=True, read_only=True)
|
submitters = SubmitterSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Motion
|
model = Motion
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
"id",
|
||||||
'identifier',
|
"identifier",
|
||||||
'title',
|
"title",
|
||||||
'text',
|
"text",
|
||||||
'amendment_paragraphs',
|
"amendment_paragraphs",
|
||||||
'modified_final_version',
|
"modified_final_version",
|
||||||
'reason',
|
"reason",
|
||||||
'parent',
|
"parent",
|
||||||
'category',
|
"category",
|
||||||
'comments',
|
"comments",
|
||||||
'motion_block',
|
"motion_block",
|
||||||
'origin',
|
"origin",
|
||||||
'submitters',
|
"submitters",
|
||||||
'supporters',
|
"supporters",
|
||||||
'state',
|
"state",
|
||||||
'state_extension',
|
"state_extension",
|
||||||
'state_required_permission_to_see',
|
"state_required_permission_to_see",
|
||||||
'statute_paragraph',
|
"statute_paragraph",
|
||||||
'workflow_id',
|
"workflow_id",
|
||||||
'recommendation',
|
"recommendation",
|
||||||
'recommendation_extension',
|
"recommendation_extension",
|
||||||
'tags',
|
"tags",
|
||||||
'attachments',
|
"attachments",
|
||||||
'polls',
|
"polls",
|
||||||
'agenda_item_id',
|
"agenda_item_id",
|
||||||
'agenda_type',
|
"agenda_type",
|
||||||
'agenda_parent_id',
|
"agenda_parent_id",
|
||||||
'log_messages',
|
"log_messages",
|
||||||
'sort_parent',
|
"sort_parent",
|
||||||
'weight',)
|
"weight",
|
||||||
read_only_fields = ('state', 'recommendation',) # Some other fields are also read_only. See definitions above.
|
)
|
||||||
|
read_only_fields = (
|
||||||
|
"state",
|
||||||
|
"recommendation",
|
||||||
|
) # Some other fields are also read_only. See definitions above.
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if 'text'in data:
|
if "text" in data:
|
||||||
data['text'] = validate_html(data['text'])
|
data["text"] = validate_html(data["text"])
|
||||||
|
|
||||||
if 'modified_final_version' in data:
|
if "modified_final_version" in data:
|
||||||
data['modified_final_version'] = validate_html(data['modified_final_version'])
|
data["modified_final_version"] = validate_html(
|
||||||
|
data["modified_final_version"]
|
||||||
|
)
|
||||||
|
|
||||||
if 'reason' in data:
|
if "reason" in data:
|
||||||
data['reason'] = validate_html(data['reason'])
|
data["reason"] = validate_html(data["reason"])
|
||||||
|
|
||||||
if 'amendment_paragraphs' in data:
|
if "amendment_paragraphs" in data:
|
||||||
data['amendment_paragraphs'] = list(map(lambda entry: validate_html(entry) if type(entry) is str else None,
|
data["amendment_paragraphs"] = list(
|
||||||
data['amendment_paragraphs']))
|
map(
|
||||||
data['text'] = ''
|
lambda entry: validate_html(entry) if type(entry) is str else None,
|
||||||
|
data["amendment_paragraphs"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data["text"] = ""
|
||||||
else:
|
else:
|
||||||
if 'text' in data and len(data['text']) == 0:
|
if "text" in data and len(data["text"]) == 0:
|
||||||
raise ValidationError({
|
raise ValidationError({"detail": _("This field may not be blank.")})
|
||||||
'detail': _('This field may not be blank.')
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -451,24 +479,28 @@ class MotionSerializer(ModelSerializer):
|
||||||
agenda_item_update_information container.
|
agenda_item_update_information container.
|
||||||
"""
|
"""
|
||||||
motion = Motion()
|
motion = Motion()
|
||||||
motion.title = validated_data['title']
|
motion.title = validated_data["title"]
|
||||||
motion.text = validated_data['text']
|
motion.text = validated_data["text"]
|
||||||
motion.amendment_paragraphs = validated_data.get('amendment_paragraphs')
|
motion.amendment_paragraphs = validated_data.get("amendment_paragraphs")
|
||||||
motion.modified_final_version = validated_data.get('modified_final_version', '')
|
motion.modified_final_version = validated_data.get("modified_final_version", "")
|
||||||
motion.reason = validated_data.get('reason', '')
|
motion.reason = validated_data.get("reason", "")
|
||||||
motion.identifier = validated_data.get('identifier')
|
motion.identifier = validated_data.get("identifier")
|
||||||
motion.category = validated_data.get('category')
|
motion.category = validated_data.get("category")
|
||||||
motion.motion_block = validated_data.get('motion_block')
|
motion.motion_block = validated_data.get("motion_block")
|
||||||
motion.origin = validated_data.get('origin', '')
|
motion.origin = validated_data.get("origin", "")
|
||||||
motion.parent = validated_data.get('parent')
|
motion.parent = validated_data.get("parent")
|
||||||
motion.statute_paragraph = validated_data.get('statute_paragraph')
|
motion.statute_paragraph = validated_data.get("statute_paragraph")
|
||||||
motion.reset_state(validated_data.get('workflow_id'))
|
motion.reset_state(validated_data.get("workflow_id"))
|
||||||
motion.agenda_item_update_information['type'] = validated_data.get('agenda_type')
|
motion.agenda_item_update_information["type"] = validated_data.get(
|
||||||
motion.agenda_item_update_information['parent_id'] = validated_data.get('agenda_parent_id')
|
"agenda_type"
|
||||||
|
)
|
||||||
|
motion.agenda_item_update_information["parent_id"] = validated_data.get(
|
||||||
|
"agenda_parent_id"
|
||||||
|
)
|
||||||
motion.save()
|
motion.save()
|
||||||
motion.supporters.add(*validated_data.get('supporters', []))
|
motion.supporters.add(*validated_data.get("supporters", []))
|
||||||
motion.attachments.add(*validated_data.get('attachments', []))
|
motion.attachments.add(*validated_data.get("attachments", []))
|
||||||
motion.tags.add(*validated_data.get('tags', []))
|
motion.tags.add(*validated_data.get("tags", []))
|
||||||
return motion
|
return motion
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
@ -477,8 +509,8 @@ class MotionSerializer(ModelSerializer):
|
||||||
Customized method to update a motion.
|
Customized method to update a motion.
|
||||||
"""
|
"""
|
||||||
workflow_id = None
|
workflow_id = None
|
||||||
if 'workflow_id' in validated_data:
|
if "workflow_id" in validated_data:
|
||||||
workflow_id = validated_data.pop('workflow_id')
|
workflow_id = validated_data.pop("workflow_id")
|
||||||
|
|
||||||
result = super().update(motion, validated_data)
|
result = super().update(motion, validated_data)
|
||||||
|
|
||||||
|
|
|
@ -14,98 +14,128 @@ def create_builtin_workflows(sender, **kwargs):
|
||||||
# If there is at least one workflow, then do nothing.
|
# If there is at least one workflow, then do nothing.
|
||||||
return
|
return
|
||||||
|
|
||||||
workflow_1 = Workflow(name='Simple Workflow')
|
workflow_1 = Workflow(name="Simple Workflow")
|
||||||
workflow_1.save(skip_autoupdate=True)
|
workflow_1.save(skip_autoupdate=True)
|
||||||
state_1_1 = State(name=ugettext_noop('submitted'),
|
state_1_1 = State(
|
||||||
workflow=workflow_1,
|
name=ugettext_noop("submitted"),
|
||||||
allow_create_poll=True,
|
workflow=workflow_1,
|
||||||
allow_support=True,
|
allow_create_poll=True,
|
||||||
allow_submitter_edit=True)
|
allow_support=True,
|
||||||
|
allow_submitter_edit=True,
|
||||||
|
)
|
||||||
state_1_1.save(skip_autoupdate=True)
|
state_1_1.save(skip_autoupdate=True)
|
||||||
state_1_2 = State(name=ugettext_noop('accepted'),
|
state_1_2 = State(
|
||||||
workflow=workflow_1,
|
name=ugettext_noop("accepted"),
|
||||||
recommendation_label='Acceptance',
|
workflow=workflow_1,
|
||||||
css_class='success',
|
recommendation_label="Acceptance",
|
||||||
merge_amendment_into_final=1)
|
css_class="success",
|
||||||
|
merge_amendment_into_final=1,
|
||||||
|
)
|
||||||
state_1_2.save(skip_autoupdate=True)
|
state_1_2.save(skip_autoupdate=True)
|
||||||
state_1_3 = State(name=ugettext_noop('rejected'),
|
state_1_3 = State(
|
||||||
workflow=workflow_1,
|
name=ugettext_noop("rejected"),
|
||||||
recommendation_label='Rejection',
|
workflow=workflow_1,
|
||||||
css_class='danger',
|
recommendation_label="Rejection",
|
||||||
merge_amendment_into_final=-1)
|
css_class="danger",
|
||||||
|
merge_amendment_into_final=-1,
|
||||||
|
)
|
||||||
state_1_3.save(skip_autoupdate=True)
|
state_1_3.save(skip_autoupdate=True)
|
||||||
state_1_4 = State(name=ugettext_noop('not decided'),
|
state_1_4 = State(
|
||||||
workflow=workflow_1,
|
name=ugettext_noop("not decided"),
|
||||||
recommendation_label='No decision',
|
workflow=workflow_1,
|
||||||
css_class='default',
|
recommendation_label="No decision",
|
||||||
merge_amendment_into_final=-1)
|
css_class="default",
|
||||||
|
merge_amendment_into_final=-1,
|
||||||
|
)
|
||||||
state_1_4.save(skip_autoupdate=True)
|
state_1_4.save(skip_autoupdate=True)
|
||||||
state_1_1.next_states.add(state_1_2, state_1_3, state_1_4)
|
state_1_1.next_states.add(state_1_2, state_1_3, state_1_4)
|
||||||
workflow_1.first_state = state_1_1
|
workflow_1.first_state = state_1_1
|
||||||
workflow_1.save(skip_autoupdate=True)
|
workflow_1.save(skip_autoupdate=True)
|
||||||
|
|
||||||
workflow_2 = Workflow(name='Complex Workflow')
|
workflow_2 = Workflow(name="Complex Workflow")
|
||||||
workflow_2.save(skip_autoupdate=True)
|
workflow_2.save(skip_autoupdate=True)
|
||||||
state_2_1 = State(name=ugettext_noop('published'),
|
state_2_1 = State(
|
||||||
workflow=workflow_2,
|
name=ugettext_noop("published"),
|
||||||
allow_support=True,
|
workflow=workflow_2,
|
||||||
allow_submitter_edit=True,
|
allow_support=True,
|
||||||
dont_set_identifier=True)
|
allow_submitter_edit=True,
|
||||||
|
dont_set_identifier=True,
|
||||||
|
)
|
||||||
state_2_1.save(skip_autoupdate=True)
|
state_2_1.save(skip_autoupdate=True)
|
||||||
state_2_2 = State(name=ugettext_noop('permitted'),
|
state_2_2 = State(
|
||||||
workflow=workflow_2,
|
name=ugettext_noop("permitted"),
|
||||||
recommendation_label='Permission',
|
workflow=workflow_2,
|
||||||
allow_create_poll=True,
|
recommendation_label="Permission",
|
||||||
allow_submitter_edit=True)
|
allow_create_poll=True,
|
||||||
|
allow_submitter_edit=True,
|
||||||
|
)
|
||||||
state_2_2.save(skip_autoupdate=True)
|
state_2_2.save(skip_autoupdate=True)
|
||||||
state_2_3 = State(name=ugettext_noop('accepted'),
|
state_2_3 = State(
|
||||||
workflow=workflow_2,
|
name=ugettext_noop("accepted"),
|
||||||
recommendation_label='Acceptance',
|
workflow=workflow_2,
|
||||||
css_class='success',
|
recommendation_label="Acceptance",
|
||||||
merge_amendment_into_final=1)
|
css_class="success",
|
||||||
|
merge_amendment_into_final=1,
|
||||||
|
)
|
||||||
state_2_3.save(skip_autoupdate=True)
|
state_2_3.save(skip_autoupdate=True)
|
||||||
state_2_4 = State(name=ugettext_noop('rejected'),
|
state_2_4 = State(
|
||||||
workflow=workflow_2,
|
name=ugettext_noop("rejected"),
|
||||||
recommendation_label='Rejection',
|
workflow=workflow_2,
|
||||||
css_class='danger',
|
recommendation_label="Rejection",
|
||||||
merge_amendment_into_final=-1)
|
css_class="danger",
|
||||||
|
merge_amendment_into_final=-1,
|
||||||
|
)
|
||||||
state_2_4.save(skip_autoupdate=True)
|
state_2_4.save(skip_autoupdate=True)
|
||||||
state_2_5 = State(name=ugettext_noop('withdrawed'),
|
state_2_5 = State(
|
||||||
workflow=workflow_2,
|
name=ugettext_noop("withdrawed"),
|
||||||
css_class='default',
|
workflow=workflow_2,
|
||||||
merge_amendment_into_final=-1)
|
css_class="default",
|
||||||
|
merge_amendment_into_final=-1,
|
||||||
|
)
|
||||||
state_2_5.save(skip_autoupdate=True)
|
state_2_5.save(skip_autoupdate=True)
|
||||||
state_2_6 = State(name=ugettext_noop('adjourned'),
|
state_2_6 = State(
|
||||||
workflow=workflow_2,
|
name=ugettext_noop("adjourned"),
|
||||||
recommendation_label='Adjournment',
|
workflow=workflow_2,
|
||||||
css_class='default',
|
recommendation_label="Adjournment",
|
||||||
merge_amendment_into_final=-1)
|
css_class="default",
|
||||||
|
merge_amendment_into_final=-1,
|
||||||
|
)
|
||||||
state_2_6.save(skip_autoupdate=True)
|
state_2_6.save(skip_autoupdate=True)
|
||||||
state_2_7 = State(name=ugettext_noop('not concerned'),
|
state_2_7 = State(
|
||||||
workflow=workflow_2,
|
name=ugettext_noop("not concerned"),
|
||||||
recommendation_label='No concernment',
|
workflow=workflow_2,
|
||||||
css_class='default',
|
recommendation_label="No concernment",
|
||||||
merge_amendment_into_final=-1)
|
css_class="default",
|
||||||
|
merge_amendment_into_final=-1,
|
||||||
|
)
|
||||||
state_2_7.save(skip_autoupdate=True)
|
state_2_7.save(skip_autoupdate=True)
|
||||||
state_2_8 = State(name=ugettext_noop('refered to committee'),
|
state_2_8 = State(
|
||||||
workflow=workflow_2,
|
name=ugettext_noop("refered to committee"),
|
||||||
recommendation_label='Referral to committee',
|
workflow=workflow_2,
|
||||||
css_class='default',
|
recommendation_label="Referral to committee",
|
||||||
merge_amendment_into_final=-1)
|
css_class="default",
|
||||||
|
merge_amendment_into_final=-1,
|
||||||
|
)
|
||||||
state_2_8.save(skip_autoupdate=True)
|
state_2_8.save(skip_autoupdate=True)
|
||||||
state_2_9 = State(name=ugettext_noop('needs review'),
|
state_2_9 = State(
|
||||||
workflow=workflow_2,
|
name=ugettext_noop("needs review"),
|
||||||
css_class='default',
|
workflow=workflow_2,
|
||||||
merge_amendment_into_final=-1)
|
css_class="default",
|
||||||
|
merge_amendment_into_final=-1,
|
||||||
|
)
|
||||||
state_2_9.save(skip_autoupdate=True)
|
state_2_9.save(skip_autoupdate=True)
|
||||||
state_2_10 = State(name=ugettext_noop('rejected (not authorized)'),
|
state_2_10 = State(
|
||||||
workflow=workflow_2,
|
name=ugettext_noop("rejected (not authorized)"),
|
||||||
recommendation_label='Rejection (not authorized)',
|
workflow=workflow_2,
|
||||||
css_class='default',
|
recommendation_label="Rejection (not authorized)",
|
||||||
merge_amendment_into_final=-1)
|
css_class="default",
|
||||||
|
merge_amendment_into_final=-1,
|
||||||
|
)
|
||||||
state_2_10.save(skip_autoupdate=True)
|
state_2_10.save(skip_autoupdate=True)
|
||||||
state_2_1.next_states.add(state_2_2, state_2_5, state_2_10)
|
state_2_1.next_states.add(state_2_2, state_2_5, state_2_10)
|
||||||
state_2_2.next_states.add(state_2_3, state_2_4, state_2_5, state_2_6, state_2_7, state_2_8, state_2_9)
|
state_2_2.next_states.add(
|
||||||
|
state_2_3, state_2_4, state_2_5, state_2_6, state_2_7, state_2_8, state_2_9
|
||||||
|
)
|
||||||
workflow_2.first_state = state_2_1
|
workflow_2.first_state = state_2_1
|
||||||
workflow_2.save(skip_autoupdate=True)
|
workflow_2.save(skip_autoupdate=True)
|
||||||
|
|
||||||
|
@ -114,8 +144,11 @@ def get_permission_change_data(sender, permissions, **kwargs):
|
||||||
"""
|
"""
|
||||||
Yields all necessary collections if 'motions.can_see' permission changes.
|
Yields all necessary collections if 'motions.can_see' permission changes.
|
||||||
"""
|
"""
|
||||||
motions_app = apps.get_app_config(app_label='motions')
|
motions_app = apps.get_app_config(app_label="motions")
|
||||||
for permission in permissions:
|
for permission in permissions:
|
||||||
# There could be only one 'motions.can_see' and then we want to return data.
|
# There could be only one 'motions.can_see' and then we want to return data.
|
||||||
if permission.content_type.app_label == motions_app.label and permission.codename == 'can_see':
|
if (
|
||||||
|
permission.content_type.app_label == motions_app.label
|
||||||
|
and permission.codename == "can_see"
|
||||||
|
):
|
||||||
yield from motions_app.get_startup_elements()
|
yield from motions_app.get_startup_elements()
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
||||||
# Common majority methods for all apps using polls. The first one should be the default.
|
# Common majority methods for all apps using polls. The first one should be the default.
|
||||||
majorityMethods = (
|
majorityMethods = (
|
||||||
{'value': 'simple_majority', 'display_name': 'Simple majority'},
|
{"value": "simple_majority", "display_name": "Simple majority"},
|
||||||
{'value': 'two-thirds_majority', 'display_name': 'Two-thirds majority'},
|
{"value": "two-thirds_majority", "display_name": "Two-thirds majority"},
|
||||||
{'value': 'three-quarters_majority', 'display_name': 'Three-quarters majority'},
|
{"value": "three-quarters_majority", "display_name": "Three-quarters majority"},
|
||||||
{'value': 'disabled', 'display_name': 'Disabled'},
|
{"value": "disabled", "display_name": "Disabled"},
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,7 +17,8 @@ class BaseOption(models.Model):
|
||||||
which has to be a subclass of BaseVote. Otherwise you have to override the
|
which has to be a subclass of BaseVote. Otherwise you have to override the
|
||||||
get_vote_class method.
|
get_vote_class method.
|
||||||
"""
|
"""
|
||||||
vote_class: Optional[Type['BaseVote']] = None
|
|
||||||
|
vote_class: Optional[Type["BaseVote"]] = None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
@ -27,7 +28,9 @@ class BaseOption(models.Model):
|
||||||
|
|
||||||
def get_vote_class(self):
|
def get_vote_class(self):
|
||||||
if self.vote_class is None:
|
if self.vote_class is None:
|
||||||
raise NotImplementedError('The option class %s has to have an attribute vote_class.' % self)
|
raise NotImplementedError(
|
||||||
|
"The option class %s has to have an attribute vote_class." % self
|
||||||
|
)
|
||||||
return self.vote_class
|
return self.vote_class
|
||||||
|
|
||||||
def __getitem__(self, name):
|
def __getitem__(self, name):
|
||||||
|
@ -44,8 +47,14 @@ class BaseVote(models.Model):
|
||||||
Subclasses have to define an option field. This must be a ForeignKeyField
|
Subclasses have to define an option field. This must be a ForeignKeyField
|
||||||
to a subclass of BasePoll.
|
to a subclass of BasePoll.
|
||||||
"""
|
"""
|
||||||
weight = models.DecimalField(default=Decimal('1'), null=True, validators=[
|
|
||||||
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
|
weight = models.DecimalField(
|
||||||
|
default=Decimal("1"),
|
||||||
|
null=True,
|
||||||
|
validators=[MinValueValidator(Decimal("-2"))],
|
||||||
|
max_digits=15,
|
||||||
|
decimal_places=6,
|
||||||
|
)
|
||||||
value = models.CharField(max_length=255, null=True)
|
value = models.CharField(max_length=255, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -73,12 +82,28 @@ class CollectDefaultVotesMixin(models.Model):
|
||||||
Mixin for a poll to collect the default vote values for valid votes,
|
Mixin for a poll to collect the default vote values for valid votes,
|
||||||
invalid votes and votes cast.
|
invalid votes and votes cast.
|
||||||
"""
|
"""
|
||||||
votesvalid = models.DecimalField(null=True, blank=True, validators=[
|
|
||||||
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
|
votesvalid = models.DecimalField(
|
||||||
votesinvalid = models.DecimalField(null=True, blank=True, validators=[
|
null=True,
|
||||||
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
|
blank=True,
|
||||||
votescast = models.DecimalField(null=True, blank=True, validators=[
|
validators=[MinValueValidator(Decimal("-2"))],
|
||||||
MinValueValidator(Decimal('-2'))], max_digits=15, decimal_places=6)
|
max_digits=15,
|
||||||
|
decimal_places=6,
|
||||||
|
)
|
||||||
|
votesinvalid = models.DecimalField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
validators=[MinValueValidator(Decimal("-2"))],
|
||||||
|
max_digits=15,
|
||||||
|
decimal_places=6,
|
||||||
|
)
|
||||||
|
votescast = models.DecimalField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
validators=[MinValueValidator(Decimal("-2"))],
|
||||||
|
max_digits=15,
|
||||||
|
decimal_places=6,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
@ -87,13 +112,16 @@ class CollectDefaultVotesMixin(models.Model):
|
||||||
"""
|
"""
|
||||||
Returns one of the strings of the percent base.
|
Returns one of the strings of the percent base.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError('You have to provide a get_percent_base_choice() method.')
|
raise NotImplementedError(
|
||||||
|
"You have to provide a get_percent_base_choice() method."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PublishPollMixin(models.Model):
|
class PublishPollMixin(models.Model):
|
||||||
"""
|
"""
|
||||||
Mixin for a poll to add a flag whether the poll is published or not.
|
Mixin for a poll to add a flag whether the poll is published or not.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
published = models.BooleanField(default=False)
|
published = models.BooleanField(default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -108,7 +136,8 @@ class BasePoll(models.Model):
|
||||||
"""
|
"""
|
||||||
Base poll class.
|
Base poll class.
|
||||||
"""
|
"""
|
||||||
vote_values = ['Votes']
|
|
||||||
|
vote_values = ["Votes"]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
@ -183,7 +212,7 @@ class BasePoll(models.Model):
|
||||||
try:
|
try:
|
||||||
vote = self.get_votes().filter(option=option_id).get(value=value)
|
vote = self.get_votes().filter(option=option_id).get(value=value)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
values.append(self.get_vote_class()(value=value, weight=''))
|
values.append(self.get_vote_class()(value=value, weight=""))
|
||||||
else:
|
else:
|
||||||
values.append(vote)
|
values.append(vote)
|
||||||
return values
|
return values
|
||||||
|
@ -195,15 +224,18 @@ def print_value(value, percent_base=0):
|
||||||
'undocumented' or the vote value with percent value if so.
|
'undocumented' or the vote value with percent value if so.
|
||||||
"""
|
"""
|
||||||
if value == -1:
|
if value == -1:
|
||||||
verbose_value = _('majority')
|
verbose_value = _("majority")
|
||||||
elif value == -2:
|
elif value == -2:
|
||||||
verbose_value = _('undocumented')
|
verbose_value = _("undocumented")
|
||||||
elif value is None:
|
elif value is None:
|
||||||
verbose_value = _('undocumented')
|
verbose_value = _("undocumented")
|
||||||
else:
|
else:
|
||||||
if percent_base:
|
if percent_base:
|
||||||
locale.setlocale(locale.LC_ALL, '')
|
locale.setlocale(locale.LC_ALL, "")
|
||||||
verbose_value = u'%d (%s %%)' % (value, locale.format('%.1f', value * percent_base))
|
verbose_value = "%d (%s %%)" % (
|
||||||
|
value,
|
||||||
|
locale.format("%.1f", value * percent_base),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
verbose_value = u'%s' % value
|
verbose_value = "%s" % value
|
||||||
return verbose_value
|
return verbose_value
|
||||||
|
|
|
@ -10,8 +10,12 @@ def default_votes_validator(data):
|
||||||
than or equal to -2.
|
than or equal to -2.
|
||||||
"""
|
"""
|
||||||
for key in data:
|
for key in data:
|
||||||
if (key in ('votesvalid', 'votesinvalid', 'votescast') and
|
if (
|
||||||
data[key] is not None and
|
key in ("votesvalid", "votesinvalid", "votescast")
|
||||||
data[key] < -2):
|
and data[key] is not None
|
||||||
raise ValidationError({'detail': _('Value for {} must not be less than -2').format(key)})
|
and data[key] < -2
|
||||||
|
):
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": _("Value for {} must not be less than -2").format(key)}
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -5,11 +5,9 @@ from openslides.utils.consumers import SiteConsumer
|
||||||
from openslides.utils.middleware import AuthMiddlewareStack
|
from openslides.utils.middleware import AuthMiddlewareStack
|
||||||
|
|
||||||
|
|
||||||
application = ProtocolTypeRouter({
|
application = ProtocolTypeRouter(
|
||||||
# WebSocket chat handler
|
{
|
||||||
"websocket": AuthMiddlewareStack(
|
# WebSocket chat handler
|
||||||
URLRouter([
|
"websocket": AuthMiddlewareStack(URLRouter([url(r"^ws/$", SiteConsumer)]))
|
||||||
url(r"^ws/$", SiteConsumer),
|
}
|
||||||
])
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue