Merge pull request #5937 from FinnStutzenstein/deleteUnusedCode
Delete unused code
This commit is contained in:
commit
365d0d55ea
2
.github/workflows/run-tests.yml
vendored
2
.github/workflows/run-tests.yml
vendored
@ -40,7 +40,7 @@ jobs:
|
|||||||
run: mypy openslides/ tests/
|
run: mypy openslides/ tests/
|
||||||
|
|
||||||
- name: test using pytest
|
- name: test using pytest
|
||||||
run: pytest --cov --cov-fail-under=74
|
run: pytest --cov --cov-fail-under=73
|
||||||
|
|
||||||
install-client-dependencies:
|
install-client-dependencies:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from ..utils.access_permissions import BaseAccessPermissions
|
|
||||||
from ..utils.auth import async_has_perm
|
|
||||||
|
|
||||||
|
|
||||||
class ItemAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for Item and ItemViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "agenda.can_see"
|
|
||||||
|
|
||||||
# TODO: In the following method we use full_data['is_hidden'] and
|
|
||||||
# full_data['is_internal'] but this can be out of date.
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Returns the restricted serialized data for the instance prepared
|
|
||||||
for the user. If the user does not have agenda.can_see, no data will
|
|
||||||
be retuned.
|
|
||||||
|
|
||||||
Hidden items can only be seen by managers with can_manage permission. If a user
|
|
||||||
does not have this permission, he is not allowed to see comments.
|
|
||||||
|
|
||||||
Internal items can only be seen by users with can_see_internal_items. If a user
|
|
||||||
does not have this permission, he is not allowed to see the duration.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def filtered_data(full_data, blocked_keys):
|
|
||||||
"""
|
|
||||||
Returns a new dict like full_data but with all blocked_keys removed.
|
|
||||||
"""
|
|
||||||
whitelist = full_data.keys() - blocked_keys
|
|
||||||
return {key: full_data[key] for key in whitelist}
|
|
||||||
|
|
||||||
# Parse data.
|
|
||||||
if full_data and await async_has_perm(user_id, "agenda.can_see"):
|
|
||||||
# Assume the user has all permissions. Restrict this below.
|
|
||||||
data = full_data
|
|
||||||
|
|
||||||
blocked_keys: List[str] = []
|
|
||||||
|
|
||||||
# Restrict data for non managers
|
|
||||||
if not await async_has_perm(user_id, "agenda.can_manage"):
|
|
||||||
data = [
|
|
||||||
full for full in data if not full["is_hidden"]
|
|
||||||
] # filter hidden items
|
|
||||||
blocked_keys.append("comment")
|
|
||||||
|
|
||||||
# Restrict data for users without can_see_internal_items
|
|
||||||
if not await async_has_perm(user_id, "agenda.can_see_internal_items"):
|
|
||||||
data = [full for full in data if not full["is_internal"]]
|
|
||||||
blocked_keys.append("duration")
|
|
||||||
|
|
||||||
if len(blocked_keys) > 0:
|
|
||||||
data = [filtered_data(full, blocked_keys) for full in data]
|
|
||||||
else:
|
|
||||||
data = []
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class ListOfSpeakersAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for ListOfSpeakers and ListOfSpeakersViewSet.
|
|
||||||
No data will be restricted, because everyone can see the list of speakers
|
|
||||||
at any time.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "agenda.can_see_list_of_speakers"
|
|
@ -1,5 +1,3 @@
|
|||||||
from typing import Any, Dict, Set
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
@ -12,7 +10,6 @@ class AgendaAppConfig(AppConfig):
|
|||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
|
|
||||||
from ..core.signals import permission_change
|
from ..core.signals import permission_change
|
||||||
from ..utils.access_permissions import required_user
|
|
||||||
from ..utils.rest_api import router
|
from ..utils.rest_api import router
|
||||||
from . import serializers # noqa
|
from . import serializers # noqa
|
||||||
from .signals import (
|
from .signals import (
|
||||||
@ -42,11 +39,6 @@ class AgendaAppConfig(AppConfig):
|
|||||||
ListOfSpeakersViewSet,
|
ListOfSpeakersViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
# register required_users
|
|
||||||
required_user.add_collection_string(
|
|
||||||
self.get_model("ListOfSpeakers").get_collection_string(), required_users
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_config_variables(self):
|
def get_config_variables(self):
|
||||||
from .config_variables import get_config_variables
|
from .config_variables import get_config_variables
|
||||||
|
|
||||||
@ -59,10 +51,3 @@ class AgendaAppConfig(AppConfig):
|
|||||||
"""
|
"""
|
||||||
yield self.get_model("Item")
|
yield self.get_model("Item")
|
||||||
yield self.get_model("ListOfSpeakers")
|
yield self.get_model("ListOfSpeakers")
|
||||||
|
|
||||||
|
|
||||||
async def required_users(element: Dict[str, Any]) -> Set[int]:
|
|
||||||
"""
|
|
||||||
Returns all user ids that are displayed as speaker in the given element.
|
|
||||||
"""
|
|
||||||
return set(speaker["user_id"] for speaker in element["speakers"])
|
|
||||||
|
@ -22,8 +22,6 @@ from openslides.utils.models import (
|
|||||||
from openslides.utils.postgres import restart_id_sequence
|
from openslides.utils.postgres import restart_id_sequence
|
||||||
from openslides.utils.utils import to_roman
|
from openslides.utils.utils import to_roman
|
||||||
|
|
||||||
from .access_permissions import ItemAccessPermissions, ListOfSpeakersAccessPermissions
|
|
||||||
|
|
||||||
|
|
||||||
class ItemManager(BaseManager):
|
class ItemManager(BaseManager):
|
||||||
"""
|
"""
|
||||||
@ -204,7 +202,6 @@ class Item(RESTModelMixin, models.Model):
|
|||||||
An Agenda Item
|
An Agenda Item
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ItemAccessPermissions()
|
|
||||||
objects = ItemManager()
|
objects = ItemManager()
|
||||||
can_see_permission = "agenda.can_see"
|
can_see_permission = "agenda.can_see"
|
||||||
|
|
||||||
@ -369,7 +366,6 @@ def list_of_speakers_initially_closed():
|
|||||||
|
|
||||||
class ListOfSpeakers(RESTModelMixin, models.Model):
|
class ListOfSpeakers(RESTModelMixin, models.Model):
|
||||||
|
|
||||||
access_permissions = ListOfSpeakersAccessPermissions()
|
|
||||||
objects = ListOfSpeakersManager()
|
objects = ListOfSpeakersManager()
|
||||||
can_see_permission = "agenda.can_see_list_of_speakers"
|
can_see_permission = "agenda.can_see_list_of_speakers"
|
||||||
|
|
||||||
@ -465,7 +461,6 @@ class SpeakerManager(models.Manager):
|
|||||||
speaker.save(
|
speaker.save(
|
||||||
force_insert=True,
|
force_insert=True,
|
||||||
skip_autoupdate=skip_autoupdate,
|
skip_autoupdate=skip_autoupdate,
|
||||||
no_delete_on_restriction=True,
|
|
||||||
)
|
)
|
||||||
return speaker
|
return speaker
|
||||||
|
|
||||||
|
@ -8,10 +8,8 @@ from openslides.utils.autoupdate import inform_changed_data
|
|||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.rest_api import (
|
from openslides.utils.rest_api import (
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
ListModelMixin,
|
|
||||||
ModelViewSet,
|
ModelViewSet,
|
||||||
Response,
|
Response,
|
||||||
RetrieveModelMixin,
|
|
||||||
UpdateModelMixin,
|
UpdateModelMixin,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
detail_route,
|
detail_route,
|
||||||
@ -22,7 +20,6 @@ from openslides.utils.views import TreeSortMixin
|
|||||||
|
|
||||||
from ..utils.auth import has_perm
|
from ..utils.auth import has_perm
|
||||||
from ..utils.utils import get_model_from_collection_string
|
from ..utils.utils import get_model_from_collection_string
|
||||||
from .access_permissions import ItemAccessPermissions
|
|
||||||
from .models import Item, ListOfSpeakers, Speaker
|
from .models import Item, ListOfSpeakers, Speaker
|
||||||
|
|
||||||
|
|
||||||
@ -36,16 +33,13 @@ class ItemViewSet(ModelViewSet, TreeSortMixin):
|
|||||||
There are some views, see check_view_permissions.
|
There are some views, see check_view_permissions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ItemAccessPermissions()
|
|
||||||
queryset = Item.objects.all()
|
queryset = Item.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve", "metadata"):
|
if self.action in (
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action in (
|
|
||||||
"partial_update",
|
"partial_update",
|
||||||
"update",
|
"update",
|
||||||
"destroy",
|
"destroy",
|
||||||
@ -268,25 +262,20 @@ class ItemViewSet(ModelViewSet, TreeSortMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ListOfSpeakersViewSet(
|
class ListOfSpeakersViewSet(UpdateModelMixin, TreeSortMixin, GenericViewSet):
|
||||||
ListModelMixin, RetrieveModelMixin, UpdateModelMixin, TreeSortMixin, GenericViewSet
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
API endpoint for agenda items.
|
API endpoint for agenda items.
|
||||||
|
|
||||||
There are some views, see check_view_permissions.
|
There are some views, see check_view_permissions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ItemAccessPermissions()
|
|
||||||
queryset = ListOfSpeakers.objects.all()
|
queryset = ListOfSpeakers.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve", "metadata"):
|
if self.action == "manage_speaker":
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action in ("manage_speaker",):
|
|
||||||
result = has_perm(self.request.user, "agenda.can_see_list_of_speakers")
|
result = has_perm(self.request.user, "agenda.can_see_list_of_speakers")
|
||||||
# For manage_speaker requests the rest of the check is
|
# For manage_speaker requests the rest of the check is
|
||||||
# done in the specific method. See below.
|
# done in the specific method. See below.
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
from ..poll.access_permissions import (
|
|
||||||
BaseOptionAccessPermissions,
|
|
||||||
BasePollAccessPermissions,
|
|
||||||
BaseVoteAccessPermissions,
|
|
||||||
)
|
|
||||||
from ..utils.access_permissions import BaseAccessPermissions
|
|
||||||
|
|
||||||
|
|
||||||
class AssignmentAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for Assignment and AssignmentViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "assignments.can_see"
|
|
||||||
|
|
||||||
|
|
||||||
class AssignmentPollAccessPermissions(BasePollAccessPermissions):
|
|
||||||
base_permission = "assignments.can_see"
|
|
||||||
manage_permission = "assignments.can_manage"
|
|
||||||
additional_fields = [
|
|
||||||
"amount_global_yes",
|
|
||||||
"amount_global_no",
|
|
||||||
"amount_global_abstain",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class AssignmentOptionAccessPermissions(BaseOptionAccessPermissions):
|
|
||||||
base_permission = "assignments.can_see"
|
|
||||||
manage_permission = "assignments.can_manage"
|
|
||||||
|
|
||||||
|
|
||||||
class AssignmentVoteAccessPermissions(BaseVoteAccessPermissions):
|
|
||||||
base_permission = "assignments.can_see"
|
|
||||||
manage_permission = "assignments.can_manage"
|
|
@ -1,5 +1,3 @@
|
|||||||
from typing import Any, Dict, Set
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
@ -10,7 +8,6 @@ class AssignmentsAppConfig(AppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
# Import all required stuff.
|
# Import all required stuff.
|
||||||
from ..core.signals import permission_change
|
from ..core.signals import permission_change
|
||||||
from ..utils.access_permissions import required_user
|
|
||||||
from ..utils.rest_api import router
|
from ..utils.rest_api import router
|
||||||
from . import serializers # noqa
|
from . import serializers # noqa
|
||||||
from .signals import get_permission_change_data
|
from .signals import get_permission_change_data
|
||||||
@ -44,20 +41,6 @@ class AssignmentsAppConfig(AppConfig):
|
|||||||
AssignmentVoteViewSet,
|
AssignmentVoteViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Register required_users
|
|
||||||
required_user.add_collection_string(
|
|
||||||
self.get_model("Assignment").get_collection_string(),
|
|
||||||
required_users_assignments,
|
|
||||||
)
|
|
||||||
required_user.add_collection_string(
|
|
||||||
self.get_model("AssignmentPoll").get_collection_string(),
|
|
||||||
required_users_assignment_polls,
|
|
||||||
)
|
|
||||||
required_user.add_collection_string(
|
|
||||||
self.get_model("AssignmentOption").get_collection_string(),
|
|
||||||
required_users_assignment_options,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_config_variables(self):
|
def get_config_variables(self):
|
||||||
from .config_variables import get_config_variables
|
from .config_variables import get_config_variables
|
||||||
|
|
||||||
@ -75,30 +58,3 @@ class AssignmentsAppConfig(AppConfig):
|
|||||||
"AssignmentOption",
|
"AssignmentOption",
|
||||||
):
|
):
|
||||||
yield self.get_model(model_name)
|
yield self.get_model(model_name)
|
||||||
|
|
||||||
|
|
||||||
async def required_users_assignments(element: Dict[str, Any]) -> Set[int]:
|
|
||||||
"""
|
|
||||||
Returns all user ids that are displayed as candidates (including poll
|
|
||||||
options) in the assignment element.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return set(
|
|
||||||
related_user["user_id"] for related_user in element["assignment_related_users"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def required_users_assignment_polls(element: Dict[str, Any]) -> Set[int]:
|
|
||||||
"""
|
|
||||||
Returns all user ids that have voted on an option and are therefore required for the single votes table.
|
|
||||||
"""
|
|
||||||
from openslides.poll.models import BasePoll
|
|
||||||
|
|
||||||
if element["state"] == BasePoll.STATE_PUBLISHED:
|
|
||||||
return element["voted_id"]
|
|
||||||
else:
|
|
||||||
return set()
|
|
||||||
|
|
||||||
|
|
||||||
async def required_users_assignment_options(element: Dict[str, Any]) -> Set[int]:
|
|
||||||
return set([element["user_id"]])
|
|
||||||
|
@ -17,12 +17,6 @@ from openslides.utils.models import RESTModelMixin
|
|||||||
from openslides.utils.rest_api import ValidationError
|
from openslides.utils.rest_api import ValidationError
|
||||||
|
|
||||||
from ..utils.models import CASCADE_AND_AUTOUPDATE, SET_NULL_AND_AUTOUPDATE
|
from ..utils.models import CASCADE_AND_AUTOUPDATE, SET_NULL_AND_AUTOUPDATE
|
||||||
from .access_permissions import (
|
|
||||||
AssignmentAccessPermissions,
|
|
||||||
AssignmentOptionAccessPermissions,
|
|
||||||
AssignmentPollAccessPermissions,
|
|
||||||
AssignmentVoteAccessPermissions,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
class AssignmentRelatedUser(RESTModelMixin, models.Model):
|
||||||
@ -93,7 +87,6 @@ class Assignment(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model
|
|||||||
Model for assignments.
|
Model for assignments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = AssignmentAccessPermissions()
|
|
||||||
can_see_permission = "assignments.can_see"
|
can_see_permission = "assignments.can_see"
|
||||||
|
|
||||||
objects = AssignmentManager()
|
objects = AssignmentManager()
|
||||||
@ -237,7 +230,6 @@ class AssignmentVoteManager(BaseManager):
|
|||||||
|
|
||||||
|
|
||||||
class AssignmentVote(RESTModelMixin, BaseVote):
|
class AssignmentVote(RESTModelMixin, BaseVote):
|
||||||
access_permissions = AssignmentVoteAccessPermissions()
|
|
||||||
objects = AssignmentVoteManager()
|
objects = AssignmentVoteManager()
|
||||||
|
|
||||||
option = models.ForeignKey(
|
option = models.ForeignKey(
|
||||||
@ -268,7 +260,6 @@ class AssignmentOptionManager(BaseManager):
|
|||||||
|
|
||||||
|
|
||||||
class AssignmentOption(RESTModelMixin, BaseOption):
|
class AssignmentOption(RESTModelMixin, BaseOption):
|
||||||
access_permissions = AssignmentOptionAccessPermissions()
|
|
||||||
can_see_permission = "assignments.can_see"
|
can_see_permission = "assignments.can_see"
|
||||||
objects = AssignmentOptionManager()
|
objects = AssignmentOptionManager()
|
||||||
vote_class = AssignmentVote
|
vote_class = AssignmentVote
|
||||||
@ -306,7 +297,6 @@ class AssignmentPollManager(BaseManager):
|
|||||||
|
|
||||||
|
|
||||||
class AssignmentPoll(RESTModelMixin, BasePoll):
|
class AssignmentPoll(RESTModelMixin, BasePoll):
|
||||||
access_permissions = AssignmentPollAccessPermissions()
|
|
||||||
can_see_permission = "assignments.can_see"
|
can_see_permission = "assignments.can_see"
|
||||||
objects = AssignmentPollManager()
|
objects = AssignmentPollManager()
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ from openslides.utils.rest_api import (
|
|||||||
)
|
)
|
||||||
from openslides.utils.utils import is_int
|
from openslides.utils.utils import is_int
|
||||||
|
|
||||||
from .access_permissions import AssignmentAccessPermissions
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Assignment,
|
Assignment,
|
||||||
AssignmentOption,
|
AssignmentOption,
|
||||||
@ -31,24 +30,15 @@ from .models import (
|
|||||||
class AssignmentViewSet(ModelViewSet):
|
class AssignmentViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint for assignments.
|
API endpoint for assignments.
|
||||||
|
|
||||||
There are the following views: metadata, list, retrieve, create,
|
|
||||||
partial_update, update, destroy, candidature_self, candidature_other and create_poll.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = AssignmentAccessPermissions()
|
|
||||||
queryset = Assignment.objects.all()
|
queryset = Assignment.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in (
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action == "metadata":
|
|
||||||
# Everybody is allowed to see the metadata.
|
|
||||||
result = True
|
|
||||||
elif self.action in (
|
|
||||||
"create",
|
"create",
|
||||||
"partial_update",
|
"partial_update",
|
||||||
"update",
|
"update",
|
||||||
@ -551,7 +541,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
weight=weight,
|
weight=weight,
|
||||||
value=value,
|
value=value,
|
||||||
)
|
)
|
||||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
inform_changed_data(vote)
|
||||||
else: # global_no or global_abstain
|
else: # global_no or global_abstain
|
||||||
option = options[0]
|
option = options[0]
|
||||||
weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1)
|
weight = vote_weight if config["users_activate_vote_weight"] else Decimal(1)
|
||||||
@ -562,7 +552,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
weight=weight,
|
weight=weight,
|
||||||
value=data,
|
value=data,
|
||||||
)
|
)
|
||||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
inform_changed_data(vote)
|
||||||
inform_changed_data(option)
|
inform_changed_data(option)
|
||||||
inform_changed_data(poll)
|
inform_changed_data(poll)
|
||||||
|
|
||||||
@ -586,8 +576,8 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
value=result,
|
value=result,
|
||||||
weight=weight,
|
weight=weight,
|
||||||
)
|
)
|
||||||
inform_changed_data(vote, no_delete_on_restriction=True)
|
inform_changed_data(vote)
|
||||||
inform_changed_data(option, no_delete_on_restriction=True)
|
inform_changed_data(option)
|
||||||
|
|
||||||
def add_user_to_voted_array(self, user, poll):
|
def add_user_to_voted_array(self, user, poll):
|
||||||
VotedModel = AssignmentPoll.voted.through
|
VotedModel = AssignmentPoll.voted.through
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from openslides.utils.access_permissions import BaseAccessPermissions
|
|
||||||
from openslides.utils.auth import async_has_perm, async_in_some_groups
|
|
||||||
|
|
||||||
|
|
||||||
class ChatGroupAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for ChatGroup and ChatGroupViewSet.
|
|
||||||
No base perm: The access permissions are done with the read/write groups.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Manage users can see all groups. Else, for each group either it has no access groups
|
|
||||||
or the user must be in an access group.
|
|
||||||
"""
|
|
||||||
data: List[Dict[str, Any]] = []
|
|
||||||
if await async_has_perm(user_id, "chat.can_manage"):
|
|
||||||
data = full_data
|
|
||||||
else:
|
|
||||||
for full in full_data:
|
|
||||||
read_groups = full.get("read_groups_id", [])
|
|
||||||
write_groups = full.get("write_groups_id", [])
|
|
||||||
if await async_in_some_groups(
|
|
||||||
user_id, read_groups
|
|
||||||
) or await async_in_some_groups(user_id, write_groups):
|
|
||||||
data.append(full)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class ChatMessageAccessPermissions(ChatGroupAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for ChatMessage and ChatMessageViewSet.
|
|
||||||
It does exaclty the same as ChatGroupAccessPermissions
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
@ -5,7 +5,6 @@ from openslides.utils.manager import BaseManager
|
|||||||
|
|
||||||
from ..utils.auth import has_perm, in_some_groups
|
from ..utils.auth import has_perm, in_some_groups
|
||||||
from ..utils.models import CASCADE_AND_AUTOUPDATE, RESTModelMixin
|
from ..utils.models import CASCADE_AND_AUTOUPDATE, RESTModelMixin
|
||||||
from .access_permissions import ChatGroupAccessPermissions, ChatMessageAccessPermissions
|
|
||||||
|
|
||||||
|
|
||||||
class ChatGroupManager(BaseManager):
|
class ChatGroupManager(BaseManager):
|
||||||
@ -14,7 +13,6 @@ class ChatGroupManager(BaseManager):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def get_prefetched_queryset(self, *args, **kwargs):
|
def get_prefetched_queryset(self, *args, **kwargs):
|
||||||
""""""
|
|
||||||
return (
|
return (
|
||||||
super()
|
super()
|
||||||
.get_prefetched_queryset(*args, **kwargs)
|
.get_prefetched_queryset(*args, **kwargs)
|
||||||
@ -23,10 +21,6 @@ class ChatGroupManager(BaseManager):
|
|||||||
|
|
||||||
|
|
||||||
class ChatGroup(RESTModelMixin, models.Model):
|
class ChatGroup(RESTModelMixin, models.Model):
|
||||||
""""""
|
|
||||||
|
|
||||||
access_permissions = ChatGroupAccessPermissions()
|
|
||||||
|
|
||||||
objects = ChatGroupManager()
|
objects = ChatGroupManager()
|
||||||
|
|
||||||
name = models.CharField(max_length=256)
|
name = models.CharField(max_length=256)
|
||||||
@ -57,7 +51,6 @@ class ChatMessageManager(BaseManager):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def get_prefetched_queryset(self, *args, **kwargs):
|
def get_prefetched_queryset(self, *args, **kwargs):
|
||||||
""""""
|
|
||||||
return (
|
return (
|
||||||
super()
|
super()
|
||||||
.get_prefetched_queryset(*args, **kwargs)
|
.get_prefetched_queryset(*args, **kwargs)
|
||||||
@ -68,10 +61,6 @@ class ChatMessageManager(BaseManager):
|
|||||||
|
|
||||||
|
|
||||||
class ChatMessage(RESTModelMixin, models.Model):
|
class ChatMessage(RESTModelMixin, models.Model):
|
||||||
""""""
|
|
||||||
|
|
||||||
access_permissions = ChatMessageAccessPermissions()
|
|
||||||
|
|
||||||
objects = ChatMessageManager()
|
objects = ChatMessageManager()
|
||||||
|
|
||||||
text = models.CharField(max_length=512)
|
text = models.CharField(max_length=512)
|
||||||
|
@ -12,15 +12,12 @@ from openslides.utils.rest_api import (
|
|||||||
CreateModelMixin,
|
CreateModelMixin,
|
||||||
DestroyModelMixin,
|
DestroyModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
ListModelMixin,
|
|
||||||
ModelViewSet,
|
ModelViewSet,
|
||||||
Response,
|
Response,
|
||||||
RetrieveModelMixin,
|
|
||||||
detail_route,
|
detail_route,
|
||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .access_permissions import ChatGroupAccessPermissions, ChatMessageAccessPermissions
|
|
||||||
from .models import ChatGroup, ChatMessage
|
from .models import ChatGroup, ChatMessage
|
||||||
|
|
||||||
|
|
||||||
@ -35,7 +32,6 @@ class ChatGroupViewSet(ModelViewSet):
|
|||||||
partial_update, update, destroy and clear.
|
partial_update, update, destroy and clear.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ChatGroupAccessPermissions()
|
|
||||||
queryset = ChatGroup.objects.all()
|
queryset = ChatGroup.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
@ -70,8 +66,6 @@ class ChatGroupViewSet(ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ChatMessageViewSet(
|
class ChatMessageViewSet(
|
||||||
ListModelMixin,
|
|
||||||
RetrieveModelMixin,
|
|
||||||
CreateModelMixin,
|
CreateModelMixin,
|
||||||
DestroyModelMixin,
|
DestroyModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
@ -82,7 +76,6 @@ class ChatMessageViewSet(
|
|||||||
There are the following views: metadata, list, retrieve, create
|
There are the following views: metadata, list, retrieve, create
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ChatMessageAccessPermissions()
|
|
||||||
queryset = ChatMessage.objects.all()
|
queryset = ChatMessage.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
from ..utils.access_permissions import BaseAccessPermissions
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectorAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for Projector and ProjectorViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "core.can_see_projector"
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectionDefaultAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for Projector and ProjectorViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "core.can_see_projector"
|
|
||||||
|
|
||||||
|
|
||||||
class TagAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for Tag and TagViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectorMessageAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions for ProjectorMessage.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "core.can_see_projector"
|
|
||||||
|
|
||||||
|
|
||||||
class CountdownAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions for Countdown.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "core.can_see_projector"
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for the config (ConfigStore and
|
|
||||||
ConfigViewSet).
|
|
||||||
"""
|
|
@ -13,15 +13,6 @@ from openslides.utils.manager import BaseManager
|
|||||||
from openslides.utils.models import SET_NULL_AND_AUTOUPDATE, RESTModelMixin
|
from openslides.utils.models import SET_NULL_AND_AUTOUPDATE, RESTModelMixin
|
||||||
from openslides.utils.postgres import is_postgres
|
from openslides.utils.postgres import is_postgres
|
||||||
|
|
||||||
from .access_permissions import (
|
|
||||||
ConfigAccessPermissions,
|
|
||||||
CountdownAccessPermissions,
|
|
||||||
ProjectionDefaultAccessPermissions,
|
|
||||||
ProjectorAccessPermissions,
|
|
||||||
ProjectorMessageAccessPermissions,
|
|
||||||
TagAccessPermissions,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectorManager(BaseManager):
|
class ProjectorManager(BaseManager):
|
||||||
"""
|
"""
|
||||||
@ -73,8 +64,6 @@ class Projector(RESTModelMixin, models.Model):
|
|||||||
on e. g. the URL /rest/core/projector/1/activate_elements/.
|
on e. g. the URL /rest/core/projector/1/activate_elements/.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ProjectorAccessPermissions()
|
|
||||||
|
|
||||||
objects = ProjectorManager()
|
objects = ProjectorManager()
|
||||||
|
|
||||||
elements = JSONField(default=list)
|
elements = JSONField(default=list)
|
||||||
@ -134,8 +123,6 @@ class ProjectionDefault(RESTModelMixin, models.Model):
|
|||||||
name on the front end for the user.
|
name on the front end for the user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ProjectionDefaultAccessPermissions()
|
|
||||||
|
|
||||||
name = models.CharField(max_length=256)
|
name = models.CharField(max_length=256)
|
||||||
|
|
||||||
display_name = models.CharField(max_length=256)
|
display_name = models.CharField(max_length=256)
|
||||||
@ -157,8 +144,6 @@ class Tag(RESTModelMixin, models.Model):
|
|||||||
motions or assignments.
|
motions or assignments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = TagAccessPermissions()
|
|
||||||
|
|
||||||
name = models.CharField(max_length=255, unique=True)
|
name = models.CharField(max_length=255, unique=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -175,8 +160,6 @@ class ConfigStore(RESTModelMixin, models.Model):
|
|||||||
A model class to store all config variables in the database.
|
A model class to store all config variables in the database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ConfigAccessPermissions()
|
|
||||||
|
|
||||||
key = models.CharField(max_length=255, unique=True, db_index=True)
|
key = models.CharField(max_length=255, unique=True, db_index=True)
|
||||||
"""A string, the key of the config variable."""
|
"""A string, the key of the config variable."""
|
||||||
|
|
||||||
@ -200,8 +183,6 @@ class ProjectorMessage(RESTModelMixin, models.Model):
|
|||||||
Model for ProjectorMessages.
|
Model for ProjectorMessages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ProjectorMessageAccessPermissions()
|
|
||||||
|
|
||||||
message = models.TextField(blank=True)
|
message = models.TextField(blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -213,8 +194,6 @@ class Countdown(RESTModelMixin, models.Model):
|
|||||||
Model for countdowns.
|
Model for countdowns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = CountdownAccessPermissions()
|
|
||||||
|
|
||||||
title = models.CharField(max_length=256, unique=True)
|
title = models.CharField(max_length=256, unique=True)
|
||||||
|
|
||||||
description = models.CharField(max_length=256, blank=True)
|
description = models.CharField(max_length=256, blank=True)
|
||||||
|
@ -32,22 +32,12 @@ from ..utils.plugins import (
|
|||||||
)
|
)
|
||||||
from ..utils.rest_api import (
|
from ..utils.rest_api import (
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
ListModelMixin,
|
|
||||||
ModelViewSet,
|
ModelViewSet,
|
||||||
Response,
|
Response,
|
||||||
RetrieveModelMixin,
|
|
||||||
ValidationError,
|
ValidationError,
|
||||||
detail_route,
|
detail_route,
|
||||||
list_route,
|
list_route,
|
||||||
)
|
)
|
||||||
from .access_permissions import (
|
|
||||||
ConfigAccessPermissions,
|
|
||||||
CountdownAccessPermissions,
|
|
||||||
ProjectionDefaultAccessPermissions,
|
|
||||||
ProjectorAccessPermissions,
|
|
||||||
ProjectorMessageAccessPermissions,
|
|
||||||
TagAccessPermissions,
|
|
||||||
)
|
|
||||||
from .config import config
|
from .config import config
|
||||||
from .exceptions import ConfigError, ConfigNotFound
|
from .exceptions import ConfigError, ConfigNotFound
|
||||||
from .models import (
|
from .models import (
|
||||||
@ -115,18 +105,13 @@ class ProjectorViewSet(ModelViewSet):
|
|||||||
There are the following views: See strings in check_view_permissions().
|
There are the following views: See strings in check_view_permissions().
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ProjectorAccessPermissions()
|
|
||||||
queryset = Projector.objects.all()
|
queryset = Projector.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in (
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action == "metadata":
|
|
||||||
result = has_perm(self.request.user, "core.can_see_projector")
|
|
||||||
elif self.action in (
|
|
||||||
"create",
|
"create",
|
||||||
"update",
|
"update",
|
||||||
"partial_update",
|
"partial_update",
|
||||||
@ -343,7 +328,7 @@ class ProjectorViewSet(ModelViewSet):
|
|||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
class ProjectionDefaultViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
class ProjectionDefaultViewSet(GenericViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint for projection defaults.
|
API endpoint for projection defaults.
|
||||||
|
|
||||||
@ -351,18 +336,13 @@ class ProjectionDefaultViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSe
|
|||||||
to projectors can be done by updating the projector.
|
to projectors can be done by updating the projector.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ProjectionDefaultAccessPermissions()
|
|
||||||
queryset = ProjectionDefault.objects.all()
|
queryset = ProjectionDefault.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
return False
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
else:
|
|
||||||
result = False
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class TagViewSet(ModelViewSet):
|
class TagViewSet(ModelViewSet):
|
||||||
@ -373,20 +353,13 @@ class TagViewSet(ModelViewSet):
|
|||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = TagAccessPermissions()
|
|
||||||
queryset = Tag.objects.all()
|
queryset = Tag.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in ("create", "partial_update", "update", "destroy"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action == "metadata":
|
|
||||||
# Every authenticated user can see the metadata.
|
|
||||||
# Anonymous users can do so if they are enabled.
|
|
||||||
result = self.request.user.is_authenticated or anonymous_is_enabled()
|
|
||||||
elif self.action in ("create", "partial_update", "update", "destroy"):
|
|
||||||
result = has_perm(self.request.user, "core.can_manage_tags")
|
result = has_perm(self.request.user, "core.can_manage_tags")
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
@ -401,7 +374,6 @@ class ConfigViewSet(ModelViewSet):
|
|||||||
partial_update.
|
partial_update.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ConfigAccessPermissions()
|
|
||||||
queryset = ConfigStore.objects.all()
|
queryset = ConfigStore.objects.all()
|
||||||
|
|
||||||
can_manage_config = None
|
can_manage_config = None
|
||||||
@ -411,9 +383,7 @@ class ConfigViewSet(ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in ("partial_update", "update"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action in ("partial_update", "update"):
|
|
||||||
result = self.check_config_permission(self.kwargs["pk"])
|
result = self.check_config_permission(self.kwargs["pk"])
|
||||||
elif self.action == "reset_groups":
|
elif self.action == "reset_groups":
|
||||||
result = has_perm(self.request.user, "core.can_manage_config")
|
result = has_perm(self.request.user, "core.can_manage_config")
|
||||||
@ -527,16 +497,13 @@ class ProjectorMessageViewSet(ModelViewSet):
|
|||||||
partial_update and destroy.
|
partial_update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = ProjectorMessageAccessPermissions()
|
|
||||||
queryset = ProjectorMessage.objects.all()
|
queryset = ProjectorMessage.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in ("create", "partial_update", "update", "destroy"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action in ("create", "partial_update", "update", "destroy"):
|
|
||||||
result = has_perm(self.request.user, "core.can_manage_projector")
|
result = has_perm(self.request.user, "core.can_manage_projector")
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
@ -551,16 +518,13 @@ class CountdownViewSet(ModelViewSet):
|
|||||||
partial_update and destroy.
|
partial_update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = CountdownAccessPermissions()
|
|
||||||
queryset = Countdown.objects.all()
|
queryset = Countdown.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in ("create", "partial_update", "update", "destroy"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action in ("create", "partial_update", "update", "destroy"):
|
|
||||||
result = has_perm(self.request.user, "core.can_manage_projector")
|
result = has_perm(self.request.user, "core.can_manage_projector")
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from ..utils.access_permissions import BaseAccessPermissions
|
|
||||||
from ..utils.auth import async_has_perm, async_in_some_groups, async_is_superadmin
|
|
||||||
|
|
||||||
|
|
||||||
class MediafileAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for Mediafile and MediafileViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "mediafiles.can_see"
|
|
||||||
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Returns the restricted serialized data for the instance prepared
|
|
||||||
for the user. Removes hidden mediafiles for some users.
|
|
||||||
"""
|
|
||||||
if not await async_has_perm(user_id, "mediafiles.can_see"):
|
|
||||||
return []
|
|
||||||
|
|
||||||
# This allows to see everything, which is important for inherited_access_groups=False.
|
|
||||||
if await async_is_superadmin(user_id):
|
|
||||||
return full_data
|
|
||||||
|
|
||||||
data = []
|
|
||||||
for full in full_data:
|
|
||||||
access_groups = full["inherited_access_groups_id"]
|
|
||||||
if (isinstance(access_groups, bool) and access_groups) or (
|
|
||||||
isinstance(access_groups, list)
|
|
||||||
and await async_in_some_groups(user_id, access_groups)
|
|
||||||
):
|
|
||||||
data.append(full)
|
|
||||||
|
|
||||||
return data
|
|
@ -13,7 +13,6 @@ from ..agenda.mixins import ListOfSpeakersMixin
|
|||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..utils.models import RESTModelMixin
|
from ..utils.models import RESTModelMixin
|
||||||
from ..utils.rest_api import ValidationError
|
from ..utils.rest_api import ValidationError
|
||||||
from .access_permissions import MediafileAccessPermissions
|
|
||||||
from .utils import bytes_to_human
|
from .utils import bytes_to_human
|
||||||
|
|
||||||
|
|
||||||
@ -62,7 +61,6 @@ class Mediafile(RESTModelMixin, ListOfSpeakersMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
objects = MediafileManager()
|
objects = MediafileManager()
|
||||||
access_permissions = MediafileAccessPermissions()
|
|
||||||
can_see_permission = "mediafiles.can_see"
|
can_see_permission = "mediafiles.can_see"
|
||||||
|
|
||||||
mediafile = models.FileField(upload_to=get_file_path, null=True)
|
mediafile = models.FileField(upload_to=get_file_path, null=True)
|
||||||
|
@ -19,7 +19,6 @@ from openslides.utils.rest_api import (
|
|||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .access_permissions import MediafileAccessPermissions
|
|
||||||
from .config import watch_and_update_configs
|
from .config import watch_and_update_configs
|
||||||
from .models import Mediafile
|
from .models import Mediafile
|
||||||
from .utils import bytes_to_human, get_pdf_information
|
from .utils import bytes_to_human, get_pdf_information
|
||||||
@ -47,16 +46,13 @@ class MediafileViewSet(ModelViewSet):
|
|||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = MediafileAccessPermissions()
|
|
||||||
queryset = Mediafile.objects.all()
|
queryset = Mediafile.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve", "metadata"):
|
if self.action in (
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action in (
|
|
||||||
"create",
|
"create",
|
||||||
"partial_update",
|
"partial_update",
|
||||||
"update",
|
"update",
|
||||||
|
@ -1,209 +0,0 @@
|
|||||||
import json
|
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from ..poll.access_permissions import (
|
|
||||||
BaseOptionAccessPermissions,
|
|
||||||
BasePollAccessPermissions,
|
|
||||||
BaseVoteAccessPermissions,
|
|
||||||
)
|
|
||||||
from ..utils.access_permissions import BaseAccessPermissions
|
|
||||||
from ..utils.auth import async_has_perm, async_in_some_groups
|
|
||||||
|
|
||||||
|
|
||||||
class MotionAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for Motion and MotionViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "motions.can_see"
|
|
||||||
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Returns the restricted serialized data for the instance prepared for
|
|
||||||
the user. Removes motion if the user has not the permission to see
|
|
||||||
the motion in this state. Removes comments sections for
|
|
||||||
some unauthorized users. Ensures that a user can only see his own
|
|
||||||
personal notes.
|
|
||||||
"""
|
|
||||||
# Parse data.
|
|
||||||
if await async_has_perm(user_id, "motions.can_see"):
|
|
||||||
# TODO: Refactor this after personal_notes system is refactored.
|
|
||||||
data = []
|
|
||||||
for full in full_data:
|
|
||||||
# Check if user is submitter of this motion.
|
|
||||||
if user_id:
|
|
||||||
is_submitter = user_id in [
|
|
||||||
submitter["user_id"] for submitter in full.get("submitters", [])
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
# Anonymous users can not be submitters.
|
|
||||||
is_submitter = False
|
|
||||||
|
|
||||||
# Check see permission for this motion.
|
|
||||||
restriction = full["state_restriction"]
|
|
||||||
|
|
||||||
# Managers can see all motions.
|
|
||||||
permission = await async_has_perm(user_id, "motions.can_manage")
|
|
||||||
# If restriction field is an empty list, everybody can see the motion.
|
|
||||||
permission = permission or not restriction
|
|
||||||
|
|
||||||
if not permission:
|
|
||||||
# Parse values of restriction field.
|
|
||||||
# If at least one restriction is ok, permissions are granted.
|
|
||||||
for value in restriction:
|
|
||||||
if (
|
|
||||||
value
|
|
||||||
in (
|
|
||||||
"motions.can_see_internal",
|
|
||||||
"motions.can_manage_metadata",
|
|
||||||
"motions.can_manage",
|
|
||||||
)
|
|
||||||
and await async_has_perm(user_id, value)
|
|
||||||
):
|
|
||||||
permission = True
|
|
||||||
break
|
|
||||||
elif value == "is_submitter" and is_submitter:
|
|
||||||
permission = True
|
|
||||||
break
|
|
||||||
|
|
||||||
# Parse single motion.
|
|
||||||
if permission:
|
|
||||||
full_copy = json.loads(json.dumps(full))
|
|
||||||
full_copy["comments"] = []
|
|
||||||
for comment in full["comments"]:
|
|
||||||
if await async_in_some_groups(
|
|
||||||
user_id, comment["read_groups_id"]
|
|
||||||
):
|
|
||||||
full_copy["comments"].append(comment)
|
|
||||||
data.append(full_copy)
|
|
||||||
else:
|
|
||||||
data = []
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for MotionChangeRecommendation and MotionChangeRecommendationViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "motions.can_see"
|
|
||||||
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Removes change recommendations if they are internal and the user has
|
|
||||||
not the can_manage permission. To see change recommendation the user needs
|
|
||||||
the can_see permission.
|
|
||||||
"""
|
|
||||||
# Parse data.
|
|
||||||
if await async_has_perm(user_id, self.base_permission):
|
|
||||||
has_manage_perms = await async_has_perm(user_id, "motions.can_manage")
|
|
||||||
data = []
|
|
||||||
for full in full_data:
|
|
||||||
if not full["internal"] or has_manage_perms:
|
|
||||||
data.append(full)
|
|
||||||
else:
|
|
||||||
data = []
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class MotionCommentSectionAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for MotionCommentSection and MotionCommentSectionViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "motions.can_see"
|
|
||||||
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
If the user has manage rights, he can see all sections. If not all sections
|
|
||||||
will be removed, when the user is not in at least one of the read_groups.
|
|
||||||
"""
|
|
||||||
data: List[Dict[str, Any]] = []
|
|
||||||
if await async_has_perm(user_id, "motions.can_manage"):
|
|
||||||
data = full_data
|
|
||||||
elif await async_has_perm(user_id, self.base_permission):
|
|
||||||
for full in full_data:
|
|
||||||
read_groups = full.get("read_groups_id", [])
|
|
||||||
if await async_in_some_groups(user_id, read_groups):
|
|
||||||
data.append(full)
|
|
||||||
else:
|
|
||||||
data = []
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class StatuteParagraphAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for StatuteParagraph and StatuteParagraphViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "motions.can_see"
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for Category and CategoryViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "motions.can_see"
|
|
||||||
|
|
||||||
|
|
||||||
class MotionBlockAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for Category and CategoryViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "motions.can_see"
|
|
||||||
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Users without `motions.can_manage` cannot see internal blocks.
|
|
||||||
"""
|
|
||||||
data: List[Dict[str, Any]] = []
|
|
||||||
if await async_has_perm(user_id, "motions.can_manage"):
|
|
||||||
data = full_data
|
|
||||||
elif await async_has_perm(user_id, self.base_permission):
|
|
||||||
data = [full for full in full_data if not full["internal"]]
|
|
||||||
else:
|
|
||||||
data = []
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for Workflow and WorkflowViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "motions.can_see"
|
|
||||||
|
|
||||||
|
|
||||||
class StateAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for State and StateViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "motions.can_see"
|
|
||||||
|
|
||||||
|
|
||||||
class MotionPollAccessPermissions(BasePollAccessPermissions):
|
|
||||||
base_permission = "motions.can_see"
|
|
||||||
manage_permission = "motions.can_manage_polls"
|
|
||||||
|
|
||||||
|
|
||||||
class MotionOptionAccessPermissions(BaseOptionAccessPermissions):
|
|
||||||
base_permission = "motions.can_see"
|
|
||||||
manage_permission = "motions.can_manage_polls"
|
|
||||||
|
|
||||||
|
|
||||||
class MotionVoteAccessPermissions(BaseVoteAccessPermissions):
|
|
||||||
base_permission = "motions.can_see"
|
|
||||||
manage_permission = "motions.can_manage_polls"
|
|
@ -1,5 +1,3 @@
|
|||||||
from typing import Any, Dict, Set
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db.models.signals import post_migrate
|
from django.db.models.signals import post_migrate
|
||||||
|
|
||||||
@ -12,7 +10,6 @@ class MotionsAppConfig(AppConfig):
|
|||||||
# Import all required stuff.
|
# Import all required stuff.
|
||||||
from openslides.core.signals import permission_change
|
from openslides.core.signals import permission_change
|
||||||
from openslides.utils.rest_api import router
|
from openslides.utils.rest_api import router
|
||||||
from ..utils.access_permissions import required_user
|
|
||||||
from . import serializers # noqa
|
from . import serializers # noqa
|
||||||
from .signals import create_builtin_workflows, get_permission_change_data
|
from .signals import create_builtin_workflows, get_permission_change_data
|
||||||
from .views import (
|
from .views import (
|
||||||
@ -72,16 +69,6 @@ class MotionsAppConfig(AppConfig):
|
|||||||
)
|
)
|
||||||
router.register(self.get_model("State").get_collection_string(), StateViewSet)
|
router.register(self.get_model("State").get_collection_string(), StateViewSet)
|
||||||
|
|
||||||
# Register required_users
|
|
||||||
required_user.add_collection_string(
|
|
||||||
self.get_model("Motion").get_collection_string(), required_users_motions
|
|
||||||
)
|
|
||||||
|
|
||||||
required_user.add_collection_string(
|
|
||||||
self.get_model("MotionPoll").get_collection_string(),
|
|
||||||
required_users_motion_polls,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_config_variables(self):
|
def get_config_variables(self):
|
||||||
from .config_variables import get_config_variables
|
from .config_variables import get_config_variables
|
||||||
|
|
||||||
@ -106,28 +93,3 @@ class MotionsAppConfig(AppConfig):
|
|||||||
"MotionVote",
|
"MotionVote",
|
||||||
):
|
):
|
||||||
yield self.get_model(model_name)
|
yield self.get_model(model_name)
|
||||||
|
|
||||||
|
|
||||||
async def required_users_motions(element: Dict[str, Any]) -> Set[int]:
|
|
||||||
"""
|
|
||||||
Returns all user ids that are displayed as as submitter or supporter in
|
|
||||||
any motion if request_user can see motions. This function may return an
|
|
||||||
empty set.
|
|
||||||
"""
|
|
||||||
submitters_supporters = set(
|
|
||||||
[submitter["user_id"] for submitter in element["submitters"]]
|
|
||||||
)
|
|
||||||
submitters_supporters.update(element["supporters_id"])
|
|
||||||
return submitters_supporters
|
|
||||||
|
|
||||||
|
|
||||||
async def required_users_motion_polls(element: Dict[str, Any]) -> Set[int]:
|
|
||||||
"""
|
|
||||||
Returns all user ids that have voted on an option and are therefore required for the single votes table.
|
|
||||||
"""
|
|
||||||
from openslides.poll.models import BasePoll
|
|
||||||
|
|
||||||
if element["state"] == BasePoll.STATE_PUBLISHED:
|
|
||||||
return element["voted_id"]
|
|
||||||
else:
|
|
||||||
return set()
|
|
||||||
|
@ -16,19 +16,6 @@ from openslides.utils.models import RESTModelMixin
|
|||||||
from openslides.utils.rest_api import ValidationError
|
from openslides.utils.rest_api import ValidationError
|
||||||
|
|
||||||
from ..utils.models import CASCADE_AND_AUTOUPDATE, SET_NULL_AND_AUTOUPDATE
|
from ..utils.models import CASCADE_AND_AUTOUPDATE, SET_NULL_AND_AUTOUPDATE
|
||||||
from .access_permissions import (
|
|
||||||
CategoryAccessPermissions,
|
|
||||||
MotionAccessPermissions,
|
|
||||||
MotionBlockAccessPermissions,
|
|
||||||
MotionChangeRecommendationAccessPermissions,
|
|
||||||
MotionCommentSectionAccessPermissions,
|
|
||||||
MotionOptionAccessPermissions,
|
|
||||||
MotionPollAccessPermissions,
|
|
||||||
MotionVoteAccessPermissions,
|
|
||||||
StateAccessPermissions,
|
|
||||||
StatuteParagraphAccessPermissions,
|
|
||||||
WorkflowAccessPermissions,
|
|
||||||
)
|
|
||||||
from .exceptions import WorkflowError
|
from .exceptions import WorkflowError
|
||||||
|
|
||||||
|
|
||||||
@ -37,8 +24,6 @@ class StatuteParagraph(RESTModelMixin, models.Model):
|
|||||||
Model for parts of the statute
|
Model for parts of the statute
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = StatuteParagraphAccessPermissions()
|
|
||||||
|
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
"""Title of the statute paragraph."""
|
"""Title of the statute paragraph."""
|
||||||
|
|
||||||
@ -96,9 +81,6 @@ class Motion(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model):
|
|||||||
This class is the main entry point to all other classes related to a motion.
|
This class is the main entry point to all other classes related to a motion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = MotionAccessPermissions()
|
|
||||||
can_see_permission = "motions.can_see"
|
|
||||||
|
|
||||||
objects = MotionManager()
|
objects = MotionManager()
|
||||||
|
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
@ -540,8 +522,6 @@ class MotionCommentSection(RESTModelMixin, models.Model):
|
|||||||
each motions has the ability to have comments from the same section.
|
each motions has the ability to have comments from the same section.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = MotionCommentSectionAccessPermissions()
|
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
"""
|
"""
|
||||||
The name of the section.
|
The name of the section.
|
||||||
@ -672,8 +652,6 @@ class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
|||||||
A MotionChangeRecommendation object saves change recommendations for a specific Motion
|
A MotionChangeRecommendation object saves change recommendations for a specific Motion
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = MotionChangeRecommendationAccessPermissions()
|
|
||||||
|
|
||||||
motion = models.ForeignKey(
|
motion = models.ForeignKey(
|
||||||
Motion, on_delete=CASCADE_AND_AUTOUPDATE, related_name="change_recommendations"
|
Motion, on_delete=CASCADE_AND_AUTOUPDATE, related_name="change_recommendations"
|
||||||
)
|
)
|
||||||
@ -763,8 +741,6 @@ class Category(RESTModelMixin, models.Model):
|
|||||||
Model for categories of motions.
|
Model for categories of motions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = CategoryAccessPermissions()
|
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
"""Name of the category."""
|
"""Name of the category."""
|
||||||
|
|
||||||
@ -831,8 +807,6 @@ class MotionBlock(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Mode
|
|||||||
Model for blocks of motions.
|
Model for blocks of motions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = MotionBlockAccessPermissions()
|
|
||||||
|
|
||||||
objects = MotionBlockManager()
|
objects = MotionBlockManager()
|
||||||
|
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
@ -872,7 +846,6 @@ class MotionVoteManager(BaseManager):
|
|||||||
|
|
||||||
|
|
||||||
class MotionVote(RESTModelMixin, BaseVote):
|
class MotionVote(RESTModelMixin, BaseVote):
|
||||||
access_permissions = MotionVoteAccessPermissions()
|
|
||||||
option = models.ForeignKey(
|
option = models.ForeignKey(
|
||||||
"MotionOption", on_delete=CASCADE_AND_AUTOUPDATE, related_name="votes"
|
"MotionOption", on_delete=CASCADE_AND_AUTOUPDATE, related_name="votes"
|
||||||
)
|
)
|
||||||
@ -903,8 +876,6 @@ class MotionOptionManager(BaseManager):
|
|||||||
|
|
||||||
|
|
||||||
class MotionOption(RESTModelMixin, BaseOption):
|
class MotionOption(RESTModelMixin, BaseOption):
|
||||||
access_permissions = MotionOptionAccessPermissions()
|
|
||||||
can_see_permission = "motions.can_see"
|
|
||||||
objects = MotionOptionManager()
|
objects = MotionOptionManager()
|
||||||
vote_class = MotionVote
|
vote_class = MotionVote
|
||||||
|
|
||||||
@ -935,8 +906,6 @@ class MotionPollManager(BaseManager):
|
|||||||
|
|
||||||
|
|
||||||
class MotionPoll(RESTModelMixin, BasePoll):
|
class MotionPoll(RESTModelMixin, BasePoll):
|
||||||
access_permissions = MotionPollAccessPermissions()
|
|
||||||
can_see_permission = "motions.can_see"
|
|
||||||
option_class = MotionOption
|
option_class = MotionOption
|
||||||
|
|
||||||
objects = MotionPollManager()
|
objects = MotionPollManager()
|
||||||
@ -973,8 +942,6 @@ class State(RESTModelMixin, models.Model):
|
|||||||
state.
|
state.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = StateAccessPermissions()
|
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
"""A string representing the state."""
|
"""A string representing the state."""
|
||||||
|
|
||||||
@ -1118,8 +1085,6 @@ class Workflow(RESTModelMixin, models.Model):
|
|||||||
Defines a workflow for a motion.
|
Defines a workflow for a motion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = WorkflowAccessPermissions()
|
|
||||||
|
|
||||||
objects = WorkflowManager()
|
objects = WorkflowManager()
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
@ -25,16 +25,6 @@ from ..utils.rest_api import (
|
|||||||
list_route,
|
list_route,
|
||||||
)
|
)
|
||||||
from ..utils.views import TreeSortMixin
|
from ..utils.views import TreeSortMixin
|
||||||
from .access_permissions import (
|
|
||||||
CategoryAccessPermissions,
|
|
||||||
MotionAccessPermissions,
|
|
||||||
MotionBlockAccessPermissions,
|
|
||||||
MotionChangeRecommendationAccessPermissions,
|
|
||||||
MotionCommentSectionAccessPermissions,
|
|
||||||
StateAccessPermissions,
|
|
||||||
StatuteParagraphAccessPermissions,
|
|
||||||
WorkflowAccessPermissions,
|
|
||||||
)
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Category,
|
Category,
|
||||||
Motion,
|
Motion,
|
||||||
@ -63,16 +53,13 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
|
|||||||
There are a lot of views. See check_view_permissions().
|
There are a lot of views. See check_view_permissions().
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = MotionAccessPermissions()
|
|
||||||
queryset = Motion.objects.all()
|
queryset = Motion.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in ("metadata", "partial_update", "update", "destroy"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action in ("metadata", "partial_update", "update", "destroy"):
|
|
||||||
result = has_perm(self.request.user, "motions.can_see")
|
result = has_perm(self.request.user, "motions.can_see")
|
||||||
# For partial_update, update and destroy requests the rest of the check is
|
# For partial_update, update and destroy requests the rest of the check is
|
||||||
# done in the update method. See below.
|
# done in the update method. See below.
|
||||||
@ -1280,7 +1267,7 @@ class MotionPollViewSet(BasePollViewSet):
|
|||||||
value=data,
|
value=data,
|
||||||
weight=weight,
|
weight=weight,
|
||||||
)
|
)
|
||||||
vote.save(no_delete_on_restriction=True)
|
vote.save()
|
||||||
inform_changed_data(option)
|
inform_changed_data(option)
|
||||||
|
|
||||||
|
|
||||||
@ -1306,18 +1293,13 @@ class MotionChangeRecommendationViewSet(ModelViewSet):
|
|||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = MotionChangeRecommendationAccessPermissions()
|
|
||||||
queryset = MotionChangeRecommendation.objects.all()
|
queryset = MotionChangeRecommendation.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in ("create", "destroy", "partial_update", "update"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action == "metadata":
|
|
||||||
result = has_perm(self.request.user, "motions.can_see")
|
|
||||||
elif self.action in ("create", "destroy", "partial_update", "update"):
|
|
||||||
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
||||||
self.request.user, "motions.can_manage"
|
self.request.user, "motions.can_manage"
|
||||||
)
|
)
|
||||||
@ -1372,16 +1354,13 @@ class MotionCommentSectionViewSet(ModelViewSet):
|
|||||||
API endpoint for motion comment fields.
|
API endpoint for motion comment fields.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = MotionCommentSectionAccessPermissions()
|
|
||||||
queryset = MotionCommentSection.objects.all()
|
queryset = MotionCommentSection.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in ("create", "destroy", "update", "partial_update", "sort"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action in ("create", "destroy", "update", "partial_update", "sort"):
|
|
||||||
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
||||||
self.request.user, "motions.can_manage"
|
self.request.user, "motions.can_manage"
|
||||||
)
|
)
|
||||||
@ -1463,16 +1442,13 @@ class StatuteParagraphViewSet(ModelViewSet):
|
|||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = StatuteParagraphAccessPermissions()
|
|
||||||
queryset = StatuteParagraph.objects.all()
|
queryset = StatuteParagraph.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in ("create", "partial_update", "update", "destroy"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action in ("create", "partial_update", "update", "destroy"):
|
|
||||||
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
||||||
self.request.user, "motions.can_manage"
|
self.request.user, "motions.can_manage"
|
||||||
)
|
)
|
||||||
@ -1489,16 +1465,13 @@ class CategoryViewSet(TreeSortMixin, ModelViewSet):
|
|||||||
partial_update, update, destroy and numbering.
|
partial_update, update, destroy and numbering.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = CategoryAccessPermissions()
|
|
||||||
queryset = Category.objects.all()
|
queryset = Category.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve", "metadata"):
|
if self.action in (
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action in (
|
|
||||||
"create",
|
"create",
|
||||||
"partial_update",
|
"partial_update",
|
||||||
"update",
|
"update",
|
||||||
@ -1606,18 +1579,13 @@ class MotionBlockViewSet(ModelViewSet):
|
|||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = MotionBlockAccessPermissions()
|
|
||||||
queryset = MotionBlock.objects.all()
|
queryset = MotionBlock.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in (
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action == "metadata":
|
|
||||||
result = has_perm(self.request.user, "motions.can_see")
|
|
||||||
elif self.action in (
|
|
||||||
"create",
|
"create",
|
||||||
"partial_update",
|
"partial_update",
|
||||||
"update",
|
"update",
|
||||||
@ -1684,16 +1652,13 @@ class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin):
|
|||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = WorkflowAccessPermissions()
|
|
||||||
queryset = Workflow.objects.all()
|
queryset = Workflow.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve", "metadata"):
|
if self.action in ("create", "partial_update", "update", "destroy"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action in ("create", "partial_update", "update", "destroy"):
|
|
||||||
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
||||||
self.request.user, "motions.can_manage"
|
self.request.user, "motions.can_manage"
|
||||||
)
|
)
|
||||||
@ -1734,15 +1699,12 @@ class StateViewSet(ModelViewSet, ProtectedErrorMessageMixin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = State.objects.all()
|
queryset = State.objects.all()
|
||||||
access_permissions = StateAccessPermissions()
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve", "metadata"):
|
if self.action in ("create", "partial_update", "update", "destroy"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action in ("create", "partial_update", "update", "destroy"):
|
|
||||||
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
result = has_perm(self.request.user, "motions.can_see") and has_perm(
|
||||||
self.request.user, "motions.can_manage"
|
self.request.user, "motions.can_manage"
|
||||||
)
|
)
|
||||||
|
@ -1,122 +0,0 @@
|
|||||||
import json
|
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from ..poll.views import BasePoll
|
|
||||||
from ..utils import logging
|
|
||||||
from ..utils.access_permissions import BaseAccessPermissions
|
|
||||||
from ..utils.auth import async_has_perm, user_collection_string
|
|
||||||
from ..utils.cache import element_cache
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseVoteAccessPermissions(BaseAccessPermissions):
|
|
||||||
manage_permission = "" # set by subclass
|
|
||||||
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Poll-managers have full access, even during an active poll.
|
|
||||||
Every user can see it's own votes.
|
|
||||||
If the pollstate is published, everyone can see the votes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if await async_has_perm(user_id, self.manage_permission):
|
|
||||||
data = full_data
|
|
||||||
elif await async_has_perm(user_id, self.base_permission):
|
|
||||||
data = [
|
|
||||||
vote
|
|
||||||
for vote in full_data
|
|
||||||
if vote["pollstate"] == BasePoll.STATE_PUBLISHED
|
|
||||||
or vote["user_id"] == user_id
|
|
||||||
or vote["delegated_user_id"] == user_id
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
data = []
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class BaseOptionAccessPermissions(BaseAccessPermissions):
|
|
||||||
manage_permission = "" # set by subclass
|
|
||||||
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
|
|
||||||
if await async_has_perm(user_id, self.manage_permission):
|
|
||||||
data = full_data
|
|
||||||
elif await async_has_perm(user_id, self.base_permission):
|
|
||||||
data = []
|
|
||||||
for option in full_data:
|
|
||||||
if option["pollstate"] != BasePoll.STATE_PUBLISHED:
|
|
||||||
option = json.loads(
|
|
||||||
json.dumps(option)
|
|
||||||
) # copy, so we can remove some fields.
|
|
||||||
del option["yes"]
|
|
||||||
del option["no"]
|
|
||||||
del option["abstain"]
|
|
||||||
data.append(option)
|
|
||||||
else:
|
|
||||||
data = []
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class BasePollAccessPermissions(BaseAccessPermissions):
|
|
||||||
manage_permission = "" # set by subclass
|
|
||||||
|
|
||||||
additional_fields: List[str] = []
|
|
||||||
""" Add fields to be removed from each unpublished poll """
|
|
||||||
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Poll-managers have full access, even during an active poll.
|
|
||||||
Non-published polls will be restricted:
|
|
||||||
- Remove votes* values from the poll
|
|
||||||
- Remove yes/no/abstain fields from options
|
|
||||||
- Remove fields given in self.additional_fields from the poll
|
|
||||||
"""
|
|
||||||
|
|
||||||
# add has_voted for all users to check whether op has voted
|
|
||||||
# also fill user_has_voted_for_delegations with all users for which he has
|
|
||||||
# already voted
|
|
||||||
user_data = await element_cache.get_element_data(
|
|
||||||
user_collection_string, user_id
|
|
||||||
)
|
|
||||||
if user_data is None:
|
|
||||||
logger.error(f"Could not find userdata for {user_id}")
|
|
||||||
vote_delegated_from_ids = set()
|
|
||||||
else:
|
|
||||||
vote_delegated_from_ids = set(user_data["vote_delegated_from_users_id"])
|
|
||||||
|
|
||||||
for poll in full_data:
|
|
||||||
poll["user_has_voted"] = user_id in poll["voted_id"]
|
|
||||||
voted_ids = set(poll["voted_id"])
|
|
||||||
voted_for_delegations = list(
|
|
||||||
vote_delegated_from_ids.intersection(voted_ids)
|
|
||||||
)
|
|
||||||
poll["user_has_voted_for_delegations"] = voted_for_delegations
|
|
||||||
|
|
||||||
data_copy = json.loads(json.dumps(full_data))
|
|
||||||
for poll in data_copy:
|
|
||||||
if poll["state"] not in (BasePoll.STATE_FINISHED, BasePoll.STATE_PUBLISHED):
|
|
||||||
del poll["voted_id"]
|
|
||||||
|
|
||||||
if await async_has_perm(user_id, self.manage_permission):
|
|
||||||
pass
|
|
||||||
elif await async_has_perm(user_id, self.base_permission):
|
|
||||||
for poll in data_copy:
|
|
||||||
if poll["state"] != BasePoll.STATE_PUBLISHED:
|
|
||||||
del poll["votesvalid"]
|
|
||||||
del poll["votesinvalid"]
|
|
||||||
del poll["votescast"]
|
|
||||||
if "voted_id" in poll: # could be removed earlier
|
|
||||||
del poll["voted_id"]
|
|
||||||
for field in self.additional_fields:
|
|
||||||
del poll[field]
|
|
||||||
else:
|
|
||||||
data_copy = []
|
|
||||||
return data_copy
|
|
@ -11,10 +11,8 @@ from openslides.utils.autoupdate import disable_history, inform_changed_data
|
|||||||
from openslides.utils.rest_api import (
|
from openslides.utils.rest_api import (
|
||||||
DecimalField,
|
DecimalField,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
ListModelMixin,
|
|
||||||
ModelViewSet,
|
ModelViewSet,
|
||||||
Response,
|
Response,
|
||||||
RetrieveModelMixin,
|
|
||||||
ValidationError,
|
ValidationError,
|
||||||
detail_route,
|
detail_route,
|
||||||
)
|
)
|
||||||
@ -373,9 +371,9 @@ class BasePollViewSet(ModelViewSet):
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class BaseVoteViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
class BaseVoteViewSet(GenericViewSet):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BaseOptionViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
class BaseOptionViewSet(GenericViewSet):
|
||||||
pass
|
pass
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
from ..utils.access_permissions import BaseAccessPermissions
|
|
||||||
|
|
||||||
|
|
||||||
class TopicAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for Topic and TopicViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = "agenda.can_see"
|
|
@ -5,7 +5,6 @@ from openslides.utils.manager import BaseManager
|
|||||||
from ..agenda.mixins import AgendaItemWithListOfSpeakersMixin
|
from ..agenda.mixins import AgendaItemWithListOfSpeakersMixin
|
||||||
from ..mediafiles.models import Mediafile
|
from ..mediafiles.models import Mediafile
|
||||||
from ..utils.models import RESTModelMixin
|
from ..utils.models import RESTModelMixin
|
||||||
from .access_permissions import TopicAccessPermissions
|
|
||||||
|
|
||||||
|
|
||||||
class TopicManager(BaseManager):
|
class TopicManager(BaseManager):
|
||||||
@ -31,8 +30,6 @@ class Topic(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model):
|
|||||||
Model for slides with custom content. Used to be called custom slide.
|
Model for slides with custom content. Used to be called custom slide.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = TopicAccessPermissions()
|
|
||||||
|
|
||||||
objects = TopicManager()
|
objects = TopicManager()
|
||||||
|
|
||||||
title = models.CharField(max_length=256)
|
title = models.CharField(max_length=256)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from openslides.utils.rest_api import ModelViewSet
|
from openslides.utils.rest_api import ModelViewSet
|
||||||
|
|
||||||
from ..utils.auth import has_perm
|
from ..utils.auth import has_perm
|
||||||
from .access_permissions import TopicAccessPermissions
|
|
||||||
from .models import Topic
|
from .models import Topic
|
||||||
|
|
||||||
|
|
||||||
@ -13,15 +12,14 @@ class TopicViewSet(ModelViewSet):
|
|||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = TopicAccessPermissions()
|
|
||||||
queryset = Topic.objects.all()
|
queryset = Topic.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in ("create", "update", "partial_update", "destroy"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
else:
|
|
||||||
result = has_perm(self.request.user, "agenda.can_manage")
|
result = has_perm(self.request.user, "agenda.can_manage")
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
return result
|
return result
|
||||||
|
@ -1,159 +0,0 @@
|
|||||||
from typing import Any, Dict, List, Set
|
|
||||||
|
|
||||||
from ..utils.access_permissions import BaseAccessPermissions, required_user
|
|
||||||
from ..utils.auth import async_has_perm
|
|
||||||
from ..utils.utils import get_model_from_collection_string
|
|
||||||
|
|
||||||
|
|
||||||
class UserAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for User and UserViewSet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Returns the restricted serialized data for the instance prepared
|
|
||||||
for the user. Removes several fields for non admins so that they do
|
|
||||||
not get the fields they should not get.
|
|
||||||
"""
|
|
||||||
from .serializers import (
|
|
||||||
USERCANSEEEXTRASERIALIZER_FIELDS,
|
|
||||||
USERCANSEESERIALIZER_FIELDS,
|
|
||||||
)
|
|
||||||
|
|
||||||
def filtered_data(full_data, whitelist, whitelist_operator=None):
|
|
||||||
"""
|
|
||||||
Returns a new dict like full_data but only with whitelisted keys.
|
|
||||||
If the whitelist_operator is given and the full_data-user is the
|
|
||||||
oeperator (the user with user_id), the whitelist_operator will
|
|
||||||
be used instead of the whitelist.
|
|
||||||
"""
|
|
||||||
if whitelist_operator is not None and full_data["id"] == user_id:
|
|
||||||
return {key: full_data[key] for key in whitelist_operator}
|
|
||||||
else:
|
|
||||||
return {key: full_data[key] for key in whitelist}
|
|
||||||
|
|
||||||
# We have some sets of data to be sent:
|
|
||||||
# * full data i. e. all fields (including session_auth_hash),
|
|
||||||
# * all data i. e. all fields but not session_auth_hash,
|
|
||||||
# * many data i. e. all fields but not the default password and session_auth_hash,
|
|
||||||
# * little data i. e. all fields but not the default password, session_auth_hash,
|
|
||||||
# comments, gender, email, last_email_send, active status and auth_type
|
|
||||||
# * own data i. e. all little data fields plus email and gender. This is applied
|
|
||||||
# to the own user, if he just can see little or no data.
|
|
||||||
# * no data.
|
|
||||||
|
|
||||||
# Prepare field set for users with "all" data, "many" data and with "little" data.
|
|
||||||
all_data_fields = set(USERCANSEEEXTRASERIALIZER_FIELDS)
|
|
||||||
all_data_fields.add("groups_id")
|
|
||||||
all_data_fields.discard("groups")
|
|
||||||
all_data_fields.add("default_password")
|
|
||||||
many_data_fields = all_data_fields.copy()
|
|
||||||
many_data_fields.discard("default_password")
|
|
||||||
little_data_fields = set(USERCANSEESERIALIZER_FIELDS)
|
|
||||||
little_data_fields.add("groups_id")
|
|
||||||
little_data_fields.discard("groups")
|
|
||||||
own_data_fields = set(little_data_fields)
|
|
||||||
own_data_fields.add("email")
|
|
||||||
own_data_fields.add("gender")
|
|
||||||
own_data_fields.add("vote_delegated_to_id")
|
|
||||||
own_data_fields.add("vote_delegated_from_users_id")
|
|
||||||
|
|
||||||
# Check user permissions.
|
|
||||||
if await async_has_perm(user_id, "users.can_see_name"):
|
|
||||||
whitelist_operator = None
|
|
||||||
if await async_has_perm(user_id, "users.can_see_extra_data"):
|
|
||||||
if await async_has_perm(user_id, "users.can_manage"):
|
|
||||||
whitelist = all_data_fields
|
|
||||||
else:
|
|
||||||
whitelist = many_data_fields
|
|
||||||
else:
|
|
||||||
whitelist = little_data_fields
|
|
||||||
whitelist_operator = own_data_fields
|
|
||||||
|
|
||||||
# for managing {motion, assignment} polls the users needs to know
|
|
||||||
# the vote delegation structure.
|
|
||||||
if await async_has_perm(
|
|
||||||
user_id, "motion.can_manage_polls"
|
|
||||||
) or await async_has_perm(user_id, "assignments.can_manage"):
|
|
||||||
whitelist.add("vote_delegated_to_id")
|
|
||||||
whitelist.add("vote_delegated_from_users_id")
|
|
||||||
|
|
||||||
data = [
|
|
||||||
filtered_data(full, whitelist, whitelist_operator) for full in full_data
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
# Build a list of users, that can be seen without any permissions (with little fields).
|
|
||||||
|
|
||||||
# Everybody can see himself. Also everybody can see every user
|
|
||||||
# that is required e. g. as speaker, motion submitter or
|
|
||||||
# assignment candidate.
|
|
||||||
|
|
||||||
can_see_collection_strings: Set[str] = set()
|
|
||||||
for collection_string in required_user.get_collection_strings():
|
|
||||||
if await async_has_perm(
|
|
||||||
user_id,
|
|
||||||
get_model_from_collection_string(
|
|
||||||
collection_string
|
|
||||||
).can_see_permission,
|
|
||||||
):
|
|
||||||
can_see_collection_strings.add(collection_string)
|
|
||||||
|
|
||||||
required_user_ids = await required_user.get_required_users(
|
|
||||||
can_see_collection_strings
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add oneself.
|
|
||||||
if user_id:
|
|
||||||
required_user_ids.add(user_id)
|
|
||||||
|
|
||||||
# add vote delegations
|
|
||||||
# Find our model in full_data and get vote_delegated_from_users_id from it.
|
|
||||||
for user in full_data:
|
|
||||||
if user["id"] == user_id:
|
|
||||||
required_user_ids.update(user["vote_delegated_from_users_id"])
|
|
||||||
break
|
|
||||||
|
|
||||||
# Parse data.
|
|
||||||
data = [
|
|
||||||
filtered_data(full, little_data_fields, own_data_fields)
|
|
||||||
for full in full_data
|
|
||||||
if full["id"] in required_user_ids
|
|
||||||
]
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class GroupAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for Groups. Everyone can see them
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PersonalNoteAccessPermissions(BaseAccessPermissions):
|
|
||||||
"""
|
|
||||||
Access permissions container for personal notes. Every authenticated user
|
|
||||||
can handle personal notes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Returns the restricted serialized data for the instance prepared
|
|
||||||
for the user. Everybody gets only his own personal notes.
|
|
||||||
"""
|
|
||||||
# Parse data.
|
|
||||||
if not user_id:
|
|
||||||
data: List[Dict[str, Any]] = []
|
|
||||||
else:
|
|
||||||
for full in full_data:
|
|
||||||
if full["user_id"] == user_id:
|
|
||||||
data = [full]
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
data = []
|
|
||||||
|
|
||||||
return data
|
|
@ -28,11 +28,6 @@ from ..utils.models import (
|
|||||||
SET_NULL_AND_AUTOUPDATE,
|
SET_NULL_AND_AUTOUPDATE,
|
||||||
RESTModelMixin,
|
RESTModelMixin,
|
||||||
)
|
)
|
||||||
from .access_permissions import (
|
|
||||||
GroupAccessPermissions,
|
|
||||||
PersonalNoteAccessPermissions,
|
|
||||||
UserAccessPermissions,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserManager(BaseUserManager):
|
class UserManager(BaseUserManager):
|
||||||
@ -130,8 +125,6 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
|
|||||||
candidates.
|
candidates.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = UserAccessPermissions()
|
|
||||||
|
|
||||||
USERNAME_FIELD = "username"
|
USERNAME_FIELD = "username"
|
||||||
|
|
||||||
username = models.CharField(max_length=255, unique=True, blank=True)
|
username = models.CharField(max_length=255, unique=True, blank=True)
|
||||||
@ -356,7 +349,6 @@ class Group(RESTModelMixin, DjangoGroup):
|
|||||||
Extend the django group with support of our REST and caching system.
|
Extend the django group with support of our REST and caching system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = GroupAccessPermissions()
|
|
||||||
objects = GroupManager()
|
objects = GroupManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -382,14 +374,6 @@ class PersonalNote(RESTModelMixin, models.Model):
|
|||||||
openslides objects like motions.
|
openslides objects like motions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = PersonalNoteAccessPermissions()
|
|
||||||
|
|
||||||
personalized_model = True
|
|
||||||
"""
|
|
||||||
Each model belongs to one user. This relation is set during creation and
|
|
||||||
will not be changed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
objects = PersonalNoteManager()
|
objects = PersonalNoteManager()
|
||||||
|
|
||||||
user = models.OneToOneField(User, on_delete=CASCADE_AND_AUTOUPDATE)
|
user = models.OneToOneField(User, on_delete=CASCADE_AND_AUTOUPDATE)
|
||||||
|
74
server/openslides/users/restrict.py
Normal file
74
server/openslides/users/restrict.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from ..utils.auth import async_has_perm
|
||||||
|
|
||||||
|
|
||||||
|
async def restrict_user(full_user: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Returns the restricted serialized data for the instance prepared
|
||||||
|
for the user. Removes several fields for non admins so that they do
|
||||||
|
not get the fields they should not get.
|
||||||
|
"""
|
||||||
|
from .serializers import (
|
||||||
|
USERCANSEEEXTRASERIALIZER_FIELDS,
|
||||||
|
USERCANSEESERIALIZER_FIELDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_id = full_user["id"]
|
||||||
|
|
||||||
|
def filtered_data(full_user, whitelist):
|
||||||
|
"""
|
||||||
|
Returns a new dict like full_user but only with whitelisted keys.
|
||||||
|
"""
|
||||||
|
return {key: full_user[key] for key in whitelist}
|
||||||
|
|
||||||
|
# We have some sets of data to be sent:
|
||||||
|
# * full data i. e. all fields (including session_auth_hash),
|
||||||
|
# * all data i. e. all fields but not session_auth_hash,
|
||||||
|
# * many data i. e. all fields but not the default password and session_auth_hash,
|
||||||
|
# * little data i. e. all fields but not the default password, session_auth_hash,
|
||||||
|
# comments, gender, email, last_email_send, active status and auth_type
|
||||||
|
# * own data i. e. all little data fields plus email and gender. This is applied
|
||||||
|
# to the own user, if he just can see little or no data.
|
||||||
|
# * no data.
|
||||||
|
|
||||||
|
# Prepare field set for users with "all" data, "many" data and with "little" data.
|
||||||
|
all_data_fields = set(USERCANSEEEXTRASERIALIZER_FIELDS)
|
||||||
|
all_data_fields.add("groups_id")
|
||||||
|
all_data_fields.discard("groups")
|
||||||
|
all_data_fields.add("default_password")
|
||||||
|
many_data_fields = all_data_fields.copy()
|
||||||
|
many_data_fields.discard("default_password")
|
||||||
|
little_data_fields = set(USERCANSEESERIALIZER_FIELDS)
|
||||||
|
little_data_fields.add("groups_id")
|
||||||
|
little_data_fields.discard("groups")
|
||||||
|
own_data_fields = set(little_data_fields)
|
||||||
|
own_data_fields.add("email")
|
||||||
|
own_data_fields.add("gender")
|
||||||
|
own_data_fields.add("vote_delegated_to_id")
|
||||||
|
own_data_fields.add("vote_delegated_from_users_id")
|
||||||
|
|
||||||
|
# Check user permissions.
|
||||||
|
if await async_has_perm(user_id, "users.can_see_name"):
|
||||||
|
if await async_has_perm(user_id, "users.can_see_extra_data"):
|
||||||
|
if await async_has_perm(user_id, "users.can_manage"):
|
||||||
|
whitelist = all_data_fields
|
||||||
|
else:
|
||||||
|
whitelist = many_data_fields
|
||||||
|
else:
|
||||||
|
whitelist = own_data_fields
|
||||||
|
|
||||||
|
# for managing {motion, assignment} polls the users needs to know
|
||||||
|
# the vote delegation structure.
|
||||||
|
if await async_has_perm(
|
||||||
|
user_id, "motion.can_manage_polls"
|
||||||
|
) or await async_has_perm(user_id, "assignments.can_manage"):
|
||||||
|
whitelist.add("vote_delegated_to_id")
|
||||||
|
whitelist.add("vote_delegated_from_users_id")
|
||||||
|
|
||||||
|
data = filtered_data(full_user, whitelist)
|
||||||
|
else:
|
||||||
|
# Parse data.
|
||||||
|
data = filtered_data(full_user, own_data_fields)
|
||||||
|
|
||||||
|
return data
|
@ -48,12 +48,8 @@ from ..utils.rest_api import (
|
|||||||
)
|
)
|
||||||
from ..utils.validate import validate_json
|
from ..utils.validate import validate_json
|
||||||
from ..utils.views import APIView
|
from ..utils.views import APIView
|
||||||
from .access_permissions import (
|
|
||||||
GroupAccessPermissions,
|
|
||||||
PersonalNoteAccessPermissions,
|
|
||||||
UserAccessPermissions,
|
|
||||||
)
|
|
||||||
from .models import Group, PersonalNote, User
|
from .models import Group, PersonalNote, User
|
||||||
|
from .restrict import restrict_user
|
||||||
from .serializers import GroupSerializer, PermissionRelatedField
|
from .serializers import GroupSerializer, PermissionRelatedField
|
||||||
from .user_backend import user_backend_manager
|
from .user_backend import user_backend_manager
|
||||||
|
|
||||||
@ -85,18 +81,13 @@ class UserViewSet(ModelViewSet):
|
|||||||
partial_update, update, destroy and reset_password.
|
partial_update, update, destroy and reset_password.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = UserAccessPermissions()
|
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in ("update", "partial_update"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action == "metadata":
|
|
||||||
result = has_perm(self.request.user, "users.can_see_name")
|
|
||||||
elif self.action in ("update", "partial_update"):
|
|
||||||
result = self.request.user.is_authenticated
|
result = self.request.user.is_authenticated
|
||||||
elif self.action in (
|
elif self.action in (
|
||||||
"create",
|
"create",
|
||||||
@ -597,19 +588,12 @@ class GroupViewSet(ModelViewSet):
|
|||||||
metadata_class = GroupViewSetMetadata
|
metadata_class = GroupViewSetMetadata
|
||||||
queryset = Group.objects.all()
|
queryset = Group.objects.all()
|
||||||
serializer_class = GroupSerializer
|
serializer_class = GroupSerializer
|
||||||
access_permissions = GroupAccessPermissions()
|
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in (
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action == "metadata":
|
|
||||||
# Every authenticated user can see the metadata.
|
|
||||||
# Anonymous users can do so if they are enabled.
|
|
||||||
result = self.request.user.is_authenticated or anonymous_is_enabled()
|
|
||||||
elif self.action in (
|
|
||||||
"create",
|
"create",
|
||||||
"partial_update",
|
"partial_update",
|
||||||
"update",
|
"update",
|
||||||
@ -762,16 +746,13 @@ class PersonalNoteViewSet(ModelViewSet):
|
|||||||
partial_update, update, and destroy.
|
partial_update, update, and destroy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions = PersonalNoteAccessPermissions()
|
|
||||||
queryset = PersonalNote.objects.all()
|
queryset = PersonalNote.objects.all()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ("list", "retrieve"):
|
if self.action in ("create_or_update", "destroy"):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
||||||
elif self.action in ("create_or_update", "destroy"):
|
|
||||||
# Every authenticated user can see metadata and create personal
|
# Every authenticated user can see metadata and create personal
|
||||||
# notes for himself and can manipulate only his own personal notes.
|
# notes for himself and can manipulate only his own personal notes.
|
||||||
# See self.perform_create(), self.update() and self.destroy().
|
# See self.perform_create(), self.update() and self.destroy().
|
||||||
@ -869,9 +850,7 @@ class WhoAmIDataView(APIView):
|
|||||||
raise APIException(f"Could not find user {user_id}", 500)
|
raise APIException(f"Could not find user {user_id}", 500)
|
||||||
|
|
||||||
auth_type = user_full_data["auth_type"]
|
auth_type = user_full_data["auth_type"]
|
||||||
user_data = async_to_sync(element_cache.restrict_element_data)(
|
user_data = async_to_sync(restrict_user)(user_full_data)
|
||||||
user_full_data, self.request.user.get_collection_string(), user_id
|
|
||||||
)
|
|
||||||
group_ids = user_data["groups_id"] or [GROUP_DEFAULT_PK]
|
group_ids = user_data["groups_id"] or [GROUP_DEFAULT_PK]
|
||||||
else:
|
else:
|
||||||
user_data = None
|
user_data = None
|
||||||
|
@ -1,104 +0,0 @@
|
|||||||
from typing import Any, Callable, Coroutine, Dict, List, Set
|
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
|
|
||||||
from .auth import async_anonymous_is_enabled, async_has_perm, user_to_user_id
|
|
||||||
from .cache import element_cache
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAccessPermissions:
|
|
||||||
"""
|
|
||||||
Base access permissions container.
|
|
||||||
|
|
||||||
Every app which has autoupdate models has to create classes subclassing
|
|
||||||
from this base class for every autoupdate root model.
|
|
||||||
"""
|
|
||||||
|
|
||||||
base_permission = ""
|
|
||||||
"""
|
|
||||||
Set to a permission the user needs to see the element.
|
|
||||||
|
|
||||||
If this string is empty, all users can see it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def check_permissions(self, user_id: int) -> bool:
|
|
||||||
"""
|
|
||||||
Returns True if the user has read access to model instances.
|
|
||||||
"""
|
|
||||||
# Convert user to right type
|
|
||||||
# TODO: Remove this and make sure, that user has always the right type
|
|
||||||
user_id = user_to_user_id(user_id)
|
|
||||||
return async_to_sync(self.async_check_permissions)(user_id)
|
|
||||||
|
|
||||||
async def async_check_permissions(self, user_id: int) -> bool:
|
|
||||||
"""
|
|
||||||
Returns True if the user has read access to model instances.
|
|
||||||
"""
|
|
||||||
if self.base_permission:
|
|
||||||
return await async_has_perm(user_id, self.base_permission)
|
|
||||||
else:
|
|
||||||
return bool(user_id) or await async_anonymous_is_enabled()
|
|
||||||
|
|
||||||
async def get_restricted_data(
|
|
||||||
self, full_data: List[Dict[str, Any]], user_id: int
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Returns the restricted serialized data for the instance prepared
|
|
||||||
for the user.
|
|
||||||
|
|
||||||
The argument full_data has to be a list of full_data dicts. The type of
|
|
||||||
the return is the same. Returns an empty list if the user has no read
|
|
||||||
access. Returns reduced data if the user has limited access. Default:
|
|
||||||
Returns full data if the user has read access to model instances.
|
|
||||||
"""
|
|
||||||
return full_data if await self.async_check_permissions(user_id) else []
|
|
||||||
|
|
||||||
|
|
||||||
class RequiredUsers:
|
|
||||||
"""
|
|
||||||
Helper class to find all users that are required by another element.
|
|
||||||
"""
|
|
||||||
|
|
||||||
callables: Dict[str, Callable[[Dict[str, Any]], Coroutine[Any, Any, Set[int]]]] = {}
|
|
||||||
|
|
||||||
def get_collection_strings(self) -> Set[str]:
|
|
||||||
"""
|
|
||||||
Returns all collection strings for elements that could have required users.
|
|
||||||
"""
|
|
||||||
return set(self.callables.keys())
|
|
||||||
|
|
||||||
def add_collection_string(
|
|
||||||
self,
|
|
||||||
collection_string: str,
|
|
||||||
callable: Callable[[Dict[str, Any]], Coroutine[Any, Any, Set[int]]],
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Add a callable for a collection_string to get the required users of the
|
|
||||||
elements.
|
|
||||||
"""
|
|
||||||
self.callables[collection_string] = callable
|
|
||||||
|
|
||||||
async def get_required_users(self, collection_strings: Set[str]) -> Set[int]:
|
|
||||||
"""
|
|
||||||
Returns the user ids that are required by other elements.
|
|
||||||
|
|
||||||
Returns only user ids required by elements with a collection_string
|
|
||||||
in the argument collection_strings.
|
|
||||||
"""
|
|
||||||
user_ids: Set[int] = set()
|
|
||||||
|
|
||||||
for collection_string in collection_strings:
|
|
||||||
collection_data = await element_cache.get_collection_data(collection_string)
|
|
||||||
# Get the callable for the collection_string
|
|
||||||
get_user_ids = self.callables.get(collection_string)
|
|
||||||
if not (get_user_ids and collection_data):
|
|
||||||
# if the collection_string is unknown or it has no data, do nothing
|
|
||||||
continue
|
|
||||||
|
|
||||||
for element in collection_data.values():
|
|
||||||
user_ids.update(await get_user_ids(element))
|
|
||||||
|
|
||||||
return user_ids
|
|
||||||
|
|
||||||
|
|
||||||
required_user = RequiredUsers()
|
|
@ -1,13 +1,10 @@
|
|||||||
from collections import defaultdict
|
|
||||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||||
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from mypy_extensions import TypedDict
|
from mypy_extensions import TypedDict
|
||||||
|
|
||||||
from .auth import UserDoesNotExist
|
|
||||||
from .autoupdate_bundle import AutoupdateElement, autoupdate_bundle
|
from .autoupdate_bundle import AutoupdateElement, autoupdate_bundle
|
||||||
from .cache import ChangeIdTooLowError, element_cache
|
from .utils import is_iterable
|
||||||
from .utils import is_iterable, split_element_id
|
|
||||||
|
|
||||||
|
|
||||||
AutoupdateFormat = TypedDict(
|
AutoupdateFormat = TypedDict(
|
||||||
@ -33,7 +30,6 @@ def inform_changed_data(
|
|||||||
information: List[str] = None,
|
information: List[str] = None,
|
||||||
user_id: Optional[int] = None,
|
user_id: Optional[int] = None,
|
||||||
disable_history: bool = False,
|
disable_history: bool = False,
|
||||||
no_delete_on_restriction: bool = False,
|
|
||||||
) -> 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
|
||||||
@ -58,7 +54,6 @@ def inform_changed_data(
|
|||||||
disable_history=disable_history,
|
disable_history=disable_history,
|
||||||
information=information,
|
information=information,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
no_delete_on_restriction=no_delete_on_restriction,
|
|
||||||
)
|
)
|
||||||
elements.append(element)
|
elements.append(element)
|
||||||
inform_elements(elements)
|
inform_elements(elements)
|
||||||
@ -101,57 +96,3 @@ def inform_elements(elements: Iterable[AutoupdateElement]) -> None:
|
|||||||
"""
|
"""
|
||||||
with autoupdate_bundle() as bundle:
|
with autoupdate_bundle() as bundle:
|
||||||
bundle.add(elements)
|
bundle.add(elements)
|
||||||
|
|
||||||
|
|
||||||
async def get_autoupdate_data(
|
|
||||||
from_change_id: int, user_id: int
|
|
||||||
) -> Tuple[int, Optional[AutoupdateFormat]]:
|
|
||||||
try:
|
|
||||||
return await _get_autoupdate_data(from_change_id, user_id)
|
|
||||||
except UserDoesNotExist:
|
|
||||||
return 0, None
|
|
||||||
|
|
||||||
|
|
||||||
async def _get_autoupdate_data(
|
|
||||||
from_change_id: int, user_id: int
|
|
||||||
) -> Tuple[int, Optional[AutoupdateFormat]]:
|
|
||||||
"""
|
|
||||||
Returns the max_change_id and the autoupdate from from_change_id to max_change_id
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
(
|
|
||||||
max_change_id,
|
|
||||||
changed_elements,
|
|
||||||
deleted_element_ids,
|
|
||||||
) = await element_cache.get_data_since(user_id, from_change_id)
|
|
||||||
except ChangeIdTooLowError:
|
|
||||||
# The change_id is lower the the lowerst change_id in redis. Return all data
|
|
||||||
(
|
|
||||||
max_change_id,
|
|
||||||
changed_elements,
|
|
||||||
) = await element_cache.get_all_data_list_with_max_change_id(user_id)
|
|
||||||
deleted_elements: Dict[str, List[int]] = {}
|
|
||||||
all_data = True
|
|
||||||
else:
|
|
||||||
all_data = False
|
|
||||||
deleted_elements = defaultdict(list)
|
|
||||||
for element_id in deleted_element_ids:
|
|
||||||
collection_string, id = split_element_id(element_id)
|
|
||||||
deleted_elements[collection_string].append(id)
|
|
||||||
|
|
||||||
# Check, if the autoupdate has any data.
|
|
||||||
if not changed_elements and not deleted_element_ids:
|
|
||||||
# Skip empty updates
|
|
||||||
return max_change_id, None
|
|
||||||
else:
|
|
||||||
# Normal autoupdate with data
|
|
||||||
return (
|
|
||||||
max_change_id,
|
|
||||||
AutoupdateFormat(
|
|
||||||
changed=changed_elements,
|
|
||||||
deleted=deleted_elements,
|
|
||||||
from_change_id=from_change_id,
|
|
||||||
to_change_id=max_change_id,
|
|
||||||
all_data=all_data,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
@ -35,17 +35,11 @@ class AutoupdateElement(AutoupdateElementBase, total=False):
|
|||||||
|
|
||||||
disable_history: If this is True, the element (and the containing full_data) won't
|
disable_history: If this is True, the element (and the containing full_data) won't
|
||||||
be saved into the history. Information and user_id is then irrelevant.
|
be saved into the history. Information and user_id is then irrelevant.
|
||||||
|
|
||||||
no_delete_on_restriction is a flag, which is saved into the models in the cache
|
|
||||||
as the _no_delete_on_restriction key. If this is true, there should neither be an
|
|
||||||
entry for one specific model in the changed *nor the deleted* part of the
|
|
||||||
autoupdate, if the model was restricted.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
information: List[str]
|
information: List[str]
|
||||||
user_id: Optional[int]
|
user_id: Optional[int]
|
||||||
disable_history: bool
|
disable_history: bool
|
||||||
no_delete_on_restriction: bool
|
|
||||||
full_data: Optional[Dict[str, Any]]
|
full_data: Optional[Dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
@ -122,12 +116,7 @@ class AutoupdateBundle:
|
|||||||
cache_elements: Dict[str, Optional[Dict[str, Any]]] = {}
|
cache_elements: Dict[str, Optional[Dict[str, Any]]] = {}
|
||||||
for element in self.element_iterator:
|
for element in self.element_iterator:
|
||||||
element_id = get_element_id(element["collection_string"], element["id"])
|
element_id = get_element_id(element["collection_string"], element["id"])
|
||||||
full_data = element.get("full_data")
|
cache_elements[element_id] = element.get("full_data")
|
||||||
if full_data:
|
|
||||||
full_data["_no_delete_on_restriction"] = element.get(
|
|
||||||
"no_delete_on_restriction", False
|
|
||||||
)
|
|
||||||
cache_elements[element_id] = full_data
|
|
||||||
return cache_elements
|
return cache_elements
|
||||||
|
|
||||||
async def dispatch_autoupdate(self) -> int:
|
async def dispatch_autoupdate(self) -> int:
|
||||||
|
@ -2,7 +2,7 @@ import json
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Type
|
from typing import Any, Callable, Dict, List, Optional, Type
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync, sync_to_async
|
from asgiref.sync import async_to_sync, sync_to_async
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
@ -23,10 +23,6 @@ from .utils import get_element_id, split_element_id
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ChangeIdTooLowError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_cachables() -> List[Cachable]:
|
def get_all_cachables() -> List[Cachable]:
|
||||||
"""
|
"""
|
||||||
Returns all element of OpenSlides.
|
Returns all element of OpenSlides.
|
||||||
@ -231,9 +227,7 @@ class ElementCache:
|
|||||||
changed_elements, deleted_elements
|
changed_elements, deleted_elements
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_all_data_list(
|
async def get_all_data_list(self) -> Dict[str, List[Dict[str, Any]]]:
|
||||||
self, user_id: Optional[int] = None
|
|
||||||
) -> Dict[str, List[Dict[str, Any]]]:
|
|
||||||
"""
|
"""
|
||||||
Returns all data with a list per collection:
|
Returns all data with a list per collection:
|
||||||
{
|
{
|
||||||
@ -242,33 +236,17 @@ class ElementCache:
|
|||||||
If the user id is given the data will be restricted for this user.
|
If the user id is given the data will be restricted for this user.
|
||||||
"""
|
"""
|
||||||
all_data = await self.cache_provider.get_all_data()
|
all_data = await self.cache_provider.get_all_data()
|
||||||
return await self.format_all_data(all_data, user_id)
|
return await self.format_all_data(all_data)
|
||||||
|
|
||||||
async def get_all_data_list_with_max_change_id(
|
|
||||||
self, user_id: Optional[int] = None
|
|
||||||
) -> Tuple[int, Dict[str, List[Dict[str, Any]]]]:
|
|
||||||
(
|
|
||||||
max_change_id,
|
|
||||||
all_data,
|
|
||||||
) = await self.cache_provider.get_all_data_with_max_change_id()
|
|
||||||
return max_change_id, await self.format_all_data(all_data, user_id)
|
|
||||||
|
|
||||||
async def format_all_data(
|
async def format_all_data(
|
||||||
self, all_data_bytes: Dict[bytes, bytes], user_id: Optional[int]
|
self, all_data_bytes: Dict[bytes, bytes]
|
||||||
) -> Dict[str, List[Dict[str, Any]]]:
|
) -> Dict[str, List[Dict[str, Any]]]:
|
||||||
all_data: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
all_data: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
||||||
for element_id, data in all_data_bytes.items():
|
for element_id, data in all_data_bytes.items():
|
||||||
collection, _ = split_element_id(element_id)
|
collection, _ = split_element_id(element_id)
|
||||||
element = json.loads(data.decode())
|
element = json.loads(data.decode())
|
||||||
element.pop(
|
|
||||||
"_no_delete_on_restriction", False
|
|
||||||
) # remove special field for get_data_since
|
|
||||||
all_data[collection].append(element)
|
all_data[collection].append(element)
|
||||||
|
|
||||||
if user_id is not None:
|
|
||||||
for collection in all_data.keys():
|
|
||||||
restricter = self.cachables[collection].restrict_elements
|
|
||||||
all_data[collection] = await restricter(user_id, all_data[collection])
|
|
||||||
return dict(all_data)
|
return dict(all_data)
|
||||||
|
|
||||||
async def get_collection_data(self, collection: str) -> Dict[int, Dict[str, Any]]:
|
async def get_collection_data(self, collection: str) -> Dict[int, Dict[str, Any]]:
|
||||||
@ -281,13 +259,10 @@ class ElementCache:
|
|||||||
collection_data = {}
|
collection_data = {}
|
||||||
for id in encoded_collection_data.keys():
|
for id in encoded_collection_data.keys():
|
||||||
collection_data[id] = json.loads(encoded_collection_data[id].decode())
|
collection_data[id] = json.loads(encoded_collection_data[id].decode())
|
||||||
collection_data[id].pop(
|
|
||||||
"_no_delete_on_restriction", False
|
|
||||||
) # remove special field for get_data_since
|
|
||||||
return collection_data
|
return collection_data
|
||||||
|
|
||||||
async def get_element_data(
|
async def get_element_data(
|
||||||
self, collection: str, id: int, user_id: Optional[int] = None
|
self, collection: str, id: int
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns one element or None, if the element does not exist.
|
Returns one element or None, if the element does not exist.
|
||||||
@ -299,108 +274,7 @@ class ElementCache:
|
|||||||
|
|
||||||
if encoded_element is None:
|
if encoded_element is None:
|
||||||
return None
|
return None
|
||||||
element = json.loads(encoded_element.decode()) # type: ignore
|
return json.loads(encoded_element.decode()) # type: ignore
|
||||||
element.pop(
|
|
||||||
"_no_delete_on_restriction", False
|
|
||||||
) # remove special field for get_data_since
|
|
||||||
|
|
||||||
if user_id is not None:
|
|
||||||
element = await self.restrict_element_data(element, collection, user_id)
|
|
||||||
return element
|
|
||||||
|
|
||||||
async def restrict_element_data(
|
|
||||||
self, element: Dict[str, Any], collection: str, user_id: int
|
|
||||||
) -> Optional[Dict[str, Any]]:
|
|
||||||
restricter = self.cachables[collection].restrict_elements
|
|
||||||
restricted_elements = await restricter(user_id, [element])
|
|
||||||
return restricted_elements[0] if restricted_elements else None
|
|
||||||
|
|
||||||
async def get_data_since(
|
|
||||||
self, user_id: Optional[int] = None, change_id: int = 0
|
|
||||||
) -> Tuple[int, Dict[str, List[Dict[str, Any]]], List[str]]:
|
|
||||||
"""
|
|
||||||
Returns all data since change_id until the max change id.cIf the user id is given the
|
|
||||||
data will be restricted for this user.
|
|
||||||
|
|
||||||
Returns three values inside a tuple. The first value is the max change id. The second
|
|
||||||
value is a dict where the key is the collection and the value is a list of data.
|
|
||||||
The third is a list of element_ids with deleted elements.
|
|
||||||
|
|
||||||
Only returns elements with the change_id or newer. When change_id is 0,
|
|
||||||
all elements are returned.
|
|
||||||
|
|
||||||
Raises a ChangeIdTooLowError when the lowest change_id in redis is higher then
|
|
||||||
the requested change_id. In this case the method has to be rerun with
|
|
||||||
change_id=0. This is importend because there could be deleted elements
|
|
||||||
that the cache does not know about.
|
|
||||||
"""
|
|
||||||
if change_id == 0:
|
|
||||||
(
|
|
||||||
max_change_id,
|
|
||||||
changed_elements,
|
|
||||||
) = await self.get_all_data_list_with_max_change_id(user_id)
|
|
||||||
return (max_change_id, changed_elements, [])
|
|
||||||
|
|
||||||
# This raises a Runtime Exception, if there is no change_id
|
|
||||||
lowest_change_id = await self.get_lowest_change_id()
|
|
||||||
|
|
||||||
if change_id < lowest_change_id:
|
|
||||||
# When change_id is lower then the lowest change_id in redis, we can
|
|
||||||
# not inform the user about deleted elements.
|
|
||||||
raise ChangeIdTooLowError(
|
|
||||||
f"change_id {change_id} is lower then the lowest change_id in redis {lowest_change_id}."
|
|
||||||
)
|
|
||||||
|
|
||||||
(
|
|
||||||
max_change_id,
|
|
||||||
raw_changed_elements,
|
|
||||||
deleted_elements,
|
|
||||||
) = await self.cache_provider.get_data_since(change_id)
|
|
||||||
changed_elements = {
|
|
||||||
collection: [json.loads(value.decode()) for value in value_list]
|
|
||||||
for collection, value_list in raw_changed_elements.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
if user_id is None:
|
|
||||||
for elements in changed_elements.values():
|
|
||||||
for element in elements:
|
|
||||||
element.pop("_no_delete_on_restriction", False)
|
|
||||||
else:
|
|
||||||
# the list(...) is important, because `changed_elements` will be
|
|
||||||
# altered during iteration and restricting data
|
|
||||||
for collection, elements in list(changed_elements.items()):
|
|
||||||
# Remove the _no_delete_on_restriction from each element. Collect all ids, where
|
|
||||||
# this field is absent or False.
|
|
||||||
unrestricted_ids = set()
|
|
||||||
for element in elements:
|
|
||||||
no_delete_on_restriction = element.pop(
|
|
||||||
"_no_delete_on_restriction", False
|
|
||||||
)
|
|
||||||
if not no_delete_on_restriction:
|
|
||||||
unrestricted_ids.add(element["id"])
|
|
||||||
|
|
||||||
cacheable = self.cachables[collection]
|
|
||||||
restricted_elements = await cacheable.restrict_elements(
|
|
||||||
user_id, elements
|
|
||||||
)
|
|
||||||
|
|
||||||
# If the model is personalized, it must not be deleted for other users
|
|
||||||
if not cacheable.personalized_model:
|
|
||||||
# Add removed objects (through restricter) to deleted elements.
|
|
||||||
restricted_element_ids = set(
|
|
||||||
[element["id"] for element in restricted_elements]
|
|
||||||
)
|
|
||||||
# Delete all ids, that are allowed to be deleted (see unrestricted_ids) and are
|
|
||||||
# not present after restricting the data.
|
|
||||||
for id in unrestricted_ids - restricted_element_ids:
|
|
||||||
deleted_elements.append(get_element_id(collection, id))
|
|
||||||
|
|
||||||
if not restricted_elements:
|
|
||||||
del changed_elements[collection]
|
|
||||||
else:
|
|
||||||
changed_elements[collection] = restricted_elements
|
|
||||||
|
|
||||||
return (max_change_id, changed_elements, deleted_elements)
|
|
||||||
|
|
||||||
async def get_current_change_id(self) -> int:
|
async def get_current_change_id(self) -> int:
|
||||||
"""
|
"""
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import functools
|
import functools
|
||||||
import hashlib
|
import hashlib
|
||||||
from collections import defaultdict
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import Any, Callable, Coroutine, Dict, List, Optional, Set, Tuple
|
from typing import Any, Callable, Coroutine, Dict, List, Optional, Set, Tuple
|
||||||
|
|
||||||
@ -70,11 +69,6 @@ class ElementCacheProvider(Protocol):
|
|||||||
) -> int:
|
) -> int:
|
||||||
...
|
...
|
||||||
|
|
||||||
async def get_data_since(
|
|
||||||
self, change_id: int
|
|
||||||
) -> Tuple[int, Dict[str, List[bytes]], List[str]]:
|
|
||||||
...
|
|
||||||
|
|
||||||
async def get_current_change_id(self) -> int:
|
async def get_current_change_id(self) -> int:
|
||||||
...
|
...
|
||||||
|
|
||||||
@ -252,34 +246,6 @@ class RedisCacheProvider:
|
|||||||
""",
|
""",
|
||||||
True,
|
True,
|
||||||
),
|
),
|
||||||
"get_data_since": (
|
|
||||||
"""
|
|
||||||
-- get max change id
|
|
||||||
local tmp = redis.call('zrevrangebyscore', KEYS[2], '+inf', '-inf', 'WITHSCORES', 'LIMIT', 0, 1)
|
|
||||||
local max_change_id
|
|
||||||
if next(tmp) == nil then
|
|
||||||
-- The key does not exist
|
|
||||||
return redis.error_reply("cache_reset")
|
|
||||||
else
|
|
||||||
max_change_id = tmp[2]
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Get change ids of changed elements
|
|
||||||
local element_ids = redis.call('zrangebyscore', KEYS[2], ARGV[1], max_change_id)
|
|
||||||
|
|
||||||
-- Save elements in array. First is the max_change_id with the key "max_change_id"
|
|
||||||
-- Than rotate element_id and element_json. This is ocnverted into a dict in python code.
|
|
||||||
local elements = {}
|
|
||||||
table.insert(elements, 'max_change_id')
|
|
||||||
table.insert(elements, max_change_id)
|
|
||||||
for _, element_id in pairs(element_ids) do
|
|
||||||
table.insert(elements, element_id)
|
|
||||||
table.insert(elements, redis.call('hget', KEYS[1], element_id))
|
|
||||||
end
|
|
||||||
return elements
|
|
||||||
""",
|
|
||||||
True,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, ensure_cache: Callable[[], Coroutine[Any, Any, None]]) -> None:
|
def __init__(self, ensure_cache: Callable[[], Coroutine[Any, Any, None]]) -> None:
|
||||||
@ -430,47 +396,6 @@ class RedisCacheProvider:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ensure_cache_wrapper()
|
|
||||||
async def get_data_since(
|
|
||||||
self, change_id: int
|
|
||||||
) -> Tuple[int, Dict[str, List[bytes]], List[str]]:
|
|
||||||
"""
|
|
||||||
Returns all elements since a change_id (included) and until the max_change_id (included).
|
|
||||||
|
|
||||||
The returend value is a two element tuple. The first value is a dict the elements where
|
|
||||||
the key is the collection and the value a list of (json-) encoded elements. The
|
|
||||||
second element is a list of element_ids, that have been deleted since the change_id.
|
|
||||||
"""
|
|
||||||
changed_elements: Dict[str, List[bytes]] = defaultdict(list)
|
|
||||||
deleted_elements: List[str] = []
|
|
||||||
|
|
||||||
# lua script that returns gets all element_ids from change_id_cache_key
|
|
||||||
# and then uses each element_id on full_data or restricted_data.
|
|
||||||
# It returns a list where the odd values are the change_id and the
|
|
||||||
# even values the element as json. The function wait_make_dict creates
|
|
||||||
# a python dict from the returned list.
|
|
||||||
elements: Dict[bytes, Optional[bytes]] = await aioredis.util.wait_make_dict(
|
|
||||||
self.eval(
|
|
||||||
"get_data_since",
|
|
||||||
keys=[self.full_data_cache_key, self.change_id_cache_key],
|
|
||||||
args=[change_id],
|
|
||||||
read_only=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
max_change_id = int(elements[b"max_change_id"].decode()) # type: ignore
|
|
||||||
for element_id, element_json in elements.items():
|
|
||||||
if element_id.startswith(b"_config") or element_id == b"max_change_id":
|
|
||||||
# Ignore config values from the change_id cache key
|
|
||||||
continue
|
|
||||||
if element_json is None:
|
|
||||||
# The element is not in the cache. It has to be deleted.
|
|
||||||
deleted_elements.append(element_id.decode())
|
|
||||||
else:
|
|
||||||
collection, id = split_element_id(element_id)
|
|
||||||
changed_elements[collection].append(element_json)
|
|
||||||
return max_change_id, changed_elements, deleted_elements
|
|
||||||
|
|
||||||
@ensure_cache_wrapper()
|
@ensure_cache_wrapper()
|
||||||
async def get_current_change_id(self) -> int:
|
async def get_current_change_id(self) -> int:
|
||||||
"""
|
"""
|
||||||
@ -662,27 +587,6 @@ class MemoryCacheProvider:
|
|||||||
|
|
||||||
return change_id
|
return change_id
|
||||||
|
|
||||||
async def get_data_since(
|
|
||||||
self, change_id: int
|
|
||||||
) -> Tuple[int, Dict[str, List[bytes]], List[str]]:
|
|
||||||
changed_elements: Dict[str, List[bytes]] = defaultdict(list)
|
|
||||||
deleted_elements: List[str] = []
|
|
||||||
|
|
||||||
all_element_ids: Set[str] = set()
|
|
||||||
for data_change_id, element_ids in self.change_id_data.items():
|
|
||||||
if data_change_id >= change_id:
|
|
||||||
all_element_ids.update(element_ids)
|
|
||||||
|
|
||||||
for element_id in all_element_ids:
|
|
||||||
element_json = self.full_data.get(element_id, None)
|
|
||||||
if element_json is None:
|
|
||||||
deleted_elements.append(element_id)
|
|
||||||
else:
|
|
||||||
collection, id = split_element_id(element_id)
|
|
||||||
changed_elements[collection].append(element_json.encode())
|
|
||||||
max_change_id = await self.get_current_change_id()
|
|
||||||
return (max_change_id, changed_elements, deleted_elements)
|
|
||||||
|
|
||||||
async def get_current_change_id(self) -> int:
|
async def get_current_change_id(self) -> int:
|
||||||
if self.change_id_data:
|
if self.change_id_data:
|
||||||
return max(self.change_id_data.keys())
|
return max(self.change_id_data.keys())
|
||||||
@ -706,8 +610,6 @@ class Cachable(Protocol):
|
|||||||
It needs at least the methods defined here.
|
It needs at least the methods defined here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
personalized_model: bool
|
|
||||||
|
|
||||||
def get_collection_string(self) -> str:
|
def get_collection_string(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the string representing the name of the cachable.
|
Returns the string representing the name of the cachable.
|
||||||
@ -717,13 +619,3 @@ class Cachable(Protocol):
|
|||||||
"""
|
"""
|
||||||
Returns all elements of the cachable.
|
Returns all elements of the cachable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def restrict_elements(
|
|
||||||
self, user_id: int, elements: List[Dict[str, Any]]
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Converts full_data to restricted_data.
|
|
||||||
|
|
||||||
elements can be an empty list, a list with some elements of the cachable or with all
|
|
||||||
elements of the cachable.
|
|
||||||
"""
|
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from . import logging
|
from . import logging
|
||||||
from .access_permissions import BaseAccessPermissions
|
|
||||||
from .auth import UserDoesNotExist
|
|
||||||
from .autoupdate import AutoupdateElement, inform_changed_data, inform_elements
|
from .autoupdate import AutoupdateElement, inform_changed_data, inform_elements
|
||||||
from .rest_api import model_serializer_classes
|
from .rest_api import model_serializer_classes
|
||||||
from .utils import convert_camel_case_to_pseudo_snake_case, get_element_id
|
from .utils import convert_camel_case_to_pseudo_snake_case, get_element_id
|
||||||
@ -37,17 +34,6 @@ class RESTModelMixin:
|
|||||||
Mixin for Django models which are used in our REST API.
|
Mixin for Django models which are used in our REST API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions: Optional[BaseAccessPermissions] = None
|
|
||||||
|
|
||||||
personalized_model = False
|
|
||||||
"""
|
|
||||||
Flag, if the model is personalized on a per-user basis.
|
|
||||||
Requires the model to have a `user_id` which should be
|
|
||||||
a OneToOne relation to User. The relation must never change,
|
|
||||||
because it won't be deleted from it's former user when the relation
|
|
||||||
changes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_root_rest_element(self) -> models.Model:
|
def get_root_rest_element(self) -> models.Model:
|
||||||
"""
|
"""
|
||||||
Returns the root rest instance.
|
Returns the root rest instance.
|
||||||
@ -56,18 +42,6 @@ class RESTModelMixin:
|
|||||||
"""
|
"""
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_access_permissions(cls) -> BaseAccessPermissions:
|
|
||||||
"""
|
|
||||||
Returns a container to handle access permissions for this model and
|
|
||||||
its corresponding viewset.
|
|
||||||
"""
|
|
||||||
if cls.access_permissions is None:
|
|
||||||
raise ImproperlyConfigured(
|
|
||||||
"A RESTModel needs to have an access_permission."
|
|
||||||
)
|
|
||||||
return cls.access_permissions
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_collection_string(cls) -> str:
|
def get_collection_string(cls) -> str:
|
||||||
"""
|
"""
|
||||||
@ -97,7 +71,6 @@ class RESTModelMixin:
|
|||||||
def save(
|
def save(
|
||||||
self,
|
self,
|
||||||
skip_autoupdate: bool = False,
|
skip_autoupdate: bool = False,
|
||||||
no_delete_on_restriction: bool = False,
|
|
||||||
disable_history: bool = False,
|
disable_history: bool = False,
|
||||||
*args: Any,
|
*args: Any,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
@ -117,7 +90,6 @@ class RESTModelMixin:
|
|||||||
if not skip_autoupdate:
|
if not skip_autoupdate:
|
||||||
inform_changed_data(
|
inform_changed_data(
|
||||||
self.get_root_rest_element(),
|
self.get_root_rest_element(),
|
||||||
no_delete_on_restriction=no_delete_on_restriction,
|
|
||||||
disable_history=disable_history,
|
disable_history=disable_history,
|
||||||
)
|
)
|
||||||
return return_value
|
return return_value
|
||||||
@ -183,20 +155,6 @@ class RESTModelMixin:
|
|||||||
|
|
||||||
return full_data
|
return full_data
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def restrict_elements(
|
|
||||||
cls, user_id: int, elements: List[Dict[str, Any]]
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Converts a list of elements from full_data to restricted_data.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return await cls.get_access_permissions().get_restricted_data(
|
|
||||||
elements, user_id
|
|
||||||
)
|
|
||||||
except UserDoesNotExist:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_full_data(self) -> Dict[str, Any]:
|
def get_full_data(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Returns the full_data of the instance.
|
Returns the full_data of the instance.
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Any, Dict, Iterable, Optional, Type
|
from typing import Any, Dict, Iterable, Type
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.http import Http404
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import detail_route, list_route
|
from rest_framework.decorators import detail_route, list_route
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
from rest_framework.metadata import SimpleMetadata
|
from rest_framework.metadata import SimpleMetadata
|
||||||
from rest_framework.mixins import (
|
from rest_framework.mixins import (
|
||||||
CreateModelMixin as _CreateModelMixin,
|
CreateModelMixin as _CreateModelMixin,
|
||||||
DestroyModelMixin,
|
DestroyModelMixin as _DestroyModelMixin,
|
||||||
ListModelMixin as _ListModelMixin,
|
|
||||||
RetrieveModelMixin as _RetrieveModelMixin,
|
|
||||||
UpdateModelMixin as _UpdateModelMixin,
|
UpdateModelMixin as _UpdateModelMixin,
|
||||||
)
|
)
|
||||||
from rest_framework.relations import MANY_RELATION_KWARGS
|
from rest_framework.relations import MANY_RELATION_KWARGS
|
||||||
@ -40,14 +36,9 @@ from rest_framework.serializers import (
|
|||||||
ValidationError,
|
ValidationError,
|
||||||
)
|
)
|
||||||
from rest_framework.utils.serializer_helpers import ReturnDict
|
from rest_framework.utils.serializer_helpers import ReturnDict
|
||||||
from rest_framework.viewsets import (
|
from rest_framework.viewsets import GenericViewSet as _GenericViewSet
|
||||||
GenericViewSet as _GenericViewSet,
|
|
||||||
ModelViewSet as _ModelViewSet,
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import logging
|
from . import logging
|
||||||
from .access_permissions import BaseAccessPermissions
|
|
||||||
from .cache import element_cache
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -156,7 +147,7 @@ class ErrorLoggingMixin:
|
|||||||
|
|
||||||
class PermissionMixin:
|
class PermissionMixin:
|
||||||
"""
|
"""
|
||||||
Mixin for subclasses of APIView like GenericViewSet and ModelViewSet.
|
Mixin for subclasses of APIView like GenericViewSet.
|
||||||
|
|
||||||
The method check_view_permissions is evaluated. If it returns False
|
The method check_view_permissions is evaluated. If it returns False
|
||||||
self.permission_denied() is called. Django REST Framework's permission
|
self.permission_denied() is called. Django REST Framework's permission
|
||||||
@ -166,8 +157,6 @@ class PermissionMixin:
|
|||||||
viewset.
|
viewset.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_permissions: Optional[BaseAccessPermissions] = None
|
|
||||||
|
|
||||||
def get_permissions(self) -> Iterable[str]:
|
def get_permissions(self) -> Iterable[str]:
|
||||||
"""
|
"""
|
||||||
Overridden method to check view permissions. Returns an empty
|
Overridden method to check view permissions. Returns an empty
|
||||||
@ -189,13 +178,6 @@ class PermissionMixin:
|
|||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_access_permissions(self) -> BaseAccessPermissions:
|
|
||||||
"""
|
|
||||||
Returns a container to handle access permissions for this viewset and
|
|
||||||
its corresponding model.
|
|
||||||
"""
|
|
||||||
return self.access_permissions # type: ignore
|
|
||||||
|
|
||||||
def get_serializer_class(self) -> Type[Serializer]:
|
def get_serializer_class(self) -> Type[Serializer]:
|
||||||
"""
|
"""
|
||||||
Overridden method to return the serializer class for the model.
|
Overridden method to return the serializer class for the model.
|
||||||
@ -265,54 +247,6 @@ class ModelSerializer(_ModelSerializer, metaclass=ModelSerializerRegisterer):
|
|||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
class ListModelMixin(_ListModelMixin):
|
|
||||||
"""
|
|
||||||
Mixin to add the caching system to list requests.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def list(self, request: Any, *args: Any, **kwargs: Any) -> Response:
|
|
||||||
model = self.get_queryset().model
|
|
||||||
try:
|
|
||||||
collection_string = model.get_collection_string()
|
|
||||||
except AttributeError:
|
|
||||||
# The corresponding queryset does not support caching.
|
|
||||||
response = super().list(request, *args, **kwargs)
|
|
||||||
else:
|
|
||||||
# TODO
|
|
||||||
# This loads all data from the cache, not only the requested data.
|
|
||||||
# If we would use the rest api, we should add a method
|
|
||||||
# element_cache.get_collection_restricted_data
|
|
||||||
all_restricted_data = async_to_sync(element_cache.get_all_data_list)(
|
|
||||||
request.user.pk or 0
|
|
||||||
)
|
|
||||||
response = Response(all_restricted_data.get(collection_string, []))
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class RetrieveModelMixin(_RetrieveModelMixin):
|
|
||||||
"""
|
|
||||||
Mixin to add the caching system to retrieve requests.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def retrieve(self, request: Any, *args: Any, **kwargs: Any) -> Response:
|
|
||||||
model = self.get_queryset().model
|
|
||||||
try:
|
|
||||||
collection_string = model.get_collection_string()
|
|
||||||
except AttributeError:
|
|
||||||
# The corresponding queryset does not support caching.
|
|
||||||
response = super().retrieve(request, *args, **kwargs)
|
|
||||||
else:
|
|
||||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
|
||||||
user_id = request.user.pk or 0
|
|
||||||
content = async_to_sync(element_cache.get_element_data)(
|
|
||||||
collection_string, self.kwargs[lookup_url_kwarg], user_id
|
|
||||||
)
|
|
||||||
if content is None:
|
|
||||||
raise Http404
|
|
||||||
response = Response(content)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class CreateModelMixin(_CreateModelMixin):
|
class CreateModelMixin(_CreateModelMixin):
|
||||||
"""
|
"""
|
||||||
Mixin to override create requests.
|
Mixin to override create requests.
|
||||||
@ -349,6 +283,10 @@ class UpdateModelMixin(_UpdateModelMixin):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class DestroyModelMixin(_DestroyModelMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class GenericViewSet(ErrorLoggingMixin, PermissionMixin, _GenericViewSet):
|
class GenericViewSet(ErrorLoggingMixin, PermissionMixin, _GenericViewSet):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -356,10 +294,9 @@ class GenericViewSet(ErrorLoggingMixin, PermissionMixin, _GenericViewSet):
|
|||||||
class ModelViewSet(
|
class ModelViewSet(
|
||||||
ErrorLoggingMixin,
|
ErrorLoggingMixin,
|
||||||
PermissionMixin,
|
PermissionMixin,
|
||||||
ListModelMixin,
|
|
||||||
RetrieveModelMixin,
|
|
||||||
CreateModelMixin,
|
CreateModelMixin,
|
||||||
UpdateModelMixin,
|
UpdateModelMixin,
|
||||||
_ModelViewSet,
|
DestroyModelMixin,
|
||||||
|
_GenericViewSet,
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -14,13 +13,9 @@ from openslides.core.models import Countdown
|
|||||||
from openslides.mediafiles.models import Mediafile
|
from openslides.mediafiles.models import Mediafile
|
||||||
from openslides.motions.models import Motion, MotionBlock
|
from openslides.motions.models import Motion, MotionBlock
|
||||||
from openslides.topics.models import Topic
|
from openslides.topics.models import Topic
|
||||||
from openslides.users.models import Group
|
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
|
||||||
from tests.count_queries import count_queries
|
from tests.count_queries import count_queries
|
||||||
from tests.test_case import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
from ...common_groups import GROUP_DEFAULT_PK
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db(transaction=False)
|
@pytest.mark.django_db(transaction=False)
|
||||||
def test_agenda_item_db_queries():
|
def test_agenda_item_db_queries():
|
||||||
@ -106,24 +101,14 @@ class ContentObjects(TestCase):
|
|||||||
|
|
||||||
assert topic.agenda_item is not None
|
assert topic.agenda_item is not None
|
||||||
assert topic.list_of_speakers is not None
|
assert topic.list_of_speakers is not None
|
||||||
response = self.client.get(reverse("item-detail", args=[topic.agenda_item.pk]))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("listofspeakers-detail", args=[topic.list_of_speakers.pk])
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def test_delete_topic(self):
|
def test_delete_topic(self):
|
||||||
topic = Topic.objects.create(title="test_title_lwOCK32jZGFb37DpmoP(")
|
topic = Topic.objects.create(title="test_title_lwOCK32jZGFb37DpmoP(")
|
||||||
item_id = topic.agenda_item_id
|
item_id = topic.agenda_item_id
|
||||||
list_of_speakers_id = topic.list_of_speakers_id
|
list_of_speakers_id = topic.list_of_speakers_id
|
||||||
topic.delete()
|
topic.delete()
|
||||||
response = self.client.get(reverse("item-detail", args=[item_id]))
|
self.assertFalse(Item.objects.filter(pk=item_id).exists())
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
self.assertFalse(ListOfSpeakers.objects.filter(pk=list_of_speakers_id).exists())
|
||||||
response = self.client.get(
|
|
||||||
reverse("listofspeakers-detail", args=[list_of_speakers_id])
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
def test_prevent_removing_topic_from_agenda(self):
|
def test_prevent_removing_topic_from_agenda(self):
|
||||||
topic = Topic.objects.create(title="test_title_lwOCK32jZGFb37DpmoP(")
|
topic = Topic.objects.create(title="test_title_lwOCK32jZGFb37DpmoP(")
|
||||||
@ -185,105 +170,6 @@ class ContentObjects(TestCase):
|
|||||||
self.assertEqual(motion.agenda_item_id, motion.agenda_item.id)
|
self.assertEqual(motion.agenda_item_id, motion.agenda_item.id)
|
||||||
|
|
||||||
|
|
||||||
class RetrieveItem(TestCase):
|
|
||||||
"""
|
|
||||||
Tests retrieving items.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.client = APIClient()
|
|
||||||
config["general_system_enable_anonymous"] = True
|
|
||||||
self.item = Topic.objects.create(
|
|
||||||
title="test_title_Idais2pheepeiz5uph1c"
|
|
||||||
).agenda_item
|
|
||||||
|
|
||||||
def test_normal_by_anonymous_without_perm_to_see_internal_items(self):
|
|
||||||
group = get_user_model().groups.field.related_model.objects.get(
|
|
||||||
pk=GROUP_DEFAULT_PK
|
|
||||||
)
|
|
||||||
permission_string = "agenda.can_see_internal_items"
|
|
||||||
app_label, codename = permission_string.split(".")
|
|
||||||
permission = group.permissions.get(
|
|
||||||
content_type__app_label=app_label, codename=codename
|
|
||||||
)
|
|
||||||
group.permissions.remove(permission)
|
|
||||||
self.item.type = Item.AGENDA_ITEM
|
|
||||||
self.item.save()
|
|
||||||
response = self.client.get(reverse("item-detail", args=[self.item.pk]))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def test_hidden_by_anonymous_without_manage_perms(self):
|
|
||||||
response = self.client.get(reverse("item-detail", args=[self.item.pk]))
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
|
|
||||||
def test_hidden_by_anonymous_with_manage_perms(self):
|
|
||||||
group = Group.objects.get(pk=GROUP_DEFAULT_PK)
|
|
||||||
permission_string = "agenda.can_manage"
|
|
||||||
app_label, codename = permission_string.split(".")
|
|
||||||
permission = Permission.objects.get(
|
|
||||||
content_type__app_label=app_label, codename=codename
|
|
||||||
)
|
|
||||||
group.permissions.add(permission)
|
|
||||||
inform_changed_data(group)
|
|
||||||
response = self.client.get(reverse("item-detail", args=[self.item.pk]))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def test_internal_by_anonymous_without_perm_to_see_internal_items(self):
|
|
||||||
group = Group.objects.get(pk=GROUP_DEFAULT_PK)
|
|
||||||
permission_string = "agenda.can_see_internal_items"
|
|
||||||
app_label, codename = permission_string.split(".")
|
|
||||||
permission = group.permissions.get(
|
|
||||||
content_type__app_label=app_label, codename=codename
|
|
||||||
)
|
|
||||||
group.permissions.remove(permission)
|
|
||||||
inform_changed_data(group)
|
|
||||||
self.item.type = Item.INTERNAL_ITEM
|
|
||||||
self.item.save()
|
|
||||||
response = self.client.get(reverse("item-detail", args=[self.item.pk]))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
def test_normal_by_anonymous_cant_see_agenda_comments(self):
|
|
||||||
self.item.type = Item.AGENDA_ITEM
|
|
||||||
self.item.comment = "comment_gbiejd67gkbmsogh8374jf$kd"
|
|
||||||
self.item.save()
|
|
||||||
response = self.client.get(reverse("item-detail", args=[self.item.pk]))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertTrue(response.data.get("comment") is None)
|
|
||||||
|
|
||||||
|
|
||||||
class RetrieveListOfSpeakers(TestCase):
|
|
||||||
"""
|
|
||||||
Tests retrieving list of speakers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.client = APIClient()
|
|
||||||
config["general_system_enable_anonymous"] = True
|
|
||||||
self.list_of_speakers = Topic.objects.create(
|
|
||||||
title="test_title_qsjem(ZUNfp7egnzp37n"
|
|
||||||
).list_of_speakers
|
|
||||||
|
|
||||||
def test_simple(self):
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("listofspeakers-detail", args=[self.list_of_speakers.pk])
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def test_without_permission(self):
|
|
||||||
group = Group.objects.get(pk=GROUP_DEFAULT_PK)
|
|
||||||
permission_string = "agenda.can_see_list_of_speakers"
|
|
||||||
app_label, codename = permission_string.split(".")
|
|
||||||
permission = Permission.objects.get(
|
|
||||||
content_type__app_label=app_label, codename=codename
|
|
||||||
)
|
|
||||||
group.permissions.remove(permission)
|
|
||||||
inform_changed_data(group)
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("listofspeakers-detail", args=[self.list_of_speakers.pk])
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
|
|
||||||
class ManageSpeaker(TestCase):
|
class ManageSpeaker(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests managing speakers.
|
Tests managing speakers.
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import random
|
import random
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
|
||||||
|
|
||||||
from openslides.assignments.models import (
|
from openslides.assignments.models import (
|
||||||
Assignment,
|
Assignment,
|
||||||
@ -19,7 +17,7 @@ from openslides.core.config import config
|
|||||||
from openslides.poll.models import BasePoll
|
from openslides.poll.models import BasePoll
|
||||||
from openslides.utils.auth import get_group_model
|
from openslides.utils.auth import get_group_model
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from tests.common_groups import GROUP_ADMIN_PK, GROUP_DELEGATE_PK
|
from tests.common_groups import GROUP_ADMIN_PK
|
||||||
from tests.count_queries import count_queries
|
from tests.count_queries import count_queries
|
||||||
from tests.test_case import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
@ -2649,424 +2647,6 @@ class VoteAssignmentPollPseudoanonymousN(VoteAssignmentPollBaseTestClass):
|
|||||||
self.assertFalse(AssignmentVote.objects.exists())
|
self.assertFalse(AssignmentVote.objects.exists())
|
||||||
|
|
||||||
|
|
||||||
# test autoupdates
|
|
||||||
class VoteAssignmentPollAutoupdatesBaseClass(TestCase):
|
|
||||||
poll_type = "" # set by subclass, defines which poll type we use
|
|
||||||
|
|
||||||
"""
|
|
||||||
3 important users:
|
|
||||||
self.admin: manager, has can_see, can_manage, can_manage_polls (in admin group)
|
|
||||||
self.user: votes, has can_see perms and in in delegate group
|
|
||||||
self.other_user: Just has can_see perms and is NOT in the delegate group.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def advancedSetUp(self):
|
|
||||||
self.delegate_group = get_group_model().objects.get(pk=GROUP_DELEGATE_PK)
|
|
||||||
self.other_user, _ = self.create_user()
|
|
||||||
inform_changed_data(self.other_user)
|
|
||||||
|
|
||||||
self.user, user_password = self.create_user()
|
|
||||||
self.user.groups.add(self.delegate_group)
|
|
||||||
self.user.is_present = True
|
|
||||||
self.user.save()
|
|
||||||
self.user_client = APIClient()
|
|
||||||
self.user_client.login(username=self.user.username, password=user_password)
|
|
||||||
|
|
||||||
self.assignment = Assignment.objects.create(
|
|
||||||
title="test_assignment_" + self._get_random_string(), open_posts=1
|
|
||||||
)
|
|
||||||
self.assignment.add_candidate(self.admin)
|
|
||||||
self.description = "test_description_paiquei5ahpie1wu8ohW"
|
|
||||||
self.poll = AssignmentPoll.objects.create(
|
|
||||||
assignment=self.assignment,
|
|
||||||
title="test_title_" + self._get_random_string(),
|
|
||||||
pollmethod=AssignmentPoll.POLLMETHOD_YNA,
|
|
||||||
type=self.poll_type,
|
|
||||||
state=AssignmentPoll.STATE_STARTED,
|
|
||||||
onehundred_percent_base=AssignmentPoll.PERCENT_BASE_CAST,
|
|
||||||
majority_method=AssignmentPoll.MAJORITY_TWO_THIRDS,
|
|
||||||
description=self.description,
|
|
||||||
)
|
|
||||||
self.poll.create_options()
|
|
||||||
self.poll.groups.add(self.delegate_group)
|
|
||||||
|
|
||||||
|
|
||||||
class VoteAssignmentPollNamedAutoupdates(VoteAssignmentPollAutoupdatesBaseClass):
|
|
||||||
poll_type = AssignmentPoll.TYPE_NAMED
|
|
||||||
|
|
||||||
def test_vote(self):
|
|
||||||
response = self.user_client.post(
|
|
||||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {"1": "A"}}
|
|
||||||
)
|
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
|
||||||
poll = AssignmentPoll.objects.get()
|
|
||||||
vote = AssignmentVote.objects.get()
|
|
||||||
|
|
||||||
# Expect the admin to see the full data in the autoupdate
|
|
||||||
autoupdate = self.get_last_autoupdate(user=self.admin)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0],
|
|
||||||
{
|
|
||||||
"assignments/assignment-poll:1": {
|
|
||||||
"allow_multiple_votes_per_candidate": False,
|
|
||||||
"assignment_id": 1,
|
|
||||||
"global_yes": True,
|
|
||||||
"global_no": True,
|
|
||||||
"global_abstain": True,
|
|
||||||
"amount_global_yes": None,
|
|
||||||
"amount_global_no": None,
|
|
||||||
"amount_global_abstain": None,
|
|
||||||
"groups_id": [GROUP_DELEGATE_PK],
|
|
||||||
"id": 1,
|
|
||||||
"options_id": [1],
|
|
||||||
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
|
|
||||||
"state": AssignmentPoll.STATE_STARTED,
|
|
||||||
"title": self.poll.title,
|
|
||||||
"description": self.description,
|
|
||||||
"type": AssignmentPoll.TYPE_NAMED,
|
|
||||||
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
|
|
||||||
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
|
|
||||||
"min_votes_amount": 1,
|
|
||||||
"max_votes_amount": 1,
|
|
||||||
"votescast": "1.000000",
|
|
||||||
"votesinvalid": "0.000000",
|
|
||||||
"votesvalid": "1.000000",
|
|
||||||
"user_has_voted": False,
|
|
||||||
"user_has_voted_for_delegations": [],
|
|
||||||
},
|
|
||||||
"assignments/assignment-option:1": {
|
|
||||||
"abstain": "1.000000",
|
|
||||||
"id": 1,
|
|
||||||
"no": "0.000000",
|
|
||||||
"poll_id": 1,
|
|
||||||
"pollstate": AssignmentPoll.STATE_STARTED,
|
|
||||||
"yes": "0.000000",
|
|
||||||
"user_id": 1,
|
|
||||||
"weight": 1,
|
|
||||||
},
|
|
||||||
"assignments/assignment-vote:1": {
|
|
||||||
"id": 1,
|
|
||||||
"option_id": 1,
|
|
||||||
"pollstate": AssignmentPoll.STATE_STARTED,
|
|
||||||
"user_id": self.user.id,
|
|
||||||
"delegated_user_id": self.user.id,
|
|
||||||
"value": "A",
|
|
||||||
"weight": "1.000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(autoupdate[1], [])
|
|
||||||
|
|
||||||
# Expect user to receive his vote
|
|
||||||
autoupdate = self.get_last_autoupdate(user=self.user)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0]["assignments/assignment-vote:1"],
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"option_id": 1,
|
|
||||||
"pollstate": AssignmentPoll.STATE_STARTED,
|
|
||||||
"user_id": self.user.id,
|
|
||||||
"delegated_user_id": self.user.id,
|
|
||||||
"value": "A",
|
|
||||||
"weight": "1.000000",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(autoupdate[1], [])
|
|
||||||
|
|
||||||
# Expect non-admins to get a restricted poll update
|
|
||||||
for user in (self.user, self.other_user):
|
|
||||||
self.assertAutoupdate(poll, user=user)
|
|
||||||
autoupdate = self.get_last_autoupdate(user=user)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0]["assignments/assignment-poll:1"],
|
|
||||||
{
|
|
||||||
"allow_multiple_votes_per_candidate": False,
|
|
||||||
"assignment_id": 1,
|
|
||||||
"global_yes": True,
|
|
||||||
"global_no": True,
|
|
||||||
"global_abstain": True,
|
|
||||||
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
|
|
||||||
"state": AssignmentPoll.STATE_STARTED,
|
|
||||||
"type": AssignmentPoll.TYPE_NAMED,
|
|
||||||
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
|
|
||||||
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
|
|
||||||
"title": self.poll.title,
|
|
||||||
"description": self.description,
|
|
||||||
"groups_id": [GROUP_DELEGATE_PK],
|
|
||||||
"options_id": [1],
|
|
||||||
"id": 1,
|
|
||||||
"min_votes_amount": 1,
|
|
||||||
"max_votes_amount": 1,
|
|
||||||
"user_has_voted": user == self.user,
|
|
||||||
"user_has_voted_for_delegations": [],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Other users should not get a vote autoupdate
|
|
||||||
self.assertNoAutoupdate(vote, user=self.other_user)
|
|
||||||
self.assertNoDeletedAutoupdate(vote, user=self.other_user)
|
|
||||||
|
|
||||||
def test_publish(self):
|
|
||||||
option = self.poll.options.get()
|
|
||||||
vote = AssignmentVote.objects.create(user=self.user, option=option)
|
|
||||||
vote.value = "A"
|
|
||||||
vote.weight = Decimal("1")
|
|
||||||
vote.save(no_delete_on_restriction=True, skip_autoupdate=True)
|
|
||||||
self.poll.voted.add(self.user.id)
|
|
||||||
self.poll.state = AssignmentPoll.STATE_FINISHED
|
|
||||||
self.poll.save(skip_autoupdate=True)
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("assignmentpoll-publish", args=[self.poll.pk])
|
|
||||||
)
|
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
|
||||||
poll = AssignmentPoll.objects.get()
|
|
||||||
vote = AssignmentVote.objects.get()
|
|
||||||
|
|
||||||
# Everyone should get the whole data
|
|
||||||
for user in (
|
|
||||||
self.admin,
|
|
||||||
self.user,
|
|
||||||
self.other_user,
|
|
||||||
):
|
|
||||||
self.assertAutoupdate(poll, user=user)
|
|
||||||
autoupdate = self.get_last_autoupdate(user=user)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0]["assignments/assignment-poll:1"],
|
|
||||||
{
|
|
||||||
"allow_multiple_votes_per_candidate": False,
|
|
||||||
"amount_global_yes": None,
|
|
||||||
"amount_global_no": None,
|
|
||||||
"amount_global_abstain": None,
|
|
||||||
"assignment_id": 1,
|
|
||||||
"description": "test_description_paiquei5ahpie1wu8ohW",
|
|
||||||
"global_yes": True,
|
|
||||||
"global_no": True,
|
|
||||||
"global_abstain": True,
|
|
||||||
"groups_id": [GROUP_DELEGATE_PK],
|
|
||||||
"id": 1,
|
|
||||||
"majority_method": "two_thirds",
|
|
||||||
"onehundred_percent_base": "cast",
|
|
||||||
"options_id": [1],
|
|
||||||
"pollmethod": "YNA",
|
|
||||||
"state": 4,
|
|
||||||
"title": self.poll.title,
|
|
||||||
"type": "named",
|
|
||||||
"min_votes_amount": 1,
|
|
||||||
"max_votes_amount": 1,
|
|
||||||
"votescast": "1.000000",
|
|
||||||
"votesinvalid": "0.000000",
|
|
||||||
"votesvalid": "1.000000",
|
|
||||||
"user_has_voted": user == self.user,
|
|
||||||
"user_has_voted_for_delegations": [],
|
|
||||||
"voted_id": [self.user.id],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0]["assignments/assignment-vote:1"],
|
|
||||||
{
|
|
||||||
"pollstate": AssignmentPoll.STATE_PUBLISHED,
|
|
||||||
"id": 1,
|
|
||||||
"weight": "1.000000",
|
|
||||||
"value": "A",
|
|
||||||
"user_id": 3,
|
|
||||||
"delegated_user_id": None,
|
|
||||||
"option_id": 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0]["assignments/assignment-option:1"],
|
|
||||||
{
|
|
||||||
"abstain": "1.000000",
|
|
||||||
"id": 1,
|
|
||||||
"no": "0.000000",
|
|
||||||
"poll_id": 1,
|
|
||||||
"pollstate": AssignmentPoll.STATE_PUBLISHED,
|
|
||||||
"yes": "0.000000",
|
|
||||||
"user_id": 1,
|
|
||||||
"weight": 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertIn("users/user:3", autoupdate[0])
|
|
||||||
|
|
||||||
|
|
||||||
class VoteAssignmentPollPseudoanonymousAutoupdates(
|
|
||||||
VoteAssignmentPollAutoupdatesBaseClass
|
|
||||||
):
|
|
||||||
poll_type = AssignmentPoll.TYPE_PSEUDOANONYMOUS
|
|
||||||
|
|
||||||
def test_votex(self):
|
|
||||||
response = self.user_client.post(
|
|
||||||
reverse("assignmentpoll-vote", args=[self.poll.pk]), {"data": {"1": "A"}}
|
|
||||||
)
|
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
|
||||||
poll = AssignmentPoll.objects.get()
|
|
||||||
vote = AssignmentVote.objects.get()
|
|
||||||
|
|
||||||
# Expect the admin to see the full data in the autoupdate
|
|
||||||
autoupdate = self.get_last_autoupdate(user=self.admin)
|
|
||||||
# TODO: mypy complains without the Any type; check why and fix it
|
|
||||||
should_be: Any = {
|
|
||||||
"assignments/assignment-poll:1": {
|
|
||||||
"allow_multiple_votes_per_candidate": False,
|
|
||||||
"assignment_id": 1,
|
|
||||||
"global_yes": True,
|
|
||||||
"global_no": True,
|
|
||||||
"global_abstain": True,
|
|
||||||
"amount_global_yes": None,
|
|
||||||
"amount_global_no": None,
|
|
||||||
"amount_global_abstain": None,
|
|
||||||
"groups_id": [GROUP_DELEGATE_PK],
|
|
||||||
"id": 1,
|
|
||||||
"options_id": [1],
|
|
||||||
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
|
|
||||||
"state": AssignmentPoll.STATE_STARTED,
|
|
||||||
"title": self.poll.title,
|
|
||||||
"description": self.description,
|
|
||||||
"type": AssignmentPoll.TYPE_PSEUDOANONYMOUS,
|
|
||||||
"user_has_voted": False,
|
|
||||||
"user_has_voted_for_delegations": [],
|
|
||||||
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
|
|
||||||
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
|
|
||||||
"min_votes_amount": 1,
|
|
||||||
"max_votes_amount": 1,
|
|
||||||
"votescast": "1.000000",
|
|
||||||
"votesinvalid": "0.000000",
|
|
||||||
"votesvalid": "1.000000",
|
|
||||||
},
|
|
||||||
"assignments/assignment-option:1": {
|
|
||||||
"abstain": "1.000000",
|
|
||||||
"id": 1,
|
|
||||||
"no": "0.000000",
|
|
||||||
"poll_id": 1,
|
|
||||||
"pollstate": AssignmentPoll.STATE_STARTED,
|
|
||||||
"yes": "0.000000",
|
|
||||||
"user_id": 1,
|
|
||||||
"weight": 1,
|
|
||||||
},
|
|
||||||
"assignments/assignment-vote:1": {
|
|
||||||
"id": 1,
|
|
||||||
"option_id": 1,
|
|
||||||
"pollstate": AssignmentPoll.STATE_STARTED,
|
|
||||||
"user_id": None,
|
|
||||||
"delegated_user_id": None,
|
|
||||||
"value": "A",
|
|
||||||
"weight": "1.000000",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
self.assertEqual(autoupdate[0], should_be)
|
|
||||||
self.assertEqual(autoupdate[1], [])
|
|
||||||
|
|
||||||
# Expect non-admins to get a restricted poll update and no autoupdate
|
|
||||||
# for a changed vote nor a deleted one
|
|
||||||
for user in (self.user, self.other_user):
|
|
||||||
self.assertAutoupdate(poll, user=user)
|
|
||||||
autoupdate = self.get_last_autoupdate(user=user)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0]["assignments/assignment-poll:1"],
|
|
||||||
{
|
|
||||||
"allow_multiple_votes_per_candidate": False,
|
|
||||||
"assignment_id": 1,
|
|
||||||
"global_yes": True,
|
|
||||||
"global_no": True,
|
|
||||||
"global_abstain": True,
|
|
||||||
"pollmethod": AssignmentPoll.POLLMETHOD_YNA,
|
|
||||||
"state": AssignmentPoll.STATE_STARTED,
|
|
||||||
"type": AssignmentPoll.TYPE_PSEUDOANONYMOUS,
|
|
||||||
"onehundred_percent_base": AssignmentPoll.PERCENT_BASE_CAST,
|
|
||||||
"majority_method": AssignmentPoll.MAJORITY_TWO_THIRDS,
|
|
||||||
"title": self.poll.title,
|
|
||||||
"description": self.description,
|
|
||||||
"groups_id": [GROUP_DELEGATE_PK],
|
|
||||||
"options_id": [1],
|
|
||||||
"id": 1,
|
|
||||||
"min_votes_amount": 1,
|
|
||||||
"max_votes_amount": 1,
|
|
||||||
"user_has_voted": user == self.user,
|
|
||||||
"user_has_voted_for_delegations": [],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertNoAutoupdate(vote, user=user)
|
|
||||||
self.assertNoDeletedAutoupdate(vote, user=user)
|
|
||||||
|
|
||||||
def test_publish(self):
|
|
||||||
option = self.poll.options.get()
|
|
||||||
vote = AssignmentVote.objects.create(option=option)
|
|
||||||
vote.value = "A"
|
|
||||||
vote.weight = Decimal("1")
|
|
||||||
vote.save(no_delete_on_restriction=True, skip_autoupdate=True)
|
|
||||||
self.poll.voted.add(self.user.id)
|
|
||||||
self.poll.state = AssignmentPoll.STATE_FINISHED
|
|
||||||
self.poll.save(skip_autoupdate=True)
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("assignmentpoll-publish", args=[self.poll.pk])
|
|
||||||
)
|
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
|
||||||
poll = AssignmentPoll.objects.get()
|
|
||||||
vote = AssignmentVote.objects.get()
|
|
||||||
|
|
||||||
# Everyone should get the whole data
|
|
||||||
for user in (
|
|
||||||
self.admin,
|
|
||||||
self.user,
|
|
||||||
self.other_user,
|
|
||||||
):
|
|
||||||
self.assertAutoupdate(poll, user=user)
|
|
||||||
autoupdate = self.get_last_autoupdate(user=user)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0],
|
|
||||||
{
|
|
||||||
"assignments/assignment-poll:1": {
|
|
||||||
"allow_multiple_votes_per_candidate": False,
|
|
||||||
"amount_global_yes": None,
|
|
||||||
"amount_global_no": None,
|
|
||||||
"amount_global_abstain": None,
|
|
||||||
"assignment_id": 1,
|
|
||||||
"description": "test_description_paiquei5ahpie1wu8ohW",
|
|
||||||
"global_yes": True,
|
|
||||||
"global_no": True,
|
|
||||||
"global_abstain": True,
|
|
||||||
"groups_id": [GROUP_DELEGATE_PK],
|
|
||||||
"id": 1,
|
|
||||||
"majority_method": "two_thirds",
|
|
||||||
"onehundred_percent_base": "cast",
|
|
||||||
"options_id": [1],
|
|
||||||
"pollmethod": "YNA",
|
|
||||||
"state": 4,
|
|
||||||
"title": self.poll.title,
|
|
||||||
"type": AssignmentPoll.TYPE_PSEUDOANONYMOUS,
|
|
||||||
"min_votes_amount": 1,
|
|
||||||
"max_votes_amount": 1,
|
|
||||||
"votescast": "1.000000",
|
|
||||||
"votesinvalid": "0.000000",
|
|
||||||
"votesvalid": "1.000000",
|
|
||||||
"user_has_voted": user == self.user,
|
|
||||||
"user_has_voted_for_delegations": [],
|
|
||||||
"voted_id": [self.user.id],
|
|
||||||
},
|
|
||||||
"assignments/assignment-vote:1": {
|
|
||||||
"pollstate": AssignmentPoll.STATE_PUBLISHED,
|
|
||||||
"id": 1,
|
|
||||||
"weight": "1.000000",
|
|
||||||
"value": "A",
|
|
||||||
"user_id": None,
|
|
||||||
"delegated_user_id": None,
|
|
||||||
"option_id": 1,
|
|
||||||
},
|
|
||||||
"assignments/assignment-option:1": {
|
|
||||||
"abstain": "1.000000",
|
|
||||||
"id": 1,
|
|
||||||
"no": "0.000000",
|
|
||||||
"poll_id": 1,
|
|
||||||
"pollstate": AssignmentPoll.STATE_PUBLISHED,
|
|
||||||
"yes": "0.000000",
|
|
||||||
"user_id": 1,
|
|
||||||
"weight": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PseudoanonymizeAssignmentPoll(TestCase):
|
class PseudoanonymizeAssignmentPoll(TestCase):
|
||||||
def advancedSetUp(self):
|
def advancedSetUp(self):
|
||||||
self.assignment = Assignment.objects.create(
|
self.assignment = Assignment.objects.create(
|
||||||
|
@ -12,20 +12,6 @@ from openslides.utils.rest_api import ValidationError
|
|||||||
from tests.test_case import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db(transaction=False)
|
|
||||||
def test_slide_on_default_projector(client):
|
|
||||||
client.login(username="admin", password="admin")
|
|
||||||
client.put(
|
|
||||||
reverse("projector-detail", args=["1"]),
|
|
||||||
{"elements": [{"name": "topics/topic", "id": 1}]},
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
|
|
||||||
response = client.get(reverse("projector-detail", args=["1"]))
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db(transaction=False)
|
@pytest.mark.django_db(transaction=False)
|
||||||
def test_invalid_element_non_existing_slide(client):
|
def test_invalid_element_non_existing_slide(client):
|
||||||
client.login(username="admin", password="admin")
|
client.login(username="admin", password="admin")
|
||||||
|
@ -213,9 +213,6 @@ class TestCreation(TestCase):
|
|||||||
self.assertFalse(Mediafile.objects.exists())
|
self.assertFalse(Mediafile.objects.exists())
|
||||||
|
|
||||||
|
|
||||||
# TODO: List and retrieve
|
|
||||||
|
|
||||||
|
|
||||||
class TestUpdate(TestCase):
|
class TestUpdate(TestCase):
|
||||||
"""
|
"""
|
||||||
Tree:
|
Tree:
|
||||||
|
@ -20,9 +20,8 @@ from openslides.motions.models import (
|
|||||||
Workflow,
|
Workflow,
|
||||||
)
|
)
|
||||||
from openslides.poll.models import BasePoll
|
from openslides.poll.models import BasePoll
|
||||||
from openslides.utils.auth import get_group_model
|
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from tests.common_groups import GROUP_ADMIN_PK, GROUP_DEFAULT_PK, GROUP_DELEGATE_PK
|
from tests.common_groups import GROUP_ADMIN_PK, GROUP_DELEGATE_PK
|
||||||
from tests.count_queries import count_queries
|
from tests.count_queries import count_queries
|
||||||
from tests.test_case import TestCase
|
from tests.test_case import TestCase
|
||||||
|
|
||||||
@ -120,78 +119,6 @@ class CreateMotion(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
motion = Motion.objects.get()
|
motion = Motion.objects.get()
|
||||||
changed_autoupdate, deleted_autoupdate = self.get_last_autoupdate()
|
|
||||||
del changed_autoupdate["motions/motion:1"]["created"]
|
|
||||||
del changed_autoupdate["motions/motion:1"]["last_modified"]
|
|
||||||
self.assertEqual(
|
|
||||||
changed_autoupdate,
|
|
||||||
{
|
|
||||||
"agenda/list-of-speakers:1": {
|
|
||||||
"id": 1,
|
|
||||||
"title_information": {
|
|
||||||
"title": "test_title_OoCoo3MeiT9li5Iengu9",
|
|
||||||
"identifier": "1",
|
|
||||||
},
|
|
||||||
"speakers": [],
|
|
||||||
"closed": False,
|
|
||||||
"content_object": {"collection": "motions/motion", "id": 1},
|
|
||||||
},
|
|
||||||
"motions/motion:1": {
|
|
||||||
"id": 1,
|
|
||||||
"identifier": "1",
|
|
||||||
"title": "test_title_OoCoo3MeiT9li5Iengu9",
|
|
||||||
"text": "test_text_thuoz0iecheiheereiCi",
|
|
||||||
"amendment_paragraphs": None,
|
|
||||||
"amendments_id": [],
|
|
||||||
"modified_final_version": "",
|
|
||||||
"reason": "",
|
|
||||||
"parent_id": None,
|
|
||||||
"category_id": None,
|
|
||||||
"category_weight": 10000,
|
|
||||||
"comments": [],
|
|
||||||
"motion_block_id": None,
|
|
||||||
"origin": "",
|
|
||||||
"submitters": [
|
|
||||||
{"id": 1, "user_id": 1, "motion_id": 1, "weight": 1}
|
|
||||||
],
|
|
||||||
"supporters_id": [],
|
|
||||||
"state_id": 1,
|
|
||||||
"state_extension": None,
|
|
||||||
"state_restriction": [],
|
|
||||||
"statute_paragraph_id": None,
|
|
||||||
"workflow_id": 1,
|
|
||||||
"recommendation_id": None,
|
|
||||||
"recommendation_extension": None,
|
|
||||||
"tags_id": [],
|
|
||||||
"attachments_id": [],
|
|
||||||
"agenda_item_id": 1,
|
|
||||||
"list_of_speakers_id": 1,
|
|
||||||
"sort_parent_id": None,
|
|
||||||
"weight": 10000,
|
|
||||||
"change_recommendations_id": [],
|
|
||||||
},
|
|
||||||
"agenda/item:1": {
|
|
||||||
"id": 1,
|
|
||||||
"item_number": "",
|
|
||||||
"title_information": {
|
|
||||||
"title": "test_title_OoCoo3MeiT9li5Iengu9",
|
|
||||||
"identifier": "1",
|
|
||||||
},
|
|
||||||
"comment": None,
|
|
||||||
"closed": False,
|
|
||||||
"type": 3,
|
|
||||||
"is_internal": False,
|
|
||||||
"is_hidden": True,
|
|
||||||
"duration": None,
|
|
||||||
"content_object": {"collection": "motions/motion", "id": 1},
|
|
||||||
"weight": 10000,
|
|
||||||
"parent_id": None,
|
|
||||||
"level": 0,
|
|
||||||
"tags_id": [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(deleted_autoupdate, [])
|
|
||||||
self.assertEqual(motion.title, "test_title_OoCoo3MeiT9li5Iengu9")
|
self.assertEqual(motion.title, "test_title_OoCoo3MeiT9li5Iengu9")
|
||||||
self.assertEqual(motion.identifier, "1")
|
self.assertEqual(motion.identifier, "1")
|
||||||
self.assertTrue(motion.submitters.exists())
|
self.assertTrue(motion.submitters.exists())
|
||||||
@ -412,92 +339,6 @@ class CreateMotion(TestCase):
|
|||||||
return Motion.objects.get(pk=int(response.data["id"]))
|
return Motion.objects.get(pk=int(response.data["id"]))
|
||||||
|
|
||||||
|
|
||||||
class RetrieveMotion(TestCase):
|
|
||||||
"""
|
|
||||||
Tests retrieving a motion
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.client = APIClient()
|
|
||||||
self.client.login(username="admin", password="admin")
|
|
||||||
self.motion = Motion(
|
|
||||||
title="test_title_uj5eeSiedohSh3ohyaaj",
|
|
||||||
text="test_text_ithohchaeThohmae5aug",
|
|
||||||
)
|
|
||||||
self.motion.save()
|
|
||||||
for index in range(10):
|
|
||||||
get_user_model().objects.create_user(
|
|
||||||
username=f"user_{index}", password="password"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_guest_state_with_restriction(self):
|
|
||||||
config["general_system_enable_anonymous"] = True
|
|
||||||
guest_client = APIClient()
|
|
||||||
state = self.motion.state
|
|
||||||
state.restriction = ["motions.can_manage"]
|
|
||||||
state.save()
|
|
||||||
# The cache has to be cleared, see:
|
|
||||||
# https://github.com/OpenSlides/OpenSlides/issues/3396
|
|
||||||
inform_changed_data(self.motion)
|
|
||||||
|
|
||||||
response = guest_client.get(reverse("motion-detail", args=[self.motion.pk]))
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
|
|
||||||
def test_admin_state_with_restriction(self):
|
|
||||||
state = self.motion.state
|
|
||||||
state.restriction = ["motions.can_manage"]
|
|
||||||
state.save()
|
|
||||||
response = self.client.get(reverse("motion-detail", args=[self.motion.pk]))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def test_submitter_state_with_restriction(self):
|
|
||||||
state = self.motion.state
|
|
||||||
state.restriction = ["is_submitter"]
|
|
||||||
state.save()
|
|
||||||
user = get_user_model().objects.create_user(
|
|
||||||
username="username_ohS2opheikaSa5theijo",
|
|
||||||
password="password_kau4eequaisheeBateef",
|
|
||||||
)
|
|
||||||
Submitter.objects.add(user, self.motion)
|
|
||||||
submitter_client = APIClient()
|
|
||||||
submitter_client.force_login(user)
|
|
||||||
response = submitter_client.get(reverse("motion-detail", args=[self.motion.pk]))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def test_user_without_can_see_user_permission_to_see_motion_and_submitter_data(
|
|
||||||
self,
|
|
||||||
):
|
|
||||||
admin = get_user_model().objects.get(username="admin")
|
|
||||||
Submitter.objects.add(admin, self.motion)
|
|
||||||
group = get_group_model().objects.get(
|
|
||||||
pk=GROUP_DEFAULT_PK
|
|
||||||
) # Group with pk 1 is for anonymous and default users.
|
|
||||||
permission_string = "users.can_see_name"
|
|
||||||
app_label, codename = permission_string.split(".")
|
|
||||||
permission = group.permissions.get(
|
|
||||||
content_type__app_label=app_label, codename=codename
|
|
||||||
)
|
|
||||||
group.permissions.remove(permission)
|
|
||||||
config["general_system_enable_anonymous"] = True
|
|
||||||
guest_client = APIClient()
|
|
||||||
inform_changed_data(group)
|
|
||||||
inform_changed_data(self.motion)
|
|
||||||
|
|
||||||
response_1 = guest_client.get(reverse("motion-detail", args=[self.motion.pk]))
|
|
||||||
self.assertEqual(response_1.status_code, status.HTTP_200_OK)
|
|
||||||
submitter_id = response_1.data["submitters"][0]["user_id"]
|
|
||||||
response_2 = guest_client.get(reverse("user-detail", args=[submitter_id]))
|
|
||||||
self.assertEqual(response_2.status_code, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
extra_user = get_user_model().objects.create_user(
|
|
||||||
username="username_wequePhieFoom0hai3wa",
|
|
||||||
password="password_ooth7taechai5Oocieya",
|
|
||||||
)
|
|
||||||
|
|
||||||
response_3 = guest_client.get(reverse("user-detail", args=[extra_user.pk]))
|
|
||||||
self.assertEqual(response_3.status_code, 404)
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateMotion(TestCase):
|
class UpdateMotion(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests updating motions.
|
Tests updating motions.
|
||||||
@ -519,9 +360,6 @@ class UpdateMotion(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
motion = Motion.objects.get()
|
motion = Motion.objects.get()
|
||||||
self.assertAutoupdate(motion)
|
|
||||||
self.assertAutoupdate(motion.agenda_item)
|
|
||||||
self.assertAutoupdate(motion.list_of_speakers)
|
|
||||||
self.assertEqual(motion.title, "test_title_aeng7ahChie3waiR8xoh")
|
self.assertEqual(motion.title, "test_title_aeng7ahChie3waiR8xoh")
|
||||||
self.assertEqual(motion.identifier, "test_identifier_jieseghohj7OoSah1Ko9")
|
self.assertEqual(motion.identifier, "test_identifier_jieseghohj7OoSah1Ko9")
|
||||||
|
|
||||||
|
@ -134,43 +134,6 @@ class CreateMotionPoll(TestCase):
|
|||||||
poll = MotionPoll.objects.get()
|
poll = MotionPoll.objects.get()
|
||||||
self.assertEqual(poll.pollmethod, "YNA")
|
self.assertEqual(poll.pollmethod, "YNA")
|
||||||
|
|
||||||
def test_autoupdate(self):
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("motionpoll-list"),
|
|
||||||
{
|
|
||||||
"title": "test_title_9Ce8OsdB8YWTVm5YOzqH",
|
|
||||||
"pollmethod": "YNA",
|
|
||||||
"type": "named",
|
|
||||||
"motion_id": self.motion.id,
|
|
||||||
"onehundred_percent_base": "YN",
|
|
||||||
"majority_method": "simple",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
autoupdate = self.get_last_autoupdate(user=self.admin)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0]["motions/motion-poll:1"],
|
|
||||||
{
|
|
||||||
"motion_id": 1,
|
|
||||||
"pollmethod": MotionPoll.POLLMETHOD_YNA,
|
|
||||||
"state": MotionPoll.STATE_CREATED,
|
|
||||||
"type": MotionPoll.TYPE_NAMED,
|
|
||||||
"title": "test_title_9Ce8OsdB8YWTVm5YOzqH",
|
|
||||||
"onehundred_percent_base": MotionPoll.PERCENT_BASE_YN,
|
|
||||||
"majority_method": MotionPoll.MAJORITY_SIMPLE,
|
|
||||||
"groups_id": [],
|
|
||||||
"votesvalid": "0.000000",
|
|
||||||
"votesinvalid": "0.000000",
|
|
||||||
"votescast": "0.000000",
|
|
||||||
"options_id": [1],
|
|
||||||
"id": 1,
|
|
||||||
"user_has_voted": False,
|
|
||||||
"user_has_voted_for_delegations": [],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(autoupdate[1], [])
|
|
||||||
|
|
||||||
def test_missing_keys(self):
|
def test_missing_keys(self):
|
||||||
complete_request_data = {
|
complete_request_data = {
|
||||||
"title": "test_title_OoCh9aitaeyaeth8nom1",
|
"title": "test_title_OoCh9aitaeyaeth8nom1",
|
||||||
@ -631,7 +594,6 @@ class VoteMotionPollAnalog(TestCase):
|
|||||||
self.assertEqual(option.yes, Decimal("1"))
|
self.assertEqual(option.yes, Decimal("1"))
|
||||||
self.assertEqual(option.no, Decimal("2.35"))
|
self.assertEqual(option.no, Decimal("2.35"))
|
||||||
self.assertEqual(option.abstain, Decimal("-1"))
|
self.assertEqual(option.abstain, Decimal("-1"))
|
||||||
self.assertAutoupdate(poll)
|
|
||||||
|
|
||||||
def test_vote_no_permissions(self):
|
def test_vote_no_permissions(self):
|
||||||
self.start_poll()
|
self.start_poll()
|
||||||
@ -924,13 +886,6 @@ class VoteMotionPollNamed(TestCase):
|
|||||||
self.assertEqual(vote.user, self.user)
|
self.assertEqual(vote.user, self.user)
|
||||||
self.assertEqual(vote.delegated_user, self.admin)
|
self.assertEqual(vote.delegated_user, self.admin)
|
||||||
|
|
||||||
autoupdate = self.get_last_autoupdate(user=self.admin)
|
|
||||||
self.assertIn("motions/motion-poll:1", autoupdate[0])
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0]["motions/motion-poll:1"]["user_has_voted_for_delegations"],
|
|
||||||
[self.user.pk],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_vote_delegation_and_self_vote(self):
|
def test_vote_delegation_and_self_vote(self):
|
||||||
self.test_vote_delegation()
|
self.test_vote_delegation()
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -1015,262 +970,6 @@ class VoteMotionPollNamed(TestCase):
|
|||||||
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
self.assertFalse(MotionPoll.objects.get().get_votes().exists())
|
||||||
|
|
||||||
|
|
||||||
class VoteMotionPollNamedAutoupdates(TestCase):
|
|
||||||
"""3 important users:
|
|
||||||
self.admin: manager, has can_see, can_manage, can_manage_polls (in admin group)
|
|
||||||
self.user1: votes, has can_see perms and in in delegate group
|
|
||||||
self.other_user: Just has can_see perms and is NOT in the delegate group.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def advancedSetUp(self):
|
|
||||||
self.motion = Motion(
|
|
||||||
title="test_title_OoK9IeChe2Jeib9Deeji",
|
|
||||||
text="test_text_eichui1oobiSeit9aifo",
|
|
||||||
)
|
|
||||||
self.motion.save()
|
|
||||||
self.delegate_group = get_group_model().objects.get(pk=GROUP_DELEGATE_PK)
|
|
||||||
self.other_user, _ = self.create_user()
|
|
||||||
inform_changed_data(self.other_user)
|
|
||||||
|
|
||||||
self.user, user_password = self.create_user()
|
|
||||||
self.user.groups.add(self.delegate_group)
|
|
||||||
self.user.is_present = True
|
|
||||||
self.user.save()
|
|
||||||
self.user_client = APIClient()
|
|
||||||
self.user_client.login(username=self.user.username, password=user_password)
|
|
||||||
|
|
||||||
self.poll = MotionPoll.objects.create(
|
|
||||||
motion=self.motion,
|
|
||||||
title="test_title_tho8PhiePh8upaex6phi",
|
|
||||||
pollmethod="YNA",
|
|
||||||
type=BasePoll.TYPE_NAMED,
|
|
||||||
state=MotionPoll.STATE_STARTED,
|
|
||||||
onehundred_percent_base="YN",
|
|
||||||
majority_method="simple",
|
|
||||||
)
|
|
||||||
self.poll.create_options()
|
|
||||||
self.poll.groups.add(self.delegate_group)
|
|
||||||
|
|
||||||
def test_vote(self):
|
|
||||||
response = self.user_client.post(
|
|
||||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"}
|
|
||||||
)
|
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
|
||||||
poll = MotionPoll.objects.get()
|
|
||||||
vote = MotionVote.objects.get()
|
|
||||||
|
|
||||||
# Expect the admin to see the full data in the autoupdate
|
|
||||||
autoupdate = self.get_last_autoupdate(user=self.admin)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0],
|
|
||||||
{
|
|
||||||
"motions/motion-poll:1": {
|
|
||||||
"motion_id": 1,
|
|
||||||
"pollmethod": "YNA",
|
|
||||||
"state": 2,
|
|
||||||
"type": "named",
|
|
||||||
"title": "test_title_tho8PhiePh8upaex6phi",
|
|
||||||
"onehundred_percent_base": "YN",
|
|
||||||
"majority_method": "simple",
|
|
||||||
"groups_id": [GROUP_DELEGATE_PK],
|
|
||||||
"votesvalid": "1.000000",
|
|
||||||
"votesinvalid": "0.000000",
|
|
||||||
"votescast": "1.000000",
|
|
||||||
"options_id": [1],
|
|
||||||
"id": 1,
|
|
||||||
"user_has_voted": False,
|
|
||||||
"user_has_voted_for_delegations": [],
|
|
||||||
},
|
|
||||||
"motions/motion-vote:1": {
|
|
||||||
"pollstate": 2,
|
|
||||||
"id": 1,
|
|
||||||
"weight": "1.000000",
|
|
||||||
"value": "A",
|
|
||||||
"user_id": self.user.id,
|
|
||||||
"delegated_user_id": self.user.id,
|
|
||||||
"option_id": 1,
|
|
||||||
},
|
|
||||||
"motions/motion-option:1": {
|
|
||||||
"abstain": "1.000000",
|
|
||||||
"id": 1,
|
|
||||||
"no": "0.000000",
|
|
||||||
"poll_id": 1,
|
|
||||||
"pollstate": 2,
|
|
||||||
"yes": "0.000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(autoupdate[1], [])
|
|
||||||
|
|
||||||
# Expect user1 to receive his vote
|
|
||||||
autoupdate = self.get_last_autoupdate(user=self.user)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0]["motions/motion-vote:1"],
|
|
||||||
{
|
|
||||||
"pollstate": 2,
|
|
||||||
"option_id": 1,
|
|
||||||
"id": 1,
|
|
||||||
"weight": "1.000000",
|
|
||||||
"value": "A",
|
|
||||||
"user_id": self.user.id,
|
|
||||||
"delegated_user_id": self.user.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0]["motions/motion-option:1"],
|
|
||||||
{"id": 1, "poll_id": 1, "pollstate": 2},
|
|
||||||
)
|
|
||||||
self.assertEqual(autoupdate[1], [])
|
|
||||||
|
|
||||||
# Expect non-admins to get a restricted poll update
|
|
||||||
for user in (self.user, self.other_user):
|
|
||||||
self.assertAutoupdate(poll, user=user)
|
|
||||||
autoupdate = self.get_last_autoupdate(user=user)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0]["motions/motion-poll:1"],
|
|
||||||
{
|
|
||||||
"motion_id": 1,
|
|
||||||
"pollmethod": "YNA",
|
|
||||||
"state": 2,
|
|
||||||
"type": "named",
|
|
||||||
"title": "test_title_tho8PhiePh8upaex6phi",
|
|
||||||
"onehundred_percent_base": "YN",
|
|
||||||
"majority_method": "simple",
|
|
||||||
"groups_id": [GROUP_DELEGATE_PK],
|
|
||||||
"options_id": [1],
|
|
||||||
"id": 1,
|
|
||||||
"user_has_voted": user == self.user,
|
|
||||||
"user_has_voted_for_delegations": [],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0]["motions/motion-option:1"],
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"poll_id": 1,
|
|
||||||
"pollstate": 2,
|
|
||||||
}, # noqa black and flake are no friends :(
|
|
||||||
)
|
|
||||||
|
|
||||||
# Other users should not get a vote autoupdate
|
|
||||||
self.assertNoAutoupdate(vote, user=self.other_user)
|
|
||||||
self.assertNoDeletedAutoupdate(vote, user=self.other_user)
|
|
||||||
|
|
||||||
|
|
||||||
class VoteMotionPollPseudoanonymousAutoupdates(TestCase):
|
|
||||||
"""3 important users:
|
|
||||||
self.admin: manager, has can_see, can_manage, can_manage_polls (in admin group)
|
|
||||||
self.user: votes, has can_see perms and in in delegate group
|
|
||||||
self.other_user: Just has can_see perms and is NOT in the delegate group.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def advancedSetUp(self):
|
|
||||||
self.motion = Motion(
|
|
||||||
title="test_title_OoK9IeChe2Jeib9Deeji",
|
|
||||||
text="test_text_eichui1oobiSeit9aifo",
|
|
||||||
)
|
|
||||||
self.motion.save()
|
|
||||||
self.delegate_group = get_group_model().objects.get(pk=GROUP_DELEGATE_PK)
|
|
||||||
self.other_user, _ = self.create_user()
|
|
||||||
inform_changed_data(self.other_user)
|
|
||||||
|
|
||||||
self.user, user_password = self.create_user()
|
|
||||||
self.user.groups.add(self.delegate_group)
|
|
||||||
self.user.is_present = True
|
|
||||||
self.user.save()
|
|
||||||
self.user_client = APIClient()
|
|
||||||
self.user_client.login(username=self.user.username, password=user_password)
|
|
||||||
|
|
||||||
self.poll = MotionPoll.objects.create(
|
|
||||||
motion=self.motion,
|
|
||||||
title="test_title_cahP1umooteehah2jeey",
|
|
||||||
pollmethod="YNA",
|
|
||||||
type=BasePoll.TYPE_PSEUDOANONYMOUS,
|
|
||||||
state=MotionPoll.STATE_STARTED,
|
|
||||||
onehundred_percent_base="YN",
|
|
||||||
majority_method="simple",
|
|
||||||
)
|
|
||||||
self.poll.create_options()
|
|
||||||
self.poll.groups.add(self.delegate_group)
|
|
||||||
|
|
||||||
def test_vote(self):
|
|
||||||
response = self.user_client.post(
|
|
||||||
reverse("motionpoll-vote", args=[self.poll.pk]), {"data": "A"}
|
|
||||||
)
|
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
|
||||||
poll = MotionPoll.objects.get()
|
|
||||||
vote = MotionVote.objects.get()
|
|
||||||
|
|
||||||
# Expect the admin to see the full data in the autoupdate
|
|
||||||
autoupdate = self.get_last_autoupdate(user=self.admin)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0],
|
|
||||||
{
|
|
||||||
"motions/motion-poll:1": {
|
|
||||||
"motion_id": 1,
|
|
||||||
"pollmethod": "YNA",
|
|
||||||
"state": 2,
|
|
||||||
"type": "pseudoanonymous",
|
|
||||||
"title": "test_title_cahP1umooteehah2jeey",
|
|
||||||
"onehundred_percent_base": "YN",
|
|
||||||
"majority_method": "simple",
|
|
||||||
"groups_id": [GROUP_DELEGATE_PK],
|
|
||||||
"votesvalid": "1.000000",
|
|
||||||
"votesinvalid": "0.000000",
|
|
||||||
"votescast": "1.000000",
|
|
||||||
"options_id": [1],
|
|
||||||
"id": 1,
|
|
||||||
"user_has_voted": False,
|
|
||||||
"user_has_voted_for_delegations": [],
|
|
||||||
},
|
|
||||||
"motions/motion-vote:1": {
|
|
||||||
"pollstate": 2,
|
|
||||||
"option_id": 1,
|
|
||||||
"id": 1,
|
|
||||||
"weight": "1.000000",
|
|
||||||
"value": "A",
|
|
||||||
"user_id": None,
|
|
||||||
"delegated_user_id": None,
|
|
||||||
},
|
|
||||||
"motions/motion-option:1": {
|
|
||||||
"abstain": "1.000000",
|
|
||||||
"id": 1,
|
|
||||||
"no": "0.000000",
|
|
||||||
"poll_id": 1,
|
|
||||||
"pollstate": 2,
|
|
||||||
"yes": "0.000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(autoupdate[1], [])
|
|
||||||
|
|
||||||
# Expect non-admins to get a restricted poll update and no autoupdate
|
|
||||||
# for a changed vote nor a deleted one
|
|
||||||
for user in (self.user, self.other_user):
|
|
||||||
self.assertAutoupdate(poll, user=user)
|
|
||||||
autoupdate = self.get_last_autoupdate(user=user)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0]["motions/motion-poll:1"],
|
|
||||||
{
|
|
||||||
"motion_id": 1,
|
|
||||||
"pollmethod": "YNA",
|
|
||||||
"state": 2,
|
|
||||||
"type": "pseudoanonymous",
|
|
||||||
"title": "test_title_cahP1umooteehah2jeey",
|
|
||||||
"onehundred_percent_base": "YN",
|
|
||||||
"majority_method": "simple",
|
|
||||||
"groups_id": [GROUP_DELEGATE_PK],
|
|
||||||
"options_id": [1],
|
|
||||||
"id": 1,
|
|
||||||
"user_has_voted": user == self.user,
|
|
||||||
"user_has_voted_for_delegations": [],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertNoAutoupdate(vote, user=user)
|
|
||||||
self.assertNoDeletedAutoupdate(vote, user=user)
|
|
||||||
|
|
||||||
|
|
||||||
class VoteMotionPollPseudoanonymous(TestCase):
|
class VoteMotionPollPseudoanonymous(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = APIClient()
|
self.client = APIClient()
|
||||||
@ -1473,51 +1172,6 @@ class PublishMotionPoll(TestCase):
|
|||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(MotionPoll.objects.get().state, MotionPoll.STATE_PUBLISHED)
|
self.assertEqual(MotionPoll.objects.get().state, MotionPoll.STATE_PUBLISHED)
|
||||||
|
|
||||||
# Test autoupdates: Every user should get the full data
|
|
||||||
for user in (self.admin, self.user):
|
|
||||||
autoupdate = self.get_last_autoupdate(user=user)
|
|
||||||
self.assertEqual(
|
|
||||||
autoupdate[0],
|
|
||||||
{
|
|
||||||
"motions/motion-poll:1": {
|
|
||||||
"motion_id": 1,
|
|
||||||
"pollmethod": "YNA",
|
|
||||||
"state": 4,
|
|
||||||
"type": "pseudoanonymous",
|
|
||||||
"title": "test_title_Nufae0iew7Iorox2thoo",
|
|
||||||
"onehundred_percent_base": "YN",
|
|
||||||
"majority_method": "simple",
|
|
||||||
"groups_id": [],
|
|
||||||
"votesvalid": "0.000000",
|
|
||||||
"votesinvalid": "0.000000",
|
|
||||||
"votescast": "0.000000",
|
|
||||||
"options_id": [1],
|
|
||||||
"id": 1,
|
|
||||||
"user_has_voted": False,
|
|
||||||
"user_has_voted_for_delegations": [],
|
|
||||||
"voted_id": [],
|
|
||||||
},
|
|
||||||
"motions/motion-vote:1": {
|
|
||||||
"pollstate": 4,
|
|
||||||
"option_id": 1,
|
|
||||||
"id": 1,
|
|
||||||
"weight": "2.000000",
|
|
||||||
"value": "N",
|
|
||||||
"user_id": None,
|
|
||||||
"delegated_user_id": None,
|
|
||||||
},
|
|
||||||
"motions/motion-option:1": {
|
|
||||||
"abstain": "0.000000",
|
|
||||||
"id": 1,
|
|
||||||
"no": "2.000000",
|
|
||||||
"poll_id": 1,
|
|
||||||
"pollstate": 4,
|
|
||||||
"yes": "0.000000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(autoupdate[1], [])
|
|
||||||
|
|
||||||
def test_publish_wrong_state(self):
|
def test_publish_wrong_state(self):
|
||||||
response = self.client.post(reverse("motionpoll-publish", args=[self.poll.pk]))
|
response = self.client.post(reverse("motionpoll-publish", args=[self.poll.pk]))
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1638,16 +1292,6 @@ class ResetMotionPoll(TestCase):
|
|||||||
self.assertEqual(option.abstain, Decimal("0"))
|
self.assertEqual(option.abstain, Decimal("0"))
|
||||||
self.assertFalse(option.votes.exists())
|
self.assertFalse(option.votes.exists())
|
||||||
|
|
||||||
def test_deleted_autoupdate(self):
|
|
||||||
response = self.client.post(reverse("motionpoll-reset", args=[self.poll.pk]))
|
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
|
||||||
poll = MotionPoll.objects.get()
|
|
||||||
option = poll.options.get()
|
|
||||||
self.assertAutoupdate(option, self.admin)
|
|
||||||
for user in (self.admin, self.user1, self.user2):
|
|
||||||
self.assertDeletedAutoupdate(self.vote1, user=user)
|
|
||||||
self.assertDeletedAutoupdate(self.vote2, user=user)
|
|
||||||
|
|
||||||
|
|
||||||
class TestMotionPollWithVoteDelegationAutoupdate(TestCase):
|
class TestMotionPollWithVoteDelegationAutoupdate(TestCase):
|
||||||
def advancedSetUp(self):
|
def advancedSetUp(self):
|
||||||
@ -1685,7 +1329,3 @@ class TestMotionPollWithVoteDelegationAutoupdate(TestCase):
|
|||||||
def test_start_poll(self):
|
def test_start_poll(self):
|
||||||
response = self.client.post(reverse("motionpoll-start", args=[self.poll.pk]))
|
response = self.client.post(reverse("motionpoll-start", args=[self.poll.pk]))
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
||||||
|
|
||||||
# other_user has to receive an autoupdate because he was delegated
|
|
||||||
autoupdate = self.get_last_autoupdate(user=self.other_user)
|
|
||||||
assert "motions/motion-poll:1" in autoupdate[0]
|
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
from django.test.client import Client
|
|
||||||
|
|
||||||
from openslides.core.config import config
|
|
||||||
from openslides.motions.models import Motion
|
|
||||||
from tests.test_case import TestCase
|
|
||||||
|
|
||||||
|
|
||||||
class AnonymousRequests(TestCase):
|
|
||||||
"""
|
|
||||||
Tests requests from the anonymous user.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.client = Client()
|
|
||||||
config["general_system_enable_anonymous"] = True
|
|
||||||
|
|
||||||
def test_motion_detail(self):
|
|
||||||
Motion.objects.create(title="test_motion")
|
|
||||||
|
|
||||||
response = self.client.get("/motions/1/")
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
@ -9,7 +9,6 @@ from openslides.motions.models import (
|
|||||||
Category,
|
Category,
|
||||||
Motion,
|
Motion,
|
||||||
MotionBlock,
|
MotionBlock,
|
||||||
MotionChangeRecommendation,
|
|
||||||
MotionComment,
|
MotionComment,
|
||||||
MotionCommentSection,
|
MotionCommentSection,
|
||||||
State,
|
State,
|
||||||
@ -130,16 +129,6 @@ class TestStatuteParagraphs(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
def test_retrieve_simple(self):
|
|
||||||
self.create_statute_paragraph()
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("statuteparagraph-detail", args=[self.cp.pk])
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(
|
|
||||||
sorted(response.data.keys()), sorted(("id", "title", "text", "weight"))
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_update_simple(self):
|
def test_update_simple(self):
|
||||||
self.create_statute_paragraph()
|
self.create_statute_paragraph()
|
||||||
response = self.client.patch(
|
response = self.client.patch(
|
||||||
@ -236,37 +225,6 @@ class ManageComments(TestCase):
|
|||||||
self.section_read_write.read_groups.add(self.group_in)
|
self.section_read_write.read_groups.add(self.group_in)
|
||||||
self.section_read_write.write_groups.add(self.group_in)
|
self.section_read_write.write_groups.add(self.group_in)
|
||||||
|
|
||||||
def test_retrieve_comment(self):
|
|
||||||
comment = MotionComment(
|
|
||||||
motion=self.motion,
|
|
||||||
section=self.section_read_write,
|
|
||||||
comment="test_comment_gwic37Csc&3lf3eo2",
|
|
||||||
)
|
|
||||||
comment.save()
|
|
||||||
|
|
||||||
response = self.client.get(reverse("motion-detail", args=[self.motion.pk]))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertTrue("comments" in response.data)
|
|
||||||
comments = response.data["comments"]
|
|
||||||
self.assertTrue(isinstance(comments, list))
|
|
||||||
self.assertEqual(len(comments), 1)
|
|
||||||
self.assertEqual(comments[0]["comment"], "test_comment_gwic37Csc&3lf3eo2")
|
|
||||||
|
|
||||||
def test_retrieve_comment_no_read_permission(self):
|
|
||||||
comment = MotionComment(
|
|
||||||
motion=self.motion,
|
|
||||||
section=self.section_no_groups,
|
|
||||||
comment="test_comment_fgkj3C7veo3ijWE(j2DJ",
|
|
||||||
)
|
|
||||||
comment.save()
|
|
||||||
|
|
||||||
response = self.client.get(reverse("motion-detail", args=[self.motion.pk]))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertTrue("comments" in response.data)
|
|
||||||
comments = response.data["comments"]
|
|
||||||
self.assertTrue(isinstance(comments, list))
|
|
||||||
self.assertEqual(len(comments), 0)
|
|
||||||
|
|
||||||
def test_wrong_data_type(self):
|
def test_wrong_data_type(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("motion-manage-comments", args=[self.motion.pk]), None
|
reverse("motion-manage-comments", args=[self.motion.pk]), None
|
||||||
@ -428,58 +386,6 @@ class TestMotionCommentSection(TestCase):
|
|||||||
pk=GROUP_DELEGATE_PK
|
pk=GROUP_DELEGATE_PK
|
||||||
) # The admin should not be in this group
|
) # The admin should not be in this group
|
||||||
|
|
||||||
def test_retrieve(self):
|
|
||||||
"""
|
|
||||||
Checks, if the sections can be seen by a manager.
|
|
||||||
"""
|
|
||||||
section = MotionCommentSection(name="test_name_f3jOF3m8fp.<qiqmf32=")
|
|
||||||
section.save()
|
|
||||||
|
|
||||||
response = self.client.get(reverse("motioncommentsection-list"))
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertTrue(isinstance(response.data, list))
|
|
||||||
self.assertEqual(len(response.data), 1)
|
|
||||||
self.assertEqual(response.data[0]["name"], "test_name_f3jOF3m8fp.<qiqmf32=")
|
|
||||||
|
|
||||||
def test_retrieve_non_manager_with_read_permission(self):
|
|
||||||
"""
|
|
||||||
Checks, if the sections can be seen by a non manager, but he is in
|
|
||||||
one of the read_groups.
|
|
||||||
"""
|
|
||||||
self.admin.groups.remove(
|
|
||||||
self.group_in
|
|
||||||
) # group_in has motions.can_manage permission
|
|
||||||
self.admin.groups.add(self.group_out) # group_out does not.
|
|
||||||
inform_changed_data(self.admin)
|
|
||||||
|
|
||||||
section = MotionCommentSection(name="test_name_f3mMD28LMcm29Coelwcm")
|
|
||||||
section.save()
|
|
||||||
section.read_groups.add(self.group_out, self.group_in)
|
|
||||||
inform_changed_data(section)
|
|
||||||
|
|
||||||
response = self.client.get(reverse("motioncommentsection-list"))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(len(response.data), 1)
|
|
||||||
self.assertEqual(response.data[0]["name"], "test_name_f3mMD28LMcm29Coelwcm")
|
|
||||||
|
|
||||||
def test_retrieve_non_manager_no_read_permission(self):
|
|
||||||
"""
|
|
||||||
Checks, if sections are removed, if the user is a non manager and is in
|
|
||||||
any of the read_groups.
|
|
||||||
"""
|
|
||||||
self.admin.groups.remove(self.group_in)
|
|
||||||
inform_changed_data(self.admin)
|
|
||||||
|
|
||||||
section = MotionCommentSection(name="test_name_f3jOF3m8fp.<qiqmf32=")
|
|
||||||
section.save()
|
|
||||||
section.read_groups.add(self.group_out)
|
|
||||||
|
|
||||||
response = self.client.get(reverse("motioncommentsection-list"))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertTrue(isinstance(response.data, list))
|
|
||||||
self.assertEqual(len(response.data), 0)
|
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
"""
|
"""
|
||||||
Create a section just with a name.
|
Create a section just with a name.
|
||||||
@ -759,55 +665,6 @@ class TestMotionCommentSectionSorting(TestCase):
|
|||||||
self.assertEqual(section.weight, 10000)
|
self.assertEqual(section.weight, 10000)
|
||||||
|
|
||||||
|
|
||||||
class RetrieveMotionChangeRecommendation(TestCase):
|
|
||||||
"""
|
|
||||||
Tests retrieving motion change recommendations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.client = APIClient()
|
|
||||||
self.client.login(username="admin", password="admin")
|
|
||||||
|
|
||||||
motion = Motion(
|
|
||||||
title="test_title_3kd)K23,c9239mdj2wcG",
|
|
||||||
text="test_text_f8FLP,gvprC;wovVEwlQ",
|
|
||||||
)
|
|
||||||
motion.save()
|
|
||||||
|
|
||||||
self.public_cr = MotionChangeRecommendation(
|
|
||||||
motion=motion, internal=False, line_from=1, line_to=1
|
|
||||||
)
|
|
||||||
self.public_cr.save()
|
|
||||||
|
|
||||||
self.internal_cr = MotionChangeRecommendation(
|
|
||||||
motion=motion, internal=True, line_from=2, line_to=2
|
|
||||||
)
|
|
||||||
self.internal_cr.save()
|
|
||||||
|
|
||||||
def test_simple(self):
|
|
||||||
"""
|
|
||||||
Test retrieving all change recommendations.
|
|
||||||
"""
|
|
||||||
response = self.client.get(reverse("motionchangerecommendation-list"))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(len(response.data), 2)
|
|
||||||
|
|
||||||
def test_non_admin(self):
|
|
||||||
"""
|
|
||||||
Test retrieving of all change recommendations that are public, if the user
|
|
||||||
has no manage perms.
|
|
||||||
"""
|
|
||||||
self.admin = get_user_model().objects.get(username="admin")
|
|
||||||
self.admin.groups.add(GROUP_DELEGATE_PK)
|
|
||||||
self.admin.groups.remove(GROUP_ADMIN_PK)
|
|
||||||
inform_changed_data(self.admin)
|
|
||||||
|
|
||||||
response = self.client.get(reverse("motionchangerecommendation-list"))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(len(response.data), 1)
|
|
||||||
self.assertEqual(response.data[0]["id"], self.public_cr.id)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateMotionChangeRecommendation(TestCase):
|
class CreateMotionChangeRecommendation(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests motion change recommendation creation.
|
Tests motion change recommendation creation.
|
||||||
@ -1108,38 +965,6 @@ class TestMotionBlock(TestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
self.assertFalse(MotionBlock.objects.exists())
|
self.assertFalse(MotionBlock.objects.exists())
|
||||||
|
|
||||||
def test_retrieve_simple(self):
|
|
||||||
motion_block = MotionBlock(title="test_title")
|
|
||||||
motion_block.save()
|
|
||||||
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("motionblock-detail", args=[motion_block.pk])
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(
|
|
||||||
sorted(response.data.keys()),
|
|
||||||
sorted(
|
|
||||||
(
|
|
||||||
"agenda_item_id",
|
|
||||||
"id",
|
|
||||||
"internal",
|
|
||||||
"list_of_speakers_id",
|
|
||||||
"title",
|
|
||||||
"motions_id",
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_retrieve_internal_non_admin(self):
|
|
||||||
self.make_admin_delegate()
|
|
||||||
motion_block = MotionBlock(title="test_title", internal=True)
|
|
||||||
motion_block.save()
|
|
||||||
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("motionblock-detail", args=[motion_block.pk])
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
|
|
||||||
class FollowRecommendationsForMotionBlock(TestCase):
|
class FollowRecommendationsForMotionBlock(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -46,42 +46,6 @@ def test_group_db_queries():
|
|||||||
assert count_queries(Group.get_elements)() == 2
|
assert count_queries(Group.get_elements)() == 2
|
||||||
|
|
||||||
|
|
||||||
class UserGetTest(TestCase):
|
|
||||||
"""
|
|
||||||
Tests to receive a users via REST API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_get_with_user_who_is_in_group_with_pk_1(self):
|
|
||||||
"""
|
|
||||||
It is invalid, that a user is in the group with the pk 1. But if the
|
|
||||||
database is invalid, the user should nevertheless be received.
|
|
||||||
"""
|
|
||||||
admin = User.objects.get(username="admin")
|
|
||||||
group1 = Group.objects.get(pk=1)
|
|
||||||
admin.groups.add(group1)
|
|
||||||
self.client.login(username="admin", password="admin")
|
|
||||||
|
|
||||||
response = self.client.get("/rest/users/user/1/")
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_get_with_user_without_permissions(self):
|
|
||||||
group = Group.objects.get(pk=1)
|
|
||||||
permission_string = "users.can_see_name"
|
|
||||||
app_label, codename = permission_string.split(".")
|
|
||||||
permission = group.permissions.get(
|
|
||||||
content_type__app_label=app_label, codename=codename
|
|
||||||
)
|
|
||||||
group.permissions.remove(permission)
|
|
||||||
inform_changed_data(group)
|
|
||||||
config["general_system_enable_anonymous"] = True
|
|
||||||
guest_client = APIClient()
|
|
||||||
|
|
||||||
response = guest_client.get("/rest/users/user/1/")
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(TestCase):
|
class UserCreate(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests creation of users via REST API.
|
Tests creation of users via REST API.
|
||||||
@ -379,26 +343,6 @@ class UserUpdate(TestCase):
|
|||||||
admin = User.objects.get(pk=self.admin.pk)
|
admin = User.objects.get(pk=self.admin.pk)
|
||||||
self.assertIsNone(admin.vote_delegated_to_id)
|
self.assertIsNone(admin.vote_delegated_to_id)
|
||||||
|
|
||||||
def test_update_vote_delegation_autoupdate(self):
|
|
||||||
self.setup_vote_delegation()
|
|
||||||
response = self.client.patch(
|
|
||||||
reverse("user-detail", args=[self.user.pk]),
|
|
||||||
{"vote_delegated_to_id": self.admin.pk},
|
|
||||||
)
|
|
||||||
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
|
|
||||||
|
|
||||||
autoupdate = self.get_last_autoupdate(user=self.admin)
|
|
||||||
user_au = autoupdate[0].get(f"users/user:{self.user.pk}")
|
|
||||||
self.assertIsNotNone(user_au)
|
|
||||||
self.assertEqual(user_au["vote_delegated_to_id"], self.admin.pk)
|
|
||||||
user2_au = autoupdate[0].get(f"users/user:{self.user2.pk}")
|
|
||||||
self.assertIsNotNone(user2_au)
|
|
||||||
self.assertEqual(user2_au["vote_delegated_from_users_id"], [])
|
|
||||||
admin_au = autoupdate[0].get(f"users/user:{self.admin.pk}")
|
|
||||||
self.assertIsNotNone(admin_au)
|
|
||||||
self.assertEqual(admin_au["vote_delegated_from_users_id"], [self.user.pk])
|
|
||||||
self.assertEqual(autoupdate[1], [])
|
|
||||||
|
|
||||||
def test_update_vote_delegated_from(self):
|
def test_update_vote_delegated_from(self):
|
||||||
self.setup_vote_delegation()
|
self.setup_vote_delegation()
|
||||||
response = self.client.patch(
|
response = self.client.patch(
|
||||||
@ -864,60 +808,6 @@ class UserSendIntivationEmail(TestCase):
|
|||||||
self.assertEqual(mail.outbox[0].to[0], self.email)
|
self.assertEqual(mail.outbox[0].to[0], self.email)
|
||||||
|
|
||||||
|
|
||||||
class GroupMetadata(TestCase):
|
|
||||||
def test_options_request_as_anonymous_user_activated(self):
|
|
||||||
config["general_system_enable_anonymous"] = True
|
|
||||||
|
|
||||||
response = self.client.options("/rest/users/group/")
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.data["name"], "Group List")
|
|
||||||
perm_list = response.data["actions"]["POST"]["permissions"]["choices"]
|
|
||||||
self.assertEqual(type(perm_list), list)
|
|
||||||
for item in perm_list:
|
|
||||||
self.assertEqual(type(item), dict)
|
|
||||||
self.assertTrue(item.get("display_name") is not None)
|
|
||||||
self.assertTrue(item.get("value") is not None)
|
|
||||||
|
|
||||||
|
|
||||||
class GroupReceive(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_groups_as_anonymous_deactivated(self):
|
|
||||||
"""
|
|
||||||
Test to get the groups with an anonymous user, when they are deactivated.
|
|
||||||
"""
|
|
||||||
response = self.client.get("/rest/users/group/")
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
|
||||||
|
|
||||||
def test_get_groups_as_anonymous_user_activated(self):
|
|
||||||
"""
|
|
||||||
Test to get the groups with an anonymous user, when they are activated.
|
|
||||||
"""
|
|
||||||
config["general_system_enable_anonymous"] = True
|
|
||||||
|
|
||||||
response = self.client.get("/rest/users/group/")
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_logged_in_user_with_no_permission(self):
|
|
||||||
"""
|
|
||||||
Test to get the groups with an logged in user with no permissions.
|
|
||||||
"""
|
|
||||||
user = User(username="test")
|
|
||||||
user.set_password("test")
|
|
||||||
user.save()
|
|
||||||
default_group = Group.objects.get(pk=GROUP_DEFAULT_PK)
|
|
||||||
default_group.permissions.all().delete()
|
|
||||||
self.client.login(username="test", password="test")
|
|
||||||
|
|
||||||
response = self.client.get("/rest/users/group/")
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
class GroupCreate(TestCase):
|
class GroupCreate(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests creation of groups via REST API.
|
Tests creation of groups via REST API.
|
||||||
@ -1158,17 +1048,6 @@ class PersonalNoteTest(TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.admin = User.objects.get(username="admin")
|
self.admin = User.objects.get(username="admin")
|
||||||
|
|
||||||
def test_anonymous_without_personal_notes(self):
|
|
||||||
personal_note = PersonalNote.objects.create(
|
|
||||||
user=self.admin, notes='["admin_personal_note_OoGh8choro0oosh0roob"]'
|
|
||||||
)
|
|
||||||
config["general_system_enable_anonymous"] = True
|
|
||||||
guest_client = APIClient()
|
|
||||||
response = guest_client.get(
|
|
||||||
reverse("personalnote-detail", args=[personal_note.pk])
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
admin_client = APIClient()
|
admin_client = APIClient()
|
||||||
admin_client.login(username="admin", password="admin")
|
admin_client.login(username="admin", password="admin")
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.test import TestCase as _TestCase
|
from django.test import TestCase as _TestCase
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
from openslides.utils.cache import element_cache
|
|
||||||
from openslides.utils.utils import get_element_id
|
|
||||||
from tests.common_groups import GROUP_ADMIN_PK, GROUP_DELEGATE_PK
|
from tests.common_groups import GROUP_ADMIN_PK, GROUP_DELEGATE_PK
|
||||||
from tests.count_queries import AssertNumQueriesContext
|
from tests.count_queries import AssertNumQueriesContext
|
||||||
|
|
||||||
@ -34,45 +31,6 @@ class TestCase(_TestCase):
|
|||||||
Adds testing for autoupdates after requests.
|
Adds testing for autoupdates after requests.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_last_autoupdate(self, user=None):
|
|
||||||
"""
|
|
||||||
Get the last autoupdate as (changed_data, deleted_element_ids) for the given user.
|
|
||||||
changed_elements is a dict with element_ids as keys and the actual element as value
|
|
||||||
user_id=None if for full data, 0 for the anonymous and regular ids for users.
|
|
||||||
"""
|
|
||||||
user_id = None if user is None else user.id
|
|
||||||
current_change_id = async_to_sync(element_cache.get_current_change_id)()
|
|
||||||
_, _changed_elements, deleted_element_ids = async_to_sync(
|
|
||||||
element_cache.get_data_since
|
|
||||||
)(user_id=user_id, change_id=current_change_id)
|
|
||||||
|
|
||||||
changed_elements = {}
|
|
||||||
for collection, elements in _changed_elements.items():
|
|
||||||
for element in elements:
|
|
||||||
changed_elements[get_element_id(collection, element["id"])] = element
|
|
||||||
|
|
||||||
return (changed_elements, deleted_element_ids)
|
|
||||||
|
|
||||||
def assertAutoupdate(self, model, user=None):
|
|
||||||
self.assertTrue(
|
|
||||||
model.get_element_id() in self.get_last_autoupdate(user=user)[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
def assertDeletedAutoupdate(self, model, user=None):
|
|
||||||
self.assertTrue(
|
|
||||||
model.get_element_id() in self.get_last_autoupdate(user=user)[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
def assertNoAutoupdate(self, model, user=None):
|
|
||||||
self.assertFalse(
|
|
||||||
model.get_element_id() in self.get_last_autoupdate(user=user)[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
def assertNoDeletedAutoupdate(self, model, user=None):
|
|
||||||
self.assertFalse(
|
|
||||||
model.get_element_id() in self.get_last_autoupdate(user=user)[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
def assertNumQueries(self, num, func=None, *args, verbose=False, **kwargs):
|
def assertNumQueries(self, num, func=None, *args, verbose=False, **kwargs):
|
||||||
context = AssertNumQueriesContext(self, num, verbose)
|
context = AssertNumQueriesContext(self, num, verbose)
|
||||||
if func is None:
|
if func is None:
|
||||||
|
@ -3,7 +3,7 @@ from typing import Any, Dict, List
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from openslides.utils.cache import ChangeIdTooLowError, ElementCache
|
from openslides.utils.cache import ElementCache
|
||||||
|
|
||||||
from .cache_provider import TTestCacheProvider, example_data, get_cachable_provider
|
from .cache_provider import TTestCacheProvider, example_data, get_cachable_provider
|
||||||
|
|
||||||
@ -139,84 +139,6 @@ async def test_get_all_data_from_redis(element_cache):
|
|||||||
assert sort_dict(result) == sort_dict(example_data())
|
assert sort_dict(result) == sort_dict(example_data())
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_data_since_change_id_0(element_cache):
|
|
||||||
element_cache.cache_provider.full_data = {
|
|
||||||
"app/collection1:1": '{"id": 1, "value": "value1"}',
|
|
||||||
"app/collection1:2": '{"id": 2, "value": "value2"}',
|
|
||||||
"app/collection2:1": '{"id": 1, "key": "value1"}',
|
|
||||||
"app/collection2:2": '{"id": 2, "key": "value2"}',
|
|
||||||
"app/personalized-collection:1": '{"id": 1, "key": "value1", "user_id": 1}',
|
|
||||||
"app/personalized-collection:2": '{"id": 2, "key": "value2", "user_id": 2}',
|
|
||||||
}
|
|
||||||
|
|
||||||
(
|
|
||||||
max_change_id,
|
|
||||||
changed_elements,
|
|
||||||
deleted_element_ids,
|
|
||||||
) = await element_cache.get_data_since(None, 0)
|
|
||||||
|
|
||||||
assert sort_dict(changed_elements) == sort_dict(example_data())
|
|
||||||
assert max_change_id == 0
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_data_since_change_id_lower_than_in_redis(element_cache):
|
|
||||||
element_cache.cache_provider.full_data = {
|
|
||||||
"app/collection1:1": '{"id": 1, "value": "value1"}',
|
|
||||||
"app/collection1:2": '{"id": 2, "value": "value2"}',
|
|
||||||
"app/collection2:1": '{"id": 1, "key": "value1"}',
|
|
||||||
"app/collection2:2": '{"id": 2, "key": "value2"}',
|
|
||||||
}
|
|
||||||
element_cache.cache_provider.default_change_id = 2
|
|
||||||
element_cache.cache_provider.change_id_data = {2: {"app/collection1:1"}}
|
|
||||||
with pytest.raises(ChangeIdTooLowError):
|
|
||||||
await element_cache.get_data_since(None, 1)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_data_since_change_id_data_in_redis(element_cache):
|
|
||||||
element_cache.cache_provider.full_data = {
|
|
||||||
"app/collection1:1": '{"id": 1, "value": "value1"}',
|
|
||||||
"app/collection1:2": '{"id": 2, "value": "value2"}',
|
|
||||||
"app/collection2:1": '{"id": 1, "key": "value1"}',
|
|
||||||
"app/collection2:2": '{"id": 2, "key": "value2"}',
|
|
||||||
}
|
|
||||||
element_cache.cache_provider.change_id_data = {
|
|
||||||
1: {"app/collection1:1", "app/collection1:3"}
|
|
||||||
}
|
|
||||||
|
|
||||||
result = await element_cache.get_data_since(None, 1)
|
|
||||||
|
|
||||||
assert result == (
|
|
||||||
1,
|
|
||||||
{"app/collection1": [{"id": 1, "value": "value1"}]},
|
|
||||||
["app/collection1:3"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_data_since_change_id_data_in_db(element_cache):
|
|
||||||
element_cache.cache_provider.change_id_data = {
|
|
||||||
1: {"app/collection1:1", "app/collection1:3"}
|
|
||||||
}
|
|
||||||
|
|
||||||
result = await element_cache.get_data_since(None, 1)
|
|
||||||
|
|
||||||
assert result == (
|
|
||||||
1,
|
|
||||||
{"app/collection1": [{"id": 1, "value": "value1"}]},
|
|
||||||
["app/collection1:3"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_gata_since_change_id_data_in_db_empty_change_id(element_cache):
|
|
||||||
result = await element_cache.get_data_since(None, 1)
|
|
||||||
|
|
||||||
assert result == (0, {}, [])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_element_data_empty_redis(element_cache):
|
async def test_get_element_data_empty_redis(element_cache):
|
||||||
result = await element_cache.get_element_data("app/collection1", 1)
|
result = await element_cache.get_element_data("app/collection1", 1)
|
||||||
@ -245,97 +167,6 @@ async def test_get_element_data_full_redis(element_cache):
|
|||||||
assert result == {"id": 1, "value": "value1"}
|
assert result == {"id": 1, "value": "value1"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_all_restricted_data(element_cache):
|
|
||||||
result = await element_cache.get_all_data_list(1)
|
|
||||||
|
|
||||||
# The output from redis has to be the same then the db_data
|
|
||||||
|
|
||||||
assert sort_dict(result) == sort_dict(
|
|
||||||
{
|
|
||||||
"app/collection1": [
|
|
||||||
{"id": 1, "value": "restricted_value1"},
|
|
||||||
{"id": 2, "value": "restricted_value2"},
|
|
||||||
],
|
|
||||||
"app/collection2": [
|
|
||||||
{"id": 1, "key": "restricted_value1"},
|
|
||||||
{"id": 2, "key": "restricted_value2"},
|
|
||||||
],
|
|
||||||
"app/personalized-collection": [{"id": 1, "key": "value1", "user_id": 1}],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_restricted_data_change_id_0(element_cache):
|
|
||||||
(
|
|
||||||
max_change_id,
|
|
||||||
changed_elements,
|
|
||||||
deleted_element_ids,
|
|
||||||
) = await element_cache.get_data_since(2, 0)
|
|
||||||
|
|
||||||
assert max_change_id == 0
|
|
||||||
assert sort_dict(changed_elements) == sort_dict(
|
|
||||||
{
|
|
||||||
"app/collection1": [
|
|
||||||
{"id": 1, "value": "restricted_value1"},
|
|
||||||
{"id": 2, "value": "restricted_value2"},
|
|
||||||
],
|
|
||||||
"app/collection2": [
|
|
||||||
{"id": 1, "key": "restricted_value1"},
|
|
||||||
{"id": 2, "key": "restricted_value2"},
|
|
||||||
],
|
|
||||||
"app/personalized-collection": [{"id": 2, "key": "value2", "user_id": 2}],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
assert deleted_element_ids == []
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_restricted_data_2(element_cache):
|
|
||||||
element_cache.cache_provider.change_id_data = {
|
|
||||||
1: {"app/collection1:1", "app/collection1:3"}
|
|
||||||
}
|
|
||||||
|
|
||||||
result = await element_cache.get_data_since(0, 1)
|
|
||||||
|
|
||||||
assert result == (
|
|
||||||
1,
|
|
||||||
{"app/collection1": [{"id": 1, "value": "restricted_value1"}]},
|
|
||||||
["app/collection1:3"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_restricted_data_from_personalized_cacheable(element_cache):
|
|
||||||
element_cache.cache_provider.change_id_data = {1: {"app/personalized-collection:2"}}
|
|
||||||
|
|
||||||
result = await element_cache.get_data_since(0, 1)
|
|
||||||
|
|
||||||
assert result == (1, {}, [])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_restricted_data_change_id_lower_than_in_redis(element_cache):
|
|
||||||
element_cache.cache_provider.default_change_id = 2
|
|
||||||
|
|
||||||
with pytest.raises(ChangeIdTooLowError):
|
|
||||||
await element_cache.get_data_since(0, 1)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_get_restricted_data_with_change_id(element_cache):
|
|
||||||
element_cache.cache_provider.change_id_data = {2: {"app/collection1:1"}}
|
|
||||||
|
|
||||||
result = await element_cache.get_data_since(0, 2)
|
|
||||||
|
|
||||||
assert result == (
|
|
||||||
2,
|
|
||||||
{"app/collection1": [{"id": 1, "value": "restricted_value1"}]},
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_lowest_change_id_after_updating_lowest_element(element_cache):
|
async def test_lowest_change_id_after_updating_lowest_element(element_cache):
|
||||||
await element_cache.change_elements(
|
await element_cache.change_elements(
|
||||||
|
Loading…
Reference in New Issue
Block a user