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:
Oskar Hahn 2016-12-17 09:30:20 +01:00
parent fb24ca0aba
commit 728576d514
36 changed files with 694 additions and 448 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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:

View File

@ -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):

View File

@ -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):
""" """

View 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',
),
]

View File

@ -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

View File

@ -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

View File

@ -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'):

View File

@ -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',
) )
} }

View File

@ -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

View File

@ -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):
""" """

View File

@ -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):
""" """

View File

@ -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

View 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()),
],
),
]

View File

@ -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 = ()

View File

@ -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))

View File

@ -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.

View File

@ -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

View File

@ -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
View 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()

View File

@ -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")

View File

@ -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

View File

@ -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()

View File

@ -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'))

View File

@ -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'))

View File

@ -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:

View File

@ -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'))

View File

@ -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)

View File

@ -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)

View File

@ -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'))

View File

@ -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()

View File

@ -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")

View 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")