commit
f1d7f85be9
@ -3,6 +3,7 @@
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Dict # noqa
|
||||||
|
|
||||||
import django
|
import django
|
||||||
from django.core.management import call_command, execute_from_command_line
|
from django.core.management import call_command, execute_from_command_line
|
||||||
@ -88,7 +89,7 @@ def get_parser():
|
|||||||
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,
|
"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='')
|
||||||
|
|
||||||
@ -155,8 +156,8 @@ def get_parser():
|
|||||||
('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(
|
subparsers._choices_actions.append( # type: ignore
|
||||||
subparsers._ChoicesPseudoAction(
|
subparsers._ChoicesPseudoAction( # type: ignore
|
||||||
django_subcommand,
|
django_subcommand,
|
||||||
(),
|
(),
|
||||||
help_text))
|
help_text))
|
||||||
@ -248,7 +249,7 @@ def createsettings(args):
|
|||||||
"""
|
"""
|
||||||
settings_path = args.settings_path
|
settings_path = args.settings_path
|
||||||
local_installation = is_local_installation()
|
local_installation = is_local_installation()
|
||||||
context = {}
|
context = {} # type: Dict[str, str]
|
||||||
|
|
||||||
if local_installation:
|
if local_installation:
|
||||||
if settings_path is None:
|
if settings_path is None:
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
from ..utils.access_permissions import BaseAccessPermissions
|
from typing import Iterable # noqa
|
||||||
|
|
||||||
|
from ..utils.access_permissions import ( # noqa
|
||||||
|
BaseAccessPermissions,
|
||||||
|
RestrictedData,
|
||||||
|
)
|
||||||
from ..utils.auth import has_perm
|
from ..utils.auth import has_perm
|
||||||
from ..utils.collection import Collection
|
from ..utils.collection import Collection
|
||||||
|
|
||||||
@ -55,17 +60,17 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
|||||||
|
|
||||||
# In hidden case managers and non managers see only some fields
|
# In hidden case managers and non managers see only some fields
|
||||||
# so that list of speakers is provided regardless.
|
# so that list of speakers is provided regardless.
|
||||||
blocked_keys_hidden_case = full_data[0].keys() - (
|
blocked_keys_hidden_case = set(full_data[0].keys()) - set((
|
||||||
'id',
|
'id',
|
||||||
'title',
|
'title',
|
||||||
'speakers',
|
'speakers',
|
||||||
'speaker_list_closed',
|
'speaker_list_closed',
|
||||||
'content_object')
|
'content_object'))
|
||||||
|
|
||||||
# In non hidden case managers see everything and non managers see
|
# In non hidden case managers see everything and non managers see
|
||||||
# everything but comments.
|
# everything but comments.
|
||||||
if has_perm(user, 'agenda.can_manage'):
|
if has_perm(user, 'agenda.can_manage'):
|
||||||
blocked_keys_non_hidden_case = []
|
blocked_keys_non_hidden_case = [] # type: Iterable[str]
|
||||||
else:
|
else:
|
||||||
blocked_keys_non_hidden_case = ('comment',)
|
blocked_keys_non_hidden_case = ('comment',)
|
||||||
|
|
||||||
@ -81,7 +86,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
|||||||
# Reduce result to a single item or None if it was not a collection at
|
# Reduce result to a single item or None if it was not a collection at
|
||||||
# the beginning of the method.
|
# the beginning of the method.
|
||||||
if isinstance(container, Collection):
|
if isinstance(container, Collection):
|
||||||
restricted_data = data
|
restricted_data = data # type: RestrictedData
|
||||||
elif data:
|
elif data:
|
||||||
restricted_data = data[0]
|
restricted_data = data[0]
|
||||||
else:
|
else:
|
||||||
@ -111,7 +116,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
|||||||
# Reduce result to a single item or None if it was not a collection at
|
# Reduce result to a single item or None if it was not a collection at
|
||||||
# the beginning of the method.
|
# the beginning of the method.
|
||||||
if isinstance(container, Collection):
|
if isinstance(container, Collection):
|
||||||
projector_data = data
|
projector_data = data # type: RestrictedData
|
||||||
elif data:
|
elif data:
|
||||||
projector_data = data[0]
|
projector_data = data[0]
|
||||||
else:
|
else:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from typing import Dict, List, Set # noqa
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
@ -79,7 +80,7 @@ class ItemManager(models.Manager):
|
|||||||
HIDDEN_ITEM and all of their children.
|
HIDDEN_ITEM and all of their children.
|
||||||
"""
|
"""
|
||||||
queryset = self.order_by('weight')
|
queryset = self.order_by('weight')
|
||||||
item_children = defaultdict(list)
|
item_children = defaultdict(list) # type: Dict[int, List[Item]]
|
||||||
root_items = []
|
root_items = []
|
||||||
for item in queryset:
|
for item in queryset:
|
||||||
if only_agenda_items and item.type == item.HIDDEN_ITEM:
|
if only_agenda_items and item.type == item.HIDDEN_ITEM:
|
||||||
@ -135,7 +136,7 @@ class ItemManager(models.Manager):
|
|||||||
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()
|
touched_items = set() # type: Set[int]
|
||||||
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
|
||||||
@ -293,7 +294,7 @@ class Item(RESTModelMixin, models.Model):
|
|||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate,
|
||||||
name='agenda/list-of-speakers',
|
name='agenda/list-of-speakers',
|
||||||
id=self.pk)
|
id=self.pk)
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs)
|
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self):
|
def title(self):
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from typing import Set # noqa
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
@ -62,7 +64,7 @@ def required_users(sender, request_user, **kwargs):
|
|||||||
if request_user can see the agenda. This function may return an empty
|
if request_user can see the agenda. This function may return an empty
|
||||||
set.
|
set.
|
||||||
"""
|
"""
|
||||||
speakers = set()
|
speakers = set() # type: Set[int]
|
||||||
if has_perm(request_user, 'agenda.can_see'):
|
if has_perm(request_user, 'agenda.can_see'):
|
||||||
for item_collection_element in Collection(Item.get_collection_string()).element_generator():
|
for item_collection_element in Collection(Item.get_collection_string()).element_generator():
|
||||||
full_data = item_collection_element.get_full_data()
|
full_data = item_collection_element.get_full_data()
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
from ..utils.access_permissions import BaseAccessPermissions
|
from ..utils.access_permissions import ( # noqa
|
||||||
|
BaseAccessPermissions,
|
||||||
|
RestrictedData,
|
||||||
|
)
|
||||||
from ..utils.auth import has_perm
|
from ..utils.auth import has_perm
|
||||||
from ..utils.collection import Collection
|
from ..utils.collection import Collection
|
||||||
|
|
||||||
@ -50,7 +53,7 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
|
|||||||
# Reduce result to a single item or None if it was not a collection at
|
# Reduce result to a single item or None if it was not a collection at
|
||||||
# the beginning of the method.
|
# the beginning of the method.
|
||||||
if isinstance(container, Collection):
|
if isinstance(container, Collection):
|
||||||
restricted_data = data
|
restricted_data = data # type: RestrictedData
|
||||||
elif data:
|
elif data:
|
||||||
restricted_data = data[0]
|
restricted_data = data[0]
|
||||||
else:
|
else:
|
||||||
@ -76,7 +79,7 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
|
|||||||
# Reduce result to a single item or None if it was not a collection at
|
# Reduce result to a single item or None if it was not a collection at
|
||||||
# the beginning of the method.
|
# the beginning of the method.
|
||||||
if isinstance(container, Collection):
|
if isinstance(container, Collection):
|
||||||
projector_data = data
|
projector_data = data # type: RestrictedData
|
||||||
elif data:
|
elif data:
|
||||||
projector_data = data[0]
|
projector_data = data[0]
|
||||||
else:
|
else:
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
from typing import Dict, List, Union # noqa
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from mypy_extensions import TypedDict
|
||||||
|
|
||||||
from ..utils.collection import Collection
|
from ..utils.collection import Collection
|
||||||
|
|
||||||
@ -46,9 +49,11 @@ class AssignmentsAppConfig(AppConfig):
|
|||||||
|
|
||||||
def get_angular_constants(self):
|
def get_angular_constants(self):
|
||||||
assignment = self.get_model('Assignment')
|
assignment = self.get_model('Assignment')
|
||||||
|
InnerItem = TypedDict('InnerItem', {'value': int, 'display_name': str})
|
||||||
|
Item = TypedDict('Item', {'name': str, 'value': List[InnerItem]}) # noqa
|
||||||
data = {
|
data = {
|
||||||
'name': 'AssignmentPhases',
|
'name': 'AssignmentPhases',
|
||||||
'value': []}
|
'value': []} # type: Item
|
||||||
for phase in assignment.PHASES:
|
for phase in assignment.PHASES:
|
||||||
data['value'].append({
|
data['value'].append({
|
||||||
'value': phase[0],
|
'value': phase[0],
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from typing import Any, Dict, List, Optional # noqa
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
@ -174,7 +175,7 @@ class Assignment(RESTModelMixin, models.Model):
|
|||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate,
|
||||||
name='assignments/assignment',
|
name='assignments/assignment',
|
||||||
id=self.pk)
|
id=self.pk)
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs)
|
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore # TODO fix typing
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def candidates(self):
|
def candidates(self):
|
||||||
@ -300,14 +301,14 @@ class Assignment(RESTModelMixin, models.Model):
|
|||||||
Returns a table represented as a list with all candidates from all
|
Returns a table represented as a list with all candidates from all
|
||||||
related polls and their vote results.
|
related polls and their vote results.
|
||||||
"""
|
"""
|
||||||
vote_results_dict = OrderedDict()
|
vote_results_dict = OrderedDict() # type: Dict[Any, List[AssignmentVote]]
|
||||||
|
|
||||||
polls = self.polls.all()
|
polls = self.polls.all()
|
||||||
if only_published:
|
if only_published:
|
||||||
polls = polls.filter(published=True)
|
polls = polls.filter(published=True)
|
||||||
|
|
||||||
# All PollOption-Objects related to this assignment
|
# All PollOption-Objects related to this assignment
|
||||||
options = []
|
options = [] # type: List[AssignmentOption]
|
||||||
for poll in polls:
|
for poll in polls:
|
||||||
options += poll.get_options()
|
options += poll.get_options()
|
||||||
|
|
||||||
@ -317,7 +318,7 @@ class Assignment(RESTModelMixin, models.Model):
|
|||||||
continue
|
continue
|
||||||
vote_results_dict[candidate] = []
|
vote_results_dict[candidate] = []
|
||||||
for poll in polls:
|
for poll in polls:
|
||||||
votes = {}
|
votes = {} # type: Any
|
||||||
try:
|
try:
|
||||||
# candidate related to this poll
|
# candidate related to this poll
|
||||||
poll_option = poll.get_options().get(candidate=candidate)
|
poll_option = poll.get_options().get(candidate=candidate)
|
||||||
@ -429,7 +430,7 @@ class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin, # type: ignore
|
|||||||
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)
|
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore # TODO: fix typing
|
||||||
|
|
||||||
def get_assignment(self):
|
def get_assignment(self):
|
||||||
return self.assignment
|
return self.assignment
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from typing import Any, Set # noqa
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
|
||||||
from ..utils.auth import has_perm
|
from ..utils.auth import has_perm
|
||||||
@ -22,7 +24,7 @@ def required_users(sender, request_user, **kwargs):
|
|||||||
options) in any assignment if request_user can see assignments. This
|
options) in any assignment if request_user can see assignments. This
|
||||||
function may return an empty set.
|
function may return an empty set.
|
||||||
"""
|
"""
|
||||||
candidates = set()
|
candidates = set() # type: Set[Any] # TODO: Replace Any
|
||||||
if has_perm(request_user, 'assignments.can_see'):
|
if has_perm(request_user, 'assignments.can_see'):
|
||||||
for assignment_collection_element in Collection(Assignment.get_collection_string()).element_generator():
|
for assignment_collection_element in Collection(Assignment.get_collection_string()).element_generator():
|
||||||
full_data = assignment_collection_element.get_full_data()
|
full_data = assignment_collection_element.get_full_data()
|
||||||
|
@ -1,13 +1,4 @@
|
|||||||
from typing import (
|
from typing import Any, Callable, Dict, Iterable, Optional, TypeVar, Union
|
||||||
Any,
|
|
||||||
Callable,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
TypeVar,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
@ -192,9 +183,9 @@ use x = config[...], to set it use config[...] = x.
|
|||||||
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
ChoiceType = Optional[List[Dict[str, str]]]
|
ChoiceType = Optional[Iterable[Dict[str, str]]]
|
||||||
ChoiceCallableType = Union[ChoiceType, Callable[[], ChoiceType]]
|
ChoiceCallableType = Union[ChoiceType, Callable[[], ChoiceType]]
|
||||||
ValidatorsType = List[Callable[[T], None]]
|
ValidatorsType = Iterable[Callable[[T], None]]
|
||||||
OnChangeType = Callable[[], None]
|
OnChangeType = Callable[[], None]
|
||||||
ConfigVariableDict = TypedDict('ConfigVariableDict', {
|
ConfigVariableDict = TypedDict('ConfigVariableDict', {
|
||||||
'key': str,
|
'key': str,
|
||||||
|
@ -51,7 +51,7 @@ class Command(BaseCommand):
|
|||||||
response = urlopen(self.get_geiss_url()).read()
|
response = urlopen(self.get_geiss_url()).read()
|
||||||
releases = json.loads(response.decode())
|
releases = json.loads(response.decode())
|
||||||
for release in releases:
|
for release in releases:
|
||||||
version = distutils.version.StrictVersion(release['tag_name'])
|
version = distutils.version.StrictVersion(release['tag_name']) # type: ignore
|
||||||
if version < self.FIRST_NOT_SUPPORTED_VERSION:
|
if version < self.FIRST_NOT_SUPPORTED_VERSION:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
@ -111,7 +111,7 @@ class Projector(RESTModelMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
# Get all elements from all apps.
|
# Get all elements from all apps.
|
||||||
elements = {}
|
elements = {}
|
||||||
for element in ProjectorElement.get_all():
|
for element in ProjectorElement.get_all(): # type: ignore
|
||||||
elements[element.name] = element
|
elements[element.name] = element
|
||||||
|
|
||||||
# Parse result
|
# Parse result
|
||||||
@ -138,7 +138,7 @@ class Projector(RESTModelMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
# Get all elements from all apps.
|
# Get all elements from all apps.
|
||||||
elements = {}
|
elements = {}
|
||||||
for element in ProjectorElement.get_all():
|
for element in ProjectorElement.get_all(): # type: ignore
|
||||||
elements[element.name] = element
|
elements[element.name] = element
|
||||||
|
|
||||||
# Generator
|
# Generator
|
||||||
@ -169,7 +169,7 @@ class Projector(RESTModelMixin, models.Model):
|
|||||||
elements = {}
|
elements = {}
|
||||||
|
|
||||||
# Build projector elements.
|
# Build projector elements.
|
||||||
for element in ProjectorElement.get_all():
|
for element in ProjectorElement.get_all(): # type: ignore
|
||||||
elements[element.name] = element
|
elements[element.name] = element
|
||||||
|
|
||||||
# Iterate over all active projector elements.
|
# Iterate over all active projector elements.
|
||||||
@ -341,7 +341,7 @@ class ProjectorMessage(RESTModelMixin, models.Model):
|
|||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate,
|
||||||
name='core/projector-message',
|
name='core/projector-message',
|
||||||
id=self.pk)
|
id=self.pk)
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs)
|
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class Countdown(RESTModelMixin, models.Model):
|
class Countdown(RESTModelMixin, models.Model):
|
||||||
@ -370,7 +370,7 @@ class Countdown(RESTModelMixin, models.Model):
|
|||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate,
|
||||||
name='core/countdown',
|
name='core/countdown',
|
||||||
id=self.pk)
|
id=self.pk)
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs)
|
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||||
|
|
||||||
def control(self, action):
|
def control(self, action):
|
||||||
if action not in ('start', 'stop', 'reset'):
|
if action not in ('start', 'stop', 'reset'):
|
||||||
|
@ -3,6 +3,7 @@ import uuid
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
from typing import Any, Dict, List # noqa
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -11,6 +12,7 @@ from django.db.models import F
|
|||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from mypy_extensions import TypedDict
|
||||||
|
|
||||||
from .. import __version__ as version
|
from .. import __version__ as version
|
||||||
from ..utils import views as utils_views
|
from ..utils import views as utils_views
|
||||||
@ -105,7 +107,7 @@ class WebclientJavaScriptView(utils_views.View):
|
|||||||
"""
|
"""
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
angular_modules = []
|
angular_modules = []
|
||||||
js_files = []
|
js_files = [] # type: List[str]
|
||||||
realm = kwargs.get('realm') # Result is 'site' or 'projector'
|
realm = kwargs.get('realm') # Result is 'site' or 'projector'
|
||||||
for app_config in apps.get_app_configs():
|
for app_config in apps.get_app_configs():
|
||||||
# Add the angular app if the module has one.
|
# Add the angular app if the module has one.
|
||||||
@ -582,7 +584,7 @@ class ConfigMetadata(SimpleMetadata):
|
|||||||
"""
|
"""
|
||||||
def determine_metadata(self, request, view):
|
def determine_metadata(self, request, view):
|
||||||
# Build tree.
|
# Build tree.
|
||||||
config_groups = []
|
config_groups = [] # type: List[Any] # TODO: Replace Any by correct type
|
||||||
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.
|
||||||
@ -787,7 +789,8 @@ class VersionView(utils_views.APIView):
|
|||||||
http_method_names = ['get']
|
http_method_names = ['get']
|
||||||
|
|
||||||
def get_context_data(self, **context):
|
def get_context_data(self, **context):
|
||||||
result = dict(openslides_version=version, plugins=[])
|
Result = TypedDict('Result', {'openslides_version': str, 'plugins': List[Dict[str, str]]}) # noqa
|
||||||
|
result = dict(openslides_version=version, plugins=[]) # type: Result
|
||||||
# Versions of plugins.
|
# Versions of plugins.
|
||||||
for plugin in settings.INSTALLED_PLUGINS:
|
for plugin in settings.INSTALLED_PLUGINS:
|
||||||
result['plugins'].append({
|
result['plugins'].append({
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
from ..utils.access_permissions import BaseAccessPermissions
|
from ..utils.access_permissions import ( # noqa
|
||||||
|
BaseAccessPermissions,
|
||||||
|
RestrictedData,
|
||||||
|
)
|
||||||
from ..utils.auth import has_perm
|
from ..utils.auth import has_perm
|
||||||
from ..utils.collection import Collection
|
from ..utils.collection import Collection
|
||||||
|
|
||||||
@ -41,7 +44,7 @@ class MediafileAccessPermissions(BaseAccessPermissions):
|
|||||||
# Reduce result to a single item or None if it was not a collection at
|
# Reduce result to a single item or None if it was not a collection at
|
||||||
# the beginning of the method.
|
# the beginning of the method.
|
||||||
if isinstance(container, Collection):
|
if isinstance(container, Collection):
|
||||||
restricted_data = data
|
restricted_data = data # type: RestrictedData
|
||||||
elif data:
|
elif data:
|
||||||
restricted_data = data[0]
|
restricted_data = data[0]
|
||||||
else:
|
else:
|
||||||
|
@ -73,7 +73,7 @@ class Mediafile(RESTModelMixin, models.Model):
|
|||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate,
|
||||||
name='mediafiles/mediafile',
|
name='mediafiles/mediafile',
|
||||||
id=self.pk)
|
id=self.pk)
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs)
|
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||||
|
|
||||||
def get_filesize(self):
|
def get_filesize(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..utils.access_permissions import BaseAccessPermissions
|
from ..utils.access_permissions import ( # noqa
|
||||||
|
BaseAccessPermissions,
|
||||||
|
RestrictedData,
|
||||||
|
)
|
||||||
from ..utils.auth import has_perm
|
from ..utils.auth import has_perm
|
||||||
from ..utils.collection import Collection, CollectionElement
|
from ..utils.collection import Collection, CollectionElement
|
||||||
|
|
||||||
@ -78,7 +81,7 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
|||||||
# Reduce result to a single item or None if it was not a collection at
|
# Reduce result to a single item or None if it was not a collection at
|
||||||
# the beginning of the method.
|
# the beginning of the method.
|
||||||
if isinstance(container, Collection):
|
if isinstance(container, Collection):
|
||||||
restricted_data = data
|
restricted_data = data # type: RestrictedData
|
||||||
elif data:
|
elif data:
|
||||||
restricted_data = data[0]
|
restricted_data = data[0]
|
||||||
else:
|
else:
|
||||||
@ -114,7 +117,7 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
|||||||
# Reduce result to a single item or None if it was not a collection at
|
# Reduce result to a single item or None if it was not a collection at
|
||||||
# the beginning of the method.
|
# the beginning of the method.
|
||||||
if isinstance(container, Collection):
|
if isinstance(container, Collection):
|
||||||
projector_data = data
|
projector_data = data # type: RestrictedData
|
||||||
elif data:
|
elif data:
|
||||||
projector_data = data[0]
|
projector_data = data[0]
|
||||||
else:
|
else:
|
||||||
|
@ -231,7 +231,7 @@ 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)
|
super(Motion, self).save(skip_autoupdate=True, *args, **kwargs) # type: ignore
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
# Identifier is already used.
|
# Identifier is already used.
|
||||||
if hasattr(self, '_identifier_prefix'):
|
if hasattr(self, '_identifier_prefix'):
|
||||||
@ -309,7 +309,7 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate,
|
||||||
name='motions/motion',
|
name='motions/motion',
|
||||||
id=self.pk)
|
id=self.pk)
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs)
|
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||||
|
|
||||||
def version_data_changed(self, version):
|
def version_data_changed(self, version):
|
||||||
"""
|
"""
|
||||||
@ -879,7 +879,7 @@ class MotionBlock(RESTModelMixin, models.Model):
|
|||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate,
|
||||||
name='motions/motion-block',
|
name='motions/motion-block',
|
||||||
id=self.pk)
|
id=self.pk)
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs)
|
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def agenda_item(self):
|
def agenda_item(self):
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from typing import Dict # noqa
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
@ -157,7 +159,7 @@ class MotionPollSerializer(ModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# The following dictionary is just a cache for several votes.
|
# The following dictionary is just a cache for several votes.
|
||||||
self._votes_dicts = {}
|
self._votes_dicts = {} # type: Dict[int, Dict[int, int]]
|
||||||
return super().__init__(*args, **kwargs)
|
return super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def get_yes(self, obj):
|
def get_yes(self, obj):
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from typing import Set # noqa
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.utils.translation import ugettext_noop
|
from django.utils.translation import ugettext_noop
|
||||||
|
|
||||||
@ -124,7 +126,7 @@ def required_users(sender, request_user, **kwargs):
|
|||||||
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()
|
submitters_supporters = set() # type: Set[int]
|
||||||
if has_perm(request_user, 'motions.can_see'):
|
if has_perm(request_user, 'motions.can_see'):
|
||||||
for motion_collection_element in Collection(Motion.get_collection_string()).element_generator():
|
for motion_collection_element in Collection(Motion.get_collection_string()).element_generator():
|
||||||
full_data = motion_collection_element.get_full_data()
|
full_data = motion_collection_element.get_full_data()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import base64
|
import base64
|
||||||
import re
|
import re
|
||||||
|
from typing import Optional # noqa
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
@ -92,7 +93,7 @@ class MotionViewSet(ModelViewSet):
|
|||||||
try:
|
try:
|
||||||
parent_motion = CollectionElement.from_values(
|
parent_motion = CollectionElement.from_values(
|
||||||
Motion.get_collection_string(),
|
Motion.get_collection_string(),
|
||||||
request.data['parent_id'])
|
request.data['parent_id']) # type: Optional[CollectionElement]
|
||||||
except Motion.DoesNotExist:
|
except Motion.DoesNotExist:
|
||||||
raise ValidationError({'detail': _('The parent motion does not exist.')})
|
raise ValidationError({'detail': _('The parent motion does not exist.')})
|
||||||
else:
|
else:
|
||||||
|
@ -52,7 +52,7 @@ class Topic(RESTModelMixin, models.Model):
|
|||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate,
|
||||||
name='topics/topic',
|
name='topics/topic',
|
||||||
id=self.pk)
|
id=self.pk)
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs)
|
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def agenda_item(self):
|
def agenda_item(self):
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
from typing import Any, Dict, List # noqa
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
|
||||||
from ..core.signals import user_data_required
|
from ..core.signals import user_data_required
|
||||||
from ..utils.access_permissions import BaseAccessPermissions
|
from ..utils.access_permissions import ( # noqa
|
||||||
|
BaseAccessPermissions,
|
||||||
|
RestrictedData,
|
||||||
|
)
|
||||||
from ..utils.auth import anonymous_is_enabled, has_perm
|
from ..utils.auth import anonymous_is_enabled, has_perm
|
||||||
from ..utils.collection import Collection
|
from ..utils.collection import Collection
|
||||||
|
|
||||||
@ -94,7 +99,7 @@ class UserAccessPermissions(BaseAccessPermissions):
|
|||||||
# Reduce result to a single item or None if it was not a collection at
|
# Reduce result to a single item or None if it was not a collection at
|
||||||
# the beginning of the method.
|
# the beginning of the method.
|
||||||
if isinstance(container, Collection):
|
if isinstance(container, Collection):
|
||||||
restricted_data = data
|
restricted_data = data # type: RestrictedData
|
||||||
elif data:
|
elif data:
|
||||||
restricted_data = data[0]
|
restricted_data = data[0]
|
||||||
else:
|
else:
|
||||||
@ -127,7 +132,7 @@ class UserAccessPermissions(BaseAccessPermissions):
|
|||||||
# Reduce result to a single item or None if it was not a collection at
|
# Reduce result to a single item or None if it was not a collection at
|
||||||
# the beginning of the method.
|
# the beginning of the method.
|
||||||
if isinstance(container, Collection):
|
if isinstance(container, Collection):
|
||||||
projector_data = data
|
projector_data = data # type: RestrictedData
|
||||||
elif data:
|
elif data:
|
||||||
projector_data = data[0]
|
projector_data = data[0]
|
||||||
else:
|
else:
|
||||||
@ -187,7 +192,7 @@ class PersonalNoteAccessPermissions(BaseAccessPermissions):
|
|||||||
|
|
||||||
# Parse data.
|
# Parse data.
|
||||||
if user is None:
|
if user is None:
|
||||||
data = []
|
data = [] # type: List[Dict[str, Any]]
|
||||||
else:
|
else:
|
||||||
for full in full_data:
|
for full in full_data:
|
||||||
if full['user_id'] == user.id:
|
if full['user_id'] == user.id:
|
||||||
@ -199,7 +204,7 @@ class PersonalNoteAccessPermissions(BaseAccessPermissions):
|
|||||||
# Reduce result to a single item or None if it was not a collection at
|
# Reduce result to a single item or None if it was not a collection at
|
||||||
# the beginning of the method.
|
# the beginning of the method.
|
||||||
if isinstance(container, Collection):
|
if isinstance(container, Collection):
|
||||||
restricted_data = data
|
restricted_data = data # type: RestrictedData
|
||||||
elif data:
|
elif data:
|
||||||
restricted_data = data[0]
|
restricted_data = data[0]
|
||||||
else:
|
else:
|
||||||
|
@ -220,7 +220,7 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
|
|||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate,
|
||||||
name='users/user',
|
name='users/user',
|
||||||
id=self.pk)
|
id=self.pk)
|
||||||
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs)
|
return super().delete(skip_autoupdate=skip_autoupdate, *args, **kwargs) # type: ignore
|
||||||
|
|
||||||
def has_perm(self, perm):
|
def has_perm(self, perm):
|
||||||
"""
|
"""
|
||||||
|
@ -1,46 +1,29 @@
|
|||||||
from django.dispatch import Signal
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
from .collection import Collection
|
from django.db.models import Model
|
||||||
from .dispatch import SignalConnectMetaClass
|
from rest_framework.serializers import Serializer
|
||||||
|
|
||||||
|
from .collection import Collection, CollectionElement
|
||||||
|
|
||||||
|
Container = Union[CollectionElement, Collection]
|
||||||
|
RestrictedData = Union[List[Dict[str, Any]], Dict[str, Any], None]
|
||||||
|
|
||||||
|
|
||||||
class BaseAccessPermissions(object, metaclass=SignalConnectMetaClass):
|
class BaseAccessPermissions:
|
||||||
"""
|
"""
|
||||||
Base access permissions container.
|
Base access permissions container.
|
||||||
|
|
||||||
Every app which has autoupdate models has to create classes subclassing
|
Every app which has autoupdate models has to create classes subclassing
|
||||||
from this base class for every autoupdate root model. Each subclass has
|
from this base class for every autoupdate root model.
|
||||||
to have a globally unique name. The metaclass (SignalConnectMetaClass)
|
|
||||||
does the rest of the magic.
|
|
||||||
"""
|
"""
|
||||||
signal = Signal()
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def check_permissions(self, user: Optional[CollectionElement]) -> bool:
|
||||||
"""
|
|
||||||
Initializes the access permission instance. This is done when the
|
|
||||||
signal is sent.
|
|
||||||
|
|
||||||
Because of Django's signal API, we have to take wildcard keyword
|
|
||||||
arguments. But they are not used here.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_dispatch_uid(cls):
|
|
||||||
"""
|
|
||||||
Returns the classname as a unique string for each class. Returns None
|
|
||||||
for the base class so it will not be connected to the signal.
|
|
||||||
"""
|
|
||||||
if not cls.__name__ == 'BaseAccessPermissions':
|
|
||||||
return cls.__name__
|
|
||||||
|
|
||||||
def check_permissions(self, user):
|
|
||||||
"""
|
"""
|
||||||
Returns True if the user has read access to model instances.
|
Returns True if the user has read access to model instances.
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user: CollectionElement=None) -> Serializer:
|
||||||
"""
|
"""
|
||||||
Returns different serializer classes according to users permissions.
|
Returns different serializer classes according to users permissions.
|
||||||
|
|
||||||
@ -51,13 +34,13 @@ class BaseAccessPermissions(object, metaclass=SignalConnectMetaClass):
|
|||||||
"You have to add the method 'get_serializer_class' to your "
|
"You have to add the method 'get_serializer_class' to your "
|
||||||
"access permissions class.".format(self))
|
"access permissions class.".format(self))
|
||||||
|
|
||||||
def get_full_data(self, instance):
|
def get_full_data(self, instance: Model) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Returns all possible serialized data for the given instance.
|
Returns all possible serialized data for the given instance.
|
||||||
"""
|
"""
|
||||||
return self.get_serializer_class(user=None)(instance).data
|
return self.get_serializer_class(user=None)(instance).data
|
||||||
|
|
||||||
def get_restricted_data(self, container, user):
|
def get_restricted_data(self, container: Container, user: Optional[CollectionElement]) -> RestrictedData:
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the user.
|
for the user.
|
||||||
@ -82,7 +65,7 @@ class BaseAccessPermissions(object, metaclass=SignalConnectMetaClass):
|
|||||||
data = None
|
data = None
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_projector_data(self, container):
|
def get_projector_data(self, container: Container) -> RestrictedData:
|
||||||
"""
|
"""
|
||||||
Returns the serialized data for the projector. Returns None if the
|
Returns the serialized data for the projector. Returns None if the
|
||||||
user has no access to this specific data. Returns reduced data if
|
user has no access to this specific data. Returns reduced data if
|
||||||
|
@ -16,6 +16,7 @@ def has_perm(user: Optional[CollectionElement], perm: str) -> bool:
|
|||||||
group_collection_string = 'users/group' # This is the hard coded collection string for openslides.users.models.Group
|
group_collection_string = 'users/group' # This is the hard coded collection string for openslides.users.models.Group
|
||||||
|
|
||||||
# Convert user to right type
|
# Convert user to right type
|
||||||
|
# TODO: Remove this and make use, that user has always the right type
|
||||||
user = user_to_collection_user(user)
|
user = user_to_collection_user(user)
|
||||||
if user is None and not anonymous_is_enabled():
|
if user is None and not anonymous_is_enabled():
|
||||||
has_perm = False
|
has_perm = False
|
||||||
|
@ -2,12 +2,14 @@ import json
|
|||||||
import time
|
import time
|
||||||
import warnings
|
import warnings
|
||||||
from collections import Iterable, defaultdict
|
from collections import Iterable, defaultdict
|
||||||
|
from typing import Any, Dict, Iterable, List, cast # noqa
|
||||||
|
|
||||||
from channels import Channel, Group
|
from channels import Channel, Group
|
||||||
from channels.asgi import get_channel_layer
|
from channels.asgi import get_channel_layer
|
||||||
from channels.auth import channel_session_user, channel_session_user_from_http
|
from channels.auth import channel_session_user, channel_session_user_from_http
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import Model
|
||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..core.models import Projector
|
from ..core.models import Projector
|
||||||
@ -16,7 +18,7 @@ from .cache import startup_cache, websocket_user_cache
|
|||||||
from .collection import Collection, CollectionElement, CollectionElementList
|
from .collection import Collection, CollectionElement, CollectionElementList
|
||||||
|
|
||||||
|
|
||||||
def send_or_wait(send_func, *args, **kwargs):
|
def send_or_wait(send_func: Any, *args: Any, **kwargs: Any) -> None:
|
||||||
"""
|
"""
|
||||||
Wrapper for channels' send() method.
|
Wrapper for channels' send() method.
|
||||||
|
|
||||||
@ -41,7 +43,7 @@ def send_or_wait(send_func, *args, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def format_for_autoupdate(collection_string, id, action, data=None):
|
def format_for_autoupdate(collection_string: str, id: int, action: str, data: Dict[str, Any]=None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Returns a dict that can be used for autoupdate.
|
Returns a dict that can be used for autoupdate.
|
||||||
"""
|
"""
|
||||||
@ -64,7 +66,7 @@ def format_for_autoupdate(collection_string, id, action, data=None):
|
|||||||
|
|
||||||
|
|
||||||
@channel_session_user_from_http
|
@channel_session_user_from_http
|
||||||
def ws_add_site(message):
|
def ws_add_site(message: Any) -> None:
|
||||||
"""
|
"""
|
||||||
Adds the websocket connection to a group specific to the connecting user.
|
Adds the websocket connection to a group specific to the connecting user.
|
||||||
|
|
||||||
@ -92,6 +94,9 @@ def ws_add_site(message):
|
|||||||
access_permissions = collection.get_access_permissions()
|
access_permissions = collection.get_access_permissions()
|
||||||
restricted_data = access_permissions.get_restricted_data(collection, user)
|
restricted_data = access_permissions.get_restricted_data(collection, user)
|
||||||
|
|
||||||
|
# At this point restricted_data has to be a list. So we have to tell it mypy
|
||||||
|
restricted_data = cast(List[Dict[str, Any]], restricted_data)
|
||||||
|
|
||||||
for data in restricted_data:
|
for data in restricted_data:
|
||||||
if data is None:
|
if data is None:
|
||||||
# We do not want to send 'deleted' objects on startup.
|
# We do not want to send 'deleted' objects on startup.
|
||||||
@ -100,7 +105,7 @@ def ws_add_site(message):
|
|||||||
output.append(
|
output.append(
|
||||||
format_for_autoupdate(
|
format_for_autoupdate(
|
||||||
collection_string=collection.collection_string,
|
collection_string=collection.collection_string,
|
||||||
id=data['id'],
|
id=int(data['id']),
|
||||||
action='changed',
|
action='changed',
|
||||||
data=data))
|
data=data))
|
||||||
|
|
||||||
@ -110,7 +115,7 @@ def ws_add_site(message):
|
|||||||
|
|
||||||
|
|
||||||
@channel_session_user
|
@channel_session_user
|
||||||
def ws_disconnect_site(message):
|
def ws_disconnect_site(message: Any) -> None:
|
||||||
"""
|
"""
|
||||||
This function is called, when a client on the site disconnects.
|
This function is called, when a client on the site disconnects.
|
||||||
"""
|
"""
|
||||||
@ -119,7 +124,7 @@ def ws_disconnect_site(message):
|
|||||||
|
|
||||||
|
|
||||||
@channel_session_user
|
@channel_session_user
|
||||||
def ws_receive_site(message):
|
def ws_receive_site(message: Any) -> None:
|
||||||
"""
|
"""
|
||||||
This function is called if a message from a client comes in. The message
|
This function is called if a message from a client comes in. The message
|
||||||
should be a list. Every item is broadcasted to the given users (or all
|
should be a list. Every item is broadcasted to the given users (or all
|
||||||
@ -137,8 +142,8 @@ def ws_receive_site(message):
|
|||||||
else:
|
else:
|
||||||
if isinstance(incomming, list):
|
if isinstance(incomming, list):
|
||||||
# Parse all items
|
# Parse all items
|
||||||
receivers_users = defaultdict(list)
|
receivers_users = defaultdict(list) # type: Dict[int, List[Any]]
|
||||||
receivers_reply_channels = defaultdict(list)
|
receivers_reply_channels = defaultdict(list) # type: Dict[str, List[Any]]
|
||||||
items_for_all = []
|
items_for_all = []
|
||||||
for item in incomming:
|
for item in incomming:
|
||||||
if item.get('collection') == 'notify':
|
if item.get('collection') == 'notify':
|
||||||
@ -184,12 +189,12 @@ def ws_receive_site(message):
|
|||||||
|
|
||||||
|
|
||||||
@channel_session_user_from_http
|
@channel_session_user_from_http
|
||||||
def ws_add_projector(message, projector_id):
|
def ws_add_projector(message: Any, projector_id: int) -> None:
|
||||||
"""
|
"""
|
||||||
Adds the websocket connection to a group specific to the projector with the given id.
|
Adds the websocket connection to a group specific to the projector with the given id.
|
||||||
Also sends all data that are shown on the projector.
|
Also sends all data that are shown on the projector.
|
||||||
"""
|
"""
|
||||||
user = message.user.id
|
user = user_to_collection_user(message.user.id)
|
||||||
|
|
||||||
if not has_perm(user, 'core.can_see_projector'):
|
if not has_perm(user, 'core.can_see_projector'):
|
||||||
send_or_wait(message.reply_channel.send, {'text': 'No permissions to see this projector.'})
|
send_or_wait(message.reply_channel.send, {'text': 'No permissions to see this projector.'})
|
||||||
@ -230,14 +235,14 @@ def ws_add_projector(message, projector_id):
|
|||||||
send_or_wait(message.reply_channel.send, {'text': json.dumps(output)})
|
send_or_wait(message.reply_channel.send, {'text': json.dumps(output)})
|
||||||
|
|
||||||
|
|
||||||
def ws_disconnect_projector(message, projector_id):
|
def ws_disconnect_projector(message: Any, projector_id: int) -> None:
|
||||||
"""
|
"""
|
||||||
This function is called, when a client on the projector disconnects.
|
This function is called, when a client on the projector disconnects.
|
||||||
"""
|
"""
|
||||||
Group('projector-{}'.format(projector_id)).discard(message.reply_channel)
|
Group('projector-{}'.format(projector_id)).discard(message.reply_channel)
|
||||||
|
|
||||||
|
|
||||||
def send_data(message):
|
def send_data(message: Any) -> None:
|
||||||
"""
|
"""
|
||||||
Informs all site users and projector clients about changed data.
|
Informs all site users and projector clients about changed data.
|
||||||
"""
|
"""
|
||||||
@ -285,7 +290,7 @@ def send_data(message):
|
|||||||
{'text': json.dumps(output)})
|
{'text': json.dumps(output)})
|
||||||
|
|
||||||
|
|
||||||
def inform_changed_data(instances, information=None):
|
def inform_changed_data(instances: Iterable[Model], information: Dict[str, Any]=None) -> None:
|
||||||
"""
|
"""
|
||||||
Informs the autoupdate system and the caching system about the creation or
|
Informs the autoupdate system and the caching system about the creation or
|
||||||
update of an element.
|
update of an element.
|
||||||
@ -317,7 +322,8 @@ def inform_changed_data(instances, information=None):
|
|||||||
transaction.on_commit(lambda: send_autoupdate(collection_elements))
|
transaction.on_commit(lambda: send_autoupdate(collection_elements))
|
||||||
|
|
||||||
|
|
||||||
def inform_deleted_data(*args, information=None):
|
# TODO: Change the input argument to tuples
|
||||||
|
def inform_deleted_data(*args: Any, information: Dict[str, Any]=None) -> None:
|
||||||
"""
|
"""
|
||||||
Informs the autoupdate system and the caching system about the deletion of
|
Informs the autoupdate system and the caching system about the deletion of
|
||||||
elements.
|
elements.
|
||||||
@ -351,7 +357,8 @@ def inform_deleted_data(*args, information=None):
|
|||||||
transaction.on_commit(lambda: send_autoupdate(collection_elements))
|
transaction.on_commit(lambda: send_autoupdate(collection_elements))
|
||||||
|
|
||||||
|
|
||||||
def inform_data_collection_element_list(collection_elements, information=None):
|
def inform_data_collection_element_list(collection_elements: CollectionElementList,
|
||||||
|
information: Dict[str, Any]=None) -> None:
|
||||||
"""
|
"""
|
||||||
Informs the autoupdate system about some collection elements. This is
|
Informs the autoupdate system about some collection elements. This is
|
||||||
used just to send some data to all users.
|
used just to send some data to all users.
|
||||||
@ -363,7 +370,7 @@ def inform_data_collection_element_list(collection_elements, information=None):
|
|||||||
transaction.on_commit(lambda: send_autoupdate(collection_elements))
|
transaction.on_commit(lambda: send_autoupdate(collection_elements))
|
||||||
|
|
||||||
|
|
||||||
def send_autoupdate(collection_elements):
|
def send_autoupdate(collection_elements: CollectionElementList) -> None:
|
||||||
"""
|
"""
|
||||||
Helper function, that sends collection_elements through a channel to the
|
Helper function, that sends collection_elements through a channel to the
|
||||||
autoupdate system.
|
autoupdate system.
|
||||||
|
@ -1,10 +1,27 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from typing import ( # noqa
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
Generator,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Set,
|
||||||
|
)
|
||||||
|
|
||||||
from channels import Group
|
from channels import Group
|
||||||
from channels.sessions import session_for_reply_channel
|
from channels.sessions import session_for_reply_channel
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.cache import cache, caches
|
from django.core.cache import cache, caches
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# Dummy import Collection for mypy
|
||||||
|
from .collection import Collection # noqa
|
||||||
|
|
||||||
|
UserCacheDataType = Dict[int, Set[str]]
|
||||||
|
|
||||||
|
|
||||||
class BaseWebsocketUserCache:
|
class BaseWebsocketUserCache:
|
||||||
"""
|
"""
|
||||||
@ -15,36 +32,36 @@ class BaseWebsocketUserCache:
|
|||||||
"""
|
"""
|
||||||
cache_key = 'current_websocket_users'
|
cache_key = 'current_websocket_users'
|
||||||
|
|
||||||
def add(self, user_id, channel_name):
|
def add(self, user_id: int, channel_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Adds a channel name to an user id.
|
Adds a channel name to an user id.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def remove(self, user_id, channel_name):
|
def remove(self, user_id: int, channel_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Removes one channel name from the cache.
|
Removes one channel name from the cache.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_all(self):
|
def get_all(self) -> UserCacheDataType:
|
||||||
"""
|
"""
|
||||||
Returns all data using a dict where the key is a user id and the value
|
Returns all data using a dict where the key is a user id and the value
|
||||||
is a set of channel_names.
|
is a set of channel_names.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def save_data(self, data):
|
def save_data(self, data: UserCacheDataType) -> None:
|
||||||
"""
|
"""
|
||||||
Saves the full data set (like created with build_data) to the cache.
|
Saves the full data set (like created with build_data) to the cache.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def build_data(self):
|
def build_data(self) -> UserCacheDataType:
|
||||||
"""
|
"""
|
||||||
Creates all the data, saves it to the cache and returns it.
|
Creates all the data, saves it to the cache and returns it.
|
||||||
"""
|
"""
|
||||||
websocket_user_ids = defaultdict(set)
|
websocket_user_ids = defaultdict(set) # type: UserCacheDataType
|
||||||
for channel_name in Group('site').channel_layer.group_channels('site'):
|
for channel_name in Group('site').channel_layer.group_channels('site'):
|
||||||
session = session_for_reply_channel(channel_name)
|
session = session_for_reply_channel(channel_name)
|
||||||
user_id = session.get('user_id', None)
|
user_id = session.get('user_id', None)
|
||||||
@ -52,7 +69,7 @@ class BaseWebsocketUserCache:
|
|||||||
self.save_data(websocket_user_ids)
|
self.save_data(websocket_user_ids)
|
||||||
return websocket_user_ids
|
return websocket_user_ids
|
||||||
|
|
||||||
def get_cache_key(self):
|
def get_cache_key(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the cache key.
|
Returns the cache key.
|
||||||
"""
|
"""
|
||||||
@ -67,7 +84,7 @@ class RedisWebsocketUserCache(BaseWebsocketUserCache):
|
|||||||
for each user another set to save the channel names.
|
for each user another set to save the channel names.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def add(self, user_id, channel_name):
|
def add(self, user_id: int, channel_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Adds a channel name to an user id.
|
Adds a channel name to an user id.
|
||||||
"""
|
"""
|
||||||
@ -77,35 +94,35 @@ class RedisWebsocketUserCache(BaseWebsocketUserCache):
|
|||||||
pipe.sadd(self.get_user_cache_key(user_id), channel_name)
|
pipe.sadd(self.get_user_cache_key(user_id), channel_name)
|
||||||
pipe.execute()
|
pipe.execute()
|
||||||
|
|
||||||
def remove(self, user_id, channel_name):
|
def remove(self, user_id: int, channel_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Removes one channel name from the cache.
|
Removes one channel name from the cache.
|
||||||
"""
|
"""
|
||||||
redis = get_redis_connection()
|
redis = get_redis_connection()
|
||||||
redis.srem(self.get_user_cache_key(user_id), channel_name)
|
redis.srem(self.get_user_cache_key(user_id), channel_name)
|
||||||
|
|
||||||
def get_all(self):
|
def get_all(self) -> UserCacheDataType:
|
||||||
"""
|
"""
|
||||||
Returns all data using a dict where the key is a user id and the value
|
Returns all data using a dict where the key is a user id and the value
|
||||||
is a set of channel_names.
|
is a set of channel_names.
|
||||||
"""
|
"""
|
||||||
redis = get_redis_connection()
|
redis = get_redis_connection()
|
||||||
user_ids = redis.smembers(self.get_cache_key())
|
user_ids = redis.smembers(self.get_cache_key()) # type: Optional[List[str]]
|
||||||
if user_ids is None:
|
if user_ids is None:
|
||||||
websocket_user_ids = self.build_data()
|
websocket_user_ids = self.build_data()
|
||||||
else:
|
else:
|
||||||
websocket_user_ids = dict()
|
websocket_user_ids = dict()
|
||||||
for user_id in user_ids:
|
for redis_user_id in user_ids:
|
||||||
# Redis returns the id as string. So we have to convert it
|
# Redis returns the id as string. So we have to convert it
|
||||||
user_id = int(user_id)
|
user_id = int(redis_user_id)
|
||||||
channel_names = redis.smembers(self.get_user_cache_key(user_id))
|
channel_names = redis.smembers(self.get_user_cache_key(user_id)) # type: Optional[List[str]]
|
||||||
if channel_names is not None:
|
if channel_names is not None:
|
||||||
# If channel name is empty, then we can assume, that the user
|
# If channel name is empty, then we can assume, that the user
|
||||||
# has no active connection.
|
# has no active connection.
|
||||||
websocket_user_ids[user_id] = set(channel_names)
|
websocket_user_ids[user_id] = set(channel_names)
|
||||||
return websocket_user_ids
|
return websocket_user_ids
|
||||||
|
|
||||||
def save_data(self, data):
|
def save_data(self, data: UserCacheDataType) -> None:
|
||||||
"""
|
"""
|
||||||
Saves the full data set (like created with the method build_data()) to
|
Saves the full data set (like created with the method build_data()) to
|
||||||
the cache.
|
the cache.
|
||||||
@ -122,13 +139,13 @@ class RedisWebsocketUserCache(BaseWebsocketUserCache):
|
|||||||
pipe.sadd(self.get_user_cache_key(user_id), *channel_names)
|
pipe.sadd(self.get_user_cache_key(user_id), *channel_names)
|
||||||
pipe.execute()
|
pipe.execute()
|
||||||
|
|
||||||
def get_cache_key(self):
|
def get_cache_key(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the cache key.
|
Returns the cache key.
|
||||||
"""
|
"""
|
||||||
return cache.make_key(self.cache_key)
|
return cache.make_key(self.cache_key)
|
||||||
|
|
||||||
def get_user_cache_key(self, user_id):
|
def get_user_cache_key(self, user_id: int) -> str:
|
||||||
"""
|
"""
|
||||||
Returns a cache key to save the channel names for a specific user.
|
Returns a cache key to save the channel names for a specific user.
|
||||||
"""
|
"""
|
||||||
@ -146,7 +163,7 @@ class DjangoCacheWebsocketUserCache(BaseWebsocketUserCache):
|
|||||||
the value is a set of channel names.
|
the value is a set of channel names.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def add(self, user_id, channel_name):
|
def add(self, user_id: int, channel_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Adds a channel name for a user using the django cache.
|
Adds a channel name for a user using the django cache.
|
||||||
"""
|
"""
|
||||||
@ -160,7 +177,7 @@ class DjangoCacheWebsocketUserCache(BaseWebsocketUserCache):
|
|||||||
websocket_user_ids[user_id] = set([channel_name])
|
websocket_user_ids[user_id] = set([channel_name])
|
||||||
cache.set(self.get_cache_key(), websocket_user_ids)
|
cache.set(self.get_cache_key(), websocket_user_ids)
|
||||||
|
|
||||||
def remove(self, user_id, channel_name):
|
def remove(self, user_id: int, channel_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Removes one channel name from the django cache.
|
Removes one channel name from the django cache.
|
||||||
"""
|
"""
|
||||||
@ -169,7 +186,7 @@ class DjangoCacheWebsocketUserCache(BaseWebsocketUserCache):
|
|||||||
websocket_user_ids[user_id].discard(channel_name)
|
websocket_user_ids[user_id].discard(channel_name)
|
||||||
cache.set(self.get_cache_key(), websocket_user_ids)
|
cache.set(self.get_cache_key(), websocket_user_ids)
|
||||||
|
|
||||||
def get_all(self):
|
def get_all(self) -> UserCacheDataType:
|
||||||
"""
|
"""
|
||||||
Returns the data using the django cache.
|
Returns the data using the django cache.
|
||||||
"""
|
"""
|
||||||
@ -178,7 +195,7 @@ class DjangoCacheWebsocketUserCache(BaseWebsocketUserCache):
|
|||||||
return self.build_data()
|
return self.build_data()
|
||||||
return websocket_user_ids
|
return websocket_user_ids
|
||||||
|
|
||||||
def save_data(self, data):
|
def save_data(self, data: UserCacheDataType) -> None:
|
||||||
"""
|
"""
|
||||||
Saves the data using the django cache.
|
Saves the data using the django cache.
|
||||||
"""
|
"""
|
||||||
@ -191,18 +208,18 @@ class StartupCache:
|
|||||||
"""
|
"""
|
||||||
cache_key = "full_data_startup_cache"
|
cache_key = "full_data_startup_cache"
|
||||||
|
|
||||||
def build(self):
|
def build(self) -> Dict[str, List[str]]:
|
||||||
"""
|
"""
|
||||||
Generate the cache by going through all apps. Returns a dict where the
|
Generate the cache by going through all apps. Returns a dict where the
|
||||||
key is the collection string and the value a list of the full_data from
|
key is the collection string and the value a list of the full_data from
|
||||||
the collection elements.
|
the collection elements.
|
||||||
"""
|
"""
|
||||||
cache_data = {}
|
cache_data = {} # type: Dict[str, List[str]]
|
||||||
for app in apps.get_app_configs():
|
for app in apps.get_app_configs():
|
||||||
try:
|
try:
|
||||||
# Get the method get_startup_elements() from an app.
|
# Get the method get_startup_elements() from an app.
|
||||||
# This method has to return an iterable of Collection objects.
|
# This method has to return an iterable of Collection objects.
|
||||||
get_startup_elements = app.get_startup_elements
|
get_startup_elements = app.get_startup_elements # type: Callable[[], Iterable[Collection]]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Skip apps that do not implement get_startup_elements.
|
# Skip apps that do not implement get_startup_elements.
|
||||||
continue
|
continue
|
||||||
@ -216,20 +233,20 @@ class StartupCache:
|
|||||||
cache.set(self.cache_key, cache_data, 86400)
|
cache.set(self.cache_key, cache_data, 86400)
|
||||||
return cache_data
|
return cache_data
|
||||||
|
|
||||||
def clear(self):
|
def clear(self) -> None:
|
||||||
"""
|
"""
|
||||||
Clears the cache.
|
Clears the cache.
|
||||||
"""
|
"""
|
||||||
cache.delete(self.cache_key)
|
cache.delete(self.cache_key)
|
||||||
|
|
||||||
def get_collections(self):
|
def get_collections(self) -> Generator['Collection', None, None]:
|
||||||
"""
|
"""
|
||||||
Generator that returns all cached Collections.
|
Generator that returns all cached Collections.
|
||||||
|
|
||||||
The data is read from the cache if it exists. It builds the cache if it
|
The data is read from the cache if it exists. It builds the cache if it
|
||||||
does not exists.
|
does not exists.
|
||||||
"""
|
"""
|
||||||
from .collection import Collection
|
from .collection import Collection # noqa
|
||||||
data = cache.get(self.cache_key)
|
data = cache.get(self.cache_key)
|
||||||
if data is None:
|
if data is None:
|
||||||
# The cache does not exist.
|
# The cache does not exist.
|
||||||
@ -241,7 +258,7 @@ class StartupCache:
|
|||||||
startup_cache = StartupCache()
|
startup_cache = StartupCache()
|
||||||
|
|
||||||
|
|
||||||
def use_redis_cache():
|
def use_redis_cache() -> bool:
|
||||||
"""
|
"""
|
||||||
Returns True if Redis is used als caching backend.
|
Returns True if Redis is used als caching backend.
|
||||||
"""
|
"""
|
||||||
@ -252,7 +269,7 @@ def use_redis_cache():
|
|||||||
return isinstance(caches['default'], RedisCache)
|
return isinstance(caches['default'], RedisCache)
|
||||||
|
|
||||||
|
|
||||||
def get_redis_connection():
|
def get_redis_connection() -> Any:
|
||||||
"""
|
"""
|
||||||
Returns an object that can be used to talk directly to redis.
|
Returns an object that can be used to talk directly to redis.
|
||||||
"""
|
"""
|
||||||
|
@ -1,38 +1,33 @@
|
|||||||
from typing import Mapping # noqa
|
from typing import Mapping # noqa
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
Generator,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.db.models import Model
|
||||||
|
|
||||||
from .cache import get_redis_connection, use_redis_cache
|
from .cache import get_redis_connection, use_redis_cache
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .access_permissions import BaseAccessPermissions # noqa
|
||||||
|
|
||||||
|
# TODO: Try to import this type from access_permission
|
||||||
|
RestrictedData = Union[List[Dict[str, Any]], Dict[str, Any], None]
|
||||||
|
|
||||||
|
|
||||||
class CollectionElement:
|
class CollectionElement:
|
||||||
@classmethod
|
def __init__(self, instance: Model=None, deleted: bool=False, collection_string: str=None,
|
||||||
def from_instance(cls, instance, deleted=False, information=None):
|
id: int=None, full_data: Dict[str, Any]=None, information: Dict[str, Any]=None) -> None:
|
||||||
"""
|
|
||||||
Returns a collection element from a database instance.
|
|
||||||
|
|
||||||
This will also update the instance in the cache.
|
|
||||||
|
|
||||||
If deleted is set to True, the element is deleted from the cache.
|
|
||||||
"""
|
|
||||||
return cls(instance=instance, deleted=deleted, information=information)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_values(cls, collection_string, id, deleted=False, full_data=None, information=None):
|
|
||||||
"""
|
|
||||||
Returns a collection element from a collection_string and an id.
|
|
||||||
|
|
||||||
If deleted is set to True, the element is deleted from the cache.
|
|
||||||
|
|
||||||
With the argument full_data, the content of the CollectionElement can be set.
|
|
||||||
It has to be a dict in the format that is used be access_permission.get_full_data().
|
|
||||||
"""
|
|
||||||
return cls(collection_string=collection_string, id=id, deleted=deleted,
|
|
||||||
full_data=full_data, information=information)
|
|
||||||
|
|
||||||
def __init__(self, instance=None, deleted=False, collection_string=None, id=None,
|
|
||||||
full_data=None, information=None):
|
|
||||||
"""
|
"""
|
||||||
Do not use this. Use the methods from_instance() or from_values().
|
Do not use this. Use the methods from_instance() or from_values().
|
||||||
"""
|
"""
|
||||||
@ -47,7 +42,7 @@ class CollectionElement:
|
|||||||
elif collection_string is not None and id is not None:
|
elif collection_string is not None and id is not None:
|
||||||
# Collection element is created via values
|
# Collection element is created via values
|
||||||
self.collection_string = collection_string
|
self.collection_string = collection_string
|
||||||
self.id = int(id)
|
self.id = id
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
'Invalid state. Use CollectionElement.from_instance() or '
|
'Invalid state. Use CollectionElement.from_instance() or '
|
||||||
@ -65,7 +60,32 @@ class CollectionElement:
|
|||||||
# neither exist in the cache nor in the database.
|
# neither exist in the cache nor in the database.
|
||||||
self.get_full_data()
|
self.get_full_data()
|
||||||
|
|
||||||
def __eq__(self, collection_element):
|
@classmethod
|
||||||
|
def from_instance(cls, instance: Model, deleted: bool=False, information: Dict[str, Any]=None) -> 'CollectionElement':
|
||||||
|
"""
|
||||||
|
Returns a collection element from a database instance.
|
||||||
|
|
||||||
|
This will also update the instance in the cache.
|
||||||
|
|
||||||
|
If deleted is set to True, the element is deleted from the cache.
|
||||||
|
"""
|
||||||
|
return cls(instance=instance, deleted=deleted, information=information)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_values(cls, collection_string: str, id: int, deleted: bool=False,
|
||||||
|
full_data: Dict[str, Any]=None, information: Dict[str, Any]=None) -> 'CollectionElement':
|
||||||
|
"""
|
||||||
|
Returns a collection element from a collection_string and an id.
|
||||||
|
|
||||||
|
If deleted is set to True, the element is deleted from the cache.
|
||||||
|
|
||||||
|
With the argument full_data, the content of the CollectionElement can be set.
|
||||||
|
It has to be a dict in the format that is used be access_permission.get_full_data().
|
||||||
|
"""
|
||||||
|
return cls(collection_string=collection_string, id=id, deleted=deleted,
|
||||||
|
full_data=full_data, information=information)
|
||||||
|
|
||||||
|
def __eq__(self, collection_element: 'CollectionElement') -> bool: # type: ignore
|
||||||
"""
|
"""
|
||||||
Compares two collection_elements.
|
Compares two collection_elements.
|
||||||
|
|
||||||
@ -75,7 +95,7 @@ class CollectionElement:
|
|||||||
return (self.collection_string == collection_element.collection_string and
|
return (self.collection_string == collection_element.collection_string and
|
||||||
self.id == collection_element.id)
|
self.id == collection_element.id)
|
||||||
|
|
||||||
def as_channels_message(self):
|
def as_channels_message(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Returns a dictonary that can be used to send the object through the
|
Returns a dictonary that can be used to send the object through the
|
||||||
channels system.
|
channels system.
|
||||||
@ -92,7 +112,7 @@ class CollectionElement:
|
|||||||
channel_message['full_data'] = self.full_data
|
channel_message['full_data'] = self.full_data
|
||||||
return channel_message
|
return channel_message
|
||||||
|
|
||||||
def as_autoupdate(self, method, *args):
|
def as_autoupdate(self, method: str, *args: Any) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Only for internal use. Do not use it directly. Use as_autoupdate_for_user()
|
Only for internal use. Do not use it directly. Use as_autoupdate_for_user()
|
||||||
or as_autoupdate_for_projector().
|
or as_autoupdate_for_projector().
|
||||||
@ -112,19 +132,16 @@ class CollectionElement:
|
|||||||
action='deleted' if self.is_deleted() else 'changed',
|
action='deleted' if self.is_deleted() else 'changed',
|
||||||
data=data)
|
data=data)
|
||||||
|
|
||||||
def as_autoupdate_for_user(self, user):
|
def as_autoupdate_for_user(self, user: Optional['CollectionElement']) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Returns a dict that can be sent through the autoupdate system for a site
|
Returns a dict that can be sent through the autoupdate system for a site
|
||||||
user.
|
user.
|
||||||
|
|
||||||
The argument `user` can be anything, that is allowd as argument for
|
|
||||||
utils.auth.has_perm().
|
|
||||||
"""
|
"""
|
||||||
return self.as_autoupdate(
|
return self.as_autoupdate(
|
||||||
'get_restricted_data',
|
'get_restricted_data',
|
||||||
user)
|
user)
|
||||||
|
|
||||||
def as_autoupdate_for_projector(self):
|
def as_autoupdate_for_projector(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Returns a dict that can be sent through the autoupdate system for the
|
Returns a dict that can be sent through the autoupdate system for the
|
||||||
projector.
|
projector.
|
||||||
@ -132,22 +149,19 @@ class CollectionElement:
|
|||||||
return self.as_autoupdate(
|
return self.as_autoupdate(
|
||||||
'get_projector_data')
|
'get_projector_data')
|
||||||
|
|
||||||
def as_dict_for_user(self, user):
|
def as_dict_for_user(self, user: Optional['CollectionElement']) -> 'RestrictedData':
|
||||||
"""
|
"""
|
||||||
Returns a dict with the data for a user. Can be used for the rest api.
|
Returns a dict with the data for a user. Can be used for the rest api.
|
||||||
|
|
||||||
The argument `user` can be anything, that is allowd as argument for
|
|
||||||
utils.auth.has_perm().
|
|
||||||
"""
|
"""
|
||||||
return self.get_access_permissions().get_restricted_data(self, user)
|
return self.get_access_permissions().get_restricted_data(self, user)
|
||||||
|
|
||||||
def get_model(self):
|
def get_model(self) -> Type[Model]:
|
||||||
"""
|
"""
|
||||||
Returns the django model that is used for this collection.
|
Returns the django model that is used for this collection.
|
||||||
"""
|
"""
|
||||||
return get_model_from_collection_string(self.collection_string)
|
return get_model_from_collection_string(self.collection_string)
|
||||||
|
|
||||||
def get_instance(self):
|
def get_instance(self) -> Model:
|
||||||
"""
|
"""
|
||||||
Returns the instance as django object.
|
Returns the instance as django object.
|
||||||
|
|
||||||
@ -165,13 +179,13 @@ class CollectionElement:
|
|||||||
self.instance = query.get(pk=self.id)
|
self.instance = query.get(pk=self.id)
|
||||||
return self.instance
|
return self.instance
|
||||||
|
|
||||||
def get_access_permissions(self):
|
def get_access_permissions(self) -> 'BaseAccessPermissions':
|
||||||
"""
|
"""
|
||||||
Returns the get_access_permissions object for the this collection element.
|
Returns the get_access_permissions object for the this collection element.
|
||||||
"""
|
"""
|
||||||
return self.get_model().get_access_permissions()
|
return self.get_model().get_access_permissions()
|
||||||
|
|
||||||
def get_full_data(self):
|
def get_full_data(self) -> Any:
|
||||||
"""
|
"""
|
||||||
Returns the full_data of this collection_element from with all other
|
Returns the full_data of this collection_element from with all other
|
||||||
dics can be generated.
|
dics can be generated.
|
||||||
@ -194,21 +208,21 @@ class CollectionElement:
|
|||||||
self.save_to_cache()
|
self.save_to_cache()
|
||||||
return self.full_data
|
return self.full_data
|
||||||
|
|
||||||
def is_deleted(self):
|
def is_deleted(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns Ture if the item is marked as deleted.
|
Returns Ture if the item is marked as deleted.
|
||||||
"""
|
"""
|
||||||
return self.deleted
|
return self.deleted
|
||||||
|
|
||||||
def get_cache_key(self):
|
def get_cache_key(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns a string that is used as cache key for a single instance.
|
Returns a string that is used as cache key for a single instance.
|
||||||
"""
|
"""
|
||||||
return get_single_element_cache_key(self.collection_string, self.id)
|
return get_single_element_cache_key(self.collection_string, self.id)
|
||||||
|
|
||||||
def delete_from_cache(self):
|
def delete_from_cache(self) -> None:
|
||||||
"""
|
"""
|
||||||
Delets an element from the cache.
|
Delets the element from the cache.
|
||||||
|
|
||||||
Does nothing if the element is not in the cache.
|
Does nothing if the element is not in the cache.
|
||||||
"""
|
"""
|
||||||
@ -218,7 +232,7 @@ class CollectionElement:
|
|||||||
# Delete the id of the instance of the instance list
|
# Delete the id of the instance of the instance list
|
||||||
Collection(self.collection_string).delete_id_from_cache(self.id)
|
Collection(self.collection_string).delete_id_from_cache(self.id)
|
||||||
|
|
||||||
def save_to_cache(self):
|
def save_to_cache(self) -> None:
|
||||||
"""
|
"""
|
||||||
Add or update the element to the cache.
|
Add or update the element to the cache.
|
||||||
"""
|
"""
|
||||||
@ -238,7 +252,7 @@ class CollectionElementList(list):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_channels_message(cls, message):
|
def from_channels_message(cls, message: Dict[str, Any]) -> 'CollectionElementList':
|
||||||
"""
|
"""
|
||||||
Creates a collection element list from a channel message.
|
Creates a collection element list from a channel message.
|
||||||
"""
|
"""
|
||||||
@ -247,16 +261,16 @@ class CollectionElementList(list):
|
|||||||
self.append(CollectionElement.from_values(**values))
|
self.append(CollectionElement.from_values(**values))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def as_channels_message(self):
|
def as_channels_message(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Returns a list of dicts that can be send through the channel system.
|
Returns a list of dicts that can be send through the channel system.
|
||||||
"""
|
"""
|
||||||
message = {'elements': []}
|
message = {'elements': []} # type: Dict[str, Any]
|
||||||
for element in self:
|
for element in self:
|
||||||
message['elements'].append(element.as_channels_message())
|
message['elements'].append(element.as_channels_message())
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def as_autoupdate_for_user(self, user):
|
def as_autoupdate_for_user(self, user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns a list of dicts, that can be send though the websocket to a user.
|
Returns a list of dicts, that can be send though the websocket to a user.
|
||||||
|
|
||||||
@ -274,7 +288,7 @@ class Collection:
|
|||||||
Represents all elements of one collection.
|
Represents all elements of one collection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, collection_string, full_data=None):
|
def __init__(self, collection_string: str, full_data: List[Dict[str, Any]]=None) -> None:
|
||||||
"""
|
"""
|
||||||
Initiates a Collection. A collection_string has to be given. If
|
Initiates a Collection. A collection_string has to be given. If
|
||||||
full_data (a list of dictionaries) is not given the method
|
full_data (a list of dictionaries) is not given the method
|
||||||
@ -284,7 +298,7 @@ class Collection:
|
|||||||
self.collection_string = collection_string
|
self.collection_string = collection_string
|
||||||
self.full_data = full_data
|
self.full_data = full_data
|
||||||
|
|
||||||
def get_cache_key(self, raw=False):
|
def get_cache_key(self, raw: bool=False) -> str:
|
||||||
"""
|
"""
|
||||||
Returns a string that is used as cache key for a collection.
|
Returns a string that is used as cache key for a collection.
|
||||||
|
|
||||||
@ -296,19 +310,19 @@ class Collection:
|
|||||||
key = cache.make_key(key)
|
key = cache.make_key(key)
|
||||||
return key
|
return key
|
||||||
|
|
||||||
def get_model(self):
|
def get_model(self) -> Type[Model]:
|
||||||
"""
|
"""
|
||||||
Returns the django model that is used for this collection.
|
Returns the django model that is used for this collection.
|
||||||
"""
|
"""
|
||||||
return get_model_from_collection_string(self.collection_string)
|
return get_model_from_collection_string(self.collection_string)
|
||||||
|
|
||||||
def get_access_permissions(self):
|
def get_access_permissions(self) -> 'BaseAccessPermissions':
|
||||||
"""
|
"""
|
||||||
Returns the get_access_permissions object for the this collection.
|
Returns the get_access_permissions object for the this collection.
|
||||||
"""
|
"""
|
||||||
return self.get_model().get_access_permissions()
|
return self.get_model().get_access_permissions()
|
||||||
|
|
||||||
def element_generator(self):
|
def element_generator(self) -> Generator[CollectionElement, None, None]:
|
||||||
"""
|
"""
|
||||||
Generator that yields all collection_elements of this collection.
|
Generator that yields all collection_elements of this collection.
|
||||||
"""
|
"""
|
||||||
@ -329,8 +343,10 @@ class Collection:
|
|||||||
|
|
||||||
# Generate collection elements that where in the cache.
|
# Generate collection elements that where in the cache.
|
||||||
for cache_key, cached_full_data in cached_full_data_dict.items():
|
for cache_key, cached_full_data in cached_full_data_dict.items():
|
||||||
|
collection_string, id = get_collection_id_from_cache_key(cache_key)
|
||||||
yield CollectionElement.from_values(
|
yield CollectionElement.from_values(
|
||||||
*get_collection_id_from_cache_key(cache_key),
|
collection_string,
|
||||||
|
id,
|
||||||
full_data=cached_full_data)
|
full_data=cached_full_data)
|
||||||
|
|
||||||
# Generate collection element that where not in the cache.
|
# Generate collection element that where not in the cache.
|
||||||
@ -343,7 +359,7 @@ class Collection:
|
|||||||
for instance in query.filter(pk__in=missing_ids):
|
for instance in query.filter(pk__in=missing_ids):
|
||||||
yield CollectionElement.from_instance(instance)
|
yield CollectionElement.from_instance(instance)
|
||||||
|
|
||||||
def get_full_data(self):
|
def get_full_data(self) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns a list of dictionaries with full_data of all collection
|
Returns a list of dictionaries with full_data of all collection
|
||||||
elements.
|
elements.
|
||||||
@ -355,7 +371,7 @@ class Collection:
|
|||||||
in self.element_generator()]
|
in self.element_generator()]
|
||||||
return self.full_data
|
return self.full_data
|
||||||
|
|
||||||
def as_autoupdate_for_projector(self):
|
def as_autoupdate_for_projector(self) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns a list of dictonaries to send them to the projector.
|
Returns a list of dictonaries to send them to the projector.
|
||||||
"""
|
"""
|
||||||
@ -368,12 +384,9 @@ class Collection:
|
|||||||
output.append(content)
|
output.append(content)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def as_autoupdate_for_user(self, user):
|
def as_autoupdate_for_user(self, user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns a list of dicts, that can be send though the websocket to a user.
|
Returns a list of dicts, that can be send though the websocket to a user.
|
||||||
|
|
||||||
The argument `user` can be anything, that is allowd as argument for
|
|
||||||
utils.auth.has_perm().
|
|
||||||
"""
|
"""
|
||||||
# TODO: This method is not used. Remove it.
|
# TODO: This method is not used. Remove it.
|
||||||
output = []
|
output = []
|
||||||
@ -383,22 +396,19 @@ class Collection:
|
|||||||
output.append(content)
|
output.append(content)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def as_list_for_user(self, user):
|
def as_list_for_user(self, user: Optional[CollectionElement]) -> List['RestrictedData']:
|
||||||
"""
|
"""
|
||||||
Returns a list of dictonaries to send them to a user, for example over
|
Returns a list of dictonaries to send them to a user, for example over
|
||||||
the rest api.
|
the rest api.
|
||||||
|
|
||||||
The argument `user` can be anything, that is allowd as argument for
|
|
||||||
utils.auth.has_perm().
|
|
||||||
"""
|
"""
|
||||||
output = []
|
output = [] # type: List[RestrictedData]
|
||||||
for collection_element in self.element_generator():
|
for collection_element in self.element_generator():
|
||||||
content = collection_element.as_dict_for_user(user)
|
content = collection_element.as_dict_for_user(user) # type: RestrictedData
|
||||||
if content is not None:
|
if content is not None:
|
||||||
output.append(content)
|
output.append(content)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_all_ids(self):
|
def get_all_ids(self) -> Set[int]:
|
||||||
"""
|
"""
|
||||||
Returns a set of all ids of instances in this collection.
|
Returns a set of all ids of instances in this collection.
|
||||||
"""
|
"""
|
||||||
@ -408,7 +418,7 @@ class Collection:
|
|||||||
ids = self.get_all_ids_other()
|
ids = self.get_all_ids_other()
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
def get_all_ids_redis(self):
|
def get_all_ids_redis(self) -> Set[int]:
|
||||||
redis = get_redis_connection()
|
redis = get_redis_connection()
|
||||||
ids = redis.smembers(self.get_cache_key(raw=True))
|
ids = redis.smembers(self.get_cache_key(raw=True))
|
||||||
if not ids:
|
if not ids:
|
||||||
@ -419,7 +429,7 @@ class Collection:
|
|||||||
ids = set(int(id) for id in ids)
|
ids = set(int(id) for id in ids)
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
def get_all_ids_other(self):
|
def get_all_ids_other(self) -> Set[int]:
|
||||||
ids = cache.get(self.get_cache_key())
|
ids = cache.get(self.get_cache_key())
|
||||||
if ids is None:
|
if ids is None:
|
||||||
# If it is not in the cache then get it from the database.
|
# If it is not in the cache then get it from the database.
|
||||||
@ -427,7 +437,7 @@ class Collection:
|
|||||||
cache.set(self.get_cache_key(), ids)
|
cache.set(self.get_cache_key(), ids)
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
def delete_id_from_cache(self, id):
|
def delete_id_from_cache(self, id: int) -> None:
|
||||||
"""
|
"""
|
||||||
Delets a id from the cache.
|
Delets a id from the cache.
|
||||||
"""
|
"""
|
||||||
@ -436,11 +446,11 @@ class Collection:
|
|||||||
else:
|
else:
|
||||||
self.delete_id_from_cache_other(id)
|
self.delete_id_from_cache_other(id)
|
||||||
|
|
||||||
def delete_id_from_cache_redis(self, id):
|
def delete_id_from_cache_redis(self, id: int) -> None:
|
||||||
redis = get_redis_connection()
|
redis = get_redis_connection()
|
||||||
redis.srem(self.get_cache_key(raw=True), id)
|
redis.srem(self.get_cache_key(raw=True), id)
|
||||||
|
|
||||||
def delete_id_from_cache_other(self, id):
|
def delete_id_from_cache_other(self, id: int) -> None:
|
||||||
ids = cache.get(self.get_cache_key())
|
ids = cache.get(self.get_cache_key())
|
||||||
if ids is not None:
|
if ids is not None:
|
||||||
ids = set(ids)
|
ids = set(ids)
|
||||||
@ -456,7 +466,7 @@ class Collection:
|
|||||||
# Delete the key, if there are not ids left
|
# Delete the key, if there are not ids left
|
||||||
cache.delete(self.get_cache_key())
|
cache.delete(self.get_cache_key())
|
||||||
|
|
||||||
def add_id_to_cache(self, id):
|
def add_id_to_cache(self, id: int) -> None:
|
||||||
"""
|
"""
|
||||||
Adds a collection id to the list of collection ids in the cache.
|
Adds a collection id to the list of collection ids in the cache.
|
||||||
"""
|
"""
|
||||||
@ -465,13 +475,13 @@ class Collection:
|
|||||||
else:
|
else:
|
||||||
self.add_id_to_cache_other(id)
|
self.add_id_to_cache_other(id)
|
||||||
|
|
||||||
def add_id_to_cache_redis(self, id):
|
def add_id_to_cache_redis(self, id: int) -> None:
|
||||||
redis = get_redis_connection()
|
redis = get_redis_connection()
|
||||||
if redis.exists(self.get_cache_key(raw=True)):
|
if redis.exists(self.get_cache_key(raw=True)):
|
||||||
# Only add the value if it is in the cache.
|
# Only add the value if it is in the cache.
|
||||||
redis.sadd(self.get_cache_key(raw=True), id)
|
redis.sadd(self.get_cache_key(raw=True), id)
|
||||||
|
|
||||||
def add_id_to_cache_other(self, id):
|
def add_id_to_cache_other(self, id: int) -> None:
|
||||||
ids = cache.get(self.get_cache_key())
|
ids = cache.get(self.get_cache_key())
|
||||||
if ids is not None:
|
if ids is not None:
|
||||||
# Only change the value if it is in the cache.
|
# Only change the value if it is in the cache.
|
||||||
@ -480,14 +490,14 @@ class Collection:
|
|||||||
cache.set(self.get_cache_key(), ids)
|
cache.set(self.get_cache_key(), ids)
|
||||||
|
|
||||||
|
|
||||||
_models_to_collection_string = {} # type: Mapping[str, object]
|
_models_to_collection_string = {} # type: Dict[str, Type[Model]]
|
||||||
|
|
||||||
|
|
||||||
def get_model_from_collection_string(collection_string):
|
def get_model_from_collection_string(collection_string: str) -> Type[Model]:
|
||||||
"""
|
"""
|
||||||
Returns a model class which belongs to the argument collection_string.
|
Returns a model class which belongs to the argument collection_string.
|
||||||
"""
|
"""
|
||||||
def model_generator():
|
def model_generator() -> Generator[Type[Model], None, None]:
|
||||||
"""
|
"""
|
||||||
Yields all models of all apps.
|
Yields all models of all apps.
|
||||||
"""
|
"""
|
||||||
@ -512,7 +522,7 @@ def get_model_from_collection_string(collection_string):
|
|||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
def get_single_element_cache_key(collection_string, id):
|
def get_single_element_cache_key(collection_string: str, id: int) -> str:
|
||||||
"""
|
"""
|
||||||
Returns a string that is used as cache key for a single instance.
|
Returns a string that is used as cache key for a single instance.
|
||||||
"""
|
"""
|
||||||
@ -521,7 +531,7 @@ def get_single_element_cache_key(collection_string, id):
|
|||||||
id=id)
|
id=id)
|
||||||
|
|
||||||
|
|
||||||
def get_single_element_cache_key_prefix(collection_string):
|
def get_single_element_cache_key_prefix(collection_string: str) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the first part of the cache key for single elements, which is the
|
Returns the first part of the cache key for single elements, which is the
|
||||||
same for all cache keys of the same collection.
|
same for all cache keys of the same collection.
|
||||||
@ -529,14 +539,14 @@ def get_single_element_cache_key_prefix(collection_string):
|
|||||||
return "{collection}:".format(collection=collection_string)
|
return "{collection}:".format(collection=collection_string)
|
||||||
|
|
||||||
|
|
||||||
def get_element_list_cache_key(collection_string):
|
def get_element_list_cache_key(collection_string: str) -> str:
|
||||||
"""
|
"""
|
||||||
Returns a string that is used as cache key for a collection.
|
Returns a string that is used as cache key for a collection.
|
||||||
"""
|
"""
|
||||||
return "{collection}".format(collection=collection_string)
|
return "{collection}".format(collection=collection_string)
|
||||||
|
|
||||||
|
|
||||||
def get_collection_id_from_cache_key(cache_key):
|
def get_collection_id_from_cache_key(cache_key: str) -> Tuple[str, int]:
|
||||||
"""
|
"""
|
||||||
Returns a tuble of the collection string and the id from a cache_key
|
Returns a tuble of the collection string and the id from a cache_key
|
||||||
created with get_instance_cache_key.
|
created with get_instance_cache_key.
|
||||||
|
@ -49,7 +49,7 @@ class SignalConnectMetaClass(type):
|
|||||||
default attributes and methods.
|
default attributes and methods.
|
||||||
"""
|
"""
|
||||||
class_attributes['get_all'] = get_all
|
class_attributes['get_all'] = get_all
|
||||||
new_class = super(SignalConnectMetaClass, metaclass).__new__(
|
new_class = super().__new__(
|
||||||
metaclass, class_name, class_parents, class_attributes)
|
metaclass, class_name, class_parents, class_attributes)
|
||||||
try:
|
try:
|
||||||
dispatch_uid = new_class.get_dispatch_uid()
|
dispatch_uid = new_class.get_dispatch_uid()
|
||||||
|
@ -6,10 +6,12 @@ import tempfile
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from django.conf import ENVIRONMENT_VARIABLE
|
from django.conf import ENVIRONMENT_VARIABLE
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
from mypy_extensions import NoReturn
|
||||||
|
|
||||||
DEVELOPMENT_VERSION = 'Development Version'
|
DEVELOPMENT_VERSION = 'Development Version'
|
||||||
UNIX_VERSION = 'Unix Version'
|
UNIX_VERSION = 'Unix Version'
|
||||||
@ -34,11 +36,11 @@ class UnknownCommand(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class ExceptionArgumentParser(argparse.ArgumentParser):
|
class ExceptionArgumentParser(argparse.ArgumentParser):
|
||||||
def error(self, message):
|
def error(self, message: str) -> NoReturn:
|
||||||
raise UnknownCommand(message)
|
raise UnknownCommand(message)
|
||||||
|
|
||||||
|
|
||||||
def detect_openslides_type():
|
def detect_openslides_type() -> str:
|
||||||
"""
|
"""
|
||||||
Returns the type of this OpenSlides version.
|
Returns the type of this OpenSlides version.
|
||||||
"""
|
"""
|
||||||
@ -58,7 +60,7 @@ def detect_openslides_type():
|
|||||||
return openslides_type
|
return openslides_type
|
||||||
|
|
||||||
|
|
||||||
def get_default_settings_path(openslides_type=None):
|
def get_default_settings_path(openslides_type: str=None) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the default settings path according to the OpenSlides type.
|
Returns the default settings path according to the OpenSlides type.
|
||||||
|
|
||||||
@ -80,7 +82,7 @@ def get_default_settings_path(openslides_type=None):
|
|||||||
return os.path.join(parent_directory, 'openslides', 'settings.py')
|
return os.path.join(parent_directory, 'openslides', 'settings.py')
|
||||||
|
|
||||||
|
|
||||||
def get_local_settings_path():
|
def get_local_settings_path() -> str:
|
||||||
"""
|
"""
|
||||||
Returns the path to a local settings.
|
Returns the path to a local settings.
|
||||||
|
|
||||||
@ -89,7 +91,7 @@ def get_local_settings_path():
|
|||||||
return os.path.join('personal_data', 'var', 'settings.py')
|
return os.path.join('personal_data', 'var', 'settings.py')
|
||||||
|
|
||||||
|
|
||||||
def setup_django_settings_module(settings_path=None, local_installation=None):
|
def setup_django_settings_module(settings_path: str =None, local_installation: bool=False) -> None:
|
||||||
"""
|
"""
|
||||||
Sets the environment variable ENVIRONMENT_VARIABLE, that means
|
Sets the environment variable ENVIRONMENT_VARIABLE, that means
|
||||||
'DJANGO_SETTINGS_MODULE', to the given settings.
|
'DJANGO_SETTINGS_MODULE', to the given settings.
|
||||||
@ -100,7 +102,7 @@ def setup_django_settings_module(settings_path=None, local_installation=None):
|
|||||||
If the argument settings_path is set, then the environment variable is
|
If the argument settings_path is set, then the environment variable is
|
||||||
always overwritten.
|
always overwritten.
|
||||||
"""
|
"""
|
||||||
if settings_path is None and os.environ.get(ENVIRONMENT_VARIABLE, None):
|
if settings_path is None and os.environ.get(ENVIRONMENT_VARIABLE, ""):
|
||||||
return
|
return
|
||||||
|
|
||||||
if settings_path is None:
|
if settings_path is None:
|
||||||
@ -128,7 +130,7 @@ def setup_django_settings_module(settings_path=None, local_installation=None):
|
|||||||
os.environ[ENVIRONMENT_VARIABLE] = settings_module_name
|
os.environ[ENVIRONMENT_VARIABLE] = settings_module_name
|
||||||
|
|
||||||
|
|
||||||
def get_default_settings_context(user_data_path=None):
|
def get_default_settings_context(user_data_path: str=None) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Returns the default context values for the settings template:
|
Returns the default context values for the settings template:
|
||||||
'openslides_user_data_path', 'import_function' and 'debug'.
|
'openslides_user_data_path', 'import_function' and 'debug'.
|
||||||
@ -154,7 +156,7 @@ def get_default_settings_context(user_data_path=None):
|
|||||||
return default_context
|
return default_context
|
||||||
|
|
||||||
|
|
||||||
def get_default_user_data_path(openslides_type):
|
def get_default_user_data_path(openslides_type: str) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the default path for user specific data according to the OpenSlides
|
Returns the default path for user specific data according to the OpenSlides
|
||||||
type.
|
type.
|
||||||
@ -174,7 +176,7 @@ def get_default_user_data_path(openslides_type):
|
|||||||
return default_user_data_path
|
return default_user_data_path
|
||||||
|
|
||||||
|
|
||||||
def get_win32_app_data_path():
|
def get_win32_app_data_path() -> str:
|
||||||
"""
|
"""
|
||||||
Returns the path to Windows' AppData directory.
|
Returns the path to Windows' AppData directory.
|
||||||
"""
|
"""
|
||||||
@ -197,7 +199,7 @@ def get_win32_app_data_path():
|
|||||||
return buf.value
|
return buf.value
|
||||||
|
|
||||||
|
|
||||||
def get_win32_portable_path():
|
def get_win32_portable_path() -> str:
|
||||||
"""
|
"""
|
||||||
Returns the path to the Windows portable version.
|
Returns the path to the Windows portable version.
|
||||||
"""
|
"""
|
||||||
@ -217,14 +219,14 @@ def get_win32_portable_path():
|
|||||||
return portable_path
|
return portable_path
|
||||||
|
|
||||||
|
|
||||||
def get_win32_portable_user_data_path():
|
def get_win32_portable_user_data_path() -> str:
|
||||||
"""
|
"""
|
||||||
Returns the user data path to the Windows portable version.
|
Returns the user data path to the Windows portable version.
|
||||||
"""
|
"""
|
||||||
return os.path.join(get_win32_portable_path(), 'openslides')
|
return os.path.join(get_win32_portable_path(), 'openslides')
|
||||||
|
|
||||||
|
|
||||||
def write_settings(settings_path=None, template=None, **context):
|
def write_settings(settings_path: str=None, template: str=None, **context: str) -> str:
|
||||||
"""
|
"""
|
||||||
Creates the settings file at the given path using the given values for the
|
Creates the settings file at the given path using the given values for the
|
||||||
file template.
|
file template.
|
||||||
@ -259,7 +261,7 @@ def write_settings(settings_path=None, template=None, **context):
|
|||||||
return os.path.realpath(settings_path)
|
return os.path.realpath(settings_path)
|
||||||
|
|
||||||
|
|
||||||
def open_browser(host, port):
|
def open_browser(host: str, port: int) -> None:
|
||||||
"""
|
"""
|
||||||
Launches the default web browser at the given host and port and opens
|
Launches the default web browser at the given host and port and opens
|
||||||
the webinterface. Uses start_browser internally.
|
the webinterface. Uses start_browser internally.
|
||||||
@ -271,7 +273,7 @@ def open_browser(host, port):
|
|||||||
start_browser('http://%s:%s' % (host, port))
|
start_browser('http://%s:%s' % (host, port))
|
||||||
|
|
||||||
|
|
||||||
def start_browser(browser_url):
|
def start_browser(browser_url: str) -> None:
|
||||||
"""
|
"""
|
||||||
Launches the default web browser at the given url and opens the
|
Launches the default web browser at the given url and opens the
|
||||||
webinterface.
|
webinterface.
|
||||||
@ -282,7 +284,7 @@ def start_browser(browser_url):
|
|||||||
print('Could not locate runnable browser: Skipping start')
|
print('Could not locate runnable browser: Skipping start')
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def function():
|
def function() -> None:
|
||||||
# TODO: Use a nonblocking sleep event here. Tornado has such features.
|
# TODO: Use a nonblocking sleep event here. Tornado has such features.
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
browser.open(browser_url)
|
browser.open(browser_url)
|
||||||
@ -291,7 +293,7 @@ def start_browser(browser_url):
|
|||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
def get_database_path_from_settings():
|
def get_database_path_from_settings() -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Retrieves the database path out of the settings file. Returns None,
|
Retrieves the database path out of the settings file. Returns None,
|
||||||
if it is not a SQLite3 database.
|
if it is not a SQLite3 database.
|
||||||
@ -313,7 +315,7 @@ def get_database_path_from_settings():
|
|||||||
return database_path
|
return database_path
|
||||||
|
|
||||||
|
|
||||||
def is_local_installation():
|
def is_local_installation() -> bool:
|
||||||
"""
|
"""
|
||||||
Returns True if the command is called for a local installation
|
Returns True if the command is called for a local installation
|
||||||
|
|
||||||
@ -322,7 +324,7 @@ def is_local_installation():
|
|||||||
return True if '--local-installation' in sys.argv or 'manage.py' in sys.argv[0] else False
|
return True if '--local-installation' in sys.argv or 'manage.py' in sys.argv[0] else False
|
||||||
|
|
||||||
|
|
||||||
def get_geiss_path():
|
def get_geiss_path() -> str:
|
||||||
"""
|
"""
|
||||||
Returns the path and file to the Geiss binary.
|
Returns the path and file to the Geiss binary.
|
||||||
"""
|
"""
|
||||||
@ -332,7 +334,7 @@ def get_geiss_path():
|
|||||||
return os.path.join(download_path, bin_name)
|
return os.path.join(download_path, bin_name)
|
||||||
|
|
||||||
|
|
||||||
def is_windows():
|
def is_windows() -> bool:
|
||||||
"""
|
"""
|
||||||
Returns True if the current system is Windows. Returns False otherwise.
|
Returns True if the current system is Windows. Returns False otherwise.
|
||||||
"""
|
"""
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from .access_permissions import BaseAccessPermissions # noqa
|
from .access_permissions import BaseAccessPermissions # noqa
|
||||||
@ -9,11 +12,11 @@ class MinMaxIntegerField(models.IntegerField):
|
|||||||
IntegerField with options to set a min- and a max-value.
|
IntegerField with options to set a min- and a max-value.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, min_value=None, max_value=None, *args, **kwargs):
|
def __init__(self, min_value: int=None, max_value: int=None, *args: Any, **kwargs: Any) -> None:
|
||||||
self.min_value, self.max_value = min_value, max_value
|
self.min_value, self.max_value = min_value, max_value
|
||||||
super(MinMaxIntegerField, self).__init__(*args, **kwargs)
|
super(MinMaxIntegerField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def formfield(self, **kwargs):
|
def formfield(self, **kwargs: Any) -> Any:
|
||||||
defaults = {'min_value': self.min_value, 'max_value': self.max_value}
|
defaults = {'min_value': self.min_value, 'max_value': self.max_value}
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
return super(MinMaxIntegerField, self).formfield(**defaults)
|
return super(MinMaxIntegerField, self).formfield(**defaults)
|
||||||
@ -26,7 +29,7 @@ class RESTModelMixin:
|
|||||||
|
|
||||||
access_permissions = None # type: BaseAccessPermissions
|
access_permissions = None # type: BaseAccessPermissions
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
def get_root_rest_element(self) -> models.Model:
|
||||||
"""
|
"""
|
||||||
Returns the root rest instance.
|
Returns the root rest instance.
|
||||||
|
|
||||||
@ -35,32 +38,36 @@ class RESTModelMixin:
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_access_permissions(cls):
|
def get_access_permissions(cls) -> BaseAccessPermissions:
|
||||||
"""
|
"""
|
||||||
Returns a container to handle access permissions for this model and
|
Returns a container to handle access permissions for this model and
|
||||||
its corresponding viewset.
|
its corresponding viewset.
|
||||||
"""
|
"""
|
||||||
|
if cls.access_permissions is None:
|
||||||
|
raise ImproperlyConfigured("A RESTModel needs to have an access_permission.")
|
||||||
return cls.access_permissions
|
return cls.access_permissions
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_collection_string(cls):
|
def get_collection_string(cls) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the string representing the name of the collection. Returns
|
Returns the string representing the name of the collection. Returns
|
||||||
None if this is not a so called root rest instance.
|
None if this is not a so called root rest instance.
|
||||||
"""
|
"""
|
||||||
# TODO Check if this is a root rest element class and return None if not.
|
# TODO Check if this is a root rest element class and return None if not.
|
||||||
|
app_label = cls._meta.app_label # type: ignore
|
||||||
|
object_name = cls._meta.object_name # type: ignore
|
||||||
return '/'.join(
|
return '/'.join(
|
||||||
(convert_camel_case_to_pseudo_snake_case(cls._meta.app_label),
|
(convert_camel_case_to_pseudo_snake_case(app_label),
|
||||||
convert_camel_case_to_pseudo_snake_case(cls._meta.object_name)))
|
convert_camel_case_to_pseudo_snake_case(object_name)))
|
||||||
|
|
||||||
def get_rest_pk(self):
|
def get_rest_pk(self) -> int:
|
||||||
"""
|
"""
|
||||||
Returns the primary key used in the REST API. By default this is
|
Returns the primary key used in the REST API. By default this is
|
||||||
the database pk.
|
the database pk.
|
||||||
"""
|
"""
|
||||||
return self.pk
|
return self.pk # type: ignore
|
||||||
|
|
||||||
def save(self, skip_autoupdate=False, information=None, *args, **kwargs):
|
def save(self, skip_autoupdate: bool=False, information: Dict[str, str]=None, *args: Any, **kwargs: Any) -> Any:
|
||||||
"""
|
"""
|
||||||
Calls Django's save() method and afterwards hits the autoupdate system.
|
Calls Django's save() method and afterwards hits the autoupdate system.
|
||||||
|
|
||||||
@ -76,12 +83,12 @@ class RESTModelMixin:
|
|||||||
"""
|
"""
|
||||||
# We don't know how to fix this circular import
|
# We don't know how to fix this circular import
|
||||||
from .autoupdate import inform_changed_data
|
from .autoupdate import inform_changed_data
|
||||||
return_value = super().save(*args, **kwargs)
|
return_value = super().save(*args, **kwargs) # type: ignore
|
||||||
if not skip_autoupdate:
|
if not skip_autoupdate:
|
||||||
inform_changed_data(self.get_root_rest_element(), information=information)
|
inform_changed_data(self.get_root_rest_element(), information=information)
|
||||||
return return_value
|
return return_value
|
||||||
|
|
||||||
def delete(self, skip_autoupdate=False, information=None, *args, **kwargs):
|
def delete(self, skip_autoupdate: bool=False, information: Dict[str, str]=None, *args: Any, **kwargs: Any) -> Any:
|
||||||
"""
|
"""
|
||||||
Calls Django's delete() method and afterwards hits the autoupdate system.
|
Calls Django's delete() method and afterwards hits the autoupdate system.
|
||||||
|
|
||||||
@ -101,8 +108,8 @@ class RESTModelMixin:
|
|||||||
"""
|
"""
|
||||||
# We don't know how to fix this circular import
|
# We don't know how to fix this circular import
|
||||||
from .autoupdate import inform_changed_data, inform_deleted_data
|
from .autoupdate import inform_changed_data, inform_deleted_data
|
||||||
instance_pk = self.pk
|
instance_pk = self.pk # type: ignore
|
||||||
return_value = super().delete(*args, **kwargs)
|
return_value = super().delete(*args, **kwargs) # type: ignore
|
||||||
if not skip_autoupdate:
|
if not skip_autoupdate:
|
||||||
if self != self.get_root_rest_element():
|
if self != self.get_root_rest_element():
|
||||||
# The deletion of a included element is a change of the root element.
|
# The deletion of a included element is a change of the root element.
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Any, List, Tuple
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -15,7 +16,7 @@ from openslides.utils.main import (
|
|||||||
|
|
||||||
# Methods to collect plugins.
|
# Methods to collect plugins.
|
||||||
|
|
||||||
def collect_plugins_from_entry_points():
|
def collect_plugins_from_entry_points() -> Tuple[str, ...]:
|
||||||
"""
|
"""
|
||||||
Collects all entry points in the group openslides_plugins from all
|
Collects all entry points in the group openslides_plugins from all
|
||||||
distributions in the default working set and returns their module names as
|
distributions in the default working set and returns their module names as
|
||||||
@ -24,7 +25,7 @@ def collect_plugins_from_entry_points():
|
|||||||
return tuple(entry_point.module_name for entry_point in iter_entry_points('openslides_plugins'))
|
return tuple(entry_point.module_name for entry_point in iter_entry_points('openslides_plugins'))
|
||||||
|
|
||||||
|
|
||||||
def collect_plugins_from_path(path):
|
def collect_plugins_from_path(path: str) -> Tuple[str, ...]:
|
||||||
"""
|
"""
|
||||||
Collects all modules/packages in the given `path` and returns a tuple
|
Collects all modules/packages in the given `path` and returns a tuple
|
||||||
of their names.
|
of their names.
|
||||||
@ -32,7 +33,7 @@ def collect_plugins_from_path(path):
|
|||||||
return tuple(x[1] for x in pkgutil.iter_modules([path]))
|
return tuple(x[1] for x in pkgutil.iter_modules([path]))
|
||||||
|
|
||||||
|
|
||||||
def collect_plugins():
|
def collect_plugins() -> Tuple[str, ...]:
|
||||||
"""
|
"""
|
||||||
Collect all plugins that can be automatically discovered.
|
Collect all plugins that can be automatically discovered.
|
||||||
"""
|
"""
|
||||||
@ -52,7 +53,7 @@ def collect_plugins():
|
|||||||
|
|
||||||
# Methods to retrieve plugin metadata and urlpatterns.
|
# Methods to retrieve plugin metadata and urlpatterns.
|
||||||
|
|
||||||
def get_plugin_verbose_name(plugin):
|
def get_plugin_verbose_name(plugin: str) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the verbose name of a plugin. The plugin argument must be a python
|
Returns the verbose name of a plugin. The plugin argument must be a python
|
||||||
dotted module path.
|
dotted module path.
|
||||||
@ -60,7 +61,7 @@ def get_plugin_verbose_name(plugin):
|
|||||||
return apps.get_app_config(plugin).verbose_name
|
return apps.get_app_config(plugin).verbose_name
|
||||||
|
|
||||||
|
|
||||||
def get_plugin_description(plugin):
|
def get_plugin_description(plugin: str) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the short descrption of a plugin. The plugin argument must be a
|
Returns the short descrption of a plugin. The plugin argument must be a
|
||||||
python dotted module path.
|
python dotted module path.
|
||||||
@ -76,7 +77,7 @@ def get_plugin_description(plugin):
|
|||||||
return description
|
return description
|
||||||
|
|
||||||
|
|
||||||
def get_plugin_version(plugin):
|
def get_plugin_version(plugin: str) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the version string of a plugin. The plugin argument must be a
|
Returns the version string of a plugin. The plugin argument must be a
|
||||||
python dotted module path.
|
python dotted module path.
|
||||||
@ -92,7 +93,7 @@ def get_plugin_version(plugin):
|
|||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
def get_plugin_urlpatterns(plugin):
|
def get_plugin_urlpatterns(plugin: str) -> Any:
|
||||||
"""
|
"""
|
||||||
Returns the urlpatterns object for a plugin. The plugin argument must be
|
Returns the urlpatterns object for a plugin. The plugin argument must be
|
||||||
a python dotted module path.
|
a python dotted module path.
|
||||||
@ -108,12 +109,12 @@ def get_plugin_urlpatterns(plugin):
|
|||||||
return urlpatterns
|
return urlpatterns
|
||||||
|
|
||||||
|
|
||||||
def get_all_plugin_urlpatterns():
|
def get_all_plugin_urlpatterns() -> List[Any]:
|
||||||
"""
|
"""
|
||||||
Helper function to return all urlpatterns of all plugins listed in
|
Helper function to return all urlpatterns of all plugins listed in
|
||||||
settings.INSTALLED_PLUGINS.
|
settings.INSTALLED_PLUGINS.
|
||||||
"""
|
"""
|
||||||
urlpatterns = []
|
urlpatterns = [] # type: List[Any]
|
||||||
for plugin in settings.INSTALLED_PLUGINS:
|
for plugin in settings.INSTALLED_PLUGINS:
|
||||||
plugin_urlpatterns = get_plugin_urlpatterns(plugin)
|
plugin_urlpatterns = get_plugin_urlpatterns(plugin)
|
||||||
if plugin_urlpatterns:
|
if plugin_urlpatterns:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Optional # noqa
|
from typing import Any, Dict, Iterable, List, Optional # noqa
|
||||||
|
|
||||||
from django.dispatch import Signal
|
from django.dispatch import Signal
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ class ProjectorElement(object, metaclass=SignalConnectMetaClass):
|
|||||||
signal = Signal()
|
signal = Signal()
|
||||||
name = None # type: Optional[str]
|
name = None # type: Optional[str]
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs: str) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes the projector element instance. This is done when the
|
Initializes the projector element instance. This is done when the
|
||||||
signal is sent.
|
signal is sent.
|
||||||
@ -29,15 +29,16 @@ class ProjectorElement(object, metaclass=SignalConnectMetaClass):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_dispatch_uid(cls):
|
def get_dispatch_uid(cls) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Returns the classname as a unique string for each class. Returns None
|
Returns the classname as a unique string for each class. Returns None
|
||||||
for the base class so it will not be connected to the signal.
|
for the base class so it will not be connected to the signal.
|
||||||
"""
|
"""
|
||||||
if not cls.__name__ == 'ProjectorElement':
|
if not cls.__name__ == 'ProjectorElement':
|
||||||
return cls.__name__
|
return cls.__name__
|
||||||
|
return None
|
||||||
|
|
||||||
def check_and_update_data(self, projector_object, config_entry):
|
def check_and_update_data(self, projector_object: Any, config_entry: Any) -> Any:
|
||||||
"""
|
"""
|
||||||
Checks projector element data via self.check_data() and updates
|
Checks projector element data via self.check_data() and updates
|
||||||
them via self.update_data(). The projector object and the config
|
them via self.update_data(). The projector object and the config
|
||||||
@ -50,7 +51,7 @@ class ProjectorElement(object, metaclass=SignalConnectMetaClass):
|
|||||||
self.check_data()
|
self.check_data()
|
||||||
return self.update_data() or {}
|
return self.update_data() or {}
|
||||||
|
|
||||||
def check_data(self):
|
def check_data(self) -> None:
|
||||||
"""
|
"""
|
||||||
Method can be overridden to validate projector element data. This
|
Method can be overridden to validate projector element data. This
|
||||||
may raise ProjectorException in case of an error.
|
may raise ProjectorException in case of an error.
|
||||||
@ -59,7 +60,7 @@ class ProjectorElement(object, metaclass=SignalConnectMetaClass):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def update_data(self):
|
def update_data(self) -> Dict[Any, Any]:
|
||||||
"""
|
"""
|
||||||
Method can be overridden to update the projector element data
|
Method can be overridden to update the projector element data
|
||||||
output. This should return a dictonary. Use this for server
|
output. This should return a dictonary. Use this for server
|
||||||
@ -69,21 +70,23 @@ class ProjectorElement(object, metaclass=SignalConnectMetaClass):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_requirements(self, config_entry):
|
def get_requirements(self, config_entry: Any) -> Iterable[Any]:
|
||||||
"""
|
"""
|
||||||
Returns an iterable of instances that are required for this projector
|
Returns an iterable of instances that are required for this projector
|
||||||
element. The config_entry has to be given.
|
element. The config_entry has to be given.
|
||||||
"""
|
"""
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
def get_requirements_as_collection_elements(self, config_entry):
|
def get_requirements_as_collection_elements(self, config_entry: Any) -> Iterable[CollectionElement]:
|
||||||
"""
|
"""
|
||||||
Returns an iterable of collection elements that are required for this
|
Returns an iterable of collection elements that are required for this
|
||||||
projector element. The config_entry has to be given.
|
projector element. The config_entry has to be given.
|
||||||
"""
|
"""
|
||||||
return (CollectionElement.from_instance(instance) for instance in self.get_requirements(config_entry))
|
return (CollectionElement.from_instance(instance) for instance in self.get_requirements(config_entry))
|
||||||
|
|
||||||
def get_collection_elements_required_for_this(self, collection_element, config_entry):
|
def get_collection_elements_required_for_this(
|
||||||
|
self, collection_element: CollectionElement,
|
||||||
|
config_entry: Any) -> List[CollectionElement]:
|
||||||
"""
|
"""
|
||||||
Returns a list of CollectionElements that have to be sent to every
|
Returns a list of CollectionElements that have to be sent to every
|
||||||
projector that shows this projector element according to the given
|
projector that shows this projector element according to the given
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Optional # noqa
|
from typing import Any, Dict, Iterable, Optional, Type # noqa
|
||||||
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from rest_framework import status # noqa
|
from rest_framework import status # noqa
|
||||||
@ -24,6 +24,7 @@ from rest_framework.serializers import ( # noqa
|
|||||||
ManyRelatedField,
|
ManyRelatedField,
|
||||||
PrimaryKeyRelatedField,
|
PrimaryKeyRelatedField,
|
||||||
RelatedField,
|
RelatedField,
|
||||||
|
Serializer,
|
||||||
SerializerMethodField,
|
SerializerMethodField,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
)
|
)
|
||||||
@ -31,7 +32,7 @@ from rest_framework.viewsets import GenericViewSet as _GenericViewSet # noqa
|
|||||||
from rest_framework.viewsets import ModelViewSet as _ModelViewSet # noqa
|
from rest_framework.viewsets import ModelViewSet as _ModelViewSet # noqa
|
||||||
from rest_framework.viewsets import ViewSet as _ViewSet # noqa
|
from rest_framework.viewsets import ViewSet as _ViewSet # noqa
|
||||||
|
|
||||||
from .access_permissions import BaseAccessPermissions # noqa
|
from .access_permissions import BaseAccessPermissions, RestrictedData # noqa
|
||||||
from .auth import user_to_collection_user
|
from .auth import user_to_collection_user
|
||||||
from .collection import Collection, CollectionElement
|
from .collection import Collection, CollectionElement
|
||||||
|
|
||||||
@ -47,7 +48,7 @@ class IdManyRelatedField(ManyRelatedField):
|
|||||||
"""
|
"""
|
||||||
field_name_suffix = '_id'
|
field_name_suffix = '_id'
|
||||||
|
|
||||||
def bind(self, field_name, parent):
|
def bind(self, field_name: str, parent: Any) -> None:
|
||||||
"""
|
"""
|
||||||
Called when the field is bound to the serializer.
|
Called when the field is bound to the serializer.
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ class IdPrimaryKeyRelatedField(PrimaryKeyRelatedField):
|
|||||||
"""
|
"""
|
||||||
field_name_suffix = '_id'
|
field_name_suffix = '_id'
|
||||||
|
|
||||||
def bind(self, field_name, parent):
|
def bind(self, field_name: str, parent: Any) -> None:
|
||||||
"""
|
"""
|
||||||
Called when the field is bound to the serializer.
|
Called when the field is bound to the serializer.
|
||||||
|
|
||||||
@ -80,7 +81,7 @@ class IdPrimaryKeyRelatedField(PrimaryKeyRelatedField):
|
|||||||
super().bind(field_name, parent)
|
super().bind(field_name, parent)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def many_init(cls, *args, **kwargs):
|
def many_init(cls, *args: Any, **kwargs: Any) -> IdManyRelatedField:
|
||||||
"""
|
"""
|
||||||
Method from rest_framework.relations.RelatedField That uses our
|
Method from rest_framework.relations.RelatedField That uses our
|
||||||
IdManyRelatedField class instead of
|
IdManyRelatedField class instead of
|
||||||
@ -106,7 +107,7 @@ class PermissionMixin:
|
|||||||
"""
|
"""
|
||||||
access_permissions = None # type: Optional[BaseAccessPermissions]
|
access_permissions = None # type: Optional[BaseAccessPermissions]
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self) -> Iterable[str]:
|
||||||
"""
|
"""
|
||||||
Overridden method to check view permissions. Returns an empty
|
Overridden method to check view permissions. Returns an empty
|
||||||
iterable so Django REST framework won't do any other permission
|
iterable so Django REST framework won't do any other permission
|
||||||
@ -114,10 +115,10 @@ class PermissionMixin:
|
|||||||
and the request passes.
|
and the request passes.
|
||||||
"""
|
"""
|
||||||
if not self.check_view_permissions():
|
if not self.check_view_permissions():
|
||||||
self.permission_denied(self.request)
|
self.permission_denied(self.request) # type: ignore
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Override this and return True if the requesting user should be able to
|
Override this and return True if the requesting user should be able to
|
||||||
get access to your view.
|
get access to your view.
|
||||||
@ -127,22 +128,22 @@ class PermissionMixin:
|
|||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_access_permissions(self):
|
def get_access_permissions(self) -> BaseAccessPermissions:
|
||||||
"""
|
"""
|
||||||
Returns a container to handle access permissions for this viewset and
|
Returns a container to handle access permissions for this viewset and
|
||||||
its corresponding model.
|
its corresponding model.
|
||||||
"""
|
"""
|
||||||
return self.access_permissions
|
return self.access_permissions # type: ignore
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self) -> Type[Serializer]:
|
||||||
"""
|
"""
|
||||||
Overridden method to return the serializer class given by the
|
Overridden method to return the serializer class given by the
|
||||||
access permissions container.
|
access permissions container.
|
||||||
"""
|
"""
|
||||||
if self.get_access_permissions() is not None:
|
if self.get_access_permissions() is not None:
|
||||||
serializer_class = self.get_access_permissions().get_serializer_class(self.request.user)
|
serializer_class = self.get_access_permissions().get_serializer_class(self.request.user) # type: ignore
|
||||||
else:
|
else:
|
||||||
serializer_class = super().get_serializer_class()
|
serializer_class = super().get_serializer_class() # type: ignore
|
||||||
return serializer_class
|
return serializer_class
|
||||||
|
|
||||||
|
|
||||||
@ -153,11 +154,11 @@ class ModelSerializer(_ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
serializer_related_field = IdPrimaryKeyRelatedField
|
serializer_related_field = IdPrimaryKeyRelatedField
|
||||||
|
|
||||||
def get_fields(self):
|
def get_fields(self) -> Any:
|
||||||
"""
|
"""
|
||||||
Returns all fields of the serializer.
|
Returns all fields of the serializer.
|
||||||
"""
|
"""
|
||||||
fields = OrderedDict()
|
fields = OrderedDict() # type: Dict[str, Field]
|
||||||
|
|
||||||
for field_name, field in super().get_fields().items():
|
for field_name, field in super().get_fields().items():
|
||||||
try:
|
try:
|
||||||
@ -177,7 +178,7 @@ class ListModelMixin(_ListModelMixin):
|
|||||||
|
|
||||||
queryset = Model.objects.all()
|
queryset = Model.objects.all()
|
||||||
"""
|
"""
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request: Any, *args: Any, **kwargs: Any) -> Response:
|
||||||
model = self.get_queryset().model
|
model = self.get_queryset().model
|
||||||
try:
|
try:
|
||||||
collection_string = model.get_collection_string()
|
collection_string = model.get_collection_string()
|
||||||
@ -200,7 +201,7 @@ class RetrieveModelMixin(_RetrieveModelMixin):
|
|||||||
|
|
||||||
queryset = Model.objects.all()
|
queryset = Model.objects.all()
|
||||||
"""
|
"""
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request: Any, *args: Any, **kwargs: Any) -> Response:
|
||||||
model = self.get_queryset().model
|
model = self.get_queryset().model
|
||||||
try:
|
try:
|
||||||
collection_string = model.get_collection_string()
|
collection_string = model.get_collection_string()
|
||||||
@ -213,7 +214,7 @@ class RetrieveModelMixin(_RetrieveModelMixin):
|
|||||||
collection_string, self.kwargs[lookup_url_kwarg])
|
collection_string, self.kwargs[lookup_url_kwarg])
|
||||||
user = user_to_collection_user(request.user)
|
user = user_to_collection_user(request.user)
|
||||||
try:
|
try:
|
||||||
content = collection_element.as_dict_for_user(user)
|
content = collection_element.as_dict_for_user(user) # type: RestrictedData
|
||||||
except collection_element.get_model().DoesNotExist:
|
except collection_element.get_model().DoesNotExist:
|
||||||
raise Http404
|
raise Http404
|
||||||
if content is None:
|
if content is None:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from contextlib import ContextDecorator
|
from contextlib import ContextDecorator
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.core.cache import caches
|
from django.core.cache import caches
|
||||||
@ -9,7 +10,7 @@ from ..core.config import config
|
|||||||
|
|
||||||
|
|
||||||
class OpenSlidesDiscoverRunner(DiscoverRunner):
|
class OpenSlidesDiscoverRunner(DiscoverRunner):
|
||||||
def run_tests(self, test_labels, extra_tests=None, **kwargs):
|
def run_tests(self, test_labels, extra_tests=None, **kwargs): # type: ignore
|
||||||
"""
|
"""
|
||||||
Test Runner which does not create a database, if only unittest are run.
|
Test Runner which does not create a database, if only unittest are run.
|
||||||
"""
|
"""
|
||||||
@ -35,7 +36,7 @@ class TestCase(_TestCase):
|
|||||||
Resets the config object after each test.
|
Resets the config object after each test.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self) -> None:
|
||||||
config.key_to_id = {}
|
config.key_to_id = {}
|
||||||
|
|
||||||
|
|
||||||
@ -48,11 +49,11 @@ class use_cache(ContextDecorator):
|
|||||||
The code inside the contextmananger starts with an empty cache.
|
The code inside the contextmananger starts with an empty cache.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self) -> None:
|
||||||
cache = caches['locmem']
|
cache = caches['locmem']
|
||||||
cache.clear()
|
cache.clear()
|
||||||
self.patch = patch('openslides.utils.collection.cache', cache)
|
self.patch = patch('openslides.utils.collection.cache', cache)
|
||||||
self.patch.start()
|
self.patch.start()
|
||||||
|
|
||||||
def __exit__(self, *exc):
|
def __exit__(self, *exc: Any) -> None:
|
||||||
self.patch.stop()
|
self.patch.stop()
|
||||||
|
@ -6,7 +6,7 @@ CAMEL_CASE_TO_PSEUDO_SNAKE_CASE_CONVERSION_REGEX_1 = re.compile('(.)([A-Z][a-z]+
|
|||||||
CAMEL_CASE_TO_PSEUDO_SNAKE_CASE_CONVERSION_REGEX_2 = re.compile('([a-z0-9])([A-Z])')
|
CAMEL_CASE_TO_PSEUDO_SNAKE_CASE_CONVERSION_REGEX_2 = re.compile('([a-z0-9])([A-Z])')
|
||||||
|
|
||||||
|
|
||||||
def convert_camel_case_to_pseudo_snake_case(text):
|
def convert_camel_case_to_pseudo_snake_case(text: str) -> str:
|
||||||
"""
|
"""
|
||||||
Converts camel case to pseudo snake case using hyphen instead of
|
Converts camel case to pseudo snake case using hyphen instead of
|
||||||
underscore.
|
underscore.
|
||||||
@ -19,12 +19,13 @@ def convert_camel_case_to_pseudo_snake_case(text):
|
|||||||
return CAMEL_CASE_TO_PSEUDO_SNAKE_CASE_CONVERSION_REGEX_2.sub(r'\1-\2', s1).lower()
|
return CAMEL_CASE_TO_PSEUDO_SNAKE_CASE_CONVERSION_REGEX_2.sub(r'\1-\2', s1).lower()
|
||||||
|
|
||||||
|
|
||||||
def to_roman(number):
|
def to_roman(number: int) -> str:
|
||||||
"""
|
"""
|
||||||
Converts an arabic number within range from 1 to 4999 to the
|
Converts an arabic number within range from 1 to 4999 to the
|
||||||
corresponding roman number. Returns None on error conditions.
|
corresponding roman number. Returns the input converted as string on error
|
||||||
|
conditions or higher numbers.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return roman.toRoman(number)
|
return roman.toRoman(number)
|
||||||
except (roman.NotIntegerError, roman.OutOfRangeError):
|
except (roman.NotIntegerError, roman.OutOfRangeError):
|
||||||
return None
|
return str(number)
|
||||||
|
@ -20,16 +20,13 @@ allowed_styles = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def validate_html(html):
|
def validate_html(html: str) -> str:
|
||||||
"""
|
"""
|
||||||
This method takes a string and escapes all non-whitelisted html entries.
|
This method takes a string and escapes all non-whitelisted html entries.
|
||||||
Every field of a model that is loaded trusted in the DOM should be validated.
|
Every field of a model that is loaded trusted in the DOM should be validated.
|
||||||
"""
|
"""
|
||||||
if isinstance(html, str):
|
return bleach.clean(
|
||||||
return bleach.clean(
|
html,
|
||||||
html,
|
tags=allowed_tags,
|
||||||
tags=allowed_tags,
|
attributes=allowed_attributes,
|
||||||
attributes=allowed_attributes,
|
styles=allowed_styles)
|
||||||
styles=allowed_styles)
|
|
||||||
else:
|
|
||||||
return html
|
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
from typing import List # noqa
|
from typing import Any, Dict, List # noqa
|
||||||
|
|
||||||
from django.views import generic as django_views
|
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
|
from django.views.generic.base import View
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView as _APIView
|
from rest_framework.views import APIView as _APIView
|
||||||
|
|
||||||
View = django_views.View
|
|
||||||
|
|
||||||
|
|
||||||
class CSRFMixin:
|
class CSRFMixin:
|
||||||
"""
|
"""
|
||||||
@ -14,8 +12,8 @@ class CSRFMixin:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_view(cls, *args, **kwargs):
|
def as_view(cls, *args: Any, **kwargs: Any) -> View:
|
||||||
view = super().as_view(*args, **kwargs)
|
view = super().as_view(*args, **kwargs) # type: ignore
|
||||||
return ensure_csrf_cookie(view)
|
return ensure_csrf_cookie(view)
|
||||||
|
|
||||||
|
|
||||||
@ -32,13 +30,13 @@ class APIView(_APIView):
|
|||||||
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
|
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_context_data(self, **context):
|
def get_context_data(self, **context: Any) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Returns the context for the response.
|
Returns the context for the response.
|
||||||
"""
|
"""
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def method_call(self, request, *args, **kwargs):
|
def method_call(self, request: Any, *args: Any, **kwargs: Any) -> Any:
|
||||||
"""
|
"""
|
||||||
Http method that returns the response object with the context data.
|
Http method that returns the response object with the context data.
|
||||||
"""
|
"""
|
||||||
|
@ -17,8 +17,12 @@ multi_line_output = 3
|
|||||||
[mypy]
|
[mypy]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
strict_optional = true
|
strict_optional = true
|
||||||
|
check_untyped_defs = true
|
||||||
|
|
||||||
[mypy-openslides.utils.auth]
|
[mypy-openslides.utils.dispatch]
|
||||||
|
ignore_errors = true
|
||||||
|
|
||||||
|
[mypy-openslides.utils.*]
|
||||||
disallow_any = unannotated
|
disallow_any = unannotated
|
||||||
|
|
||||||
[mypy-openslides.core.config]
|
[mypy-openslides.core.config]
|
||||||
|
@ -8,4 +8,4 @@ class ToRomanTest(TestCase):
|
|||||||
self.assertEqual(utils.to_roman(3), 'III')
|
self.assertEqual(utils.to_roman(3), 'III')
|
||||||
|
|
||||||
def test_to_roman_none(self):
|
def test_to_roman_none(self):
|
||||||
self.assertTrue(utils.to_roman(-3) is None)
|
self.assertEqual(utils.to_roman(-3), '-3')
|
||||||
|
Loading…
Reference in New Issue
Block a user