Merge pull request #3390 from ostcar/rewrite_restricted_data
CollectionElement and Autoupdate cleanups to help mypy
This commit is contained in:
commit
b824e0387c
@ -98,6 +98,7 @@ General:
|
|||||||
to pdfmake 0.1.30) [#3278, #3285].
|
to pdfmake 0.1.30) [#3278, #3285].
|
||||||
- Bugfixes for PDF creation [#3227, #3251, #3279, #3286, #3346, #3347, #3342].
|
- Bugfixes for PDF creation [#3227, #3251, #3279, #3286, #3346, #3347, #3342].
|
||||||
- Improvements for plugin integration [#3330].
|
- Improvements for plugin integration [#3330].
|
||||||
|
- Cleanups for the collection and autoupdate system [#3390]
|
||||||
|
|
||||||
|
|
||||||
Version 2.1.1 (2017-04-05)
|
Version 2.1.1 (2017-04-05)
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
from typing import Iterable # noqa
|
from typing import Any, Dict, Iterable, List, Optional # noqa
|
||||||
|
|
||||||
from ..utils.access_permissions import ( # noqa
|
from ..utils.access_permissions import BaseAccessPermissions
|
||||||
BaseAccessPermissions,
|
|
||||||
RestrictedData,
|
|
||||||
)
|
|
||||||
from ..utils.auth import has_perm
|
from ..utils.auth import has_perm
|
||||||
from ..utils.collection import Collection
|
from ..utils.collection import CollectionElement
|
||||||
|
|
||||||
|
|
||||||
class ItemAccessPermissions(BaseAccessPermissions):
|
class ItemAccessPermissions(BaseAccessPermissions):
|
||||||
@ -28,7 +25,10 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
|||||||
|
|
||||||
# TODO: In the following method we use full_data['is_hidden'] but this can be out of date.
|
# TODO: In the following method we use full_data['is_hidden'] but this can be out of date.
|
||||||
|
|
||||||
def get_restricted_data(self, container, user):
|
def get_restricted_data(
|
||||||
|
self,
|
||||||
|
full_data: List[Dict[str, Any]],
|
||||||
|
user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the user.
|
for the user.
|
||||||
@ -43,9 +43,6 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
|||||||
whitelist = full_data.keys() - blocked_keys
|
whitelist = full_data.keys() - blocked_keys
|
||||||
return {key: full_data[key] for key in whitelist}
|
return {key: full_data[key] for key in whitelist}
|
||||||
|
|
||||||
# Expand full_data to a list if it is not one.
|
|
||||||
full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
|
|
||||||
|
|
||||||
# Parse data.
|
# Parse data.
|
||||||
if has_perm(user, 'agenda.can_see'):
|
if has_perm(user, 'agenda.can_see'):
|
||||||
if has_perm(user, 'agenda.can_manage') and has_perm(user, 'agenda.can_see_hidden_items'):
|
if has_perm(user, 'agenda.can_manage') and has_perm(user, 'agenda.can_see_hidden_items'):
|
||||||
@ -83,18 +80,9 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
|||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
# Reduce result to a single item or None if it was not a collection at
|
return data
|
||||||
# the beginning of the method.
|
|
||||||
if isinstance(container, Collection):
|
|
||||||
restricted_data = data # type: RestrictedData
|
|
||||||
elif data:
|
|
||||||
restricted_data = data[0]
|
|
||||||
else:
|
|
||||||
restricted_data = None
|
|
||||||
|
|
||||||
return restricted_data
|
def get_projector_data(self, full_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
|
||||||
def get_projector_data(self, container):
|
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the projector. Removes field 'comment'.
|
for the projector. Removes field 'comment'.
|
||||||
@ -106,20 +94,8 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
|||||||
whitelist = full_data.keys() - blocked_keys
|
whitelist = full_data.keys() - blocked_keys
|
||||||
return {key: full_data[key] for key in whitelist}
|
return {key: full_data[key] for key in whitelist}
|
||||||
|
|
||||||
# Expand full_data to a list if it is not one.
|
|
||||||
full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
|
|
||||||
|
|
||||||
# Parse data.
|
# Parse data.
|
||||||
blocked_keys = ('comment',)
|
blocked_keys = ('comment',)
|
||||||
data = [filtered_data(full, blocked_keys) for full in full_data]
|
data = [filtered_data(full, blocked_keys) for full in full_data]
|
||||||
|
|
||||||
# Reduce result to a single item or None if it was not a collection at
|
return data
|
||||||
# the beginning of the method.
|
|
||||||
if isinstance(container, Collection):
|
|
||||||
projector_data = data # type: RestrictedData
|
|
||||||
elif data:
|
|
||||||
projector_data = data[0]
|
|
||||||
else:
|
|
||||||
projector_data = None
|
|
||||||
|
|
||||||
return projector_data
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
from ..utils.access_permissions import ( # noqa
|
from typing import Any, Dict, List, Optional
|
||||||
BaseAccessPermissions,
|
|
||||||
RestrictedData,
|
from ..utils.access_permissions import BaseAccessPermissions # noqa
|
||||||
)
|
|
||||||
from ..utils.auth import has_perm
|
from ..utils.auth import has_perm
|
||||||
from ..utils.collection import Collection
|
from ..utils.collection import CollectionElement
|
||||||
|
|
||||||
|
|
||||||
class AssignmentAccessPermissions(BaseAccessPermissions):
|
class AssignmentAccessPermissions(BaseAccessPermissions):
|
||||||
@ -28,15 +27,15 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
|
|||||||
serializer_class = AssignmentShortSerializer
|
serializer_class = AssignmentShortSerializer
|
||||||
return serializer_class
|
return serializer_class
|
||||||
|
|
||||||
def get_restricted_data(self, container, user):
|
def get_restricted_data(
|
||||||
|
self,
|
||||||
|
full_data: List[Dict[str, Any]],
|
||||||
|
user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the user. Removes unpublished polls for non admins so that they
|
for the user. Removes unpublished polls for non admins so that they
|
||||||
only get a result like the AssignmentShortSerializer would give them.
|
only get a result like the AssignmentShortSerializer would give them.
|
||||||
"""
|
"""
|
||||||
# Expand full_data to a list if it is not one.
|
|
||||||
full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
|
|
||||||
|
|
||||||
# Parse data.
|
# Parse data.
|
||||||
if has_perm(user, 'assignments.can_see') and has_perm(user, 'assignments.can_manage'):
|
if has_perm(user, 'assignments.can_see') and has_perm(user, 'assignments.can_manage'):
|
||||||
data = full_data
|
data = full_data
|
||||||
@ -50,25 +49,13 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
|
|||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
# Reduce result to a single item or None if it was not a collection at
|
return data
|
||||||
# the beginning of the method.
|
|
||||||
if isinstance(container, Collection):
|
|
||||||
restricted_data = data # type: RestrictedData
|
|
||||||
elif data:
|
|
||||||
restricted_data = data[0]
|
|
||||||
else:
|
|
||||||
restricted_data = None
|
|
||||||
|
|
||||||
return restricted_data
|
def get_projector_data(self, full_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
|
||||||
def get_projector_data(self, container):
|
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the projector. Removes unpublished polls.
|
for the projector. Removes unpublished polls.
|
||||||
"""
|
"""
|
||||||
# Expand full_data to a list if it is not one.
|
|
||||||
full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
|
|
||||||
|
|
||||||
# Parse data. Exclude unpublished polls.
|
# Parse data. Exclude unpublished polls.
|
||||||
data = []
|
data = []
|
||||||
for full in full_data:
|
for full in full_data:
|
||||||
@ -76,13 +63,4 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
|
|||||||
full_copy['polls'] = [poll for poll in full['polls'] if poll['published']]
|
full_copy['polls'] = [poll for poll in full['polls'] if poll['published']]
|
||||||
data.append(full_copy)
|
data.append(full_copy)
|
||||||
|
|
||||||
# Reduce result to a single item or None if it was not a collection at
|
return data
|
||||||
# the beginning of the method.
|
|
||||||
if isinstance(container, Collection):
|
|
||||||
projector_data = data # type: RestrictedData
|
|
||||||
elif data:
|
|
||||||
projector_data = data[0]
|
|
||||||
else:
|
|
||||||
projector_data = None
|
|
||||||
|
|
||||||
return projector_data
|
|
||||||
|
@ -714,12 +714,11 @@ class ChatMessageViewSet(ModelViewSet):
|
|||||||
chatmessages = ChatMessage.objects.all()
|
chatmessages = ChatMessage.objects.all()
|
||||||
args = []
|
args = []
|
||||||
for chatmessage in chatmessages:
|
for chatmessage in chatmessages:
|
||||||
args.append(chatmessage.get_collection_string())
|
args.append((chatmessage.get_collection_string(), chatmessage.pk))
|
||||||
args.append(chatmessage.pk)
|
|
||||||
chatmessages.delete()
|
chatmessages.delete()
|
||||||
# Trigger autoupdate and setup response.
|
# Trigger autoupdate and setup response.
|
||||||
if len(args) > 0:
|
if len(args) > 0:
|
||||||
inform_deleted_data(*args)
|
inform_deleted_data(args)
|
||||||
return Response({'detail': _('All chat messages deleted successfully.')})
|
return Response({'detail': _('All chat messages deleted successfully.')})
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
from ..utils.access_permissions import ( # noqa
|
from typing import Any, Dict, List, Optional
|
||||||
BaseAccessPermissions,
|
|
||||||
RestrictedData,
|
from ..utils.access_permissions import BaseAccessPermissions # noqa
|
||||||
)
|
|
||||||
from ..utils.auth import has_perm
|
from ..utils.auth import has_perm
|
||||||
from ..utils.collection import Collection
|
from ..utils.collection import CollectionElement
|
||||||
|
|
||||||
|
|
||||||
class MediafileAccessPermissions(BaseAccessPermissions):
|
class MediafileAccessPermissions(BaseAccessPermissions):
|
||||||
@ -24,14 +23,14 @@ class MediafileAccessPermissions(BaseAccessPermissions):
|
|||||||
|
|
||||||
return MediafileSerializer
|
return MediafileSerializer
|
||||||
|
|
||||||
def get_restricted_data(self, container, user):
|
def get_restricted_data(
|
||||||
|
self,
|
||||||
|
full_data: List[Dict[str, Any]],
|
||||||
|
user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the user. Removes hidden mediafiles for some users.
|
for the user. Removes hidden mediafiles for some users.
|
||||||
"""
|
"""
|
||||||
# Expand full_data to a list if it is not one.
|
|
||||||
full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
|
|
||||||
|
|
||||||
# Parse data.
|
# Parse data.
|
||||||
if has_perm(user, 'mediafiles.can_see') and has_perm(user, 'mediafiles.can_see_hidden'):
|
if has_perm(user, 'mediafiles.can_see') and has_perm(user, 'mediafiles.can_see_hidden'):
|
||||||
data = full_data
|
data = full_data
|
||||||
@ -41,13 +40,4 @@ class MediafileAccessPermissions(BaseAccessPermissions):
|
|||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
# Reduce result to a single item or None if it was not a collection at
|
return data
|
||||||
# the beginning of the method.
|
|
||||||
if isinstance(container, Collection):
|
|
||||||
restricted_data = data # type: RestrictedData
|
|
||||||
elif data:
|
|
||||||
restricted_data = data[0]
|
|
||||||
else:
|
|
||||||
restricted_data = None
|
|
||||||
|
|
||||||
return restricted_data
|
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..utils.access_permissions import ( # noqa
|
from ..utils.access_permissions import BaseAccessPermissions # noqa
|
||||||
BaseAccessPermissions,
|
|
||||||
RestrictedData,
|
|
||||||
)
|
|
||||||
from ..utils.auth import has_perm
|
from ..utils.auth import has_perm
|
||||||
from ..utils.collection import Collection, CollectionElement
|
from ..utils.collection import CollectionElement
|
||||||
|
|
||||||
|
|
||||||
class MotionAccessPermissions(BaseAccessPermissions):
|
class MotionAccessPermissions(BaseAccessPermissions):
|
||||||
@ -27,7 +25,10 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
|||||||
|
|
||||||
return MotionSerializer
|
return MotionSerializer
|
||||||
|
|
||||||
def get_restricted_data(self, container, user):
|
def get_restricted_data(
|
||||||
|
self,
|
||||||
|
full_data: List[Dict[str, Any]],
|
||||||
|
user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared for
|
Returns the restricted serialized data for the instance prepared for
|
||||||
the user. Removes motion if the user has not the permission to see
|
the user. Removes motion if the user has not the permission to see
|
||||||
@ -35,9 +36,6 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
|||||||
some unauthorized users. Ensures that a user can only see his own
|
some unauthorized users. Ensures that a user can only see his own
|
||||||
personal notes.
|
personal notes.
|
||||||
"""
|
"""
|
||||||
# Expand full_data to a list if it is not one.
|
|
||||||
full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
|
|
||||||
|
|
||||||
# Parse data.
|
# Parse data.
|
||||||
if has_perm(user, 'motions.can_see'):
|
if has_perm(user, 'motions.can_see'):
|
||||||
# TODO: Refactor this after personal_notes system is refactored.
|
# TODO: Refactor this after personal_notes system is refactored.
|
||||||
@ -78,25 +76,13 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
|||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
# Reduce result to a single item or None if it was not a collection at
|
return data
|
||||||
# the beginning of the method.
|
|
||||||
if isinstance(container, Collection):
|
|
||||||
restricted_data = data # type: RestrictedData
|
|
||||||
elif data:
|
|
||||||
restricted_data = data[0]
|
|
||||||
else:
|
|
||||||
restricted_data = None
|
|
||||||
|
|
||||||
return restricted_data
|
def get_projector_data(self, full_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
|
||||||
def get_projector_data(self, container):
|
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the projector. Removes several comment fields.
|
for the projector. Removes several comment fields.
|
||||||
"""
|
"""
|
||||||
# Expand full_data to a list if it is not one.
|
|
||||||
full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
|
|
||||||
|
|
||||||
# Parse data.
|
# Parse data.
|
||||||
data = []
|
data = []
|
||||||
for full in full_data:
|
for full in full_data:
|
||||||
@ -114,16 +100,7 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
|||||||
else:
|
else:
|
||||||
data.append(full)
|
data.append(full)
|
||||||
|
|
||||||
# Reduce result to a single item or None if it was not a collection at
|
return data
|
||||||
# the beginning of the method.
|
|
||||||
if isinstance(container, Collection):
|
|
||||||
projector_data = data # type: RestrictedData
|
|
||||||
elif data:
|
|
||||||
projector_data = data[0]
|
|
||||||
else:
|
|
||||||
projector_data = None
|
|
||||||
|
|
||||||
return projector_data
|
|
||||||
|
|
||||||
|
|
||||||
class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
|
class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
from typing import Any, Dict, List # noqa
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
|
||||||
from ..core.signals import user_data_required
|
from ..core.signals import user_data_required
|
||||||
from ..utils.access_permissions import ( # noqa
|
from ..utils.access_permissions import BaseAccessPermissions # noqa
|
||||||
BaseAccessPermissions,
|
|
||||||
RestrictedData,
|
|
||||||
)
|
|
||||||
from ..utils.auth import anonymous_is_enabled, has_perm
|
from ..utils.auth import anonymous_is_enabled, has_perm
|
||||||
from ..utils.collection import Collection
|
from ..utils.collection import CollectionElement
|
||||||
|
|
||||||
|
|
||||||
class UserAccessPermissions(BaseAccessPermissions):
|
class UserAccessPermissions(BaseAccessPermissions):
|
||||||
@ -29,7 +26,10 @@ class UserAccessPermissions(BaseAccessPermissions):
|
|||||||
|
|
||||||
return UserFullSerializer
|
return UserFullSerializer
|
||||||
|
|
||||||
def get_restricted_data(self, container, user):
|
def get_restricted_data(
|
||||||
|
self,
|
||||||
|
full_data: List[Dict[str, Any]],
|
||||||
|
user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the user. Removes several fields for non admins so that they do
|
for the user. Removes several fields for non admins so that they do
|
||||||
@ -43,9 +43,6 @@ class UserAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
return {key: full_data[key] for key in whitelist}
|
return {key: full_data[key] for key in whitelist}
|
||||||
|
|
||||||
# Expand full_data to a list if it is not one.
|
|
||||||
full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
|
|
||||||
|
|
||||||
# We have four sets of data to be sent:
|
# We have four sets of data to be sent:
|
||||||
# * full data i. e. all fields,
|
# * full data i. e. all fields,
|
||||||
# * many data i. e. all fields but not the default password,
|
# * many data i. e. all fields but not the default password,
|
||||||
@ -96,18 +93,9 @@ class UserAccessPermissions(BaseAccessPermissions):
|
|||||||
in full_data
|
in full_data
|
||||||
if full['id'] in user_ids]
|
if full['id'] in user_ids]
|
||||||
|
|
||||||
# Reduce result to a single item or None if it was not a collection at
|
return data
|
||||||
# the beginning of the method.
|
|
||||||
if isinstance(container, Collection):
|
|
||||||
restricted_data = data # type: RestrictedData
|
|
||||||
elif data:
|
|
||||||
restricted_data = data[0]
|
|
||||||
else:
|
|
||||||
restricted_data = None
|
|
||||||
|
|
||||||
return restricted_data
|
def get_projector_data(self, full_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
|
||||||
def get_projector_data(self, container):
|
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the projector. Removes several fields.
|
for the projector. Removes several fields.
|
||||||
@ -120,25 +108,13 @@ class UserAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
return {key: full_data[key] for key in whitelist}
|
return {key: full_data[key] for key in whitelist}
|
||||||
|
|
||||||
# Expand full_data to a list if it is not one.
|
|
||||||
full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
|
|
||||||
|
|
||||||
# Parse data.
|
# Parse data.
|
||||||
litte_data_fields = set(USERCANSEESERIALIZER_FIELDS)
|
litte_data_fields = set(USERCANSEESERIALIZER_FIELDS)
|
||||||
litte_data_fields.add('groups_id')
|
litte_data_fields.add('groups_id')
|
||||||
litte_data_fields.discard('groups')
|
litte_data_fields.discard('groups')
|
||||||
data = [filtered_data(full, litte_data_fields) for full in full_data]
|
data = [filtered_data(full, litte_data_fields) for full in full_data]
|
||||||
|
|
||||||
# Reduce result to a single item or None if it was not a collection at
|
return data
|
||||||
# the beginning of the method.
|
|
||||||
if isinstance(container, Collection):
|
|
||||||
projector_data = data # type: RestrictedData
|
|
||||||
elif data:
|
|
||||||
projector_data = data[0]
|
|
||||||
else:
|
|
||||||
projector_data = None
|
|
||||||
|
|
||||||
return projector_data
|
|
||||||
|
|
||||||
|
|
||||||
class GroupAccessPermissions(BaseAccessPermissions):
|
class GroupAccessPermissions(BaseAccessPermissions):
|
||||||
@ -182,14 +158,14 @@ class PersonalNoteAccessPermissions(BaseAccessPermissions):
|
|||||||
|
|
||||||
return PersonalNoteSerializer
|
return PersonalNoteSerializer
|
||||||
|
|
||||||
def get_restricted_data(self, container, user):
|
def get_restricted_data(
|
||||||
|
self,
|
||||||
|
full_data: List[Dict[str, Any]],
|
||||||
|
user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the user. Everybody gets only his own personal notes.
|
for the user. Everybody gets only his own personal notes.
|
||||||
"""
|
"""
|
||||||
# Expand full_data to a list if it is not one.
|
|
||||||
full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
|
|
||||||
|
|
||||||
# Parse data.
|
# Parse data.
|
||||||
if user is None:
|
if user is None:
|
||||||
data = [] # type: List[Dict[str, Any]]
|
data = [] # type: List[Dict[str, Any]]
|
||||||
@ -201,13 +177,4 @@ class PersonalNoteAccessPermissions(BaseAccessPermissions):
|
|||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
# Reduce result to a single item or None if it was not a collection at
|
return data
|
||||||
# the beginning of the method.
|
|
||||||
if isinstance(container, Collection):
|
|
||||||
restricted_data = data # type: RestrictedData
|
|
||||||
elif data:
|
|
||||||
restricted_data = data[0]
|
|
||||||
else:
|
|
||||||
restricted_data = None
|
|
||||||
|
|
||||||
return restricted_data
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from typing import List # noqa
|
||||||
|
|
||||||
from django.contrib.auth import login as auth_login
|
from django.contrib.auth import login as auth_login
|
||||||
from django.contrib.auth import logout as auth_logout
|
from django.contrib.auth import logout as auth_logout
|
||||||
from django.contrib.auth import update_session_auth_hash
|
from django.contrib.auth import update_session_auth_hash
|
||||||
@ -15,7 +17,7 @@ from ..utils.autoupdate import (
|
|||||||
inform_changed_data,
|
inform_changed_data,
|
||||||
inform_data_collection_element_list,
|
inform_data_collection_element_list,
|
||||||
)
|
)
|
||||||
from ..utils.collection import CollectionElement, CollectionElementList
|
from ..utils.collection import CollectionElement
|
||||||
from ..utils.rest_api import (
|
from ..utils.rest_api import (
|
||||||
ModelViewSet,
|
ModelViewSet,
|
||||||
Response,
|
Response,
|
||||||
@ -250,7 +252,7 @@ class GroupViewSet(ModelViewSet):
|
|||||||
|
|
||||||
# Some permissions are added.
|
# Some permissions are added.
|
||||||
if len(new_permissions) > 0:
|
if len(new_permissions) > 0:
|
||||||
collection_elements = CollectionElementList()
|
collection_elements = [] # type: List[CollectionElement]
|
||||||
signal_results = permission_change.send(None, permissions=new_permissions, action='added')
|
signal_results = permission_change.send(None, permissions=new_permissions, action='added')
|
||||||
for receiver, signal_collections in signal_results:
|
for receiver, signal_collections in signal_results:
|
||||||
for collection in signal_collections:
|
for collection in signal_collections:
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import Serializer
|
||||||
|
|
||||||
from .collection import Collection, CollectionElement
|
from .collection import CollectionElement
|
||||||
|
|
||||||
Container = Union[CollectionElement, Collection]
|
|
||||||
RestrictedData = Union[List[Dict[str, Any]], Dict[str, Any], None]
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAccessPermissions:
|
class BaseAccessPermissions:
|
||||||
@ -40,35 +37,30 @@ class BaseAccessPermissions:
|
|||||||
"""
|
"""
|
||||||
return self.get_serializer_class(user=None)(instance).data
|
return self.get_serializer_class(user=None)(instance).data
|
||||||
|
|
||||||
def get_restricted_data(self, container: Container, user: Optional[CollectionElement]) -> RestrictedData:
|
def get_restricted_data(
|
||||||
|
self, full_data: List[Dict[str, Any]],
|
||||||
|
user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the user.
|
for the user.
|
||||||
|
|
||||||
The argument container should be a CollectionElement or a
|
The argument full_data has to be a list of full_data dicts as they are
|
||||||
Collection. The type of the return value is a dictionary or a list
|
created with CollectionElement.get_full_data(). The type of the return
|
||||||
according to the given type (or None). Returns None or an empty
|
is the same. Returns an empty list if the user has no read access.
|
||||||
list if the user has no read access. Returns reduced data if the
|
Returns reduced data if the user has limited access.
|
||||||
user has limited access. Default: Returns full data if the user has
|
Default: Returns full data if the user has read access to model instances.
|
||||||
read access to model instances.
|
|
||||||
|
|
||||||
Hint: You should override this method if your get_serializer_class()
|
Hint: You should override this method if your get_serializer_class()
|
||||||
method returns different serializers for different users or if you
|
method returns different serializers for different users or if you
|
||||||
have access restrictions in your view or viewset in methods like
|
have access restrictions in your view or viewset in methods like
|
||||||
retrieve() or list().
|
retrieve() or list().
|
||||||
"""
|
"""
|
||||||
if self.check_permissions(user):
|
return full_data if self.check_permissions(user) else []
|
||||||
data = container.get_full_data()
|
|
||||||
elif isinstance(container, Collection):
|
|
||||||
data = []
|
|
||||||
else:
|
|
||||||
data = None
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get_projector_data(self, container: Container) -> RestrictedData:
|
def get_projector_data(self, full_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns the serialized data for the projector. Returns None if the
|
Returns the serialized data for the projector. Returns an empty list if
|
||||||
user has no access to this specific data. Returns reduced data if
|
the user has no access to this specific data. Returns reduced data if
|
||||||
the user has limited access. Default: Returns full data.
|
the user has limited access. Default: Returns full data.
|
||||||
"""
|
"""
|
||||||
return container.get_full_data()
|
return full_data
|
||||||
|
@ -2,7 +2,7 @@ import json
|
|||||||
import time
|
import time
|
||||||
import warnings
|
import warnings
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any, Dict, Generator, Iterable, List, Union, cast
|
from typing import Any, Dict, Generator, Iterable, List, Tuple, Union
|
||||||
|
|
||||||
from channels import Channel, Group
|
from channels import Channel, Group
|
||||||
from channels.asgi import get_channel_layer
|
from channels.asgi import get_channel_layer
|
||||||
@ -16,7 +16,15 @@ from ..core.config import config
|
|||||||
from ..core.models import Projector
|
from ..core.models import Projector
|
||||||
from .auth import anonymous_is_enabled, has_perm, user_to_collection_user
|
from .auth import anonymous_is_enabled, has_perm, user_to_collection_user
|
||||||
from .cache import restricted_data_cache, websocket_user_cache
|
from .cache import restricted_data_cache, websocket_user_cache
|
||||||
from .collection import Collection, CollectionElement, CollectionElementList
|
from .collection import AutoupdateFormat # noqa
|
||||||
|
from .collection import (
|
||||||
|
ChannelMessageFormat,
|
||||||
|
Collection,
|
||||||
|
CollectionElement,
|
||||||
|
format_for_autoupdate,
|
||||||
|
from_channel_message,
|
||||||
|
to_channel_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def send_or_wait(send_func: Any, *args: Any, **kwargs: Any) -> None:
|
def send_or_wait(send_func: Any, *args: Any, **kwargs: Any) -> None:
|
||||||
@ -44,28 +52,6 @@ def send_or_wait(send_func: Any, *args: Any, **kwargs: Any) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def format_for_autoupdate(collection_string: str, id: int, action: str, data: Dict[str, Any]=None) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Returns a dict that can be used for autoupdate.
|
|
||||||
"""
|
|
||||||
if not data:
|
|
||||||
# If the data is None or is empty, then the action has to be deleted,
|
|
||||||
# even when it says diffrently. This can happen when the object is not
|
|
||||||
# deleted, but the user has no permission to see it.
|
|
||||||
action = 'deleted'
|
|
||||||
|
|
||||||
output = {
|
|
||||||
'collection': collection_string,
|
|
||||||
'id': id,
|
|
||||||
'action': action,
|
|
||||||
}
|
|
||||||
|
|
||||||
if action != 'deleted':
|
|
||||||
output['data'] = data
|
|
||||||
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
@channel_session_user_from_http
|
@channel_session_user_from_http
|
||||||
def ws_add_site(message: Any) -> None:
|
def ws_add_site(message: Any) -> None:
|
||||||
"""
|
"""
|
||||||
@ -97,10 +83,7 @@ def ws_add_site(message: Any) -> None:
|
|||||||
output = []
|
output = []
|
||||||
for collection in get_startup_collections():
|
for collection in get_startup_collections():
|
||||||
access_permissions = collection.get_access_permissions()
|
access_permissions = collection.get_access_permissions()
|
||||||
restricted_data = access_permissions.get_restricted_data(collection, user)
|
restricted_data = access_permissions.get_restricted_data(collection.get_full_data(), user)
|
||||||
|
|
||||||
# At this point restricted_data has to be a list. So we have to tell it mypy
|
|
||||||
restricted_data = cast(List[Dict[str, Any]], restricted_data)
|
|
||||||
|
|
||||||
for data in restricted_data:
|
for data in restricted_data:
|
||||||
if data is None:
|
if data is None:
|
||||||
@ -109,10 +92,10 @@ def ws_add_site(message: Any) -> None:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
formatted_data = format_for_autoupdate(
|
formatted_data = format_for_autoupdate(
|
||||||
collection_string=collection.collection_string,
|
collection_string=collection.collection_string,
|
||||||
id=data['id'],
|
id=data['id'],
|
||||||
action='changed',
|
action='changed',
|
||||||
data=data)
|
data=data)
|
||||||
|
|
||||||
output.append(formatted_data)
|
output.append(formatted_data)
|
||||||
# Cache restricted data for user
|
# Cache restricted data for user
|
||||||
@ -231,14 +214,21 @@ def ws_add_projector(message: Any, projector_id: int) -> None:
|
|||||||
projector = Projector.objects.get(pk=config['projector_broadcast'])
|
projector = Projector.objects.get(pk=config['projector_broadcast'])
|
||||||
|
|
||||||
# Collect all elements that are on the projector.
|
# Collect all elements that are on the projector.
|
||||||
output = []
|
output = [] # type: List[AutoupdateFormat]
|
||||||
for requirement in projector.get_all_requirements():
|
for requirement in projector.get_all_requirements():
|
||||||
required_collection_element = CollectionElement.from_instance(requirement)
|
required_collection_element = CollectionElement.from_instance(requirement)
|
||||||
output.append(required_collection_element.as_autoupdate_for_projector())
|
output.append(required_collection_element.as_autoupdate_for_projector())
|
||||||
|
|
||||||
# Collect all config elements.
|
# Collect all config elements.
|
||||||
collection = Collection(config.get_collection_string())
|
config_collection = Collection(config.get_collection_string())
|
||||||
output.extend(collection.as_autoupdate_for_projector())
|
projector_data = (config_collection.get_access_permissions()
|
||||||
|
.get_projector_data(config_collection.get_full_data()))
|
||||||
|
for data in projector_data:
|
||||||
|
output.append(format_for_autoupdate(
|
||||||
|
config_collection.collection_string,
|
||||||
|
data['id'],
|
||||||
|
'changed',
|
||||||
|
data))
|
||||||
|
|
||||||
# Collect the projector instance.
|
# Collect the projector instance.
|
||||||
collection_element = CollectionElement.from_instance(projector)
|
collection_element = CollectionElement.from_instance(projector)
|
||||||
@ -255,11 +245,11 @@ def ws_disconnect_projector(message: Any, projector_id: int) -> None:
|
|||||||
Group('projector-{}'.format(projector_id)).discard(message.reply_channel)
|
Group('projector-{}'.format(projector_id)).discard(message.reply_channel)
|
||||||
|
|
||||||
|
|
||||||
def send_data(message: Any) -> None:
|
def send_data(message: ChannelMessageFormat) -> None:
|
||||||
"""
|
"""
|
||||||
Informs all site users and projector clients about changed data.
|
Informs all site users and projector clients about changed data.
|
||||||
"""
|
"""
|
||||||
collection_elements = CollectionElementList.from_channels_message(message)
|
collection_elements = from_channel_message(message)
|
||||||
|
|
||||||
# Send data to site users.
|
# Send data to site users.
|
||||||
for user_id, channel_names in websocket_user_cache.get_all().items():
|
for user_id, channel_names in websocket_user_cache.get_all().items():
|
||||||
@ -338,7 +328,7 @@ def inform_changed_data(instances: Union[Iterable[Model], Model], information: D
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Generates an collection element list for the root_instances.
|
# Generates an collection element list for the root_instances.
|
||||||
collection_elements = CollectionElementList()
|
collection_elements = [] # type: List[CollectionElement]
|
||||||
for root_instance in root_instances:
|
for root_instance in root_instances:
|
||||||
collection_elements.append(
|
collection_elements.append(
|
||||||
CollectionElement.from_instance(
|
CollectionElement.from_instance(
|
||||||
@ -351,32 +341,20 @@ def inform_changed_data(instances: Union[Iterable[Model], Model], information: D
|
|||||||
transaction.on_commit(lambda: send_autoupdate(collection_elements))
|
transaction.on_commit(lambda: send_autoupdate(collection_elements))
|
||||||
|
|
||||||
|
|
||||||
# TODO: Change the input argument to tuples
|
def inform_deleted_data(elements: Iterable[Tuple[str, int]], information: Dict[str, Any]=None) -> None:
|
||||||
def inform_deleted_data(*args: Any, information: Dict[str, Any]=None) -> None:
|
|
||||||
"""
|
"""
|
||||||
Informs the autoupdate system and the caching system about the deletion of
|
Informs the autoupdate system and the caching system about the deletion of
|
||||||
elements.
|
elements.
|
||||||
|
|
||||||
The function has to be called with the attributes collection_string and id.
|
|
||||||
Multible elements can be used. For example:
|
|
||||||
|
|
||||||
inform_deleted_data('motions/motion', 1, 'assignments/assignment', 5)
|
|
||||||
|
|
||||||
The argument information is added to each collection element.
|
The argument information is added to each collection element.
|
||||||
"""
|
"""
|
||||||
if len(args) % 2 or not args:
|
|
||||||
raise ValueError(
|
|
||||||
"inform_deleted_data has to be called with the same number of "
|
|
||||||
"collection strings and ids. It has to be at least one collection "
|
|
||||||
"string and one id.")
|
|
||||||
|
|
||||||
# Go through each pair of collection_string and id and generate a collection
|
# Go through each pair of collection_string and id and generate a collection
|
||||||
# element from it.
|
# element from it.
|
||||||
collection_elements = CollectionElementList()
|
collection_elements = [] # type: List[CollectionElement]
|
||||||
for index in range(0, len(args), 2):
|
for element in elements:
|
||||||
collection_elements.append(CollectionElement.from_values(
|
collection_elements.append(CollectionElement.from_values(
|
||||||
collection_string=args[index],
|
collection_string=element[0],
|
||||||
id=args[index + 1],
|
id=element[1],
|
||||||
deleted=True,
|
deleted=True,
|
||||||
information=information))
|
information=information))
|
||||||
# If currently there is an open database transaction, then the
|
# If currently there is an open database transaction, then the
|
||||||
@ -386,7 +364,7 @@ def inform_deleted_data(*args: Any, information: Dict[str, Any]=None) -> None:
|
|||||||
transaction.on_commit(lambda: send_autoupdate(collection_elements))
|
transaction.on_commit(lambda: send_autoupdate(collection_elements))
|
||||||
|
|
||||||
|
|
||||||
def inform_data_collection_element_list(collection_elements: CollectionElementList,
|
def inform_data_collection_element_list(collection_elements: List[CollectionElement],
|
||||||
information: Dict[str, Any]=None) -> None:
|
information: Dict[str, Any]=None) -> None:
|
||||||
"""
|
"""
|
||||||
Informs the autoupdate system about some collection elements. This is
|
Informs the autoupdate system about some collection elements. This is
|
||||||
@ -399,7 +377,7 @@ def inform_data_collection_element_list(collection_elements: CollectionElementLi
|
|||||||
transaction.on_commit(lambda: send_autoupdate(collection_elements))
|
transaction.on_commit(lambda: send_autoupdate(collection_elements))
|
||||||
|
|
||||||
|
|
||||||
def send_autoupdate(collection_elements: CollectionElementList) -> None:
|
def send_autoupdate(collection_elements: List[CollectionElement]) -> None:
|
||||||
"""
|
"""
|
||||||
Helper function, that sends collection_elements through a channel to the
|
Helper function, that sends collection_elements through a channel to the
|
||||||
autoupdate system.
|
autoupdate system.
|
||||||
@ -409,7 +387,7 @@ def send_autoupdate(collection_elements: CollectionElementList) -> None:
|
|||||||
if collection_elements:
|
if collection_elements:
|
||||||
send_or_wait(
|
send_or_wait(
|
||||||
Channel('autoupdate.send_data').send,
|
Channel('autoupdate.send_data').send,
|
||||||
collection_elements.as_channels_message())
|
to_channel_message(collection_elements))
|
||||||
|
|
||||||
|
|
||||||
def get_startup_collections() -> Generator[Collection, None, None]:
|
def get_startup_collections() -> Generator[Collection, None, None]:
|
||||||
|
@ -10,6 +10,7 @@ from typing import ( # noqa
|
|||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
Set,
|
Set,
|
||||||
|
Type,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -204,9 +205,158 @@ class DjangoCacheWebsocketUserCache(BaseWebsocketUserCache):
|
|||||||
cache.set(self.get_cache_key(), data)
|
cache.set(self.get_cache_key(), data)
|
||||||
|
|
||||||
|
|
||||||
|
class FullDataCache:
|
||||||
|
"""
|
||||||
|
Caches all data as full data.
|
||||||
|
|
||||||
|
Helps to get all data from one collection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
base_cache_key = 'full_data_cache'
|
||||||
|
|
||||||
|
def build_for_collection(self, collection_string: str) -> None:
|
||||||
|
"""
|
||||||
|
Build the cache for collection from a django model.
|
||||||
|
|
||||||
|
Rebuilds the cache for that collection, if it already exists.
|
||||||
|
"""
|
||||||
|
redis = get_redis_connection()
|
||||||
|
pipe = redis.pipeline()
|
||||||
|
|
||||||
|
# Clear the cache for collection
|
||||||
|
pipe.delete(self.get_cache_key(collection_string))
|
||||||
|
|
||||||
|
# Save all elements
|
||||||
|
from .collection import get_model_from_collection_string
|
||||||
|
model = get_model_from_collection_string(collection_string)
|
||||||
|
try:
|
||||||
|
query = model.objects.get_full_queryset()
|
||||||
|
except AttributeError:
|
||||||
|
# If the model des not have to method get_full_queryset(), then use
|
||||||
|
# the default queryset from django.
|
||||||
|
query = model.objects
|
||||||
|
|
||||||
|
# Build a dict from the instance id to the full_data
|
||||||
|
mapping = {instance.pk: json.dumps(model.get_access_permissions().get_full_data(instance))
|
||||||
|
for instance in query.all()}
|
||||||
|
|
||||||
|
if mapping:
|
||||||
|
# Save the dict into a redis map, if there is at least one value
|
||||||
|
pipe.hmset(
|
||||||
|
self.get_cache_key(collection_string),
|
||||||
|
mapping)
|
||||||
|
|
||||||
|
pipe.execute()
|
||||||
|
|
||||||
|
def add_element(self, collection_string: str, id: int, data: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Adds one element to the cache. If the cache does not exists for the collection,
|
||||||
|
it is created.
|
||||||
|
"""
|
||||||
|
redis = get_redis_connection()
|
||||||
|
|
||||||
|
# If the cache does not exist for the collection, then create it first.
|
||||||
|
if not self.exists_for_collection(collection_string):
|
||||||
|
self.build_for_collection(collection_string)
|
||||||
|
|
||||||
|
redis.hset(
|
||||||
|
self.get_cache_key(collection_string),
|
||||||
|
id,
|
||||||
|
json.dumps(data))
|
||||||
|
|
||||||
|
def del_element(self, collection_string: str, id: int) -> None:
|
||||||
|
"""
|
||||||
|
Removes one element from the cache.
|
||||||
|
|
||||||
|
Does nothing if the cache does not exist.
|
||||||
|
"""
|
||||||
|
redis = get_redis_connection()
|
||||||
|
redis.hdel(
|
||||||
|
self.get_cache_key(collection_string),
|
||||||
|
id)
|
||||||
|
|
||||||
|
def exists_for_collection(self, collection_string: str) -> bool:
|
||||||
|
"""
|
||||||
|
Returns True if the cache for the collection exists, else False.
|
||||||
|
"""
|
||||||
|
redis = get_redis_connection()
|
||||||
|
return redis.exists(self.get_cache_key(collection_string))
|
||||||
|
|
||||||
|
def get_data(self, collection_string: str) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Returns all data for the collection.
|
||||||
|
"""
|
||||||
|
redis = get_redis_connection()
|
||||||
|
return [json.loads(element.decode()) for element in redis.hvals(self.get_cache_key(collection_string))]
|
||||||
|
|
||||||
|
def get_element(self, collection_string: str, id: int) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Returns one element from the collection.
|
||||||
|
|
||||||
|
Raises model.DoesNotExist if the element is not in the cache.
|
||||||
|
"""
|
||||||
|
redis = get_redis_connection()
|
||||||
|
element = redis.hget(self.get_cache_key(collection_string), id)
|
||||||
|
if element is None:
|
||||||
|
from .collection import get_model_from_collection_string
|
||||||
|
model = get_model_from_collection_string(collection_string)
|
||||||
|
raise model.DoesNotExist(collection_string, id)
|
||||||
|
return json.loads(element.decode())
|
||||||
|
|
||||||
|
def get_cache_key(self, collection_string: str) -> str:
|
||||||
|
"""
|
||||||
|
Returns the cache key for a collection.
|
||||||
|
"""
|
||||||
|
return cache.make_key('{}:{}'.format(self.base_cache_key, collection_string))
|
||||||
|
|
||||||
|
|
||||||
|
class DummyFullDataCache:
|
||||||
|
"""
|
||||||
|
Dummy FullDataCache that does nothing.
|
||||||
|
"""
|
||||||
|
def build_for_collection(self, collection_string: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def add_element(self, collection_string: str, id: int, data: Dict[str, Any]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def del_element(self, collection_string: str, id: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def exists_for_collection(self, collection_string: str) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_data(self, collection_string: str) -> List[Dict[str, Any]]:
|
||||||
|
from .collection import get_model_from_collection_string
|
||||||
|
model = get_model_from_collection_string(collection_string)
|
||||||
|
try:
|
||||||
|
query = model.objects.get_full_queryset()
|
||||||
|
except AttributeError:
|
||||||
|
# If the model des not have to method get_full_queryset(), then use
|
||||||
|
# the default queryset from django.
|
||||||
|
query = model.objects
|
||||||
|
|
||||||
|
return [model.get_access_permissions().get_full_data(instance)
|
||||||
|
for instance in query.all()]
|
||||||
|
|
||||||
|
def get_element(self, collection_string: str, id: int) -> Dict[str, Any]:
|
||||||
|
from .collection import get_model_from_collection_string
|
||||||
|
model = get_model_from_collection_string(collection_string)
|
||||||
|
try:
|
||||||
|
query = model.objects.get_full_queryset()
|
||||||
|
except AttributeError:
|
||||||
|
# If the model des not have to method get_full_queryset(), then use
|
||||||
|
# the default queryset from django.
|
||||||
|
query = model.objects
|
||||||
|
|
||||||
|
return model.get_access_permissions().get_full_data(query.get(pk=id))
|
||||||
|
|
||||||
|
|
||||||
class RestrictedDataCache:
|
class RestrictedDataCache:
|
||||||
"""
|
"""
|
||||||
Caches all Data for a specific users.
|
Caches all data for a specific users.
|
||||||
|
|
||||||
|
Helps to get all data from all collections for a specific user.
|
||||||
|
|
||||||
The cached values are expected to be formatted for outout via websocket.
|
The cached values are expected to be formatted for outout via websocket.
|
||||||
"""
|
"""
|
||||||
@ -249,7 +399,7 @@ class RestrictedDataCache:
|
|||||||
The returned value is a list of the elements.
|
The returned value is a list of the elements.
|
||||||
"""
|
"""
|
||||||
redis = get_redis_connection()
|
redis = get_redis_connection()
|
||||||
return [json.loads(element) for element in redis.hvals(self.get_cache_key(user_id))]
|
return [json.loads(element.decode()) for element in redis.hvals(self.get_cache_key(user_id))]
|
||||||
|
|
||||||
def get_cache_key(self, user_id: int) -> str:
|
def get_cache_key(self, user_id: int) -> str:
|
||||||
"""
|
"""
|
||||||
@ -301,6 +451,8 @@ if use_redis_cache():
|
|||||||
restricted_data_cache = DummyRestrictedDataCache() # type: Union[RestrictedDataCache, DummyRestrictedDataCache]
|
restricted_data_cache = DummyRestrictedDataCache() # type: Union[RestrictedDataCache, DummyRestrictedDataCache]
|
||||||
else:
|
else:
|
||||||
restricted_data_cache = RestrictedDataCache()
|
restricted_data_cache = RestrictedDataCache()
|
||||||
|
full_data_cache = FullDataCache() # type: Union[FullDataCache, DummyFullDataCache]
|
||||||
else:
|
else:
|
||||||
websocket_user_cache = DjangoCacheWebsocketUserCache()
|
websocket_user_cache = DjangoCacheWebsocketUserCache()
|
||||||
restricted_data_cache = DummyRestrictedDataCache()
|
restricted_data_cache = DummyRestrictedDataCache()
|
||||||
|
full_data_cache = DummyFullDataCache()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from typing import Mapping # noqa
|
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
@ -6,23 +5,48 @@ from typing import (
|
|||||||
Generator,
|
Generator,
|
||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
Set,
|
|
||||||
Tuple,
|
|
||||||
Type,
|
Type,
|
||||||
Union,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.cache import cache
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
|
from mypy_extensions import TypedDict
|
||||||
|
|
||||||
from .cache import get_redis_connection, use_redis_cache
|
from .cache import full_data_cache
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .access_permissions import BaseAccessPermissions # noqa
|
from .access_permissions import BaseAccessPermissions # noqa
|
||||||
|
|
||||||
# TODO: Try to import this type from access_permission
|
|
||||||
RestrictedData = Union[List[Dict[str, Any]], Dict[str, Any], None]
|
AutoupdateFormat = TypedDict(
|
||||||
|
'AutoupdateFormat',
|
||||||
|
{
|
||||||
|
'collection': str,
|
||||||
|
'id': int,
|
||||||
|
'action': 'str',
|
||||||
|
'data': Dict[str, Any],
|
||||||
|
},
|
||||||
|
total=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
InnerChannelMessageFormat = TypedDict(
|
||||||
|
'InnerChannelMessageFormat',
|
||||||
|
{
|
||||||
|
'collection_string': str,
|
||||||
|
'id': int,
|
||||||
|
'deleted': bool,
|
||||||
|
'information': Dict[str, Any],
|
||||||
|
'full_data': Optional[Dict[str, Any]],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ChannelMessageFormat = TypedDict(
|
||||||
|
'ChannelMessageFormat',
|
||||||
|
{
|
||||||
|
'elements': List[InnerChannelMessageFormat],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CollectionElement:
|
class CollectionElement:
|
||||||
@ -51,7 +75,7 @@ class CollectionElement:
|
|||||||
|
|
||||||
if self.is_deleted():
|
if self.is_deleted():
|
||||||
# Delete the element from the cache, if self.is_deleted() is True:
|
# Delete the element from the cache, if self.is_deleted() is True:
|
||||||
self.delete_from_cache()
|
full_data_cache.del_element(self.collection_string, self.id)
|
||||||
else:
|
else:
|
||||||
# The call to get_full_data() has some sideeffects. When the object
|
# The call to get_full_data() has some sideeffects. When the object
|
||||||
# was created with from_instance() or the object is not in the cache
|
# was created with from_instance() or the object is not in the cache
|
||||||
@ -95,34 +119,14 @@ class CollectionElement:
|
|||||||
return (self.collection_string == collection_element.collection_string and
|
return (self.collection_string == collection_element.collection_string and
|
||||||
self.id == collection_element.id)
|
self.id == collection_element.id)
|
||||||
|
|
||||||
def as_channels_message(self) -> Dict[str, Any]:
|
def as_autoupdate_for_user(self, user: Optional['CollectionElement']) -> AutoupdateFormat:
|
||||||
"""
|
"""
|
||||||
Returns a dictonary that can be used to send the object through the
|
Returns a dict that can be sent through the autoupdate system for a site
|
||||||
channels system.
|
user.
|
||||||
"""
|
"""
|
||||||
channel_message = {
|
|
||||||
'collection_string': self.collection_string,
|
|
||||||
'id': self.id,
|
|
||||||
'deleted': self.is_deleted()}
|
|
||||||
if self.information:
|
|
||||||
channel_message['information'] = self.information
|
|
||||||
if self.full_data:
|
|
||||||
# Do not use the method get_full_data but the attribute, so the
|
|
||||||
# full_data is not generated.
|
|
||||||
channel_message['full_data'] = self.full_data
|
|
||||||
return channel_message
|
|
||||||
|
|
||||||
def as_autoupdate(self, method: str, *args: Any) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Only for internal use. Do not use it directly. Use as_autoupdate_for_user()
|
|
||||||
or as_autoupdate_for_projector().
|
|
||||||
"""
|
|
||||||
from .autoupdate import format_for_autoupdate
|
|
||||||
|
|
||||||
if not self.is_deleted():
|
if not self.is_deleted():
|
||||||
data = getattr(self.get_access_permissions(), method)(
|
restricted_data = self.get_access_permissions().get_restricted_data([self.get_full_data()], user)
|
||||||
self,
|
data = restricted_data[0] if restricted_data else None
|
||||||
*args)
|
|
||||||
else:
|
else:
|
||||||
data = None
|
data = None
|
||||||
|
|
||||||
@ -132,28 +136,31 @@ class CollectionElement:
|
|||||||
action='deleted' if self.is_deleted() else 'changed',
|
action='deleted' if self.is_deleted() else 'changed',
|
||||||
data=data)
|
data=data)
|
||||||
|
|
||||||
def as_autoupdate_for_user(self, user: Optional['CollectionElement']) -> Dict[str, Any]:
|
def as_autoupdate_for_projector(self) -> AutoupdateFormat:
|
||||||
"""
|
|
||||||
Returns a dict that can be sent through the autoupdate system for a site
|
|
||||||
user.
|
|
||||||
"""
|
|
||||||
return self.as_autoupdate(
|
|
||||||
'get_restricted_data',
|
|
||||||
user)
|
|
||||||
|
|
||||||
def as_autoupdate_for_projector(self) -> Dict[str, Any]:
|
|
||||||
"""
|
"""
|
||||||
Returns a dict that can be sent through the autoupdate system for the
|
Returns a dict that can be sent through the autoupdate system for the
|
||||||
projector.
|
projector.
|
||||||
"""
|
"""
|
||||||
return self.as_autoupdate(
|
if not self.is_deleted():
|
||||||
'get_projector_data')
|
restricted_data = self.get_access_permissions().get_projector_data([self.get_full_data()])
|
||||||
|
data = restricted_data[0] if restricted_data else None
|
||||||
|
else:
|
||||||
|
data = None
|
||||||
|
|
||||||
def as_dict_for_user(self, user: Optional['CollectionElement']) -> 'RestrictedData':
|
return format_for_autoupdate(
|
||||||
|
collection_string=self.collection_string,
|
||||||
|
id=self.id,
|
||||||
|
action='deleted' if self.is_deleted() else 'changed',
|
||||||
|
data=data)
|
||||||
|
|
||||||
|
def as_dict_for_user(self, user: Optional['CollectionElement']) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns a dict with the data for a user. Can be used for the rest api.
|
Returns a dict with the data for a user. Can be used for the rest api.
|
||||||
|
|
||||||
|
Returns None if the user does not has the permission to see the element.
|
||||||
"""
|
"""
|
||||||
return self.get_access_permissions().get_restricted_data(self, user)
|
restricted_data = self.get_access_permissions().get_restricted_data([self.get_full_data()], user)
|
||||||
|
return restricted_data[0] if restricted_data else None
|
||||||
|
|
||||||
def get_model(self) -> Type[Model]:
|
def get_model(self) -> Type[Model]:
|
||||||
"""
|
"""
|
||||||
@ -161,31 +168,13 @@ class CollectionElement:
|
|||||||
"""
|
"""
|
||||||
return get_model_from_collection_string(self.collection_string)
|
return get_model_from_collection_string(self.collection_string)
|
||||||
|
|
||||||
def get_instance(self) -> Model:
|
|
||||||
"""
|
|
||||||
Returns the instance as django object.
|
|
||||||
|
|
||||||
May raise a DoesNotExist exception.
|
|
||||||
"""
|
|
||||||
if self.is_deleted():
|
|
||||||
raise RuntimeError("The collection element is deleted.")
|
|
||||||
|
|
||||||
if self.instance is None:
|
|
||||||
model = self.get_model()
|
|
||||||
try:
|
|
||||||
query = model.objects.get_full_queryset()
|
|
||||||
except AttributeError:
|
|
||||||
query = model.objects
|
|
||||||
self.instance = query.get(pk=self.id)
|
|
||||||
return self.instance
|
|
||||||
|
|
||||||
def get_access_permissions(self) -> 'BaseAccessPermissions':
|
def get_access_permissions(self) -> 'BaseAccessPermissions':
|
||||||
"""
|
"""
|
||||||
Returns the get_access_permissions object for the this collection element.
|
Returns the get_access_permissions object for the this collection element.
|
||||||
"""
|
"""
|
||||||
return self.get_model().get_access_permissions()
|
return self.get_model().get_access_permissions()
|
||||||
|
|
||||||
def get_full_data(self) -> Any:
|
def get_full_data(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Returns the full_data of this collection_element from with all other
|
Returns the full_data of this collection_element from with all other
|
||||||
dics can be generated.
|
dics can be generated.
|
||||||
@ -195,17 +184,17 @@ class CollectionElement:
|
|||||||
"""
|
"""
|
||||||
# If the full_data is already loaded, return it
|
# If the full_data is already loaded, return it
|
||||||
# If there is a db_instance, use it to get the full_data
|
# If there is a db_instance, use it to get the full_data
|
||||||
# else: try to use the cache.
|
# else: use the cache.
|
||||||
# If there is no value in the cache, get the content from the db and save
|
|
||||||
# it to the cache.
|
|
||||||
if self.full_data is None and self.instance is None:
|
|
||||||
# Use the cache version if self.instance is not set.
|
|
||||||
# After this line full_data can be None, if the element is not in the cache.
|
|
||||||
self.full_data = cache.get(self.get_cache_key())
|
|
||||||
|
|
||||||
if self.full_data is None:
|
if self.full_data is None:
|
||||||
self.full_data = self.get_access_permissions().get_full_data(self.get_instance())
|
if self.instance is None:
|
||||||
self.save_to_cache()
|
# Make sure the cache exists
|
||||||
|
if not full_data_cache.exists_for_collection(self.collection_string):
|
||||||
|
# Build the cache if it does not exists.
|
||||||
|
full_data_cache.build_for_collection(self.collection_string)
|
||||||
|
self.full_data = full_data_cache.get_element(self.collection_string, self.id)
|
||||||
|
else:
|
||||||
|
self.full_data = self.get_access_permissions().get_full_data(self.instance)
|
||||||
|
full_data_cache.add_element(self.collection_string, self.id, self.full_data)
|
||||||
return self.full_data
|
return self.full_data
|
||||||
|
|
||||||
def is_deleted(self) -> bool:
|
def is_deleted(self) -> bool:
|
||||||
@ -214,74 +203,6 @@ class CollectionElement:
|
|||||||
"""
|
"""
|
||||||
return self.deleted
|
return self.deleted
|
||||||
|
|
||||||
def get_cache_key(self) -> str:
|
|
||||||
"""
|
|
||||||
Returns a string that is used as cache key for a single instance.
|
|
||||||
"""
|
|
||||||
return get_single_element_cache_key(self.collection_string, self.id)
|
|
||||||
|
|
||||||
def delete_from_cache(self) -> None:
|
|
||||||
"""
|
|
||||||
Delets the element from the cache.
|
|
||||||
|
|
||||||
Does nothing if the element is not in the cache.
|
|
||||||
"""
|
|
||||||
# Deletes the element from the cache.
|
|
||||||
cache.delete(self.get_cache_key())
|
|
||||||
|
|
||||||
# Delete the id of the instance of the instance list
|
|
||||||
Collection(self.collection_string).delete_id_from_cache(self.id)
|
|
||||||
|
|
||||||
def save_to_cache(self) -> None:
|
|
||||||
"""
|
|
||||||
Add or update the element to the cache.
|
|
||||||
"""
|
|
||||||
# Set the element to the cache.
|
|
||||||
cache.set(self.get_cache_key(), self.get_full_data())
|
|
||||||
|
|
||||||
# Add the id of the element to the collection
|
|
||||||
Collection(self.collection_string).add_id_to_cache(self.id)
|
|
||||||
|
|
||||||
|
|
||||||
class CollectionElementList(list):
|
|
||||||
"""
|
|
||||||
List for collection elements that can hold collection elements from
|
|
||||||
different collections.
|
|
||||||
|
|
||||||
It acts like a normal python list but with the following methods.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_channels_message(cls, message: Dict[str, Any]) -> 'CollectionElementList':
|
|
||||||
"""
|
|
||||||
Creates a collection element list from a channel message.
|
|
||||||
"""
|
|
||||||
self = cls()
|
|
||||||
for values in message['elements']:
|
|
||||||
self.append(CollectionElement.from_values(**values))
|
|
||||||
return self
|
|
||||||
|
|
||||||
def as_channels_message(self) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Returns a list of dicts that can be send through the channel system.
|
|
||||||
"""
|
|
||||||
message = {'elements': []} # type: Dict[str, Any]
|
|
||||||
for element in self:
|
|
||||||
message['elements'].append(element.as_channels_message())
|
|
||||||
return message
|
|
||||||
|
|
||||||
def as_autoupdate_for_user(self, user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Returns a list of dicts, that can be send though the websocket to a user.
|
|
||||||
|
|
||||||
The argument `user` can be anything, that is allowd as argument for
|
|
||||||
utils.auth.has_perm().
|
|
||||||
"""
|
|
||||||
result = []
|
|
||||||
for element in self:
|
|
||||||
result.append(element.as_autoupdate_for_user(user))
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class Collection:
|
class Collection:
|
||||||
"""
|
"""
|
||||||
@ -298,18 +219,6 @@ class Collection:
|
|||||||
self.collection_string = collection_string
|
self.collection_string = collection_string
|
||||||
self.full_data = full_data
|
self.full_data = full_data
|
||||||
|
|
||||||
def get_cache_key(self, raw: bool=False) -> str:
|
|
||||||
"""
|
|
||||||
Returns a string that is used as cache key for a collection.
|
|
||||||
|
|
||||||
Django adds a prefix to the cache key when using the django cache api.
|
|
||||||
In other cases use raw=True to add the same cache key.
|
|
||||||
"""
|
|
||||||
key = get_element_list_cache_key(self.collection_string)
|
|
||||||
if raw:
|
|
||||||
key = cache.make_key(key)
|
|
||||||
return key
|
|
||||||
|
|
||||||
def get_model(self) -> Type[Model]:
|
def get_model(self) -> Type[Model]:
|
||||||
"""
|
"""
|
||||||
Returns the django model that is used for this collection.
|
Returns the django model that is used for this collection.
|
||||||
@ -326,38 +235,11 @@ class Collection:
|
|||||||
"""
|
"""
|
||||||
Generator that yields all collection_elements of this collection.
|
Generator that yields all collection_elements of this collection.
|
||||||
"""
|
"""
|
||||||
# TODO: This method should use self.full_data if it already exists.
|
for full_data in self.get_full_data():
|
||||||
|
|
||||||
# Get all cache keys.
|
|
||||||
ids = self.get_all_ids()
|
|
||||||
cache_keys = [
|
|
||||||
get_single_element_cache_key(self.collection_string, id)
|
|
||||||
for id in ids]
|
|
||||||
cached_full_data_dict = cache.get_many(cache_keys)
|
|
||||||
|
|
||||||
# Get all ids that are missing.
|
|
||||||
missing_cache_keys = set(cache_keys).difference(cached_full_data_dict.keys())
|
|
||||||
missing_ids = set(
|
|
||||||
get_collection_id_from_cache_key(cache_key)[1]
|
|
||||||
for cache_key in missing_cache_keys)
|
|
||||||
|
|
||||||
# Generate collection elements that where in the cache.
|
|
||||||
for cache_key, cached_full_data in cached_full_data_dict.items():
|
|
||||||
collection_string, id = get_collection_id_from_cache_key(cache_key)
|
|
||||||
yield CollectionElement.from_values(
|
yield CollectionElement.from_values(
|
||||||
collection_string,
|
self.collection_string,
|
||||||
id,
|
full_data['id'],
|
||||||
full_data=cached_full_data)
|
full_data=full_data)
|
||||||
|
|
||||||
# Generate collection element that where not in the cache.
|
|
||||||
if missing_ids:
|
|
||||||
model = self.get_model()
|
|
||||||
try:
|
|
||||||
query = model.objects.get_full_queryset()
|
|
||||||
except AttributeError:
|
|
||||||
query = model.objects
|
|
||||||
for instance in query.filter(pk__in=missing_ids):
|
|
||||||
yield CollectionElement.from_instance(instance)
|
|
||||||
|
|
||||||
def get_full_data(self) -> List[Dict[str, Any]]:
|
def get_full_data(self) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@ -365,129 +247,19 @@ class Collection:
|
|||||||
elements.
|
elements.
|
||||||
"""
|
"""
|
||||||
if self.full_data is None:
|
if self.full_data is None:
|
||||||
self.full_data = [
|
# Build the cache, if it does not exists.
|
||||||
collection_element.get_full_data()
|
if not full_data_cache.exists_for_collection(self.collection_string):
|
||||||
for collection_element
|
full_data_cache.build_for_collection(self.collection_string)
|
||||||
in self.element_generator()]
|
|
||||||
|
self.full_data = full_data_cache.get_data(self.collection_string)
|
||||||
return self.full_data
|
return self.full_data
|
||||||
|
|
||||||
def as_autoupdate_for_projector(self) -> List[Dict[str, Any]]:
|
def as_list_for_user(self, user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
|
||||||
"""
|
|
||||||
Returns a list of dictonaries to send them to the projector.
|
|
||||||
"""
|
|
||||||
# TODO: This method is only used in one case. Remove it.
|
|
||||||
output = []
|
|
||||||
for collection_element in self.element_generator():
|
|
||||||
content = collection_element.as_autoupdate_for_projector()
|
|
||||||
# Content can not be None. If the projector can not see an element,
|
|
||||||
# then it is marked as deleted.
|
|
||||||
output.append(content)
|
|
||||||
return output
|
|
||||||
|
|
||||||
def as_autoupdate_for_user(self, user: Optional[CollectionElement]) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Returns a list of dicts, that can be send though the websocket to a user.
|
|
||||||
"""
|
|
||||||
# TODO: This method is not used. Remove it.
|
|
||||||
output = []
|
|
||||||
for collection_element in self.element_generator():
|
|
||||||
content = collection_element.as_autoupdate_for_user(user)
|
|
||||||
if content is not None:
|
|
||||||
output.append(content)
|
|
||||||
return output
|
|
||||||
|
|
||||||
def as_list_for_user(self, user: Optional[CollectionElement]) -> List['RestrictedData']:
|
|
||||||
"""
|
"""
|
||||||
Returns a list of dictonaries to send them to a user, for example over
|
Returns a list of dictonaries to send them to a user, for example over
|
||||||
the rest api.
|
the rest api.
|
||||||
"""
|
"""
|
||||||
output = [] # type: List[RestrictedData]
|
return self.get_access_permissions().get_restricted_data(self.get_full_data(), user)
|
||||||
for collection_element in self.element_generator():
|
|
||||||
content = collection_element.as_dict_for_user(user) # type: RestrictedData
|
|
||||||
if content is not None:
|
|
||||||
output.append(content)
|
|
||||||
return output
|
|
||||||
|
|
||||||
def get_all_ids(self) -> Set[int]:
|
|
||||||
"""
|
|
||||||
Returns a set of all ids of instances in this collection.
|
|
||||||
"""
|
|
||||||
if use_redis_cache():
|
|
||||||
ids = self.get_all_ids_redis()
|
|
||||||
else:
|
|
||||||
ids = self.get_all_ids_other()
|
|
||||||
return ids
|
|
||||||
|
|
||||||
def get_all_ids_redis(self) -> Set[int]:
|
|
||||||
redis = get_redis_connection()
|
|
||||||
ids = redis.smembers(self.get_cache_key(raw=True))
|
|
||||||
if not ids:
|
|
||||||
ids = set(self.get_model().objects.values_list('pk', flat=True))
|
|
||||||
if ids:
|
|
||||||
redis.sadd(self.get_cache_key(raw=True), *ids)
|
|
||||||
# Redis returns the ids as string.
|
|
||||||
ids = set(int(id) for id in ids)
|
|
||||||
return ids
|
|
||||||
|
|
||||||
def get_all_ids_other(self) -> Set[int]:
|
|
||||||
ids = cache.get(self.get_cache_key())
|
|
||||||
if ids is None:
|
|
||||||
# If it is not in the cache then get it from the database.
|
|
||||||
ids = set(self.get_model().objects.values_list('pk', flat=True))
|
|
||||||
cache.set(self.get_cache_key(), ids)
|
|
||||||
return ids
|
|
||||||
|
|
||||||
def delete_id_from_cache(self, id: int) -> None:
|
|
||||||
"""
|
|
||||||
Delets a id from the cache.
|
|
||||||
"""
|
|
||||||
if use_redis_cache():
|
|
||||||
self.delete_id_from_cache_redis(id)
|
|
||||||
else:
|
|
||||||
self.delete_id_from_cache_other(id)
|
|
||||||
|
|
||||||
def delete_id_from_cache_redis(self, id: int) -> None:
|
|
||||||
redis = get_redis_connection()
|
|
||||||
redis.srem(self.get_cache_key(raw=True), id)
|
|
||||||
|
|
||||||
def delete_id_from_cache_other(self, id: int) -> None:
|
|
||||||
ids = cache.get(self.get_cache_key())
|
|
||||||
if ids is not None:
|
|
||||||
ids = set(ids)
|
|
||||||
try:
|
|
||||||
ids.remove(id)
|
|
||||||
except KeyError:
|
|
||||||
# The id is not part of id list
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if ids:
|
|
||||||
cache.set(self.get_cache_key(), ids)
|
|
||||||
else:
|
|
||||||
# Delete the key, if there are not ids left
|
|
||||||
cache.delete(self.get_cache_key())
|
|
||||||
|
|
||||||
def add_id_to_cache(self, id: int) -> None:
|
|
||||||
"""
|
|
||||||
Adds a collection id to the list of collection ids in the cache.
|
|
||||||
"""
|
|
||||||
if use_redis_cache():
|
|
||||||
self.add_id_to_cache_redis(id)
|
|
||||||
else:
|
|
||||||
self.add_id_to_cache_other(id)
|
|
||||||
|
|
||||||
def add_id_to_cache_redis(self, id: int) -> None:
|
|
||||||
redis = get_redis_connection()
|
|
||||||
if redis.exists(self.get_cache_key(raw=True)):
|
|
||||||
# Only add the value if it is in the cache.
|
|
||||||
redis.sadd(self.get_cache_key(raw=True), id)
|
|
||||||
|
|
||||||
def add_id_to_cache_other(self, id: int) -> None:
|
|
||||||
ids = cache.get(self.get_cache_key())
|
|
||||||
if ids is not None:
|
|
||||||
# Only change the value if it is in the cache.
|
|
||||||
ids = set(ids)
|
|
||||||
ids.add(id)
|
|
||||||
cache.set(self.get_cache_key(), ids)
|
|
||||||
|
|
||||||
|
|
||||||
_models_to_collection_string = {} # type: Dict[str, Type[Model]]
|
_models_to_collection_string = {} # type: Dict[str, Type[Model]]
|
||||||
@ -522,36 +294,52 @@ def get_model_from_collection_string(collection_string: str) -> Type[Model]:
|
|||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
def get_single_element_cache_key(collection_string: str, id: int) -> str:
|
def format_for_autoupdate(collection_string: str, id: int, action: str, data: Dict[str, Any]=None) -> AutoupdateFormat:
|
||||||
"""
|
"""
|
||||||
Returns a string that is used as cache key for a single instance.
|
Returns a dict that can be used for autoupdate.
|
||||||
"""
|
"""
|
||||||
return "{prefix}{id}".format(
|
if data is None:
|
||||||
prefix=get_single_element_cache_key_prefix(collection_string),
|
# If the data is None then the action has to be deleted,
|
||||||
id=id)
|
# even when it says diffrently. This can happen when the object is not
|
||||||
|
# deleted, but the user has no permission to see it.
|
||||||
|
action = 'deleted'
|
||||||
|
|
||||||
|
output = AutoupdateFormat(
|
||||||
|
collection=collection_string,
|
||||||
|
id=id,
|
||||||
|
action=action,
|
||||||
|
)
|
||||||
|
|
||||||
|
if action != 'deleted':
|
||||||
|
data = cast(Dict[str, Any], data) # In this case data can not be None
|
||||||
|
output['data'] = data
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
def get_single_element_cache_key_prefix(collection_string: str) -> str:
|
def to_channel_message(elements: List[CollectionElement]) -> ChannelMessageFormat:
|
||||||
"""
|
"""
|
||||||
Returns the first part of the cache key for single elements, which is the
|
Converts a list of collection elements to a dict, that can be send to the
|
||||||
same for all cache keys of the same collection.
|
channels system.
|
||||||
"""
|
"""
|
||||||
return "{collection}:".format(collection=collection_string)
|
output = []
|
||||||
|
for element in elements:
|
||||||
|
output.append(InnerChannelMessageFormat(
|
||||||
|
collection_string=element.collection_string,
|
||||||
|
id=element.id,
|
||||||
|
deleted=element.is_deleted(),
|
||||||
|
information=element.information,
|
||||||
|
full_data=element.full_data,
|
||||||
|
))
|
||||||
|
return ChannelMessageFormat(elements=output)
|
||||||
|
|
||||||
|
|
||||||
def get_element_list_cache_key(collection_string: str) -> str:
|
def from_channel_message(message: ChannelMessageFormat) -> List[CollectionElement]:
|
||||||
"""
|
"""
|
||||||
Returns a string that is used as cache key for a collection.
|
Converts a list of collection elements back from a dict, that was created
|
||||||
|
via to_channel_message.
|
||||||
"""
|
"""
|
||||||
return "{collection}".format(collection=collection_string)
|
elements = []
|
||||||
|
for value in message['elements']:
|
||||||
|
elements.append(CollectionElement.from_values(**value))
|
||||||
def get_collection_id_from_cache_key(cache_key: str) -> Tuple[str, int]:
|
return elements
|
||||||
"""
|
|
||||||
Returns a tuble of the collection string and the id from a cache_key
|
|
||||||
created with get_instance_cache_key.
|
|
||||||
|
|
||||||
The returned id can be an integer or an string.
|
|
||||||
"""
|
|
||||||
collection_string, id = cache_key.rsplit(':', 1)
|
|
||||||
return (collection_string, int(id))
|
|
||||||
|
@ -115,5 +115,5 @@ class RESTModelMixin:
|
|||||||
# The deletion of a included element is a change of the root element.
|
# The deletion of a included element is a change of the root element.
|
||||||
inform_changed_data(self.get_root_rest_element(), information=information)
|
inform_changed_data(self.get_root_rest_element(), information=information)
|
||||||
else:
|
else:
|
||||||
inform_deleted_data(self.get_collection_string(), instance_pk, information=information)
|
inform_deleted_data([(self.get_collection_string(), instance_pk)], information=information)
|
||||||
return return_value
|
return return_value
|
||||||
|
@ -32,7 +32,7 @@ from rest_framework.viewsets import GenericViewSet as _GenericViewSet # noqa
|
|||||||
from rest_framework.viewsets import ModelViewSet as _ModelViewSet # noqa
|
from rest_framework.viewsets import ModelViewSet as _ModelViewSet # noqa
|
||||||
from rest_framework.viewsets import ViewSet as _ViewSet # noqa
|
from rest_framework.viewsets import ViewSet as _ViewSet # noqa
|
||||||
|
|
||||||
from .access_permissions import BaseAccessPermissions, RestrictedData # noqa
|
from .access_permissions import BaseAccessPermissions
|
||||||
from .auth import user_to_collection_user
|
from .auth import user_to_collection_user
|
||||||
from .collection import Collection, CollectionElement
|
from .collection import Collection, CollectionElement
|
||||||
|
|
||||||
@ -214,7 +214,7 @@ class RetrieveModelMixin(_RetrieveModelMixin):
|
|||||||
collection_string, self.kwargs[lookup_url_kwarg])
|
collection_string, self.kwargs[lookup_url_kwarg])
|
||||||
user = user_to_collection_user(request.user)
|
user = user_to_collection_user(request.user)
|
||||||
try:
|
try:
|
||||||
content = collection_element.as_dict_for_user(user) # type: RestrictedData
|
content = collection_element.as_dict_for_user(user)
|
||||||
except collection_element.get_model().DoesNotExist:
|
except collection_element.get_model().DoesNotExist:
|
||||||
raise Http404
|
raise Http404
|
||||||
if content is None:
|
if content is None:
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
from contextlib import ContextDecorator
|
|
||||||
from typing import Any
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from django.core.cache import caches
|
|
||||||
from django.test import TestCase as _TestCase
|
from django.test import TestCase as _TestCase
|
||||||
from django.test.runner import DiscoverRunner
|
from django.test.runner import DiscoverRunner
|
||||||
|
|
||||||
@ -37,23 +32,6 @@ class TestCase(_TestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
def tearDown(self) -> None:
|
||||||
|
from django_redis import get_redis_connection
|
||||||
config.key_to_id = {}
|
config.key_to_id = {}
|
||||||
|
get_redis_connection("default").flushall()
|
||||||
|
|
||||||
class use_cache(ContextDecorator):
|
|
||||||
"""
|
|
||||||
Contextmanager that changes the code to use the local memory cache.
|
|
||||||
|
|
||||||
Can also be used as decorator for a function.
|
|
||||||
|
|
||||||
The code inside the contextmananger starts with an empty cache.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __enter__(self) -> None:
|
|
||||||
cache = caches['locmem']
|
|
||||||
cache.clear()
|
|
||||||
self.patch = patch('openslides.utils.collection.cache', cache)
|
|
||||||
self.patch.start()
|
|
||||||
|
|
||||||
def __exit__(self, *exc: Any) -> None:
|
|
||||||
self.patch.stop()
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
# Requirements for OpenSlides in production
|
# Requirements for OpenSlides in production
|
||||||
-r requirements_production.txt
|
-r requirements_big_mode.txt
|
||||||
|
|
||||||
# Requirements for development and tests in alphabetical order
|
# Requirements for development and tests in alphabetical order
|
||||||
coverage
|
coverage
|
||||||
flake8
|
flake8
|
||||||
isort==4.2.5
|
isort==4.2.5
|
||||||
mypy
|
mypy
|
||||||
|
fakeredis
|
||||||
|
@ -24,3 +24,6 @@ disallow_any = unannotated
|
|||||||
|
|
||||||
[mypy-openslides.core.config]
|
[mypy-openslides.core.config]
|
||||||
disallow_any = unannotated
|
disallow_any = unannotated
|
||||||
|
|
||||||
|
[mypy-tests.*]
|
||||||
|
ignore_errors = true
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.utils.translation import ugettext
|
from django.utils.translation import ugettext
|
||||||
|
from django_redis import get_redis_connection
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
@ -11,7 +12,8 @@ from openslides.core.models import Countdown
|
|||||||
from openslides.motions.models import Motion
|
from openslides.motions.models import Motion
|
||||||
from openslides.topics.models import Topic
|
from openslides.topics.models import Topic
|
||||||
from openslides.users.models import User
|
from openslides.users.models import User
|
||||||
from openslides.utils.test import TestCase, use_cache
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
class RetrieveItem(TestCase):
|
class RetrieveItem(TestCase):
|
||||||
@ -91,12 +93,11 @@ class TestDBQueries(TestCase):
|
|||||||
Motion.objects.create(title='motion2')
|
Motion.objects.create(title='motion2')
|
||||||
Assignment.objects.create(title='assignment', open_posts=5)
|
Assignment.objects.create(title='assignment', open_posts=5)
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 4 requests to get the session an the request user with its permissions,
|
* 7 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all agenda items,
|
* 1 requests to get the list of all agenda items,
|
||||||
* 1 request to get all speakers,
|
* 1 request to get all speakers,
|
||||||
* 3 requests to get the assignments, motions and topics and
|
* 3 requests to get the assignments, motions and topics and
|
||||||
|
|
||||||
@ -105,15 +106,15 @@ class TestDBQueries(TestCase):
|
|||||||
TODO: The last two request for the motionsversions are a bug.
|
TODO: The last two request for the motionsversions are a bug.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(User.objects.get(pk=1))
|
self.client.force_login(User.objects.get(pk=1))
|
||||||
with self.assertNumQueries(12):
|
get_redis_connection("default").flushall()
|
||||||
|
with self.assertNumQueries(14):
|
||||||
self.client.get(reverse('item-list'))
|
self.client.get(reverse('item-list'))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_anonymous(self):
|
def test_anonymous(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 3 requests to get the permission for anonymous,
|
* 3 requests to get the permission for anonymous,
|
||||||
* 2 requests to get the list of all agenda items,
|
* 1 requests to get the list of all agenda items,
|
||||||
* 1 request to get all speakers,
|
* 1 request to get all speakers,
|
||||||
* 3 requests to get the assignments, motions and topics and
|
* 3 requests to get the assignments, motions and topics and
|
||||||
|
|
||||||
@ -121,7 +122,8 @@ class TestDBQueries(TestCase):
|
|||||||
|
|
||||||
TODO: The last two request for the motionsversions are a bug.
|
TODO: The last two request for the motionsversions are a bug.
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(11):
|
get_redis_connection("default").flushall()
|
||||||
|
with self.assertNumQueries(10):
|
||||||
self.client.get(reverse('item-list'))
|
self.client.get(reverse('item-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -205,6 +207,8 @@ class ManageSpeaker(TestCase):
|
|||||||
group_delegates = type(group_staff).objects.get(name='Delegates')
|
group_delegates = type(group_staff).objects.get(name='Delegates')
|
||||||
admin.groups.add(group_delegates)
|
admin.groups.add(group_delegates)
|
||||||
admin.groups.remove(group_staff)
|
admin.groups.remove(group_staff)
|
||||||
|
inform_changed_data(admin)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('item-manage-speaker', args=[self.item.pk]),
|
reverse('item-manage-speaker', args=[self.item.pk]),
|
||||||
{'user': self.user.pk})
|
{'user': self.user.pk})
|
||||||
@ -240,7 +244,9 @@ class ManageSpeaker(TestCase):
|
|||||||
group_delegates = type(group_staff).objects.get(name='Delegates')
|
group_delegates = type(group_staff).objects.get(name='Delegates')
|
||||||
admin.groups.add(group_delegates)
|
admin.groups.add(group_delegates)
|
||||||
admin.groups.remove(group_staff)
|
admin.groups.remove(group_staff)
|
||||||
|
inform_changed_data(admin)
|
||||||
speaker = Speaker.objects.add(self.user, self.item)
|
speaker = Speaker.objects.add(self.user, self.item)
|
||||||
|
|
||||||
response = self.client.delete(
|
response = self.client.delete(
|
||||||
reverse('item-manage-speaker', args=[self.item.pk]),
|
reverse('item-manage-speaker', args=[self.item.pk]),
|
||||||
{'speaker': speaker.pk})
|
{'speaker': speaker.pk})
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django_redis import get_redis_connection
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from openslides.assignments.models import Assignment
|
from openslides.assignments.models import Assignment
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.users.models import User
|
from openslides.users.models import User
|
||||||
from openslides.utils.test import TestCase, use_cache
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestDBQueries(TestCase):
|
class TestDBQueries(TestCase):
|
||||||
@ -24,12 +26,11 @@ class TestDBQueries(TestCase):
|
|||||||
for index in range(10):
|
for index in range(10):
|
||||||
Assignment.objects.create(title='motion{}'.format(index), open_posts=1)
|
Assignment.objects.create(title='motion{}'.format(index), open_posts=1)
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 4 requests to get the session an the request user with its permissions,
|
* 7 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all assignments,
|
* 1 requests to get the list of all assignments,
|
||||||
* 1 request to get all related users,
|
* 1 request to get all related users,
|
||||||
* 1 request to get the agenda item,
|
* 1 request to get the agenda item,
|
||||||
* 1 request to get the polls,
|
* 1 request to get the polls,
|
||||||
@ -40,15 +41,15 @@ class TestDBQueries(TestCase):
|
|||||||
TODO: The last request are a bug.
|
TODO: The last request are a bug.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(User.objects.get(pk=1))
|
self.client.force_login(User.objects.get(pk=1))
|
||||||
with self.assertNumQueries(20):
|
get_redis_connection("default").flushall()
|
||||||
|
with self.assertNumQueries(22):
|
||||||
self.client.get(reverse('assignment-list'))
|
self.client.get(reverse('assignment-list'))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_anonymous(self):
|
def test_anonymous(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 3 requests to get the permission for anonymous,
|
* 3 requests to get the permission for anonymous,
|
||||||
* 2 requests to get the list of all assignments,
|
* 1 requests to get the list of all assignments,
|
||||||
* 1 request to get all related users,
|
* 1 request to get all related users,
|
||||||
* 1 request to get the agenda item,
|
* 1 request to get the agenda item,
|
||||||
* 1 request to get the polls,
|
* 1 request to get the polls,
|
||||||
@ -58,7 +59,8 @@ class TestDBQueries(TestCase):
|
|||||||
|
|
||||||
TODO: The last 10 requests are an bug.
|
TODO: The last 10 requests are an bug.
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(19):
|
get_redis_connection("default").flushall()
|
||||||
|
with self.assertNumQueries(18):
|
||||||
self.client.get(reverse('assignment-list'))
|
self.client.get(reverse('assignment-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -109,6 +111,7 @@ class CanidatureSelf(TestCase):
|
|||||||
group_delegates = type(group_staff).objects.get(name='Delegates')
|
group_delegates = type(group_staff).objects.get(name='Delegates')
|
||||||
admin.groups.add(group_delegates)
|
admin.groups.add(group_delegates)
|
||||||
admin.groups.remove(group_staff)
|
admin.groups.remove(group_staff)
|
||||||
|
inform_changed_data(admin)
|
||||||
|
|
||||||
response = self.client.post(reverse('assignment-candidature-self', args=[self.assignment.pk]))
|
response = self.client.post(reverse('assignment-candidature-self', args=[self.assignment.pk]))
|
||||||
|
|
||||||
@ -155,6 +158,7 @@ class CanidatureSelf(TestCase):
|
|||||||
group_delegates = type(group_staff).objects.get(name='Delegates')
|
group_delegates = type(group_staff).objects.get(name='Delegates')
|
||||||
admin.groups.add(group_delegates)
|
admin.groups.add(group_delegates)
|
||||||
admin.groups.remove(group_staff)
|
admin.groups.remove(group_staff)
|
||||||
|
inform_changed_data(admin)
|
||||||
|
|
||||||
response = self.client.delete(reverse('assignment-candidature-self', args=[self.assignment.pk]))
|
response = self.client.delete(reverse('assignment-candidature-self', args=[self.assignment.pk]))
|
||||||
|
|
||||||
@ -235,6 +239,7 @@ class CandidatureOther(TestCase):
|
|||||||
group_delegates = type(group_staff).objects.get(name='Delegates')
|
group_delegates = type(group_staff).objects.get(name='Delegates')
|
||||||
admin.groups.add(group_delegates)
|
admin.groups.add(group_delegates)
|
||||||
admin.groups.remove(group_staff)
|
admin.groups.remove(group_staff)
|
||||||
|
inform_changed_data(admin)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('assignment-candidature-other', args=[self.assignment.pk]),
|
reverse('assignment-candidature-other', args=[self.assignment.pk]),
|
||||||
@ -290,6 +295,7 @@ class CandidatureOther(TestCase):
|
|||||||
group_delegates = type(group_staff).objects.get(name='Delegates')
|
group_delegates = type(group_staff).objects.get(name='Delegates')
|
||||||
admin.groups.add(group_delegates)
|
admin.groups.add(group_delegates)
|
||||||
admin.groups.remove(group_staff)
|
admin.groups.remove(group_staff)
|
||||||
|
inform_changed_data(admin)
|
||||||
|
|
||||||
response = self.client.delete(
|
response = self.client.delete(
|
||||||
reverse('assignment-candidature-other', args=[self.assignment.pk]),
|
reverse('assignment-candidature-other', args=[self.assignment.pk]),
|
||||||
|
@ -108,6 +108,7 @@ class ConfigViewSet(TestCase):
|
|||||||
# Save the old value of the config object and add the test values
|
# Save the old value of the config object and add the test values
|
||||||
# TODO: Can be changed to setUpClass when Django 1.8 is no longer supported
|
# TODO: Can be changed to setUpClass when Django 1.8 is no longer supported
|
||||||
self._config_values = config.config_variables.copy()
|
self._config_values = config.config_variables.copy()
|
||||||
|
config.key_to_id = {}
|
||||||
config.update_config_variables(set_simple_config_view_integration_config_test())
|
config.update_config_variables(set_simple_config_view_integration_config_test())
|
||||||
config.save_default_values()
|
config.save_default_values()
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django_redis import get_redis_connection
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
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.core.models import ChatMessage, Projector, Tag
|
from openslides.core.models import ChatMessage, Projector, Tag
|
||||||
from openslides.users.models import User
|
from openslides.users.models import User
|
||||||
from openslides.utils.test import TestCase, use_cache
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProjectorDBQueries(TestCase):
|
class TestProjectorDBQueries(TestCase):
|
||||||
@ -23,27 +24,27 @@ class TestProjectorDBQueries(TestCase):
|
|||||||
for index in range(10):
|
for index in range(10):
|
||||||
Projector.objects.create(name="Projector{}".format(index))
|
Projector.objects.create(name="Projector{}".format(index))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 4 requests to get the session an the request user with its permissions,
|
* 7 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all projectors,
|
* 1 requests to get the list of all projectors,
|
||||||
* 1 request to get the list of the projector defaults.
|
* 1 request to get the list of the projector defaults.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(User.objects.get(pk=1))
|
self.client.force_login(User.objects.get(pk=1))
|
||||||
with self.assertNumQueries(7):
|
get_redis_connection("default").flushall()
|
||||||
|
with self.assertNumQueries(9):
|
||||||
self.client.get(reverse('projector-list'))
|
self.client.get(reverse('projector-list'))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_anonymous(self):
|
def test_anonymous(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 3 requests to get the permission for anonymous,
|
* 3 requests to get the permission for anonymous,
|
||||||
* 2 requests to get the list of all projectors,
|
* 1 requests to get the list of all projectors,
|
||||||
* 1 request to get the list of the projector defaults and
|
* 1 request to get the list of the projector defaults and
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(6):
|
get_redis_connection("default").flushall()
|
||||||
|
with self.assertNumQueries(5):
|
||||||
self.client.get(reverse('projector-list'))
|
self.client.get(reverse('projector-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -63,15 +64,15 @@ class TestCharmessageDBQueries(TestCase):
|
|||||||
for index in range(10):
|
for index in range(10):
|
||||||
ChatMessage.objects.create(user=user)
|
ChatMessage.objects.create(user=user)
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 4 requests to get the session an the request user with its permissions,
|
* 7 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all chatmessages,
|
* 1 requests to get the list of all chatmessages,
|
||||||
"""
|
"""
|
||||||
self.client.force_login(User.objects.get(pk=1))
|
self.client.force_login(User.objects.get(pk=1))
|
||||||
with self.assertNumQueries(6):
|
get_redis_connection("default").flushall()
|
||||||
|
with self.assertNumQueries(8):
|
||||||
self.client.get(reverse('chatmessage-list'))
|
self.client.get(reverse('chatmessage-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -90,25 +91,25 @@ class TestTagDBQueries(TestCase):
|
|||||||
for index in range(10):
|
for index in range(10):
|
||||||
Tag.objects.create(name='tag{}'.format(index))
|
Tag.objects.create(name='tag{}'.format(index))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 2 requests to get the session an the request user with its permissions,
|
* 5 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all tags,
|
* 1 requests to get the list of all tags,
|
||||||
"""
|
"""
|
||||||
self.client.force_login(User.objects.get(pk=1))
|
self.client.force_login(User.objects.get(pk=1))
|
||||||
with self.assertNumQueries(4):
|
get_redis_connection("default").flushall()
|
||||||
|
with self.assertNumQueries(6):
|
||||||
self.client.get(reverse('tag-list'))
|
self.client.get(reverse('tag-list'))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_anonymous(self):
|
def test_anonymous(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 1 requests to see if anonyomus is enabled
|
* 1 requests to see if anonyomus is enabled
|
||||||
* 2 requests to get the list of all projectors,
|
* 1 requests to get the list of all projectors,
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(3):
|
get_redis_connection("default").flushall()
|
||||||
|
with self.assertNumQueries(2):
|
||||||
self.client.get(reverse('tag-list'))
|
self.client.get(reverse('tag-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -125,29 +126,24 @@ class TestConfigDBQueries(TestCase):
|
|||||||
config['general_system_enable_anonymous'] = True
|
config['general_system_enable_anonymous'] = True
|
||||||
config.save_default_values()
|
config.save_default_values()
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 2 requests to get the session an the request user with its permissions and
|
* 5 requests to get the session an the request user with its permissions and
|
||||||
* 1 requests to get the list of all config values
|
* 1 requests to get the list of all config values
|
||||||
|
|
||||||
* 1 more that I do not understand
|
|
||||||
"""
|
"""
|
||||||
self.client.force_login(User.objects.get(pk=1))
|
self.client.force_login(User.objects.get(pk=1))
|
||||||
with self.assertNumQueries(4):
|
get_redis_connection("default").flushall()
|
||||||
|
with self.assertNumQueries(6):
|
||||||
self.client.get(reverse('config-list'))
|
self.client.get(reverse('config-list'))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_anonymous(self):
|
def test_anonymous(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 1 requests to see if anonymous is enabled
|
* 1 requests to see if anonymous is enabled and get all config values
|
||||||
* 1 to get all config value and
|
|
||||||
|
|
||||||
* 1 more that I do not understand
|
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(3):
|
get_redis_connection("default").flushall()
|
||||||
|
with self.assertNumQueries(1):
|
||||||
self.client.get(reverse('config-list'))
|
self.client.get(reverse('config-list'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django_redis import get_redis_connection
|
||||||
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.mediafiles.models import Mediafile
|
from openslides.mediafiles.models import Mediafile
|
||||||
from openslides.users.models import User
|
from openslides.users.models import User
|
||||||
from openslides.utils.test import TestCase, use_cache
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestDBQueries(TestCase):
|
class TestDBQueries(TestCase):
|
||||||
@ -27,23 +28,23 @@ class TestDBQueries(TestCase):
|
|||||||
'some_file{}'.format(index),
|
'some_file{}'.format(index),
|
||||||
b'some content.'))
|
b'some content.'))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 4 requests to get the session an the request user with its permissions and
|
* 7 requests to get the session an the request user with its permissions and
|
||||||
* 2 requests to get the list of all files.
|
* 1 requests to get the list of all files.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(User.objects.get(pk=1))
|
self.client.force_login(User.objects.get(pk=1))
|
||||||
with self.assertNumQueries(6):
|
get_redis_connection('default').flushall()
|
||||||
|
with self.assertNumQueries(8):
|
||||||
self.client.get(reverse('mediafile-list'))
|
self.client.get(reverse('mediafile-list'))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_anonymous(self):
|
def test_anonymous(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 3 requests to get the permission for anonymous and
|
* 3 requests to get the permission for anonymous and
|
||||||
* 2 requests to get the list of all projectors.
|
* 1 requests to get the list of all projectors.
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(5):
|
get_redis_connection('default').flushall()
|
||||||
|
with self.assertNumQueries(4):
|
||||||
self.client.get(reverse('mediafile-list'))
|
self.client.get(reverse('mediafile-list'))
|
||||||
|
@ -3,13 +3,16 @@ import json
|
|||||||
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.contrib.auth.models import Permission
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django_redis import get_redis_connection
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
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.core.models import Tag
|
from openslides.core.models import Tag
|
||||||
from openslides.motions.models import Category, Motion, MotionBlock, State
|
from openslides.motions.models import Category, Motion, MotionBlock, State
|
||||||
from openslides.utils.test import TestCase, use_cache
|
from openslides.users.models import Group
|
||||||
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestMotionDBQueries(TestCase):
|
class TestMotionDBQueries(TestCase):
|
||||||
@ -31,12 +34,11 @@ class TestMotionDBQueries(TestCase):
|
|||||||
password='password')
|
password='password')
|
||||||
# TODO: Create some polls etc.
|
# TODO: Create some polls etc.
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 4 requests to get the session an the request user with its permissions,
|
* 7 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all motions,
|
* 1 requests to get the list of all motions,
|
||||||
* 1 request to get the motion versions,
|
* 1 request to get the motion versions,
|
||||||
* 1 request to get the agenda item,
|
* 1 request to get the agenda item,
|
||||||
* 1 request to get the motion log,
|
* 1 request to get the motion log,
|
||||||
@ -46,15 +48,15 @@ class TestMotionDBQueries(TestCase):
|
|||||||
* 2 requests to get the submitters and supporters.
|
* 2 requests to get the submitters and supporters.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(get_user_model().objects.get(pk=1))
|
self.client.force_login(get_user_model().objects.get(pk=1))
|
||||||
with self.assertNumQueries(14):
|
get_redis_connection('default').flushall()
|
||||||
|
with self.assertNumQueries(16):
|
||||||
self.client.get(reverse('motion-list'))
|
self.client.get(reverse('motion-list'))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_anonymous(self):
|
def test_anonymous(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 3 requests to get the permission for anonymous,
|
* 3 requests to get the permission for anonymous,
|
||||||
* 2 requests to get the list of all motions,
|
* 1 requests to get the list of all motions,
|
||||||
* 1 request to get the motion versions,
|
* 1 request to get the motion versions,
|
||||||
* 1 request to get the agenda item,
|
* 1 request to get the agenda item,
|
||||||
* 1 request to get the motion log,
|
* 1 request to get the motion log,
|
||||||
@ -63,7 +65,8 @@ class TestMotionDBQueries(TestCase):
|
|||||||
* 1 request to get the tags,
|
* 1 request to get the tags,
|
||||||
* 2 requests to get the submitters and supporters.
|
* 2 requests to get the submitters and supporters.
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(13):
|
get_redis_connection('default').flushall()
|
||||||
|
with self.assertNumQueries(12):
|
||||||
self.client.get(reverse('motion-list'))
|
self.client.get(reverse('motion-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -82,25 +85,25 @@ class TestCategoryDBQueries(TestCase):
|
|||||||
for index in range(10):
|
for index in range(10):
|
||||||
Category.objects.create(name='category{}'.format(index))
|
Category.objects.create(name='category{}'.format(index))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 4 requests to get the session an the request user with its permissions and
|
* 7 requests to get the session an the request user with its permissions and
|
||||||
* 2 requests to get the list of all categories.
|
* 1 requests to get the list of all categories.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(get_user_model().objects.get(pk=1))
|
self.client.force_login(get_user_model().objects.get(pk=1))
|
||||||
with self.assertNumQueries(6):
|
get_redis_connection('default').flushall()
|
||||||
|
with self.assertNumQueries(8):
|
||||||
self.client.get(reverse('category-list'))
|
self.client.get(reverse('category-list'))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_anonymous(self):
|
def test_anonymous(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 3 requests to get the permission for anonymous (config and permissions)
|
* 3 requests to get the permission for anonymous (config and permissions)
|
||||||
* 2 requests to get the list of all motions and
|
* 1 requests to get the list of all motions and
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(5):
|
get_redis_connection('default').flushall()
|
||||||
|
with self.assertNumQueries(4):
|
||||||
self.client.get(reverse('category-list'))
|
self.client.get(reverse('category-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -115,29 +118,29 @@ class TestWorkflowDBQueries(TestCase):
|
|||||||
config['general_system_enable_anonymous'] = True
|
config['general_system_enable_anonymous'] = True
|
||||||
# There do not need to be more workflows
|
# There do not need to be more workflows
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 4 requests to get the session an the request user with its permissions,
|
* 7 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all workflows,
|
* 1 requests to get the list of all workflows,
|
||||||
* 1 request to get all states and
|
* 1 request to get all states and
|
||||||
* 1 request to get the next states of all states.
|
* 1 request to get the next states of all states.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(get_user_model().objects.get(pk=1))
|
self.client.force_login(get_user_model().objects.get(pk=1))
|
||||||
with self.assertNumQueries(8):
|
get_redis_connection('default').flushall()
|
||||||
|
with self.assertNumQueries(10):
|
||||||
self.client.get(reverse('workflow-list'))
|
self.client.get(reverse('workflow-list'))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_anonymous(self):
|
def test_anonymous(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 3 requests to get the permission for anonymous,
|
* 3 requests to get the permission for anonymous,
|
||||||
* 2 requests to get the list of all workflows,
|
* 1 requests to get the list of all workflows,
|
||||||
* 1 request to get all states and
|
* 1 request to get all states and
|
||||||
* 1 request to get the next states of all states.
|
* 1 request to get the next states of all states.
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(7):
|
get_redis_connection('default').flushall()
|
||||||
|
with self.assertNumQueries(6):
|
||||||
self.client.get(reverse('workflow-list'))
|
self.client.get(reverse('workflow-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -372,6 +375,7 @@ class CreateMotion(TestCase):
|
|||||||
self.admin = get_user_model().objects.get(username='admin')
|
self.admin = get_user_model().objects.get(username='admin')
|
||||||
self.admin.groups.add(2)
|
self.admin.groups.add(2)
|
||||||
self.admin.groups.remove(3)
|
self.admin.groups.remove(3)
|
||||||
|
inform_changed_data(self.admin)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse('motion-list'),
|
reverse('motion-list'),
|
||||||
@ -412,7 +416,6 @@ class RetrieveMotion(TestCase):
|
|||||||
username='user_{}'.format(index),
|
username='user_{}'.format(index),
|
||||||
password='password')
|
password='password')
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_number_of_queries(self):
|
def test_number_of_queries(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
@ -427,6 +430,7 @@ class RetrieveMotion(TestCase):
|
|||||||
* 2 requests to get the submitters and supporters.
|
* 2 requests to get the submitters and supporters.
|
||||||
TODO: Fix all bugs.
|
TODO: Fix all bugs.
|
||||||
"""
|
"""
|
||||||
|
get_redis_connection('default').flushall()
|
||||||
with self.assertNumQueries(18):
|
with self.assertNumQueries(18):
|
||||||
self.client.get(reverse('motion-detail', args=[self.motion.pk]))
|
self.client.get(reverse('motion-detail', args=[self.motion.pk]))
|
||||||
|
|
||||||
@ -436,6 +440,9 @@ class RetrieveMotion(TestCase):
|
|||||||
state = self.motion.state
|
state = self.motion.state
|
||||||
state.required_permission_to_see = 'permission_that_the_user_does_not_have_leeceiz9hi7iuta4ahY2'
|
state.required_permission_to_see = 'permission_that_the_user_does_not_have_leeceiz9hi7iuta4ahY2'
|
||||||
state.save()
|
state.save()
|
||||||
|
# The cache has to be cleared, see:
|
||||||
|
# https://github.com/OpenSlides/OpenSlides/issues/3396
|
||||||
|
get_redis_connection("default").flushall()
|
||||||
response = guest_client.get(reverse('motion-detail', args=[self.motion.pk]))
|
response = guest_client.get(reverse('motion-detail', args=[self.motion.pk]))
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
@ -461,11 +468,13 @@ class RetrieveMotion(TestCase):
|
|||||||
|
|
||||||
def test_user_without_can_see_user_permission_to_see_motion_and_submitter_data(self):
|
def test_user_without_can_see_user_permission_to_see_motion_and_submitter_data(self):
|
||||||
self.motion.submitters.add(get_user_model().objects.get(username='admin'))
|
self.motion.submitters.add(get_user_model().objects.get(username='admin'))
|
||||||
group = get_user_model().groups.field.related_model.objects.get(pk=1) # Group with pk 1 is for anonymous and default users.
|
inform_changed_data(self.motion)
|
||||||
|
group = Group.objects.get(pk=1) # Group with pk 1 is for anonymous and default users.
|
||||||
permission_string = 'users.can_see_name'
|
permission_string = 'users.can_see_name'
|
||||||
app_label, codename = permission_string.split('.')
|
app_label, codename = permission_string.split('.')
|
||||||
permission = group.permissions.get(content_type__app_label=app_label, codename=codename)
|
permission = group.permissions.get(content_type__app_label=app_label, codename=codename)
|
||||||
group.permissions.remove(permission)
|
group.permissions.remove(permission)
|
||||||
|
inform_changed_data(group)
|
||||||
config['general_system_enable_anonymous'] = True
|
config['general_system_enable_anonymous'] = True
|
||||||
guest_client = APIClient()
|
guest_client = APIClient()
|
||||||
|
|
||||||
@ -549,6 +558,7 @@ class UpdateMotion(TestCase):
|
|||||||
admin = get_user_model().objects.get(username='admin')
|
admin = get_user_model().objects.get(username='admin')
|
||||||
group_staff = admin.groups.get(name='Staff')
|
group_staff = admin.groups.get(name='Staff')
|
||||||
admin.groups.remove(group_staff)
|
admin.groups.remove(group_staff)
|
||||||
|
inform_changed_data(admin)
|
||||||
self.motion.submitters.add(admin)
|
self.motion.submitters.add(admin)
|
||||||
supporter = get_user_model().objects.create_user(
|
supporter = get_user_model().objects.create_user(
|
||||||
username='test_username_ahshi4oZin0OoSh9chee',
|
username='test_username_ahshi4oZin0OoSh9chee',
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django_redis import get_redis_connection
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from openslides.agenda.models import Item
|
from openslides.agenda.models import Item
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.topics.models import Topic
|
from openslides.topics.models import Topic
|
||||||
from openslides.utils.test import TestCase, use_cache
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestDBQueries(TestCase):
|
class TestDBQueries(TestCase):
|
||||||
@ -24,29 +25,29 @@ class TestDBQueries(TestCase):
|
|||||||
for index in range(10):
|
for index in range(10):
|
||||||
Topic.objects.create(title='topic-{}'.format(index))
|
Topic.objects.create(title='topic-{}'.format(index))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 4 requests to get the session an the request user with its permissions,
|
* 7 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all topics,
|
* 1 requests to get the list of all topics,
|
||||||
* 1 request to get attachments,
|
* 1 request to get attachments,
|
||||||
* 1 request to get the agenda item
|
* 1 request to get the agenda item
|
||||||
"""
|
"""
|
||||||
self.client.force_login(get_user_model().objects.get(pk=1))
|
self.client.force_login(get_user_model().objects.get(pk=1))
|
||||||
with self.assertNumQueries(8):
|
get_redis_connection('default').flushall()
|
||||||
|
with self.assertNumQueries(10):
|
||||||
self.client.get(reverse('topic-list'))
|
self.client.get(reverse('topic-list'))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_anonymous(self):
|
def test_anonymous(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 3 requests to get the permission for anonymous,
|
* 3 requests to get the permission for anonymous,
|
||||||
* 2 requests to get the list of all topics,
|
* 1 requests to get the list of all topics,
|
||||||
* 1 request to get attachments,
|
* 1 request to get attachments,
|
||||||
* 1 request to get the agenda item,
|
* 1 request to get the agenda item,
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(7):
|
get_redis_connection('default').flushall()
|
||||||
|
with self.assertNumQueries(6):
|
||||||
self.client.get(reverse('topic-list'))
|
self.client.get(reverse('topic-list'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django_redis import get_redis_connection
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
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.users.models import Group, PersonalNote, User
|
from openslides.users.models import Group, PersonalNote, User
|
||||||
from openslides.users.serializers import UserFullSerializer
|
from openslides.users.serializers import UserFullSerializer
|
||||||
from openslides.utils.test import TestCase, use_cache
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestUserDBQueries(TestCase):
|
class TestUserDBQueries(TestCase):
|
||||||
@ -23,7 +24,6 @@ class TestUserDBQueries(TestCase):
|
|||||||
for index in range(10):
|
for index in range(10):
|
||||||
User.objects.create(username='user{}'.format(index))
|
User.objects.create(username='user{}'.format(index))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
@ -32,18 +32,19 @@ class TestUserDBQueries(TestCase):
|
|||||||
* 1 requests to get the list of all groups.
|
* 1 requests to get the list of all groups.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(User.objects.get(pk=1))
|
self.client.force_login(User.objects.get(pk=1))
|
||||||
|
get_redis_connection('default').flushall()
|
||||||
with self.assertNumQueries(7):
|
with self.assertNumQueries(7):
|
||||||
self.client.get(reverse('user-list'))
|
self.client.get(reverse('user-list'))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_anonymous(self):
|
def test_anonymous(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 3 requests to get the permission for anonymous,
|
* 3 requests to get the permission for anonymous,
|
||||||
* 2 requests to get the list of all users and
|
* 1 requests to get the list of all users and
|
||||||
* 2 request to get all groups (needed by the user serializer).
|
* 2 request to get all groups (needed by the user serializer).
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(7):
|
get_redis_connection('default').flushall()
|
||||||
|
with self.assertNumQueries(6):
|
||||||
self.client.get(reverse('user-list'))
|
self.client.get(reverse('user-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -62,28 +63,28 @@ class TestGroupDBQueries(TestCase):
|
|||||||
for index in range(10):
|
for index in range(10):
|
||||||
Group.objects.create(name='group{}'.format(index))
|
Group.objects.create(name='group{}'.format(index))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 4 requests to get the session an the request user with its permissions and
|
* 6 requests to get the session an the request user with its permissions and
|
||||||
* 1 request to get the list of all groups.
|
* 1 request to get the list of all groups.
|
||||||
|
|
||||||
The data of the groups where loaded when the admin was authenticated. So
|
The data of the groups where loaded when the admin was authenticated. So
|
||||||
only the list of all groups has be fetched from the db.
|
only the list of all groups has be fetched from the db.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(User.objects.get(pk=1))
|
self.client.force_login(User.objects.get(pk=1))
|
||||||
with self.assertNumQueries(5):
|
get_redis_connection('default').flushall()
|
||||||
|
with self.assertNumQueries(7):
|
||||||
self.client.get(reverse('group-list'))
|
self.client.get(reverse('group-list'))
|
||||||
|
|
||||||
@use_cache()
|
|
||||||
def test_anonymous(self):
|
def test_anonymous(self):
|
||||||
"""
|
"""
|
||||||
Tests that only the following db queries are done:
|
Tests that only the following db queries are done:
|
||||||
* 1 requests to find out if anonymous is enabled
|
* 1 requests to find out if anonymous is enabled
|
||||||
* 3 request to get the list of all groups and
|
* 2 request to get the list of all groups and
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(4):
|
get_redis_connection('default').flushall()
|
||||||
|
with self.assertNumQueries(3):
|
||||||
self.client.get(reverse('group-list'))
|
self.client.get(reverse('group-list'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ class TestsInformChangedData(ChannelTestCase):
|
|||||||
def test_delete_one_element(self):
|
def test_delete_one_element(self):
|
||||||
channel_layers[DEFAULT_CHANNEL_LAYER].flush()
|
channel_layers[DEFAULT_CHANNEL_LAYER].flush()
|
||||||
|
|
||||||
inform_deleted_data('topics/topic', 1)
|
inform_deleted_data([('topics/topic', 1)])
|
||||||
|
|
||||||
channel_message = self.get_next_message('autoupdate.send_data', require=True)
|
channel_message = self.get_next_message('autoupdate.send_data', require=True)
|
||||||
self.assertEqual(len(channel_message['elements']), 1)
|
self.assertEqual(len(channel_message['elements']), 1)
|
||||||
@ -104,18 +104,7 @@ class TestsInformChangedData(ChannelTestCase):
|
|||||||
def test_delete_many_elements(self):
|
def test_delete_many_elements(self):
|
||||||
channel_layers[DEFAULT_CHANNEL_LAYER].flush()
|
channel_layers[DEFAULT_CHANNEL_LAYER].flush()
|
||||||
|
|
||||||
inform_deleted_data('topics/topic', 1, 'topics/topic', 2, 'testmodule/model', 1)
|
inform_deleted_data([('topics/topic', 1), ('topics/topic', 2), ('testmodule/model', 1)])
|
||||||
|
|
||||||
channel_message = self.get_next_message('autoupdate.send_data', require=True)
|
channel_message = self.get_next_message('autoupdate.send_data', require=True)
|
||||||
self.assertEqual(len(channel_message['elements']), 3)
|
self.assertEqual(len(channel_message['elements']), 3)
|
||||||
|
|
||||||
def test_delete_no_element(self):
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
inform_deleted_data()
|
|
||||||
|
|
||||||
def test_delete_wrong_arguments(self):
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
inform_deleted_data('testmodule/model')
|
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
inform_deleted_data('testmodule/model', 5, 'testmodule/model')
|
|
||||||
|
@ -1,34 +1,17 @@
|
|||||||
from unittest.mock import patch
|
from channels.tests import ChannelTestCase as TestCase
|
||||||
|
from django_redis import get_redis_connection
|
||||||
from channels.tests import ChannelTestCase
|
|
||||||
from django.core.cache import caches
|
|
||||||
|
|
||||||
from openslides.topics.models import Topic
|
from openslides.topics.models import Topic
|
||||||
from openslides.utils import collection
|
from openslides.utils import collection
|
||||||
|
|
||||||
|
|
||||||
class TestCase(ChannelTestCase):
|
|
||||||
"""
|
|
||||||
Testcase that uses the local mem cache and clears the cache after each test.
|
|
||||||
"""
|
|
||||||
def setUp(self):
|
|
||||||
cache = caches['locmem']
|
|
||||||
cache.clear()
|
|
||||||
self.patch = patch('openslides.utils.collection.cache', cache)
|
|
||||||
self.patch.start()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.patch.stop()
|
|
||||||
super().tearDown()
|
|
||||||
|
|
||||||
|
|
||||||
class TestCollectionElementCache(TestCase):
|
class TestCollectionElementCache(TestCase):
|
||||||
def test_clean_cache(self):
|
def test_clean_cache(self):
|
||||||
"""
|
"""
|
||||||
Tests that the data is retrieved from the database.
|
Tests that the data is retrieved from the database.
|
||||||
"""
|
"""
|
||||||
topic = Topic.objects.create(title='test topic')
|
topic = Topic.objects.create(title='test topic')
|
||||||
caches['locmem'].clear()
|
get_redis_connection("default").flushall()
|
||||||
|
|
||||||
with self.assertNumQueries(3):
|
with self.assertNumQueries(3):
|
||||||
collection_element = collection.CollectionElement.from_values('topics/topic', 1)
|
collection_element = collection.CollectionElement.from_values('topics/topic', 1)
|
||||||
@ -51,19 +34,6 @@ class TestCollectionElementCache(TestCase):
|
|||||||
instance = collection_element.get_full_data()
|
instance = collection_element.get_full_data()
|
||||||
self.assertEqual(topic.title, instance['title'])
|
self.assertEqual(topic.title, instance['title'])
|
||||||
|
|
||||||
@patch('openslides.utils.collection.cache')
|
|
||||||
def test_save_to_cache_called_once(self, mock_cache):
|
|
||||||
"""
|
|
||||||
Makes sure, that save_to_cache ins called (only) once, if CollectionElement
|
|
||||||
is created with "from_instance()".
|
|
||||||
"""
|
|
||||||
topic = Topic.objects.create(title='test topic')
|
|
||||||
mock_cache.set.reset_mock()
|
|
||||||
collection.CollectionElement.from_instance(topic)
|
|
||||||
|
|
||||||
# cache.set() is called two times. Once for the object and once for the collection.
|
|
||||||
self.assertEqual(mock_cache.set.call_count, 2)
|
|
||||||
|
|
||||||
def test_fail_early(self):
|
def test_fail_early(self):
|
||||||
"""
|
"""
|
||||||
Tests that a CollectionElement.from_values fails, if the object does
|
Tests that a CollectionElement.from_values fails, if the object does
|
||||||
@ -82,10 +52,10 @@ class TestCollectionCache(TestCase):
|
|||||||
Topic.objects.create(title='test topic2')
|
Topic.objects.create(title='test topic2')
|
||||||
Topic.objects.create(title='test topic3')
|
Topic.objects.create(title='test topic3')
|
||||||
topic_collection = collection.Collection('topics/topic')
|
topic_collection = collection.Collection('topics/topic')
|
||||||
caches['locmem'].clear()
|
get_redis_connection("default").flushall()
|
||||||
|
|
||||||
with self.assertNumQueries(4):
|
with self.assertNumQueries(3):
|
||||||
instance_list = list(topic_collection.as_autoupdate_for_projector())
|
instance_list = list(topic_collection.get_full_data())
|
||||||
self.assertEqual(len(instance_list), 3)
|
self.assertEqual(len(instance_list), 3)
|
||||||
|
|
||||||
def test_with_cache(self):
|
def test_with_cache(self):
|
||||||
@ -96,24 +66,10 @@ class TestCollectionCache(TestCase):
|
|||||||
Topic.objects.create(title='test topic2')
|
Topic.objects.create(title='test topic2')
|
||||||
Topic.objects.create(title='test topic3')
|
Topic.objects.create(title='test topic3')
|
||||||
topic_collection = collection.Collection('topics/topic')
|
topic_collection = collection.Collection('topics/topic')
|
||||||
list(topic_collection.as_autoupdate_for_projector())
|
list(topic_collection.get_full_data())
|
||||||
|
|
||||||
with self.assertNumQueries(0):
|
with self.assertNumQueries(0):
|
||||||
instance_list = list(topic_collection.as_autoupdate_for_projector())
|
instance_list = list(topic_collection.get_full_data())
|
||||||
self.assertEqual(len(instance_list), 3)
|
|
||||||
|
|
||||||
def test_with_some_objects_in_the_cache(self):
|
|
||||||
"""
|
|
||||||
One element (topic3) is in the cache and two are not.
|
|
||||||
"""
|
|
||||||
Topic.objects.create(title='test topic1')
|
|
||||||
Topic.objects.create(title='test topic2')
|
|
||||||
caches['locmem'].clear()
|
|
||||||
Topic.objects.create(title='test topic3')
|
|
||||||
topic_collection = collection.Collection('topics/topic')
|
|
||||||
|
|
||||||
with self.assertNumQueries(4):
|
|
||||||
instance_list = list(topic_collection.as_autoupdate_for_projector())
|
|
||||||
self.assertEqual(len(instance_list), 3)
|
self.assertEqual(len(instance_list), 3)
|
||||||
|
|
||||||
def test_deletion(self):
|
def test_deletion(self):
|
||||||
@ -125,10 +81,10 @@ class TestCollectionCache(TestCase):
|
|||||||
Topic.objects.create(title='test topic2')
|
Topic.objects.create(title='test topic2')
|
||||||
topic3 = Topic.objects.create(title='test topic3')
|
topic3 = Topic.objects.create(title='test topic3')
|
||||||
topic_collection = collection.Collection('topics/topic')
|
topic_collection = collection.Collection('topics/topic')
|
||||||
list(topic_collection.as_autoupdate_for_projector())
|
list(topic_collection.get_full_data())
|
||||||
|
|
||||||
topic3.delete()
|
topic3.delete()
|
||||||
|
|
||||||
with self.assertNumQueries(0):
|
with self.assertNumQueries(0):
|
||||||
instance_list = list(topic_collection.as_autoupdate_for_projector())
|
instance_list = list(collection.Collection('topics/topic').get_full_data())
|
||||||
self.assertEqual(len(instance_list), 2)
|
self.assertEqual(len(instance_list), 2)
|
||||||
|
@ -41,6 +41,10 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# When use_redis is True, the restricted data cache caches the data individuel
|
||||||
|
# for each user. This requires a lot of memory if there are a lot of active
|
||||||
|
# users. If use_redis is False, this setting has no effect.
|
||||||
|
DISABLE_USER_CACHE = False
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.10/topics/i18n/
|
# https://docs.djangoproject.com/en/1.10/topics/i18n/
|
||||||
@ -75,13 +79,12 @@ PASSWORD_HASHERS = [
|
|||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Use the dummy cache that does not cache anything
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
'default': {
|
"default": {
|
||||||
'BACKEND': 'django.core.cache.backends.dummy.DummyCache'
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
},
|
"LOCATION": "redis://127.0.0.1:6379/0",
|
||||||
'locmem': {
|
"OPTIONS": {
|
||||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
|
"REDIS_CLIENT_CLASS": "fakeredis.FakeStrictRedis",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ class UserGetProjectorDataTest(TestCase):
|
|||||||
"""
|
"""
|
||||||
This test ensures that comment field is removed.
|
This test ensures that comment field is removed.
|
||||||
"""
|
"""
|
||||||
container = CollectionElement.from_values('users/user', 42, full_data={
|
full_data = {
|
||||||
'id': 42,
|
'id': 42,
|
||||||
'username': 'username_ai3Oofu7eit0eeyu1sie',
|
'username': 'username_ai3Oofu7eit0eeyu1sie',
|
||||||
'title': '',
|
'title': '',
|
||||||
@ -25,9 +25,10 @@ class UserGetProjectorDataTest(TestCase):
|
|||||||
'is_present': False,
|
'is_present': False,
|
||||||
'is_committee': False,
|
'is_committee': False,
|
||||||
'comment': 'comment_gah7aipeJohv9xethoku',
|
'comment': 'comment_gah7aipeJohv9xethoku',
|
||||||
})
|
}
|
||||||
data = UserAccessPermissions().get_projector_data(container)
|
|
||||||
self.assertEqual(data, {
|
data = UserAccessPermissions().get_projector_data([full_data])
|
||||||
|
self.assertEqual(data[0], {
|
||||||
'id': 42,
|
'id': 42,
|
||||||
'username': 'username_ai3Oofu7eit0eeyu1sie',
|
'username': 'username_ai3Oofu7eit0eeyu1sie',
|
||||||
'title': '',
|
'title': '',
|
||||||
@ -46,19 +47,16 @@ class TestPersonalNoteAccessPermissions(TestCase):
|
|||||||
def test_get_restricted_data(self):
|
def test_get_restricted_data(self):
|
||||||
ap = PersonalNoteAccessPermissions()
|
ap = PersonalNoteAccessPermissions()
|
||||||
rd = ap.get_restricted_data(
|
rd = ap.get_restricted_data(
|
||||||
CollectionElement.from_values(
|
[{'user_id': 1}],
|
||||||
'users/personal_note',
|
|
||||||
1,
|
|
||||||
full_data={'user_id': 1}),
|
|
||||||
CollectionElement.from_values('users/user', 5, full_data={}))
|
CollectionElement.from_values('users/user', 5, full_data={}))
|
||||||
self.assertEqual(rd, None)
|
self.assertEqual(rd, [])
|
||||||
|
|
||||||
def test_get_restricted_data_for_anonymous(self):
|
def test_get_restricted_data_for_anonymous(self):
|
||||||
ap = PersonalNoteAccessPermissions()
|
ap = PersonalNoteAccessPermissions()
|
||||||
rd = ap.get_restricted_data(
|
rd = ap.get_restricted_data(
|
||||||
CollectionElement.from_values(
|
[CollectionElement.from_values(
|
||||||
'users/personal_note',
|
'users/personal_note',
|
||||||
1,
|
1,
|
||||||
full_data={'user_id': 1}),
|
full_data={'user_id': 1})],
|
||||||
None)
|
None)
|
||||||
self.assertEqual(rd, None)
|
self.assertEqual(rd, [])
|
||||||
|
@ -5,42 +5,6 @@ from openslides.core.models import Projector
|
|||||||
from openslides.utils import collection
|
from openslides.utils import collection
|
||||||
|
|
||||||
|
|
||||||
class TestCacheKeys(TestCase):
|
|
||||||
def test_get_collection_id_from_cache_key(self):
|
|
||||||
"""
|
|
||||||
Test that get_collection_id_from_cache_key works together with
|
|
||||||
get_single_element_cache_key.
|
|
||||||
"""
|
|
||||||
element = ('some/testkey', 42)
|
|
||||||
self.assertEqual(
|
|
||||||
element,
|
|
||||||
collection.get_collection_id_from_cache_key(
|
|
||||||
collection.get_single_element_cache_key(*element)))
|
|
||||||
|
|
||||||
def test_get_single_element_cache_key_prefix(self):
|
|
||||||
"""
|
|
||||||
Tests that the cache prefix is realy a prefix.
|
|
||||||
"""
|
|
||||||
element = ('some/testkey', 42)
|
|
||||||
|
|
||||||
cache_key = collection.get_single_element_cache_key(*element)
|
|
||||||
prefix = collection.get_single_element_cache_key_prefix(element[0])
|
|
||||||
|
|
||||||
self.assertTrue(cache_key.startswith(prefix))
|
|
||||||
|
|
||||||
def test_prefix_different_then_list(self):
|
|
||||||
"""
|
|
||||||
Test that the return value of get_single_element_cache_key_prefix is
|
|
||||||
something different then get_element_list_cache_key.
|
|
||||||
"""
|
|
||||||
collection_string = 'some/testkey'
|
|
||||||
|
|
||||||
prefix = collection.get_single_element_cache_key_prefix(collection_string)
|
|
||||||
list_cache_key = collection.get_element_list_cache_key(collection_string)
|
|
||||||
|
|
||||||
self.assertNotEqual(prefix, list_cache_key)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetModelFromCollectionString(TestCase):
|
class TestGetModelFromCollectionString(TestCase):
|
||||||
def test_known_app(self):
|
def test_known_app(self):
|
||||||
projector_model = collection.get_model_from_collection_string('core/projector')
|
projector_model = collection.get_model_from_collection_string('core/projector')
|
||||||
@ -60,34 +24,9 @@ class TestCollectionElement(TestCase):
|
|||||||
self.assertEqual(collection_element.collection_string, 'testmodule/model')
|
self.assertEqual(collection_element.collection_string, 'testmodule/model')
|
||||||
self.assertEqual(collection_element.id, 42)
|
self.assertEqual(collection_element.id, 42)
|
||||||
|
|
||||||
@patch('openslides.utils.collection.Collection')
|
|
||||||
@patch('openslides.utils.collection.cache')
|
|
||||||
def test_from_values_deleted(self, mock_cache, mock_collection):
|
|
||||||
"""
|
|
||||||
Tests that when createing a CollectionElement with deleted=True the element
|
|
||||||
is deleted from the cache.
|
|
||||||
"""
|
|
||||||
collection_element = collection.CollectionElement.from_values('testmodule/model', 42, deleted=True)
|
|
||||||
|
|
||||||
self.assertTrue(collection_element.is_deleted())
|
|
||||||
mock_cache.delete.assert_called_with('testmodule/model:42')
|
|
||||||
mock_collection.assert_called_with('testmodule/model')
|
|
||||||
mock_collection().delete_id_from_cache.assert_called_with(42)
|
|
||||||
|
|
||||||
def test_as_channel_message(self):
|
|
||||||
with patch.object(collection.CollectionElement, 'get_full_data'):
|
|
||||||
collection_element = collection.CollectionElement.from_values('testmodule/model', 42)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
collection_element.as_channels_message(),
|
|
||||||
{'collection_string': 'testmodule/model',
|
|
||||||
'id': 42,
|
|
||||||
'deleted': False})
|
|
||||||
|
|
||||||
def test_channel_message(self):
|
def test_channel_message(self):
|
||||||
"""
|
"""
|
||||||
Test that CollectionElement.from_values() works together with
|
Test that to_channel_message works together with from_channel_message.
|
||||||
collection_element.as_channels_message().
|
|
||||||
"""
|
"""
|
||||||
collection_element = collection.CollectionElement.from_values(
|
collection_element = collection.CollectionElement.from_values(
|
||||||
'testmodule/model',
|
'testmodule/model',
|
||||||
@ -95,8 +34,8 @@ class TestCollectionElement(TestCase):
|
|||||||
full_data={'data': 'value'},
|
full_data={'data': 'value'},
|
||||||
information={'some': 'information'})
|
information={'some': 'information'})
|
||||||
|
|
||||||
created_collection_element = collection.CollectionElement.from_values(
|
created_collection_element = collection.from_channel_message(
|
||||||
**collection_element.as_channels_message())
|
collection.to_channel_message([collection_element]))[0]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
collection_element,
|
collection_element,
|
||||||
@ -109,7 +48,7 @@ class TestCollectionElement(TestCase):
|
|||||||
collection_element = collection.CollectionElement.from_values('testmodule/model', 42)
|
collection_element = collection.CollectionElement.from_values('testmodule/model', 42)
|
||||||
fake_user = MagicMock()
|
fake_user = MagicMock()
|
||||||
collection_element.get_access_permissions = MagicMock()
|
collection_element.get_access_permissions = MagicMock()
|
||||||
collection_element.get_access_permissions().get_restricted_data.return_value = 'restricted_data'
|
collection_element.get_access_permissions().get_restricted_data.return_value = ['restricted_data']
|
||||||
collection_element.get_full_data = MagicMock()
|
collection_element.get_full_data = MagicMock()
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -118,7 +57,6 @@ class TestCollectionElement(TestCase):
|
|||||||
'id': 42,
|
'id': 42,
|
||||||
'action': 'changed',
|
'action': 'changed',
|
||||||
'data': 'restricted_data'})
|
'data': 'restricted_data'})
|
||||||
collection_element.get_full_data.assert_not_called()
|
|
||||||
|
|
||||||
def test_as_autoupdate_for_user_no_permission(self):
|
def test_as_autoupdate_for_user_no_permission(self):
|
||||||
with patch.object(collection.CollectionElement, 'get_full_data'):
|
with patch.object(collection.CollectionElement, 'get_full_data'):
|
||||||
@ -133,7 +71,6 @@ class TestCollectionElement(TestCase):
|
|||||||
{'collection': 'testmodule/model',
|
{'collection': 'testmodule/model',
|
||||||
'id': 42,
|
'id': 42,
|
||||||
'action': 'deleted'})
|
'action': 'deleted'})
|
||||||
collection_element.get_full_data.assert_not_called()
|
|
||||||
|
|
||||||
def test_as_autoupdate_for_user_deleted(self):
|
def test_as_autoupdate_for_user_deleted(self):
|
||||||
collection_element = collection.CollectionElement.from_values('testmodule/model', 42, deleted=True)
|
collection_element = collection.CollectionElement.from_values('testmodule/model', 42, deleted=True)
|
||||||
@ -145,76 +82,6 @@ class TestCollectionElement(TestCase):
|
|||||||
'id': 42,
|
'id': 42,
|
||||||
'action': 'deleted'})
|
'action': 'deleted'})
|
||||||
|
|
||||||
def test_get_instance_deleted(self):
|
|
||||||
collection_element = collection.CollectionElement.from_values('testmodule/model', 42, deleted=True)
|
|
||||||
|
|
||||||
with self.assertRaises(RuntimeError):
|
|
||||||
collection_element.get_instance()
|
|
||||||
|
|
||||||
def test_get_instance(self):
|
|
||||||
with patch.object(collection.CollectionElement, 'get_full_data'):
|
|
||||||
collection_element = collection.CollectionElement.from_values('testmodule/model', 42)
|
|
||||||
collection_element.get_model = MagicMock()
|
|
||||||
|
|
||||||
collection_element.get_instance()
|
|
||||||
|
|
||||||
collection_element.get_model().objects.get_full_queryset().get.assert_called_once_with(pk=42)
|
|
||||||
|
|
||||||
@patch('openslides.utils.collection.cache')
|
|
||||||
def test_get_full_data_already_loaded(self, mock_cache):
|
|
||||||
"""
|
|
||||||
Test that the cache and the self.get_instance() is not hit, when the
|
|
||||||
instance is already loaded.
|
|
||||||
"""
|
|
||||||
with patch.object(collection.CollectionElement, 'get_full_data'):
|
|
||||||
collection_element = collection.CollectionElement.from_values('testmodule/model', 42)
|
|
||||||
collection_element.full_data = 'my_full_data'
|
|
||||||
collection_element.get_instance = MagicMock()
|
|
||||||
|
|
||||||
collection_element.get_full_data()
|
|
||||||
|
|
||||||
mock_cache.get.assert_not_called()
|
|
||||||
collection_element.get_instance.assert_not_called()
|
|
||||||
|
|
||||||
@patch('openslides.utils.collection.cache')
|
|
||||||
def test_get_full_data_from_cache(self, mock_cache):
|
|
||||||
"""
|
|
||||||
Test that the value from the cache is used not get_instance is not
|
|
||||||
called.
|
|
||||||
"""
|
|
||||||
with patch.object(collection.CollectionElement, 'get_full_data'):
|
|
||||||
collection_element = collection.CollectionElement.from_values('testmodule/model', 42)
|
|
||||||
collection_element.get_instance = MagicMock()
|
|
||||||
mock_cache.get.return_value = 'cache_value'
|
|
||||||
|
|
||||||
instance = collection_element.get_full_data()
|
|
||||||
|
|
||||||
self.assertEqual(instance, 'cache_value')
|
|
||||||
mock_cache.get.assert_called_once_with('testmodule/model:42')
|
|
||||||
collection_element.get_instance.assert_not_called
|
|
||||||
|
|
||||||
@patch('openslides.utils.collection.Collection')
|
|
||||||
@patch('openslides.utils.collection.cache')
|
|
||||||
def test_get_full_data_from_get_instance(self, mock_cache, mock_Collection):
|
|
||||||
"""
|
|
||||||
Test that the value from get_instance is used and saved to the cache
|
|
||||||
"""
|
|
||||||
with patch.object(collection.CollectionElement, 'get_full_data'):
|
|
||||||
collection_element = collection.CollectionElement.from_values('testmodule/model', 42)
|
|
||||||
collection_element.get_instance = MagicMock()
|
|
||||||
collection_element.get_access_permissions = MagicMock()
|
|
||||||
collection_element.get_access_permissions().get_full_data.return_value = 'get_instance_value'
|
|
||||||
mock_cache.get.return_value = None
|
|
||||||
|
|
||||||
instance = collection_element.get_full_data()
|
|
||||||
|
|
||||||
self.assertEqual(instance, 'get_instance_value')
|
|
||||||
mock_cache.get.assert_called_once_with('testmodule/model:42')
|
|
||||||
collection_element.get_instance.assert_called_once_with()
|
|
||||||
mock_cache.set.assert_called_once_with('testmodule/model:42', 'get_instance_value')
|
|
||||||
mock_Collection.assert_called_once_with('testmodule/model')
|
|
||||||
mock_Collection().add_id_to_cache.assert_called_once_with(42)
|
|
||||||
|
|
||||||
@patch.object(collection.CollectionElement, 'get_full_data')
|
@patch.object(collection.CollectionElement, 'get_full_data')
|
||||||
def test_equal(self, mock_get_full_data):
|
def test_equal(self, mock_get_full_data):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -229,67 +96,3 @@ class TestCollectionElement(TestCase):
|
|||||||
self.assertNotEqual(
|
self.assertNotEqual(
|
||||||
collection.CollectionElement.from_values('testmodule/model', 1),
|
collection.CollectionElement.from_values('testmodule/model', 1),
|
||||||
collection.CollectionElement.from_values('testmodule/other_model', 1))
|
collection.CollectionElement.from_values('testmodule/other_model', 1))
|
||||||
|
|
||||||
|
|
||||||
class TestcollectionElementList(TestCase):
|
|
||||||
@patch.object(collection.CollectionElement, 'get_full_data')
|
|
||||||
def test_channel_message(self, mock_get_full_data):
|
|
||||||
"""
|
|
||||||
Test that a channel message from three collection elements can crate
|
|
||||||
the same collection element list.
|
|
||||||
"""
|
|
||||||
collection_elements = collection.CollectionElementList((
|
|
||||||
collection.CollectionElement.from_values('testmodule/model', 1),
|
|
||||||
collection.CollectionElement.from_values('testmodule/model', 2),
|
|
||||||
collection.CollectionElement.from_values('testmodule/model2', 1)))
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
collection_elements,
|
|
||||||
collection.CollectionElementList.from_channels_message(collection_elements.as_channels_message()))
|
|
||||||
|
|
||||||
@patch.object(collection.CollectionElement, 'get_full_data')
|
|
||||||
def test_as_autoupdate_for_user(self, mock_get_full_data):
|
|
||||||
"""
|
|
||||||
Test that as_autoupdate_for_user is a list of as_autoupdate_for_user
|
|
||||||
for each individual element in the list.
|
|
||||||
"""
|
|
||||||
fake_user = MagicMock()
|
|
||||||
collection_elements = collection.CollectionElementList((
|
|
||||||
collection.CollectionElement.from_values('testmodule/model', 1),
|
|
||||||
collection.CollectionElement.from_values('testmodule/model', 2),
|
|
||||||
collection.CollectionElement.from_values('testmodule/model2', 1)))
|
|
||||||
|
|
||||||
with patch.object(collection.CollectionElement, 'as_autoupdate_for_user', return_value='for_user'):
|
|
||||||
value = collection_elements.as_autoupdate_for_user(fake_user)
|
|
||||||
|
|
||||||
self.assertEqual(value, ['for_user'] * 3)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCollection(TestCase):
|
|
||||||
@patch('openslides.utils.collection.CollectionElement')
|
|
||||||
@patch('openslides.utils.collection.cache')
|
|
||||||
def test_element_generator(self, mock_cache, mock_CollectionElement):
|
|
||||||
"""
|
|
||||||
Test with the following scenario: The collection has three elements. Two
|
|
||||||
are in the cache and one is not.
|
|
||||||
"""
|
|
||||||
test_collection = collection.Collection('testmodule/model')
|
|
||||||
test_collection.get_all_ids = MagicMock(return_value=set([1, 2, 3]))
|
|
||||||
test_collection.get_model = MagicMock()
|
|
||||||
test_collection.get_model().objects.get_full_queryset().filter.return_value = ['my_instance']
|
|
||||||
mock_cache.get_many.return_value = {
|
|
||||||
'testmodule/model:1': 'element1',
|
|
||||||
'testmodule/model:2': 'element2'}
|
|
||||||
|
|
||||||
list(test_collection.element_generator())
|
|
||||||
|
|
||||||
mock_cache.get_many.assert_called_once_with(
|
|
||||||
['testmodule/model:1', 'testmodule/model:2', 'testmodule/model:3'])
|
|
||||||
test_collection.get_model().objects.get_full_queryset().filter.assert_called_once_with(pk__in={3})
|
|
||||||
self.assertEqual(mock_CollectionElement.from_values.call_count, 2)
|
|
||||||
self.assertEqual(mock_CollectionElement.from_instance.call_count, 1)
|
|
||||||
|
|
||||||
def test_raw_cache_key(self):
|
|
||||||
test_collection = collection.Collection('testmodule/model')
|
|
||||||
|
|
||||||
self.assertEqual(test_collection.get_cache_key(raw=True), ':1:testmodule/model')
|
|
||||||
|
Loading…
Reference in New Issue
Block a user