Performance improvements
* Add caching support to users/group * Add a function has_perm that works with the cache. * Removed our session backend so other session backends (without the database) can be used
This commit is contained in:
parent
fb24ca0aba
commit
728576d514
@ -33,6 +33,9 @@ Core:
|
|||||||
backend.
|
backend.
|
||||||
- Use a separate dialog with CKEditor for editing projector messages.
|
- Use a separate dialog with CKEditor for editing projector messages.
|
||||||
- Use CKEditor in settings for text markup.
|
- Use CKEditor in settings for text markup.
|
||||||
|
- Add a version of has_perm that can work with cached users.
|
||||||
|
- Cache the group with there permissions.
|
||||||
|
- Removed our db-session backend and added possibility to use any django session backend.
|
||||||
|
|
||||||
Motions:
|
Motions:
|
||||||
- Added adjustable line numbering mode (outside, inside, none) for each
|
- Added adjustable line numbering mode (outside, inside, none) for each
|
||||||
|
@ -122,7 +122,7 @@ TODO: Configure postgresql
|
|||||||
|
|
||||||
2. Install python dependencies
|
2. Install python dependencies
|
||||||
|
|
||||||
pip install django-redis asgi-redis psycopg2
|
pip install django-redis asgi-redis django-redis-sessions psycopg2
|
||||||
|
|
||||||
3. Change settings.py
|
3. Change settings.py
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from ..utils.access_permissions import BaseAccessPermissions
|
from ..utils.access_permissions import BaseAccessPermissions
|
||||||
|
from ..utils.auth import has_perm
|
||||||
|
|
||||||
|
|
||||||
class ItemAccessPermissions(BaseAccessPermissions):
|
class ItemAccessPermissions(BaseAccessPermissions):
|
||||||
@ -9,7 +10,7 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has read access model instances.
|
Returns True if the user has read access model instances.
|
||||||
"""
|
"""
|
||||||
return user.has_perm('agenda.can_see')
|
return has_perm(user, 'agenda.can_see')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
@ -26,9 +27,9 @@ class ItemAccessPermissions(BaseAccessPermissions):
|
|||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the user.
|
for the user.
|
||||||
"""
|
"""
|
||||||
if (user.has_perm('agenda.can_see') and
|
if (has_perm(user, 'agenda.can_see') and
|
||||||
(not full_data['is_hidden'] or
|
(not full_data['is_hidden'] or
|
||||||
user.has_perm('agenda.can_see_hidden_items'))):
|
has_perm(user, 'agenda.can_see_hidden_items'))):
|
||||||
data = full_data
|
data = full_data
|
||||||
else:
|
else:
|
||||||
data = None
|
data = None
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
@ -15,6 +14,7 @@ from openslides.utils.exceptions import OpenSlidesError
|
|||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
from openslides.utils.utils import to_roman
|
from openslides.utils.utils import to_roman
|
||||||
|
|
||||||
|
from ..utils.auth import DjangoAnonymousUser
|
||||||
from .access_permissions import ItemAccessPermissions
|
from .access_permissions import ItemAccessPermissions
|
||||||
|
|
||||||
|
|
||||||
@ -340,7 +340,7 @@ class SpeakerManager(models.Manager):
|
|||||||
if self.filter(user=user, item=item, begin_time=None).exists():
|
if self.filter(user=user, item=item, begin_time=None).exists():
|
||||||
raise OpenSlidesError(
|
raise OpenSlidesError(
|
||||||
_('{user} is already on the list of speakers.').format(user=user))
|
_('{user} is already on the list of speakers.').format(user=user))
|
||||||
if isinstance(user, AnonymousUser):
|
if isinstance(user, DjangoAnonymousUser):
|
||||||
raise OpenSlidesError(
|
raise OpenSlidesError(
|
||||||
_('An anonymous user can not be on lists of speakers.'))
|
_('An anonymous user can not be on lists of speakers.'))
|
||||||
weight = (self.filter(item=item).aggregate(
|
weight = (self.filter(item=item).aggregate(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from ..utils.access_permissions import BaseAccessPermissions
|
from ..utils.access_permissions import BaseAccessPermissions
|
||||||
|
from ..utils.auth import has_perm
|
||||||
|
|
||||||
|
|
||||||
class AssignmentAccessPermissions(BaseAccessPermissions):
|
class AssignmentAccessPermissions(BaseAccessPermissions):
|
||||||
@ -9,7 +10,7 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has read access model instances.
|
Returns True if the user has read access model instances.
|
||||||
"""
|
"""
|
||||||
return user.has_perm('assignments.can_see')
|
return has_perm(user, 'assignments.can_see')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
@ -17,7 +18,7 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
from .serializers import AssignmentFullSerializer, AssignmentShortSerializer
|
from .serializers import AssignmentFullSerializer, AssignmentShortSerializer
|
||||||
|
|
||||||
if user is None or (user.has_perm('assignments.can_see') and user.has_perm('assignments.can_manage')):
|
if user is None or (has_perm(user, 'assignments.can_see') and has_perm(user, 'assignments.can_manage')):
|
||||||
serializer_class = AssignmentFullSerializer
|
serializer_class = AssignmentFullSerializer
|
||||||
else:
|
else:
|
||||||
serializer_class = AssignmentShortSerializer
|
serializer_class = AssignmentShortSerializer
|
||||||
@ -29,9 +30,9 @@ class AssignmentAccessPermissions(BaseAccessPermissions):
|
|||||||
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.
|
||||||
"""
|
"""
|
||||||
if user.has_perm('assignments.can_see') and user.has_perm('assignments.can_manage'):
|
if has_perm(user, 'assignments.can_see') and has_perm(user, 'assignments.can_manage'):
|
||||||
data = full_data
|
data = full_data
|
||||||
elif user.has_perm('assignments.can_see'):
|
elif has_perm(user, 'assignments.can_see'):
|
||||||
data = full_data.copy()
|
data = full_data.copy()
|
||||||
data['polls'] = [poll for poll in data['polls'] if poll['published']]
|
data['polls'] = [poll for poll in data['polls'] if poll['published']]
|
||||||
else:
|
else:
|
||||||
|
@ -81,7 +81,8 @@ class AssignmentManager(models.Manager):
|
|||||||
return self.get_queryset().prefetch_related(
|
return self.get_queryset().prefetch_related(
|
||||||
'related_users',
|
'related_users',
|
||||||
'agenda_items',
|
'agenda_items',
|
||||||
'polls')
|
'polls',
|
||||||
|
'tags')
|
||||||
|
|
||||||
|
|
||||||
class Assignment(RESTModelMixin, models.Model):
|
class Assignment(RESTModelMixin, models.Model):
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from ..utils.access_permissions import BaseAccessPermissions
|
from ..utils.access_permissions import BaseAccessPermissions
|
||||||
|
from ..utils.auth import DjangoAnonymousUser, has_perm
|
||||||
|
|
||||||
|
|
||||||
class ProjectorAccessPermissions(BaseAccessPermissions):
|
class ProjectorAccessPermissions(BaseAccessPermissions):
|
||||||
@ -9,7 +10,7 @@ class ProjectorAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has read access model instances.
|
Returns True if the user has read access model instances.
|
||||||
"""
|
"""
|
||||||
return user.has_perm('core.can_see_projector')
|
return has_perm(user, 'core.can_see_projector')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
@ -32,7 +33,7 @@ class TagAccessPermissions(BaseAccessPermissions):
|
|||||||
|
|
||||||
# Every authenticated user can retrieve tags. Anonymous users can do
|
# Every authenticated user can retrieve tags. Anonymous users can do
|
||||||
# so if they are enabled.
|
# so if they are enabled.
|
||||||
return user.is_authenticated() or config['general_system_enable_anonymous']
|
return not isinstance(user, DjangoAnonymousUser) or config['general_system_enable_anonymous']
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
@ -53,7 +54,7 @@ class ChatMessageAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
# Anonymous users can see the chat if the anonymous group has the
|
# Anonymous users can see the chat if the anonymous group has the
|
||||||
# permission core.can_use_chat. But they can not use it. See views.py.
|
# permission core.can_use_chat. But they can not use it. See views.py.
|
||||||
return user.has_perm('core.can_use_chat')
|
return has_perm(user, 'core.can_use_chat')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
@ -72,7 +73,7 @@ class ProjectorMessageAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has read access model instances.
|
Returns True if the user has read access model instances.
|
||||||
"""
|
"""
|
||||||
return user.has_perm('core.can_see_projector')
|
return has_perm(user, 'core.can_see_projector')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
@ -91,7 +92,7 @@ class CountdownAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has read access model instances.
|
Returns True if the user has read access model instances.
|
||||||
"""
|
"""
|
||||||
return user.has_perm('core.can_see_projector')
|
return has_perm(user, 'core.can_see_projector')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
@ -115,7 +116,7 @@ class ConfigAccessPermissions(BaseAccessPermissions):
|
|||||||
|
|
||||||
# Every authenticated user can see the metadata and list or retrieve
|
# Every authenticated user can see the metadata and list or retrieve
|
||||||
# the config. Anonymous users can do so if they are enabled.
|
# the config. Anonymous users can do so if they are enabled.
|
||||||
return user.is_authenticated() or config['general_system_enable_anonymous']
|
return not isinstance(user, DjangoAnonymousUser) or config['general_system_enable_anonymous']
|
||||||
|
|
||||||
def get_full_data(self, instance):
|
def get_full_data(self, instance):
|
||||||
"""
|
"""
|
||||||
|
32
openslides/core/migrations/0003_auto_20161217_1158.py
Normal file
32
openslides/core/migrations/0003_auto_20161217_1158.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Generated by Django 1.10.4 on 2016-12-17 10:58
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def remove_session_content_type(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Remove session content_type because we want to delete the session model in
|
||||||
|
the next step.
|
||||||
|
"""
|
||||||
|
# We get the model from the versioned app registry;
|
||||||
|
# if we directly import it, it will be the wrong version.
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
Session = apps.get_model('core', 'Session')
|
||||||
|
ContentType.objects.get_for_model(Session).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0002_misc_features'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
remove_session_content_type
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Session',
|
||||||
|
),
|
||||||
|
]
|
@ -1,5 +1,4 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.sessions.models import Session as DjangoSession
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from jsonfield import JSONField
|
from jsonfield import JSONField
|
||||||
@ -340,22 +339,3 @@ class Countdown(RESTModelMixin, models.Model):
|
|||||||
self.running = False
|
self.running = False
|
||||||
self.countdown_time = self.default_time
|
self.countdown_time = self.default_time
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
class Session(DjangoSession):
|
|
||||||
"""
|
|
||||||
Model like the Django db session, which saves the user as ForeignKey instead
|
|
||||||
of an encoded value.
|
|
||||||
"""
|
|
||||||
user = models.ForeignKey(
|
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
null=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
default_permissions = ()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_session_store_class(cls):
|
|
||||||
from .session_backend import SessionStore
|
|
||||||
return SessionStore
|
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
from django.contrib.sessions.backends.db import \
|
|
||||||
SessionStore as DjangoSessionStore
|
|
||||||
|
|
||||||
|
|
||||||
class SessionStore(DjangoSessionStore):
|
|
||||||
"""
|
|
||||||
Like the Django db Session store, but saves the user into the db field.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_model_class(cls):
|
|
||||||
# Avoids a circular import
|
|
||||||
from .models import Session
|
|
||||||
return Session
|
|
||||||
|
|
||||||
def create_model_instance(self, data):
|
|
||||||
"""
|
|
||||||
Set the user from data to the db field. Set to None, if its a session
|
|
||||||
from an anonymous user.
|
|
||||||
"""
|
|
||||||
model = super().create_model_instance(data)
|
|
||||||
model.user_id = data['_auth_user_id'] if '_auth_user_id' in data else None
|
|
||||||
return model
|
|
@ -571,7 +571,7 @@ class TagViewSet(ModelViewSet):
|
|||||||
if self.action in ('list', 'retrieve'):
|
if self.action in ('list', 'retrieve'):
|
||||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
elif self.action == 'metadata':
|
elif self.action == 'metadata':
|
||||||
# Every authenticated user can see the metadata and list tags.
|
# Every authenticated user can see the metadata.
|
||||||
# Anonymous users can do so if they are enabled.
|
# Anonymous users can do so if they are enabled.
|
||||||
result = self.request.user.is_authenticated() or config['general_system_enable_anonymous']
|
result = self.request.user.is_authenticated() or config['general_system_enable_anonymous']
|
||||||
elif self.action in ('create', 'update', 'destroy'):
|
elif self.action in ('create', 'update', 'destroy'):
|
||||||
|
@ -94,11 +94,9 @@ STATICFILES_DIRS = [
|
|||||||
AUTH_USER_MODEL = 'users.User'
|
AUTH_USER_MODEL = 'users.User'
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
'openslides.users.auth.CustomizedModelBackend',
|
'openslides.utils.auth.CustomizedModelBackend',
|
||||||
]
|
]
|
||||||
|
|
||||||
SESSION_ENGINE = 'openslides.core.session_backend'
|
|
||||||
|
|
||||||
SESSION_COOKIE_NAME = 'OpenSlidesSessionID'
|
SESSION_COOKIE_NAME = 'OpenSlidesSessionID'
|
||||||
|
|
||||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||||
@ -140,7 +138,7 @@ CACHES = {
|
|||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
'openslides.users.auth.RESTFrameworkAnonymousAuthentication',
|
'openslides.utils.auth.RESTFrameworkAnonymousAuthentication',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from ..utils.access_permissions import BaseAccessPermissions
|
from ..utils.access_permissions import BaseAccessPermissions
|
||||||
|
from ..utils.auth import has_perm
|
||||||
|
|
||||||
|
|
||||||
class MediafileAccessPermissions(BaseAccessPermissions):
|
class MediafileAccessPermissions(BaseAccessPermissions):
|
||||||
@ -9,7 +10,7 @@ class MediafileAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has read access model instances.
|
Returns True if the user has read access model instances.
|
||||||
"""
|
"""
|
||||||
return user.has_perm('mediafiles.can_see')
|
return has_perm(user, 'mediafiles.can_see')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
@ -24,7 +25,7 @@ class MediafileAccessPermissions(BaseAccessPermissions):
|
|||||||
Returns the restricted serialized data for the instance prepared
|
Returns the restricted serialized data for the instance prepared
|
||||||
for the user.
|
for the user.
|
||||||
"""
|
"""
|
||||||
if (not full_data['hidden'] or user.has_perm('mediafiles.can_see_hidden')):
|
if (not full_data['hidden'] or has_perm(user, 'mediafiles.can_see_hidden')):
|
||||||
data = full_data
|
data = full_data
|
||||||
else:
|
else:
|
||||||
data = None
|
data = None
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..utils.access_permissions import BaseAccessPermissions
|
from ..utils.access_permissions import BaseAccessPermissions
|
||||||
|
from ..utils.auth import has_perm
|
||||||
|
from ..utils.collection import CollectionElement
|
||||||
|
|
||||||
|
|
||||||
class MotionAccessPermissions(BaseAccessPermissions):
|
class MotionAccessPermissions(BaseAccessPermissions):
|
||||||
@ -12,7 +16,7 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has read access model instances.
|
Returns True if the user has read access model instances.
|
||||||
"""
|
"""
|
||||||
return user.has_perm('motions.can_see')
|
return has_perm(user, 'motions.can_see')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
@ -29,12 +33,25 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
|||||||
the motion in this state. Removes non public comment fields for
|
the motion in this state. Removes non public comment fields for
|
||||||
some unauthorized users.
|
some unauthorized users.
|
||||||
"""
|
"""
|
||||||
required_permission_to_see = full_data.get('state_required_permission_to_see')
|
if isinstance(user, get_user_model()):
|
||||||
|
# Converts a user object to a collection element.
|
||||||
|
# from_instance can not be used because the user serializer loads
|
||||||
|
# the group from the db. So each call to from_instance(user) consts
|
||||||
|
# one db query.
|
||||||
|
user = CollectionElement.from_values('users/user', user.id)
|
||||||
|
|
||||||
|
if isinstance(user, CollectionElement):
|
||||||
|
is_submitter = user.get_full_data()['id'] in full_data.get('submitters_id', [])
|
||||||
|
else:
|
||||||
|
# Anonymous users can not be submitters
|
||||||
|
is_submitter = False
|
||||||
|
|
||||||
|
required_permission_to_see = full_data['state_required_permission_to_see']
|
||||||
if (not required_permission_to_see or
|
if (not required_permission_to_see or
|
||||||
user.has_perm(required_permission_to_see) or
|
has_perm(user, required_permission_to_see) or
|
||||||
user.has_perm('motions.can_manage') or
|
has_perm(user, 'motions.can_manage') or
|
||||||
user.pk in full_data.get('submitters_id', [])):
|
is_submitter):
|
||||||
if user.has_perm('motions.can_see_and_manage_comments') or not full_data.get('comments'):
|
if has_perm(user, 'motions.can_see_and_manage_comments') or not full_data.get('comments'):
|
||||||
data = full_data
|
data = full_data
|
||||||
else:
|
else:
|
||||||
data = deepcopy(full_data)
|
data = deepcopy(full_data)
|
||||||
@ -74,7 +91,7 @@ class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has read access model instances.
|
Returns True if the user has read access model instances.
|
||||||
"""
|
"""
|
||||||
return user.has_perm('motions.can_see')
|
return has_perm(user, 'motions.can_see')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
@ -93,7 +110,7 @@ class CategoryAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has read access model instances.
|
Returns True if the user has read access model instances.
|
||||||
"""
|
"""
|
||||||
return user.has_perm('motions.can_see')
|
return has_perm(user, 'motions.can_see')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
@ -112,7 +129,7 @@ class MotionBlockAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has read access model instances.
|
Returns True if the user has read access model instances.
|
||||||
"""
|
"""
|
||||||
return user.has_perm('motions.can_see')
|
return has_perm(user, 'motions.can_see')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
@ -131,7 +148,7 @@ class WorkflowAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has read access model instances.
|
Returns True if the user has read access model instances.
|
||||||
"""
|
"""
|
||||||
return user.has_perm('motions.can_see')
|
return has_perm(user, 'motions.can_see')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from ..utils.access_permissions import BaseAccessPermissions
|
from ..utils.access_permissions import BaseAccessPermissions
|
||||||
|
from ..utils.auth import has_perm
|
||||||
|
|
||||||
|
|
||||||
class TopicAccessPermissions(BaseAccessPermissions):
|
class TopicAccessPermissions(BaseAccessPermissions):
|
||||||
@ -9,7 +10,7 @@ class TopicAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has read access model instances.
|
Returns True if the user has read access model instances.
|
||||||
"""
|
"""
|
||||||
return user.has_perm('agenda.can_see')
|
return has_perm(user, 'agenda.can_see')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from ..utils.access_permissions import BaseAccessPermissions
|
from ..utils.access_permissions import BaseAccessPermissions
|
||||||
|
from ..utils.auth import DjangoAnonymousUser, has_perm
|
||||||
|
|
||||||
|
|
||||||
class UserAccessPermissions(BaseAccessPermissions):
|
class UserAccessPermissions(BaseAccessPermissions):
|
||||||
@ -9,7 +10,7 @@ class UserAccessPermissions(BaseAccessPermissions):
|
|||||||
"""
|
"""
|
||||||
Returns True if the user has read access model instances.
|
Returns True if the user has read access model instances.
|
||||||
"""
|
"""
|
||||||
return user.has_perm('users.can_see_name')
|
return has_perm(user, 'users.can_see_name')
|
||||||
|
|
||||||
def get_serializer_class(self, user=None):
|
def get_serializer_class(self, user=None):
|
||||||
"""
|
"""
|
||||||
@ -33,9 +34,9 @@ class UserAccessPermissions(BaseAccessPermissions):
|
|||||||
FULL_DATA = 3
|
FULL_DATA = 3
|
||||||
|
|
||||||
# Check user permissions.
|
# Check user permissions.
|
||||||
if user.has_perm('users.can_see_name'):
|
if has_perm(user, 'users.can_see_name'):
|
||||||
if user.has_perm('users.can_see_extra_data'):
|
if has_perm(user, 'users.can_see_extra_data'):
|
||||||
if user.has_perm('users.can_manage'):
|
if has_perm(user, 'users.can_manage'):
|
||||||
case = FULL_DATA
|
case = FULL_DATA
|
||||||
else:
|
else:
|
||||||
case = MANY_DATA
|
case = MANY_DATA
|
||||||
@ -77,3 +78,29 @@ class UserAccessPermissions(BaseAccessPermissions):
|
|||||||
if key in USERCANSEESERIALIZER_FIELDS:
|
if key in USERCANSEESERIALIZER_FIELDS:
|
||||||
data[key] = full_data[key]
|
data[key] = full_data[key]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class GroupAccessPermissions(BaseAccessPermissions):
|
||||||
|
"""
|
||||||
|
Access permissions container for Groups. Everyone can see them
|
||||||
|
"""
|
||||||
|
def check_permissions(self, user):
|
||||||
|
"""
|
||||||
|
Returns True if the user has read access model instances.
|
||||||
|
"""
|
||||||
|
from ..core.config import config
|
||||||
|
|
||||||
|
# Every authenticated user can retrieve groups. Anonymous users can do
|
||||||
|
# so if they are enabled.
|
||||||
|
# Our AnonymousUser is a subclass of the DjangoAnonymousUser. Normaly, a
|
||||||
|
# DjangoAnonymousUser means, that AnonymousUser is disabled. But this is
|
||||||
|
# no garanty. send_data uses the AnonymousUser in any case.
|
||||||
|
return not isinstance(user, DjangoAnonymousUser) or config['general_system_enable_anonymous']
|
||||||
|
|
||||||
|
def get_serializer_class(self, user=None):
|
||||||
|
"""
|
||||||
|
Returns serializer class.
|
||||||
|
"""
|
||||||
|
from .serializers import GroupSerializer
|
||||||
|
|
||||||
|
return GroupSerializer
|
||||||
|
38
openslides/users/migrations/0003_group.py
Normal file
38
openslides/users/migrations/0003_group.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 1.10.5 on 2017-01-11 21:45
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.users.models
|
||||||
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0008_alter_user_username_max_length'),
|
||||||
|
('users', '0002_user_misc_default_groups'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Group',
|
||||||
|
fields=[(
|
||||||
|
'group_ptr',
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to='auth.Group'))],
|
||||||
|
options={
|
||||||
|
'default_permissions': (),
|
||||||
|
},
|
||||||
|
bases=(openslides.utils.models.RESTModelMixin, 'auth.group'),
|
||||||
|
managers=[
|
||||||
|
('objects', openslides.users.models.GroupManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -1,20 +1,21 @@
|
|||||||
from random import choice
|
from random import choice
|
||||||
|
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.contrib.auth.models import Group as DjangoGroup
|
||||||
from django.contrib.auth.models import (
|
from django.contrib.auth.models import (
|
||||||
AbstractBaseUser,
|
AbstractBaseUser,
|
||||||
BaseUserManager,
|
BaseUserManager,
|
||||||
Group,
|
GroupManager,
|
||||||
Permission,
|
Permission,
|
||||||
PermissionsMixin,
|
PermissionsMixin,
|
||||||
)
|
)
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Prefetch, Q
|
||||||
|
|
||||||
from openslides.utils.search import user_name_helper
|
from openslides.utils.search import user_name_helper
|
||||||
|
|
||||||
from ..utils.models import RESTModelMixin
|
from ..utils.models import RESTModelMixin
|
||||||
from .access_permissions import UserAccessPermissions
|
from .access_permissions import GroupAccessPermissions, UserAccessPermissions
|
||||||
|
|
||||||
|
|
||||||
class UserManager(BaseUserManager):
|
class UserManager(BaseUserManager):
|
||||||
@ -25,9 +26,16 @@ class UserManager(BaseUserManager):
|
|||||||
def get_full_queryset(self):
|
def get_full_queryset(self):
|
||||||
"""
|
"""
|
||||||
Returns the normal queryset with all users. In the background all
|
Returns the normal queryset with all users. In the background all
|
||||||
groups are prefetched from the database.
|
groups are prefetched from the database together with all permissions
|
||||||
|
and content types.
|
||||||
"""
|
"""
|
||||||
return self.get_queryset().prefetch_related('groups')
|
return self.get_queryset().prefetch_related(Prefetch(
|
||||||
|
'groups',
|
||||||
|
queryset=Group.objects
|
||||||
|
.select_related('group_ptr')
|
||||||
|
.prefetch_related(Prefetch(
|
||||||
|
'permissions',
|
||||||
|
queryset=Permission.objects.select_related('content_type')))))
|
||||||
|
|
||||||
def create_user(self, username, password, **kwargs):
|
def create_user(self, username, password, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -196,3 +204,30 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
|
|||||||
user_name_helper(self),
|
user_name_helper(self),
|
||||||
self.structure_level,
|
self.structure_level,
|
||||||
self.about_me))
|
self.about_me))
|
||||||
|
|
||||||
|
|
||||||
|
class GroupManager(GroupManager):
|
||||||
|
"""
|
||||||
|
Customized manager that supports our get_full_queryset method.
|
||||||
|
"""
|
||||||
|
def get_full_queryset(self):
|
||||||
|
"""
|
||||||
|
Returns the normal queryset with all groups. In the background all
|
||||||
|
permissions with the content types are prefetched from the database.
|
||||||
|
"""
|
||||||
|
return (self.get_queryset()
|
||||||
|
.select_related('group_ptr')
|
||||||
|
.prefetch_related(Prefetch(
|
||||||
|
'permissions',
|
||||||
|
queryset=Permission.objects.select_related('content_type'))))
|
||||||
|
|
||||||
|
|
||||||
|
class Group(RESTModelMixin, DjangoGroup):
|
||||||
|
"""
|
||||||
|
Extend the django group with support of our REST and caching system.
|
||||||
|
"""
|
||||||
|
access_permissions = GroupAccessPermissions()
|
||||||
|
objects = GroupManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from ..utils.autoupdate import inform_changed_data
|
||||||
from .models import Group, User
|
from .models import Group, User
|
||||||
|
|
||||||
|
|
||||||
@ -143,3 +144,8 @@ def create_builtin_groups_and_admin(**kwargs):
|
|||||||
|
|
||||||
# Create or reset admin user
|
# Create or reset admin user
|
||||||
User.objects.create_or_reset_admin_user()
|
User.objects.create_or_reset_admin_user()
|
||||||
|
|
||||||
|
# After each group was created, the permissions (many to many fields) where
|
||||||
|
# added to the group. So we have to update the cache by calling
|
||||||
|
# inform_changed_data().
|
||||||
|
inform_changed_data((group_default, group_delegates, group_staff, group_committee))
|
||||||
|
@ -14,7 +14,7 @@ from ..utils.rest_api import (
|
|||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
from ..utils.views import APIView
|
from ..utils.views import APIView
|
||||||
from .access_permissions import UserAccessPermissions
|
from .access_permissions import GroupAccessPermissions, UserAccessPermissions
|
||||||
from .models import Group, User
|
from .models import Group, User
|
||||||
from .serializers import GroupSerializer
|
from .serializers import GroupSerializer
|
||||||
|
|
||||||
@ -123,16 +123,19 @@ class GroupViewSet(ModelViewSet):
|
|||||||
partial_update, update and destroy.
|
partial_update, update and destroy.
|
||||||
"""
|
"""
|
||||||
metadata_class = GroupViewSetMetadata
|
metadata_class = GroupViewSetMetadata
|
||||||
queryset = Group.objects.prefetch_related('permissions', 'permissions__content_type')
|
queryset = Group.objects.all()
|
||||||
serializer_class = GroupSerializer
|
serializer_class = GroupSerializer
|
||||||
|
access_permissions = GroupAccessPermissions()
|
||||||
|
|
||||||
def check_view_permissions(self):
|
def check_view_permissions(self):
|
||||||
"""
|
"""
|
||||||
Returns True if the user has required permissions.
|
Returns True if the user has required permissions.
|
||||||
"""
|
"""
|
||||||
if self.action in ('metadata', 'list', 'retrieve'):
|
if self.action in ('list', 'retrieve'):
|
||||||
# Every authenticated user can see the metadata and list or
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
# retrieve groups. Anonymous users can do so if they are enabled.
|
elif self.action == 'metadata':
|
||||||
|
# Every authenticated user can see the metadata.
|
||||||
|
# Anonymous users can do so if they are enabled.
|
||||||
result = self.request.user.is_authenticated() or config['general_system_enable_anonymous']
|
result = self.request.user.is_authenticated() or config['general_system_enable_anonymous']
|
||||||
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
||||||
# Users with all app permissions can edit groups.
|
# Users with all app permissions can edit groups.
|
||||||
|
@ -6,7 +6,7 @@ from django.contrib.auth.models import Permission
|
|||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from rest_framework.authentication import BaseAuthentication
|
from rest_framework.authentication import BaseAuthentication
|
||||||
|
|
||||||
from ..core.config import config
|
from .collection import CollectionElement
|
||||||
|
|
||||||
|
|
||||||
# Registered users
|
# Registered users
|
||||||
@ -48,33 +48,13 @@ class AnonymousUser(DjangoAnonymousUser):
|
|||||||
Class for anonymous user instances which have the permissions from the
|
Class for anonymous user instances which have the permissions from the
|
||||||
group 'Anonymous' (pk=1).
|
group 'Anonymous' (pk=1).
|
||||||
"""
|
"""
|
||||||
def get_all_permissions(self, obj=None):
|
|
||||||
"""
|
|
||||||
Returns the permissions a user is granted by his group membership(s).
|
|
||||||
|
|
||||||
Try to return the permissions for the 'Anonymous' group (pk=1).
|
|
||||||
"""
|
|
||||||
perms = Permission.objects.filter(group__pk=1)
|
|
||||||
if perms is None:
|
|
||||||
return set()
|
|
||||||
# TODO: Test without order_by()
|
|
||||||
perms = perms.values_list('content_type__app_label', 'codename').order_by()
|
|
||||||
return set(['%s.%s' % (content_type, codename) for content_type, codename in perms])
|
|
||||||
|
|
||||||
def has_perm(self, perm, obj=None):
|
def has_perm(self, perm, obj=None):
|
||||||
"""
|
"""
|
||||||
Checks if the user has a specific permission.
|
Checks if the user has a specific permission.
|
||||||
"""
|
"""
|
||||||
return (perm in self.get_all_permissions())
|
default_group = CollectionElement.from_values('users/group', 1)
|
||||||
|
return perm in default_group.get_full_data()['permissions']
|
||||||
def has_module_perms(self, app_label):
|
|
||||||
"""
|
|
||||||
Checks if the user has permissions on the module app_label.
|
|
||||||
"""
|
|
||||||
for perm in self.get_all_permissions():
|
|
||||||
if perm[:perm.index('.')] == app_label:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class RESTFrameworkAnonymousAuthentication(BaseAuthentication):
|
class RESTFrameworkAnonymousAuthentication(BaseAuthentication):
|
||||||
@ -86,6 +66,7 @@ class RESTFrameworkAnonymousAuthentication(BaseAuthentication):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
|
from ..core.config import config
|
||||||
if config['general_system_enable_anonymous']:
|
if config['general_system_enable_anonymous']:
|
||||||
return (AnonymousUser(), None)
|
return (AnonymousUser(), None)
|
||||||
return None
|
return None
|
||||||
@ -106,7 +87,7 @@ class AuthenticationMiddleware:
|
|||||||
"The authentication middleware requires session middleware "
|
"The authentication middleware requires session middleware "
|
||||||
"to be installed. Edit your MIDDLEWARE_CLASSES setting to insert "
|
"to be installed. Edit your MIDDLEWARE_CLASSES setting to insert "
|
||||||
"'django.contrib.sessions.middleware.SessionMiddleware' before "
|
"'django.contrib.sessions.middleware.SessionMiddleware' before "
|
||||||
"'openslides.users.auth.AuthenticationMiddleware'."
|
"'openslides.utils.auth.AuthenticationMiddleware'."
|
||||||
)
|
)
|
||||||
request.user = SimpleLazyObject(lambda: get_user(request))
|
request.user = SimpleLazyObject(lambda: get_user(request))
|
||||||
|
|
||||||
@ -118,6 +99,7 @@ def get_user(request):
|
|||||||
This is a mix of django.contrib.auth.get_user and
|
This is a mix of django.contrib.auth.get_user and
|
||||||
django.contrib.auth.middleware.get_user which uses our anonymous user.
|
django.contrib.auth.middleware.get_user which uses our anonymous user.
|
||||||
"""
|
"""
|
||||||
|
from ..core.config import config
|
||||||
try:
|
try:
|
||||||
return_user = request._cached_user
|
return_user = request._cached_user
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -127,3 +109,36 @@ def get_user(request):
|
|||||||
return_user = AnonymousUser()
|
return_user = AnonymousUser()
|
||||||
request._cached_user = return_user
|
request._cached_user = return_user
|
||||||
return return_user
|
return return_user
|
||||||
|
|
||||||
|
|
||||||
|
def has_perm(user, perm):
|
||||||
|
"""
|
||||||
|
Checks that user has a specific permission.
|
||||||
|
"""
|
||||||
|
if isinstance(user, AnonymousUser):
|
||||||
|
# Our anonymous user has a has_perm-method that works with the cache
|
||||||
|
# system. So we can use it here.
|
||||||
|
has_perm = user.has_perm(perm)
|
||||||
|
elif isinstance(user, DjangoAnonymousUser):
|
||||||
|
# The django anonymous user is only used when anonymous user is disabled
|
||||||
|
# So he never has permissions to see anything.
|
||||||
|
has_perm = False
|
||||||
|
else:
|
||||||
|
if isinstance(user, get_user_model()):
|
||||||
|
# Converts a user object to a collection element.
|
||||||
|
# from_instance can not be used because the user serializer loads
|
||||||
|
# the group from the db. So each call to from_instance(user) consts
|
||||||
|
# one db query.
|
||||||
|
user = CollectionElement.from_values('users/user', user.id)
|
||||||
|
|
||||||
|
# Get all groups of the user and then see, if one group has the required
|
||||||
|
# permission. If the user has no groups, then use group 1.
|
||||||
|
group_ids = user.get_full_data()['groups_id'] or [1]
|
||||||
|
for group_id in group_ids:
|
||||||
|
group = CollectionElement.from_values('users/group', group_id)
|
||||||
|
if perm in group.get_full_data()['permissions']:
|
||||||
|
has_perm = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
has_perm = False
|
||||||
|
return has_perm
|
@ -1,4 +1,3 @@
|
|||||||
import itertools
|
|
||||||
import json
|
import json
|
||||||
from collections import Iterable
|
from collections import Iterable
|
||||||
|
|
||||||
@ -6,24 +5,14 @@ from asgiref.inmemory import ChannelLayer
|
|||||||
from channels import Channel, Group
|
from channels import Channel, Group
|
||||||
from channels.auth import channel_session_user, channel_session_user_from_http
|
from channels.auth import channel_session_user, channel_session_user_from_http
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..core.models import Projector
|
from ..core.models import Projector
|
||||||
from ..users.auth import AnonymousUser
|
from .auth import AnonymousUser
|
||||||
from ..users.models import User
|
from .cache import websocket_user_cache
|
||||||
from .collection import Collection, CollectionElement, CollectionElementList
|
from .collection import Collection, CollectionElement, CollectionElementList
|
||||||
|
|
||||||
|
|
||||||
def get_logged_in_users():
|
|
||||||
"""
|
|
||||||
Helper to get all logged in users.
|
|
||||||
|
|
||||||
Only works with the OpenSlides session backend.
|
|
||||||
"""
|
|
||||||
return User.objects.exclude(session=None).filter(session__expire_date__gte=timezone.now()).distinct()
|
|
||||||
|
|
||||||
|
|
||||||
@channel_session_user_from_http
|
@channel_session_user_from_http
|
||||||
def ws_add_site(message):
|
def ws_add_site(message):
|
||||||
"""
|
"""
|
||||||
@ -31,7 +20,10 @@ def ws_add_site(message):
|
|||||||
|
|
||||||
The group with the name 'user-None' stands for all anonymous users.
|
The group with the name 'user-None' stands for all anonymous users.
|
||||||
"""
|
"""
|
||||||
Group('user-{}'.format(message.user.id)).add(message.reply_channel)
|
Group('site').add(message.reply_channel)
|
||||||
|
message.channel_session['user_id'] = message.user.id
|
||||||
|
# Saves the reply channel to the user. Uses 0 for anonymous users.
|
||||||
|
websocket_user_cache.add(message.user.id or 0, message.reply_channel.name)
|
||||||
|
|
||||||
|
|
||||||
@channel_session_user
|
@channel_session_user
|
||||||
@ -39,7 +31,8 @@ def ws_disconnect_site(message):
|
|||||||
"""
|
"""
|
||||||
This function is called, when a client on the site disconnects.
|
This function is called, when a client on the site disconnects.
|
||||||
"""
|
"""
|
||||||
Group('user-{}'.format(message.user.id)).discard(message.reply_channel)
|
Group('site').discard(message.reply_channel)
|
||||||
|
websocket_user_cache.remove(message.user.id or 0, message.reply_channel.name)
|
||||||
|
|
||||||
|
|
||||||
@channel_session_user_from_http
|
@channel_session_user_from_http
|
||||||
@ -104,12 +97,15 @@ def send_data(message):
|
|||||||
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 = CollectionElementList.from_channels_message(message)
|
||||||
|
for user_id, channel_names in websocket_user_cache.get_all().items():
|
||||||
# Loop over all logged in site users and the anonymous user and send changed data.
|
if not user_id:
|
||||||
for user in itertools.chain(get_logged_in_users(), [AnonymousUser()]):
|
# Anonymous user
|
||||||
channel = Group('user-{}'.format(user.id))
|
user = AnonymousUser()
|
||||||
|
else:
|
||||||
|
user = CollectionElement.from_values('users/user', user_id)
|
||||||
output = collection_elements.as_autoupdate_for_user(user)
|
output = collection_elements.as_autoupdate_for_user(user)
|
||||||
channel.send({'text': json.dumps(output)})
|
for channel_name in channel_names:
|
||||||
|
Channel(channel_name).send({'text': json.dumps(output)})
|
||||||
|
|
||||||
# Check whether broadcast is active at the moment and set the local
|
# Check whether broadcast is active at the moment and set the local
|
||||||
# projector queryset.
|
# projector queryset.
|
||||||
|
209
openslides/utils/cache.py
Normal file
209
openslides/utils/cache.py
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from channels import Group
|
||||||
|
from channels.sessions import session_for_reply_channel
|
||||||
|
from django.core.cache import cache, caches
|
||||||
|
|
||||||
|
|
||||||
|
class BaseWebsocketUserCache:
|
||||||
|
"""
|
||||||
|
Caches the reply channel names of all open websocket connections. The id of
|
||||||
|
the user that that opened the connection is used as reference.
|
||||||
|
|
||||||
|
This is the Base cache that has to be overriden.
|
||||||
|
"""
|
||||||
|
cache_key = 'current_websocket_users'
|
||||||
|
|
||||||
|
def add(self, user_id, channel_name):
|
||||||
|
"""
|
||||||
|
Adds a channel name to an user id.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def remove(self, user_id, channel_name):
|
||||||
|
"""
|
||||||
|
Removes one channel name from the cache.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_all(self):
|
||||||
|
"""
|
||||||
|
Returns all data using a dict where the key is a user id and the value
|
||||||
|
is a set of channel_names.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def save_data(self, data):
|
||||||
|
"""
|
||||||
|
Saves the full data set (like created with build_data) to the cache.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def build_data(self):
|
||||||
|
"""
|
||||||
|
Creates all the data, saves it to the cache and returns it.
|
||||||
|
"""
|
||||||
|
websocket_user_ids = defaultdict(set)
|
||||||
|
for channel_name in Group('site').channel_layer.group_channels('site'):
|
||||||
|
session = session_for_reply_channel(channel_name)
|
||||||
|
user_id = session.get('user_id', None)
|
||||||
|
websocket_user_ids[user_id or 0].add(channel_name)
|
||||||
|
self.save_data(websocket_user_ids)
|
||||||
|
return websocket_user_ids
|
||||||
|
|
||||||
|
def get_cache_key(self):
|
||||||
|
"""
|
||||||
|
Returns the cache key.
|
||||||
|
"""
|
||||||
|
return self.cache_key
|
||||||
|
|
||||||
|
|
||||||
|
class RedisWebsocketUserCache(BaseWebsocketUserCache):
|
||||||
|
"""
|
||||||
|
Implementation of the WebsocketUserCache that uses redis.
|
||||||
|
|
||||||
|
This uses one cache key to store all connected user ids in a set and
|
||||||
|
for each user another set to save the channel names.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add(self, user_id, channel_name):
|
||||||
|
"""
|
||||||
|
Adds a channel name to an user id.
|
||||||
|
"""
|
||||||
|
redis = get_redis_connection()
|
||||||
|
pipe = redis.pipeline()
|
||||||
|
pipe.sadd(self.get_cache_key(), user_id)
|
||||||
|
pipe.sadd(self.get_user_cache_key(user_id), channel_name)
|
||||||
|
pipe.execute()
|
||||||
|
|
||||||
|
def remove(self, user_id, channel_name):
|
||||||
|
"""
|
||||||
|
Removes one channel name from the cache.
|
||||||
|
"""
|
||||||
|
redis = get_redis_connection()
|
||||||
|
redis.srem(self.get_user_cache_key(user_id), channel_name)
|
||||||
|
|
||||||
|
def get_all(self):
|
||||||
|
"""
|
||||||
|
Returns all data using a dict where the key is a user id and the value
|
||||||
|
is a set of channel_names.
|
||||||
|
"""
|
||||||
|
redis = get_redis_connection()
|
||||||
|
user_ids = redis.smembers(self.get_cache_key())
|
||||||
|
if user_ids is None:
|
||||||
|
websocket_user_ids = self.build_data()
|
||||||
|
else:
|
||||||
|
websocket_user_ids = dict()
|
||||||
|
for user_id in user_ids:
|
||||||
|
# Redis returns the id as string. So we have to convert it
|
||||||
|
user_id = int(user_id)
|
||||||
|
channel_names = redis.smembers(self.get_user_cache_key(user_id))
|
||||||
|
if channel_names is not None:
|
||||||
|
# If channel name is empty, then we can assume, that the user
|
||||||
|
# has no active connection.
|
||||||
|
websocket_user_ids[user_id] = set(channel_names)
|
||||||
|
return websocket_user_ids
|
||||||
|
|
||||||
|
def save_data(self, data):
|
||||||
|
"""
|
||||||
|
Saves the full data set (like created with the method build_data()) to
|
||||||
|
the cache.
|
||||||
|
"""
|
||||||
|
redis = get_redis_connection()
|
||||||
|
pipe = redis.pipeline()
|
||||||
|
|
||||||
|
# Save all user ids
|
||||||
|
pipe.delete(self.get_cache_key())
|
||||||
|
pipe.sadd(self.get_cache_key(), *data.keys())
|
||||||
|
|
||||||
|
for user_id, channel_names in data.items():
|
||||||
|
pipe.delete(self.get_user_cache_key(user_id))
|
||||||
|
pipe.sadd(self.get_user_cache_key(user_id), *channel_names)
|
||||||
|
pipe.execute()
|
||||||
|
|
||||||
|
def get_cache_key(self):
|
||||||
|
"""
|
||||||
|
Returns the cache key.
|
||||||
|
"""
|
||||||
|
return cache.make_key(self.cache_key)
|
||||||
|
|
||||||
|
def get_user_cache_key(self, user_id):
|
||||||
|
"""
|
||||||
|
Returns a cache key to save the channel names for a specific user.
|
||||||
|
"""
|
||||||
|
return cache.make_key('{}:{}'.format(self.cache_key, user_id))
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoCacheWebsocketUserCache(BaseWebsocketUserCache):
|
||||||
|
"""
|
||||||
|
Implementation of the WebsocketUserCache that uses the django cache.
|
||||||
|
|
||||||
|
If you use this with the inmemory cache, then you should only use one
|
||||||
|
worker.
|
||||||
|
|
||||||
|
This uses only one cache key to save a dict where the key is the user id and
|
||||||
|
the value is a set of channel names.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add(self, user_id, channel_name):
|
||||||
|
"""
|
||||||
|
Adds a channel name for a user using the django cache.
|
||||||
|
"""
|
||||||
|
websocket_user_ids = cache.get(self.get_cache_key())
|
||||||
|
if websocket_user_ids is None:
|
||||||
|
websocket_user_ids = dict()
|
||||||
|
|
||||||
|
if user_id in websocket_user_ids:
|
||||||
|
websocket_user_ids[user_id].add(channel_name)
|
||||||
|
else:
|
||||||
|
websocket_user_ids[user_id] = set([channel_name])
|
||||||
|
cache.set(self.get_cache_key(), websocket_user_ids)
|
||||||
|
|
||||||
|
def remove(self, user_id, channel_name):
|
||||||
|
"""
|
||||||
|
Removes one channel name from the django cache.
|
||||||
|
"""
|
||||||
|
websocket_user_ids = cache.get(self.get_cache_key())
|
||||||
|
if websocket_user_ids is not None and user_id in websocket_user_ids:
|
||||||
|
websocket_user_ids[user_id].discard(channel_name)
|
||||||
|
cache.set(self.get_cache_key(), websocket_user_ids)
|
||||||
|
|
||||||
|
def get_all(self):
|
||||||
|
"""
|
||||||
|
Returns the data using the django cache.
|
||||||
|
"""
|
||||||
|
websocket_user_ids = cache.get(self.get_cache_key())
|
||||||
|
if websocket_user_ids is None:
|
||||||
|
return self.build_data()
|
||||||
|
return websocket_user_ids
|
||||||
|
|
||||||
|
def save_data(self, data):
|
||||||
|
"""
|
||||||
|
Saves the data using the django cache.
|
||||||
|
"""
|
||||||
|
cache.set(self.get_cache_key(), data)
|
||||||
|
|
||||||
|
|
||||||
|
def use_redis_cache():
|
||||||
|
"""
|
||||||
|
Returns True if Redis is used als caching backend.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from django_redis.cache import RedisCache
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
return isinstance(caches['default'], RedisCache)
|
||||||
|
|
||||||
|
|
||||||
|
def get_redis_connection():
|
||||||
|
"""
|
||||||
|
Returns an object that can be used to talk directly to redis.
|
||||||
|
"""
|
||||||
|
from django_redis import get_redis_connection
|
||||||
|
return get_redis_connection("default")
|
||||||
|
|
||||||
|
|
||||||
|
if use_redis_cache():
|
||||||
|
websocket_user_cache = RedisWebsocketUserCache()
|
||||||
|
else:
|
||||||
|
websocket_user_cache = DjangoCacheWebsocketUserCache()
|
@ -1,5 +1,7 @@
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.cache import cache, caches
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
from .cache import get_redis_connection, use_redis_cache
|
||||||
|
|
||||||
|
|
||||||
class CollectionElement:
|
class CollectionElement:
|
||||||
@ -37,6 +39,7 @@ class CollectionElement:
|
|||||||
self.full_data = full_data
|
self.full_data = full_data
|
||||||
self.information = information or {}
|
self.information = information or {}
|
||||||
if instance is not None:
|
if instance is not None:
|
||||||
|
# Collection element is created via instance
|
||||||
self.collection_string = instance.get_collection_string()
|
self.collection_string = instance.get_collection_string()
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
if self.collection_string == config.get_collection_string():
|
if self.collection_string == config.get_collection_string():
|
||||||
@ -45,6 +48,7 @@ class CollectionElement:
|
|||||||
else:
|
else:
|
||||||
self.id = instance.pk
|
self.id = instance.pk
|
||||||
elif collection_string is not None and id is not None:
|
elif collection_string is not None and id is not None:
|
||||||
|
# Collection element is created via values
|
||||||
self.collection_string = collection_string
|
self.collection_string = collection_string
|
||||||
self.id = id
|
self.id = id
|
||||||
else:
|
else:
|
||||||
@ -511,19 +515,3 @@ def get_collection_id_from_cache_key(cache_key):
|
|||||||
# The id is no integer. This can happen on config elements
|
# The id is no integer. This can happen on config elements
|
||||||
pass
|
pass
|
||||||
return (collection_string, id)
|
return (collection_string, id)
|
||||||
|
|
||||||
|
|
||||||
def use_redis_cache():
|
|
||||||
"""
|
|
||||||
Returns True if Redis is used als caching backend.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from django_redis.cache import RedisCache
|
|
||||||
except ImportError:
|
|
||||||
return False
|
|
||||||
return isinstance(caches['default'], RedisCache)
|
|
||||||
|
|
||||||
|
|
||||||
def get_redis_connection():
|
|
||||||
from django_redis import get_redis_connection
|
|
||||||
return get_redis_connection("default")
|
|
||||||
|
@ -65,32 +65,49 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Django Channels
|
# Set use_redis to True to activate redis as cache-, asgi- and session backend.
|
||||||
|
use_redis = False
|
||||||
# Unless you have only a small assembly uncomment the following lines to
|
|
||||||
# activate Redis as backend for Django Channels and Cache. You have to install
|
|
||||||
# a Redis server and the python packages asgi_redis and django-redis.
|
|
||||||
|
|
||||||
# https://channels.readthedocs.io/en/latest/backends.html#redis
|
|
||||||
|
|
||||||
# CHANNEL_LAYERS['default']['BACKEND'] = 'asgi_redis.RedisChannelLayer'
|
|
||||||
|
|
||||||
|
|
||||||
# Caching
|
if use_redis:
|
||||||
|
|
||||||
# Django uses a inmemory cache at default. This supports only one thread. If
|
# Django Channels
|
||||||
# you use more then one thread another caching backend is required. We recommand
|
|
||||||
# django-redis: https://niwinz.github.io/django-redis/latest/#_user_guide
|
# Unless you have only a small assembly uncomment the following lines to
|
||||||
|
# activate Redis as backend for Django Channels and Cache. You have to install
|
||||||
|
# a Redis server and the python packages asgi_redis and django-redis.
|
||||||
|
|
||||||
|
# https://channels.readthedocs.io/en/latest/backends.html#redis
|
||||||
|
|
||||||
|
CHANNEL_LAYERS['default']['BACKEND'] = 'asgi_redis.RedisChannelLayer'
|
||||||
|
|
||||||
|
|
||||||
|
# Caching
|
||||||
|
|
||||||
|
# Django uses a inmemory cache at default. This supports only one thread. If
|
||||||
|
# you use more then one thread another caching backend is required. We recommand
|
||||||
|
# django-redis: https://niwinz.github.io/django-redis/latest/#_user_guide
|
||||||
|
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
|
"LOCATION": "redis://127.0.0.1:6379/0",
|
||||||
|
"OPTIONS": {
|
||||||
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Session backend
|
||||||
|
|
||||||
|
# Per default django uses the database as session backend. This can be slow.
|
||||||
|
# One possibility is to use the cache session backend with redis as cache backend
|
||||||
|
# Another possibility is to use a native redis session backend. For example:
|
||||||
|
# https://github.com/martinrusev/django-redis-sessions
|
||||||
|
|
||||||
|
# SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
|
||||||
|
SESSION_ENGINE = 'redis_sessions.session'
|
||||||
|
|
||||||
# CACHES = {
|
|
||||||
# "default": {
|
|
||||||
# "BACKEND": "django_redis.cache.RedisCache",
|
|
||||||
# "LOCATION": "redis://127.0.0.1:6379/0",
|
|
||||||
# "OPTIONS": {
|
|
||||||
# "CLIENT_CLASS": "django_redis.client.DefaultClient",
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
from contextlib import ContextDecorator
|
||||||
|
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
|
||||||
|
|
||||||
@ -31,3 +35,22 @@ class TestCase(_TestCase):
|
|||||||
Could be used in the future. Use this this for the integration test suit.
|
Could be used in the future. Use this this for the integration test suit.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
cache = caches['locmem']
|
||||||
|
cache.clear()
|
||||||
|
self.patch = patch('openslides.utils.collection.cache', cache)
|
||||||
|
self.patch.start()
|
||||||
|
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
self.patch.stop()
|
||||||
|
@ -10,7 +10,7 @@ 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
|
from openslides.utils.test import TestCase, use_cache
|
||||||
|
|
||||||
|
|
||||||
class RetrieveItem(TestCase):
|
class RetrieveItem(TestCase):
|
||||||
@ -64,36 +64,37 @@ 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:
|
||||||
* 5 requests to get the session an the request user with its permissions,
|
* 4 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all agenda items,
|
* 2 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
|
||||||
|
|
||||||
* 2 requests for the motionsversions.
|
* 2 requests for the motionsversions.
|
||||||
|
|
||||||
TODO: There could be less requests to get the session and the request user.
|
TODO: The last two request for the motionsversions are a bug.
|
||||||
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(13):
|
with self.assertNumQueries(12):
|
||||||
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:
|
||||||
* 2 requests to get the permission for anonymous (config and permissions)
|
* 3 requests to get the permission for anonymous,
|
||||||
* 2 requests to get the list of all agenda items,
|
* 2 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
|
||||||
|
|
||||||
* 32 requests for the motionsversions.
|
* 2 requests for the motionsversions.
|
||||||
|
|
||||||
TODO: The last 32 requests are a bug.
|
TODO: The last two request for the motionsversions are a bug.
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(40):
|
with self.assertNumQueries(11):
|
||||||
self.client.get(reverse('item-list'))
|
self.client.get(reverse('item-list'))
|
||||||
|
|
||||||
|
|
@ -6,7 +6,7 @@ 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
|
from openslides.utils.test import TestCase, use_cache
|
||||||
|
|
||||||
|
|
||||||
class TestDBQueries(TestCase):
|
class TestDBQueries(TestCase):
|
||||||
@ -23,39 +23,41 @@ 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:
|
||||||
* 5 requests to get the session an the request user with its permissions,
|
* 4 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all assignments,
|
* 2 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,
|
||||||
|
* 1 request to get the tags and
|
||||||
|
|
||||||
* 10 request to featch each related user again.
|
* 10 request to fetch each related user again.
|
||||||
|
|
||||||
TODO: There could be less requests to get the session and the request user.
|
TODO: The last request are a bug.
|
||||||
The eleven 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(30):
|
with self.assertNumQueries(20):
|
||||||
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:
|
||||||
* 2 requests to get the permission for anonymous (config and permissions)
|
* 3 requests to get the permission for anonymous,
|
||||||
* 2 requests to get the list of all assignments,
|
* 2 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,
|
||||||
* 1 request to get the tags,
|
* 1 request to get the tags and
|
||||||
|
|
||||||
* lots of permissions requests.
|
* 10 request to fetch each related user again.
|
||||||
|
|
||||||
TODO: The last requests are a bug.
|
TODO: The last 10 requests are an bug.
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(57):
|
with self.assertNumQueries(19):
|
||||||
self.client.get(reverse('assignment-list'))
|
self.client.get(reverse('assignment-list'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ 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
|
from openslides.utils.test import TestCase, use_cache
|
||||||
|
|
||||||
|
|
||||||
class TestProjectorDBQueries(TestCase):
|
class TestProjectorDBQueries(TestCase):
|
||||||
@ -22,29 +22,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:
|
||||||
* 5 requests to get the session an the request user with its permissions,
|
* 4 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all projectors,
|
* 2 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(8):
|
with self.assertNumQueries(7):
|
||||||
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:
|
||||||
* 2 requests to get the permission for anonymous (config and permissions)
|
* 3 requests to get the permission for anonymous,
|
||||||
* 2 requests to get the list of all projectors,
|
* 2 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
|
||||||
|
|
||||||
* 11 requests for permissions.
|
|
||||||
|
|
||||||
TODO: The last 11 requests are a bug.
|
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(16):
|
with self.assertNumQueries(6):
|
||||||
self.client.get(reverse('projector-list'))
|
self.client.get(reverse('projector-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -63,14 +61,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:
|
||||||
* 5 requests to get the session an the request user with its permissions,
|
* 4 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all chatmessages,
|
* 2 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(7):
|
with self.assertNumQueries(6):
|
||||||
self.client.get(reverse('chatmessage-list'))
|
self.client.get(reverse('chatmessage-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -88,6 +87,7 @@ 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:
|
||||||
@ -98,6 +98,7 @@ class TestTagDBQueries(TestCase):
|
|||||||
with self.assertNumQueries(4):
|
with self.assertNumQueries(4):
|
||||||
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:
|
||||||
@ -124,6 +125,7 @@ class TestConfigDBQueries(TestCase):
|
|||||||
self.client = APIClient()
|
self.client = APIClient()
|
||||||
config['general_system_enable_anonymous'] = True
|
config['general_system_enable_anonymous'] = True
|
||||||
|
|
||||||
|
@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:
|
||||||
@ -134,6 +136,7 @@ class TestConfigDBQueries(TestCase):
|
|||||||
with self.assertNumQueries(3):
|
with self.assertNumQueries(3):
|
||||||
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:
|
||||||
|
@ -5,7 +5,7 @@ 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
|
from openslides.utils.test import TestCase, use_cache
|
||||||
|
|
||||||
|
|
||||||
class TestDBQueries(TestCase):
|
class TestDBQueries(TestCase):
|
||||||
@ -26,21 +26,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:
|
||||||
* 5 requests to get the session an the request user with its permissions and
|
* 4 requests to get the session an the request user with its permissions and
|
||||||
* 2 requests to get the list of all files.
|
* 2 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(7):
|
with self.assertNumQueries(6):
|
||||||
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:
|
||||||
* 2 requests to get the permission for anonymous (config and permissions) and
|
* 3 requests to get the permission for anonymous and
|
||||||
* 2 requests to get the list of all projectors.
|
* 2 requests to get the list of all projectors.
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(4):
|
with self.assertNumQueries(5):
|
||||||
self.client.get(reverse('mediafile-list'))
|
self.client.get(reverse('mediafile-list'))
|
||||||
|
@ -9,7 +9,7 @@ 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.users.models import User
|
from openslides.users.models import User
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase, use_cache
|
||||||
|
|
||||||
|
|
||||||
class TestMotionDBQueries(TestCase):
|
class TestMotionDBQueries(TestCase):
|
||||||
@ -27,10 +27,11 @@ class TestMotionDBQueries(TestCase):
|
|||||||
Motion.objects.create(title='motion{}'.format(index))
|
Motion.objects.create(title='motion{}'.format(index))
|
||||||
# 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:
|
||||||
* 5 requests to get the session an the request user with its permissions,
|
* 4 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all motions,
|
* 2 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,
|
||||||
@ -38,16 +39,17 @@ class TestMotionDBQueries(TestCase):
|
|||||||
* 1 request to get the polls,
|
* 1 request to get the polls,
|
||||||
* 1 request to get the attachments,
|
* 1 request to get the attachments,
|
||||||
* 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 and
|
||||||
"""
|
"""
|
||||||
self.client.force_login(User.objects.get(pk=1))
|
self.client.force_login(User.objects.get(pk=1))
|
||||||
with self.assertNumQueries(15):
|
with self.assertNumQueries(14):
|
||||||
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:
|
||||||
* 2 requests to get the permission for anonymous (config and permissions)
|
* 3 requests to get the permission for anonymous,
|
||||||
* 2 requests to get the list of all motions,
|
* 2 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,
|
||||||
@ -56,12 +58,8 @@ class TestMotionDBQueries(TestCase):
|
|||||||
* 1 request to get the attachments,
|
* 1 request to get the attachments,
|
||||||
* 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
|
||||||
|
|
||||||
* 10 requests for permissions.
|
|
||||||
|
|
||||||
TODO: The last 10 requests are a bug.
|
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(22):
|
with self.assertNumQueries(13):
|
||||||
self.client.get(reverse('motion-list'))
|
self.client.get(reverse('motion-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -79,27 +77,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:
|
||||||
* 5 requests to get the session an the request user with its permissions and
|
* 4 requests to get the session an the request user with its permissions and
|
||||||
* 2 requests to get the list of all categories.
|
* 2 requests to get the list of all categories.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(User.objects.get(pk=1))
|
self.client.force_login(User.objects.get(pk=1))
|
||||||
with self.assertNumQueries(7):
|
with self.assertNumQueries(6):
|
||||||
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:
|
||||||
* 2 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
|
* 2 requests to get the list of all motions and
|
||||||
|
|
||||||
* 10 requests for permissions.
|
|
||||||
|
|
||||||
TODO: The last 10 requests are a bug.
|
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(14):
|
with self.assertNumQueries(5):
|
||||||
self.client.get(reverse('category-list'))
|
self.client.get(reverse('category-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -113,31 +109,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:
|
||||||
* 5 requests to get the session an the request user with its permissions,
|
* 4 requests to get the session an the request user with its permissions,
|
||||||
* 2 requests to get the list of all workflows,
|
* 2 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(User.objects.get(pk=1))
|
self.client.force_login(User.objects.get(pk=1))
|
||||||
with self.assertNumQueries(9):
|
with self.assertNumQueries(8):
|
||||||
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:
|
||||||
* 2 requests to get the permission for anonymous (config and permissions),
|
* 3 requests to get the permission for anonymous,
|
||||||
* 2 requests to get the list of all workflows,
|
* 2 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.
|
||||||
|
|
||||||
* 2 requests for permissions.
|
|
||||||
|
|
||||||
TODO: The last 2 requests are a bug.
|
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(8):
|
with self.assertNumQueries(7):
|
||||||
self.client.get(reverse('workflow-list'))
|
self.client.get(reverse('workflow-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -294,11 +288,12 @@ class RetrieveMotion(TestCase):
|
|||||||
self.motion.save()
|
self.motion.save()
|
||||||
self.motion.create_poll()
|
self.motion.create_poll()
|
||||||
|
|
||||||
|
@use_cache()
|
||||||
def test_number_of_queries(self):
|
def test_number_of_queries(self):
|
||||||
with self.assertNumQueries(16):
|
with self.assertNumQueries(18):
|
||||||
self.client.get(reverse('motion-detail', args=[self.motion.pk]))
|
self.client.get(reverse('motion-detail', args=[self.motion.pk]))
|
||||||
|
|
||||||
def test__guest_state_with_required_permission_to_see(self):
|
def test_guest_state_with_required_permission_to_see(self):
|
||||||
config['general_system_enable_anonymous'] = True
|
config['general_system_enable_anonymous'] = True
|
||||||
guest_client = APIClient()
|
guest_client = APIClient()
|
||||||
state = self.motion.state
|
state = self.motion.state
|
||||||
@ -323,9 +318,7 @@ class RetrieveMotion(TestCase):
|
|||||||
password='password_kau4eequaisheeBateef')
|
password='password_kau4eequaisheeBateef')
|
||||||
self.motion.submitters.add(user)
|
self.motion.submitters.add(user)
|
||||||
submitter_client = APIClient()
|
submitter_client = APIClient()
|
||||||
submitter_client.login(
|
submitter_client.force_login(user)
|
||||||
username='username_ohS2opheikaSa5theijo',
|
|
||||||
password='password_kau4eequaisheeBateef')
|
|
||||||
response = submitter_client.get(reverse('motion-detail', args=[self.motion.pk]))
|
response = submitter_client.get(reverse('motion-detail', args=[self.motion.pk]))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from openslides.utils.test import TestCase
|
|
||||||
|
|
||||||
|
|
||||||
class TestAnonymousRequests(TestCase):
|
|
||||||
"""
|
|
||||||
Test that a request with an user that is not logged in gets only the
|
|
||||||
requested data, if the anonymous user is activated in the config.
|
|
||||||
|
|
||||||
Expects that the page '/rest/users/user/' needs a permission and the
|
|
||||||
anonymous user has this permission.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@patch('openslides.users.auth.config', {'general_system_enable_anonymous': True})
|
|
||||||
def test_with_anonymous_user(self):
|
|
||||||
response = self.client.get('/rest/users/user/')
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
@patch('openslides.users.auth.config', {'general_system_enable_anonymous': False})
|
|
||||||
def test_without_anonymous_user(self):
|
|
||||||
response = self.client.get('/rest/users/user/')
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
|
@ -5,7 +5,7 @@ from rest_framework.test import APIClient
|
|||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.users.models import Group, User
|
from openslides.users.models import Group, User
|
||||||
from openslides.users.serializers import UserFullSerializer
|
from openslides.users.serializers import UserFullSerializer
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase, use_cache
|
||||||
|
|
||||||
|
|
||||||
class TestUserDBQueries(TestCase):
|
class TestUserDBQueries(TestCase):
|
||||||
@ -22,29 +22,27 @@ 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:
|
||||||
* 5 requests to get the session an the request user with its permissions,
|
* 2 requests to get the session and the request user with its permissions,
|
||||||
* 2 requests to get the list of all assignments and
|
* 2 requests to get the list of all users and
|
||||||
* 1 request to get 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))
|
||||||
with self.assertNumQueries(8):
|
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:
|
||||||
* 2 requests to get the permission for anonymous (config and permissions)
|
* 3 requests to get the permission for anonymous,
|
||||||
* 2 requests to get the list of all users,
|
* 2 requests to get the list of all users and
|
||||||
* 1 request to get all groups and
|
* 2 request to get all groups (needed by the user serializer).
|
||||||
|
|
||||||
* lots of permissions requests.
|
|
||||||
|
|
||||||
TODO: The last requests are a bug.
|
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(27):
|
with self.assertNumQueries(7):
|
||||||
self.client.get(reverse('user-list'))
|
self.client.get(reverse('user-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -62,29 +60,36 @@ 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:
|
||||||
* 2 requests to get the session an the request user with its permissions,
|
* 4 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.
|
||||||
* 1 request to get the permissions and
|
|
||||||
* 1 request to get the content_object for the permissions.
|
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.
|
||||||
"""
|
"""
|
||||||
self.client.force_login(User.objects.get(pk=1))
|
self.client.force_login(User.objects.get(pk=1))
|
||||||
with self.assertNumQueries(5):
|
with self.assertNumQueries(5):
|
||||||
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:
|
||||||
* 2 requests to find out if anonymous is enabled
|
* 2 requests to find out if anonymous is enabled
|
||||||
* 1 request to get the list of all groups,
|
* 3 request to get the list of all groups and
|
||||||
* 1 request to get the permissions and
|
|
||||||
* 1 request to get the content_object for the permissions.
|
* 14 Requests to find out if anonymous is enabled.
|
||||||
|
|
||||||
TODO: There should be only one request to find out if anonymous is enabled.
|
TODO: There should be only one request to find out if anonymous is enabled.
|
||||||
|
The reason for the last 14 requests is the base get_restricted_data()
|
||||||
|
method that is used by the group access_permissions. It calls check_permissions().
|
||||||
|
But this is important for the autoupdate-case and can only be fixt by
|
||||||
|
caching the config object.
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(5):
|
with self.assertNumQueries(19):
|
||||||
self.client.get(reverse('group-list'))
|
self.client.get(reverse('group-list'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,75 +1,16 @@
|
|||||||
from datetime import timedelta
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from channels import DEFAULT_CHANNEL_LAYER
|
from channels import DEFAULT_CHANNEL_LAYER
|
||||||
from channels.asgi import channel_layers
|
from channels.asgi import channel_layers
|
||||||
from channels.tests import ChannelTestCase
|
from channels.tests import ChannelTestCase
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from openslides.assignments.models import Assignment
|
from openslides.assignments.models import Assignment
|
||||||
from openslides.core.models import Session
|
|
||||||
from openslides.topics.models import Topic
|
from openslides.topics.models import Topic
|
||||||
from openslides.users.models import User
|
|
||||||
from openslides.utils.autoupdate import (
|
from openslides.utils.autoupdate import (
|
||||||
get_logged_in_users,
|
|
||||||
inform_changed_data,
|
inform_changed_data,
|
||||||
inform_deleted_data,
|
inform_deleted_data,
|
||||||
)
|
)
|
||||||
from openslides.utils.test import TestCase
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetLoggedInUsers(TestCase):
|
|
||||||
def test_call(self):
|
|
||||||
"""
|
|
||||||
Test to call the function with:
|
|
||||||
* A user that session has not expired
|
|
||||||
* A user that session has expired
|
|
||||||
* A user that has no session
|
|
||||||
* An anonymous user that session hot not expired
|
|
||||||
|
|
||||||
Only the user with the session that has not expired should be returned
|
|
||||||
"""
|
|
||||||
user1 = User.objects.create(username='user1')
|
|
||||||
user2 = User.objects.create(username='user2')
|
|
||||||
User.objects.create(username='user3')
|
|
||||||
|
|
||||||
# Create a session with a user, that expires in 5 hours
|
|
||||||
Session.objects.create(
|
|
||||||
user=user1,
|
|
||||||
expire_date=timezone.now() + timedelta(hours=5),
|
|
||||||
session_key='1')
|
|
||||||
|
|
||||||
# Create a session with a user, that is expired before 5 hours
|
|
||||||
Session.objects.create(
|
|
||||||
user=user2,
|
|
||||||
expire_date=timezone.now() + timedelta(hours=-5),
|
|
||||||
session_key='2')
|
|
||||||
|
|
||||||
# Create a session with an anonymous user, that expires in 5 hours
|
|
||||||
Session.objects.create(
|
|
||||||
user=None,
|
|
||||||
expire_date=timezone.now() + timedelta(hours=5),
|
|
||||||
session_key='3')
|
|
||||||
|
|
||||||
self.assertEqual(list(get_logged_in_users()), [user1])
|
|
||||||
|
|
||||||
def test_unique(self):
|
|
||||||
"""
|
|
||||||
Test the function with a user that has two not expired session.
|
|
||||||
The user should be returned only once.
|
|
||||||
"""
|
|
||||||
user1 = User.objects.create(username='user1')
|
|
||||||
Session.objects.create(
|
|
||||||
user=user1,
|
|
||||||
expire_date=timezone.now() + timedelta(hours=1),
|
|
||||||
session_key='1')
|
|
||||||
Session.objects.create(
|
|
||||||
user=user1,
|
|
||||||
expire_date=timezone.now() + timedelta(hours=2),
|
|
||||||
session_key='2')
|
|
||||||
|
|
||||||
self.assertEqual(list(get_logged_in_users()), [user1])
|
|
||||||
|
|
||||||
|
|
||||||
@patch('openslides.utils.autoupdate.transaction.on_commit', lambda func: func())
|
@patch('openslides.utils.autoupdate.transaction.on_commit', lambda func: func())
|
||||||
@ -138,7 +79,8 @@ class TestsInformChangedData(ChannelTestCase):
|
|||||||
def test_change_no_autoupdate_model(self):
|
def test_change_no_autoupdate_model(self):
|
||||||
"""
|
"""
|
||||||
Tests that if inform_changed_data() is called with a model that does
|
Tests that if inform_changed_data() is called with a model that does
|
||||||
not support autoupdate, nothing happens.
|
not support autoupdate, nothing happens. We use the django Group for
|
||||||
|
this (not the OpenSlides Group)
|
||||||
"""
|
"""
|
||||||
group = Group.objects.create(name='test_group')
|
group = Group.objects.create(name='test_group')
|
||||||
channel_layers[DEFAULT_CHANNEL_LAYER].flush()
|
channel_layers[DEFAULT_CHANNEL_LAYER].flush()
|
||||||
|
@ -1,100 +0,0 @@
|
|||||||
from unittest import TestCase
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
from openslides.users.auth import AnonymousUser, get_user
|
|
||||||
|
|
||||||
|
|
||||||
class TestAnonymousUser(TestCase):
|
|
||||||
def test_get_all_permissions_from_group_1(self):
|
|
||||||
"""
|
|
||||||
Tests, that get_all_permissions looks in the permissions of the group with
|
|
||||||
pk=1
|
|
||||||
"""
|
|
||||||
anonymous = AnonymousUser()
|
|
||||||
|
|
||||||
with patch('openslides.users.auth.Permission') as mock_permission:
|
|
||||||
anonymous.get_all_permissions()
|
|
||||||
|
|
||||||
mock_permission.objects.filter.assert_called_once_with(group__pk=1)
|
|
||||||
|
|
||||||
def test_has_perm_in_list(self):
|
|
||||||
anonymous = AnonymousUser()
|
|
||||||
anonymous.get_all_permissions = MagicMock(return_value=('p1', 'p2'))
|
|
||||||
|
|
||||||
self.assertTrue(
|
|
||||||
anonymous.has_perm('p1'),
|
|
||||||
"has_perm() should return True when the user has the permission")
|
|
||||||
|
|
||||||
def test_has_perm_not_in_list(self):
|
|
||||||
anonymous = AnonymousUser()
|
|
||||||
anonymous.get_all_permissions = MagicMock(return_value=('p1', 'p2'))
|
|
||||||
|
|
||||||
self.assertFalse(
|
|
||||||
anonymous.has_perm('p3'),
|
|
||||||
"has_perm() should return False when the user has not the permission")
|
|
||||||
|
|
||||||
def test_has_module_perms_in_list(self):
|
|
||||||
anonymous = AnonymousUser()
|
|
||||||
anonymous.get_all_permissions = MagicMock(return_value=('test_app.perm', ))
|
|
||||||
|
|
||||||
self.assertTrue(
|
|
||||||
anonymous.has_module_perms('test_app'),
|
|
||||||
"has_module_perms() should return True when the user has the "
|
|
||||||
"permission test_app.perm")
|
|
||||||
|
|
||||||
def test_has_module_perms_not_in_list(self):
|
|
||||||
anonymous = AnonymousUser()
|
|
||||||
anonymous.get_all_permissions = MagicMock(return_value=('test_otherapp.perm', ))
|
|
||||||
|
|
||||||
self.assertFalse(
|
|
||||||
anonymous.has_module_perms('test_app'),
|
|
||||||
"has_module_perms() should return False when the user does not have "
|
|
||||||
"the permission test_app.perm")
|
|
||||||
|
|
||||||
|
|
||||||
@patch('openslides.users.auth.config')
|
|
||||||
@patch('openslides.users.auth._get_user')
|
|
||||||
class TestGetUser(TestCase):
|
|
||||||
def test_not_in_cache(self, mock_get_user, mock_config):
|
|
||||||
mock_config.__getitem__.return_value = True
|
|
||||||
mock_get_user.return_value = AnonymousUser()
|
|
||||||
request = MagicMock()
|
|
||||||
del request._cached_user
|
|
||||||
|
|
||||||
user = get_user(request)
|
|
||||||
|
|
||||||
mock_get_user.assert_called_once_with(request)
|
|
||||||
self.assertEqual(user, AnonymousUser())
|
|
||||||
self.assertEqual(request._cached_user, AnonymousUser())
|
|
||||||
|
|
||||||
def test_in_cache(self, mock_get_user, mock_config):
|
|
||||||
request = MagicMock()
|
|
||||||
request._cached_user = 'my_user'
|
|
||||||
|
|
||||||
user = get_user(request)
|
|
||||||
|
|
||||||
self.assertFalse(
|
|
||||||
mock_get_user.called,
|
|
||||||
"_get_user should not have been called when the user object is in cache")
|
|
||||||
self.assertEqual(
|
|
||||||
user,
|
|
||||||
'my_user',
|
|
||||||
"The user in cache should be returned")
|
|
||||||
|
|
||||||
def test_disabled_anonymous_user(self, mock_get_user, mock_config):
|
|
||||||
mock_config.__getitem__.return_value = False
|
|
||||||
mock_get_user.return_value = 'django_anonymous_user'
|
|
||||||
request = MagicMock()
|
|
||||||
del request._cached_user
|
|
||||||
|
|
||||||
user = get_user(request)
|
|
||||||
|
|
||||||
mock_get_user.assert_called_once_with(request)
|
|
||||||
self.assertEqual(
|
|
||||||
user,
|
|
||||||
'django_anonymous_user',
|
|
||||||
"The django user should be returned")
|
|
||||||
self.assertEqual(
|
|
||||||
request._cached_user,
|
|
||||||
'django_anonymous_user',
|
|
||||||
"The django user should be cached")
|
|
53
tests/unit/utils/test_auth.py
Normal file
53
tests/unit/utils/test_auth.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from unittest import TestCase, skip
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from openslides.utils.auth import AnonymousUser, get_user
|
||||||
|
|
||||||
|
|
||||||
|
@skip # I don't know how to patch the config if it is not imported in the global space
|
||||||
|
@patch('openslides.utils.auth.config')
|
||||||
|
@patch('openslides.utils.auth._get_user')
|
||||||
|
class TestGetUser(TestCase):
|
||||||
|
def test_not_in_cache(self, mock_get_user, mock_config):
|
||||||
|
mock_config.__getitem__.return_value = True
|
||||||
|
mock_get_user.return_value = AnonymousUser()
|
||||||
|
request = MagicMock()
|
||||||
|
del request._cached_user
|
||||||
|
|
||||||
|
user = get_user(request)
|
||||||
|
|
||||||
|
mock_get_user.assert_called_once_with(request)
|
||||||
|
self.assertEqual(user, AnonymousUser())
|
||||||
|
self.assertEqual(request._cached_user, AnonymousUser())
|
||||||
|
|
||||||
|
def test_in_cache(self, mock_get_user, mock_config):
|
||||||
|
request = MagicMock()
|
||||||
|
request._cached_user = 'my_user'
|
||||||
|
|
||||||
|
user = get_user(request)
|
||||||
|
|
||||||
|
self.assertFalse(
|
||||||
|
mock_get_user.called,
|
||||||
|
"_get_user should not have been called when the user object is in cache")
|
||||||
|
self.assertEqual(
|
||||||
|
user,
|
||||||
|
'my_user',
|
||||||
|
"The user in cache should be returned")
|
||||||
|
|
||||||
|
def test_disabled_anonymous_user(self, mock_get_user, mock_config):
|
||||||
|
mock_config.__getitem__.return_value = False
|
||||||
|
mock_get_user.return_value = 'django_anonymous_user'
|
||||||
|
request = MagicMock()
|
||||||
|
del request._cached_user
|
||||||
|
|
||||||
|
user = get_user(request)
|
||||||
|
|
||||||
|
mock_get_user.assert_called_once_with(request)
|
||||||
|
self.assertEqual(
|
||||||
|
user,
|
||||||
|
'django_anonymous_user',
|
||||||
|
"The django user should be returned")
|
||||||
|
self.assertEqual(
|
||||||
|
request._cached_user,
|
||||||
|
'django_anonymous_user',
|
||||||
|
"The django user should be cached")
|
Loading…
Reference in New Issue
Block a user