Merge pull request #1458 from normanjaeckel/feature/rest-api/users

Refactoring users app.
This commit is contained in:
Oskar Hahn 2015-02-14 02:35:56 +01:00
commit 411213b1ce
24 changed files with 695 additions and 428 deletions

View File

@ -2,28 +2,26 @@ import os
from django.core.management.commands.migrate import Command as _Command from django.core.management.commands.migrate import Command as _Command
from openslides.users.api import create_builtin_groups_and_admin from ...signals import post_permission_creation
class Command(_Command): class Command(_Command):
""" """
Migration command that does the same like the django migration command but Migration command that does nearly the same as Django's migration command
calles also creates the default groups but also calls the post_permission_creation signal.
""" """
# TODO: Try to get rid of this code. The problem are the ContentType
# and Permission objects, which are created in the post_migrate signal, but
# we need to things later.
def handle(self, *args, **options): def handle(self, *args, **options):
from django.conf import settings from django.conf import settings
# Creates the folder for a sqlite database if necessary # Creates the folder for a SQLite3 database if necessary.
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3': if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3':
try: try:
os.makedirs(settings.OPENSLIDES_USER_DATA_PATH) os.makedirs(settings.OPENSLIDES_USER_DATA_PATH)
except (FileExistsError, AttributeError): except (FileExistsError, AttributeError):
# If the folder already exist or the settings OPENSLIDES_USER_DATA_PATH # If the folder already exists or the settings
# is unknown, then do nothing # OPENSLIDES_USER_DATA_PATH is unknown, just do nothing.
pass pass
super().handle(*args, **options) super().handle(*args, **options)
create_builtin_groups_and_admin()
# Send this signal after sending post_migrate (inside super()) so that
# all Permission objects are created previously.
post_permission_creation.send(self)

View File

@ -1,9 +1,15 @@
from django import forms from django import forms
from django.dispatch import Signal
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable
# This signal is sent when the migrate command is done. That means it is sent
# after post_migrate sending and creating all Permission objects. Don't use it
# for other things than dealing with Permission objects.
post_permission_creation = Signal()
def setup_general_config(sender, **kwargs): def setup_general_config(sender, **kwargs):
""" """

View File

@ -9,6 +9,8 @@ SITE_ROOT = os.path.realpath(os.path.dirname(__file__))
AUTH_USER_MODEL = 'users.User' AUTH_USER_MODEL = 'users.User'
AUTHENTICATION_BACKENDS = ('openslides.users.auth.CustomizedModelBackend',)
LOGIN_URL = '/login/' LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
@ -76,6 +78,7 @@ ROOT_URLCONF = 'openslides.urls'
INSTALLED_APPS = ( INSTALLED_APPS = (
'openslides.core', 'openslides.core',
'openslides.users',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
@ -92,7 +95,6 @@ INSTALLED_APPS = (
'openslides.agenda', 'openslides.agenda',
'openslides.motion', 'openslides.motion',
'openslides.assignment', 'openslides.assignment',
'openslides.users',
'openslides.mediafile', 'openslides.mediafile',
'openslides.config', 'openslides.config',
) )

View File

@ -1,160 +1,4 @@
from random import choice from .models import Permission
from django.contrib.auth.models import Permission, Group
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_noop
from .models import User
def gen_password():
"""
Generates a random passwort.
"""
chars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
size = 8
return ''.join([choice(chars) for i in range(size)])
def gen_username(first_name, last_name):
"""
Generates a username from a first- and lastname.
"""
first_name = first_name.strip()
last_name = last_name.strip()
if first_name and last_name:
base_name = " ".join((first_name, last_name))
else:
base_name = first_name or last_name
if not base_name:
raise ValueError('Either \'first_name\' or \'last_name\' can not be '
'empty')
if not User.objects.filter(username=base_name).exists():
return base_name
counter = 0
while True:
counter += 1
test_name = "%s %d" % (base_name, counter)
if not User.objects.filter(username=test_name).exists():
return test_name
def get_registered_group():
"""
Returns the group 'Registered' (pk=2).
"""
return Group.objects.get(pk=2)
def create_builtin_groups_and_admin():
"""
Creates the builtin groups: Anonymous, Registered, Delegates and Staff.
Creates the builtin user: admin.
"""
# Check whether the group pks 1 to 4 are free
if Group.objects.filter(pk__in=range(1, 5)).exists():
# Do completely nothing if there are already some of our groups in the database.
return
# Anonymous (pk 1) and Registered (pk 2)
ct_core = ContentType.objects.get(app_label='core', model='customslide')
perm_11 = Permission.objects.get(content_type=ct_core, codename='can_see_projector')
perm_12 = Permission.objects.get(content_type=ct_core, codename='can_see_dashboard')
ct_agenda = ContentType.objects.get(app_label='agenda', model='item')
ct_speaker = ContentType.objects.get(app_label='agenda', model='speaker')
perm_13 = Permission.objects.get(content_type=ct_agenda, codename='can_see_agenda')
perm_14 = Permission.objects.get(content_type=ct_agenda, codename='can_see_orga_items')
can_speak = Permission.objects.get(content_type=ct_speaker, codename='can_be_speaker')
ct_motion = ContentType.objects.get(app_label='motion', model='motion')
perm_15 = Permission.objects.get(content_type=ct_motion, codename='can_see_motion')
ct_assignment = ContentType.objects.get(app_label='assignment', model='assignment')
perm_16 = Permission.objects.get(content_type=ct_assignment, codename='can_see_assignments')
ct_users = ContentType.objects.get(app_label='users', model='user')
perm_users_can_see_name = Permission.objects.get(content_type=ct_users, codename='can_see_name')
perm_users_can_see_extra_data = Permission.objects.get(content_type=ct_users, codename='can_see_extra_data')
ct_mediafile = ContentType.objects.get(app_label='mediafile', model='mediafile')
perm_18 = Permission.objects.get(content_type=ct_mediafile, codename='can_see')
base_permission_list = (
perm_11,
perm_12,
perm_13,
perm_14,
perm_15,
perm_16,
perm_users_can_see_name,
perm_users_can_see_extra_data,
perm_18)
group_anonymous = Group.objects.create(name=ugettext_noop('Anonymous'), pk=1)
group_anonymous.permissions.add(*base_permission_list)
group_registered = Group.objects.create(name=ugettext_noop('Registered'), pk=2)
group_registered.permissions.add(can_speak, *base_permission_list)
# Delegates (pk 3)
perm_31 = Permission.objects.get(content_type=ct_motion, codename='can_create_motion')
perm_32 = Permission.objects.get(content_type=ct_motion, codename='can_support_motion')
perm_33 = Permission.objects.get(content_type=ct_assignment, codename='can_nominate_other')
perm_34 = Permission.objects.get(content_type=ct_assignment, codename='can_nominate_self')
perm_35 = Permission.objects.get(content_type=ct_mediafile, codename='can_upload')
group_delegates = Group.objects.create(name=ugettext_noop('Delegates'), pk=3)
group_delegates.permissions.add(perm_31, perm_32, perm_33, perm_34, perm_35)
# Staff (pk 4)
perm_41 = Permission.objects.get(content_type=ct_agenda, codename='can_manage_agenda')
perm_42 = Permission.objects.get(content_type=ct_motion, codename='can_manage_motion')
perm_43 = Permission.objects.get(content_type=ct_assignment, codename='can_manage_assignments')
perm_44 = Permission.objects.get(content_type=ct_users, codename='can_manage')
perm_45 = Permission.objects.get(content_type=ct_core, codename='can_manage_projector')
perm_46 = Permission.objects.get(content_type=ct_core, codename='can_use_chat')
perm_47 = Permission.objects.get(content_type=ct_mediafile, codename='can_manage')
ct_config = ContentType.objects.get(app_label='config', model='configstore')
perm_48 = Permission.objects.get(content_type=ct_config, codename='can_manage')
ct_tag = ContentType.objects.get(app_label='core', model='tag')
can_manage_tags = Permission.objects.get(content_type=ct_tag, codename='can_manage_tags')
group_staff = Group.objects.create(name=ugettext_noop('Staff'), pk=4)
# add delegate permissions (without can_support_motion)
group_staff.permissions.add(perm_31, perm_33, perm_34, perm_35)
# add staff permissions
group_staff.permissions.add(perm_41, perm_42, perm_43, perm_44, perm_45, perm_46, perm_47, perm_48, can_manage_tags)
# add can_see_name and can_see_extra_data permissions
# TODO: Remove this redundancy after cleanup of the permission system.
group_staff.permissions.add(perm_users_can_see_name, perm_users_can_see_extra_data)
# Admin user
create_or_reset_admin_user()
def create_or_reset_admin_user():
group_staff = Group.objects.get(pk=4)
try:
admin = User.objects.get(username="admin")
except User.DoesNotExist:
admin = User()
admin.username = 'admin'
admin.last_name = 'Administrator'
created = True
else:
created = False
admin.default_password = 'admin'
admin.set_password(admin.default_password)
admin.save()
admin.groups.add(group_staff)
return created
def get_protected_perm(): def get_protected_perm():

View File

@ -11,11 +11,11 @@ class UsersAppConfig(AppConfig):
from . import main_menu, widgets # noqa from . import main_menu, widgets # noqa
# Import all required stuff. # Import all required stuff.
from django.db.models.signals import post_save
from openslides.config.signals import config_signal from openslides.config.signals import config_signal
from openslides.core.signals import post_permission_creation
from openslides.projector.api import register_slide_model from openslides.projector.api import register_slide_model
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
from .signals import setup_users_config, user_post_save from .signals import create_builtin_groups_and_admin, setup_users_config
from .views import GroupViewSet, UserViewSet from .views import GroupViewSet, UserViewSet
# Load User model. # Load User model.
@ -23,7 +23,9 @@ class UsersAppConfig(AppConfig):
# Connect signals. # Connect signals.
config_signal.connect(setup_users_config, dispatch_uid='setup_users_config') config_signal.connect(setup_users_config, dispatch_uid='setup_users_config')
post_save.connect(user_post_save, sender=User, dispatch_uid='users_user_post_save') post_permission_creation.connect(
create_builtin_groups_and_admin,
dispatch_uid='create_builtin_groups_and_admin')
# Register slides. # Register slides.
register_slide_model(User, 'participant/user_slide.html') register_slide_model(User, 'participant/user_slide.html')

View File

@ -1,10 +1,15 @@
from django.contrib.auth.models import AnonymousUser as DjangoAnonymousUser, Permission from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import AnonymousUser as DjangoAnonymousUser
from django.contrib.auth.context_processors import auth as _auth from django.contrib.auth.context_processors import auth as _auth
from django.contrib.auth import get_user as _get_user from django.contrib.auth import get_user as _get_user
from django.db.models import Q
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from openslides.config.api import config from openslides.config.api import config
from .models import Permission
class AnonymousUser(DjangoAnonymousUser): class AnonymousUser(DjangoAnonymousUser):
""" """
@ -41,6 +46,34 @@ class AnonymousUser(DjangoAnonymousUser):
return False return False
class CustomizedModelBackend(ModelBackend):
"""
Customized backend for authentication. Ensures that registered users have
all permission of the group 'Registered' (pk=2).
"""
def get_group_permissions(self, user_obj, obj=None):
"""
Returns a set of permission strings that this user has through his/her
groups.
"""
# TODO: Refactor this after Django 1.8 release. Add also anonymous
# permission check to this backend.
if user_obj.is_anonymous() or obj is not None:
return set()
if not hasattr(user_obj, '_group_perm_cache'):
if user_obj.is_superuser:
perms = Permission.objects.all()
else:
user_groups_field = get_user_model()._meta.get_field('groups')
user_groups_query = 'group__%s' % user_groups_field.related_query_name()
# The next two lines are the customization.
query = Q(**{user_groups_query: user_obj}) | Q(group__pk=2)
perms = Permission.objects.filter(query)
perms = perms.values_list('content_type__app_label', 'codename').order_by()
user_obj._group_perm_cache = set("%s.%s" % (ct, name) for ct, name in perms)
return user_obj._group_perm_cache
class AuthenticationMiddleware(object): class AuthenticationMiddleware(object):
""" """
Middleware to get the logged in user in users. Middleware to get the logged in user in users.

View File

@ -6,7 +6,6 @@ from django.utils.translation import ugettext as _
from openslides.utils import csv_ext from openslides.utils import csv_ext
from openslides.utils.utils import html_strong from openslides.utils.utils import html_strong
from .api import gen_password, gen_username
from .models import Group, User from .models import Group, User
@ -39,7 +38,7 @@ def import_users(csvfile):
user.title = title user.title = title
user.last_name = last_name user.last_name = last_name
user.first_name = first_name user.first_name = first_name
user.username = gen_username(first_name, last_name) user.username = User.objects.generate_username(first_name, last_name)
user.gender = gender user.gender = gender
user.email = email user.email = email
user.structure_level = structure_level user.structure_level = structure_level
@ -50,7 +49,7 @@ def import_users(csvfile):
user.is_active = True user.is_active = True
else: else:
user.is_active = False user.is_active = False
user.default_password = gen_password() user.default_password = User.objects.generate_password()
user.save() user.save()
for groupid in groups.split(','): for groupid in groups.split(','):
try: try:

View File

@ -0,0 +1,5 @@
from openslides.utils.exceptions import OpenSlidesError
class UserError(OpenSlidesError):
pass

View File

@ -1,13 +1,12 @@
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Permission
from django.utils.translation import ugettext as _, ugettext_lazy from django.utils.translation import ugettext as _, ugettext_lazy
from openslides.config.api import config from openslides.config.api import config
from openslides.utils.forms import (CssClassMixin, from openslides.utils.forms import (CssClassMixin,
LocalizedModelMultipleChoiceField) LocalizedModelMultipleChoiceField)
from .models import Group, User from .models import Group, Permission, User
from .api import get_protected_perm from .api import get_protected_perm

View File

View File

@ -1,15 +1,15 @@
from django.core.management.base import NoArgsCommand from django.core.management.base import NoArgsCommand
from openslides.users.api import create_or_reset_admin_user from openslides.users.models import User
class Command(NoArgsCommand): class Command(NoArgsCommand):
""" """
Commands to create or reset the adminuser Command to create or reset the admin user.
""" """
def handle_noargs(self, **options): def handle_noargs(self, **options):
if create_or_reset_admin_user(): created = User.objects.create_or_reset_admin_user()
if created:
self.stdout.write('Admin user successfully created.') self.stdout.write('Admin user successfully created.')
else: else:
self.stdout.write('Admin user successfully reset.') self.stdout.write('Admin user successfully reset.')

View File

@ -1,10 +1,13 @@
# TODO: Check every app, that they do not import Group or User from here. # TODO: Check every app, that they do not import Group or User from here.
from django.contrib.auth.models import (PermissionsMixin, AbstractBaseUser, from random import choice
BaseUserManager)
# TODO: Do not import the Group in here, but in core.models (if necessary) from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import Group # noqa from django.contrib.auth.models import (
AbstractBaseUser,
BaseUserManager,
PermissionsMixin)
from django.contrib.auth.models import Group, Permission # noqa
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy, ugettext_noop
@ -14,20 +17,83 @@ from openslides.projector.models import SlideMixin
from openslides.utils.models import AbsoluteUrlMixin from openslides.utils.models import AbsoluteUrlMixin
from openslides.utils.rest_api import RESTModelMixin from openslides.utils.rest_api import RESTModelMixin
from .exceptions import UserError
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
""" """
UserManager that creates new users only with a password and a username. UserManager that creates new users only with a password and a username.
""" """
def create_user(self, username, password, **kwargs): def create_user(self, username, password, **kwargs):
user = self.model(username=username, **kwargs) user = self.model(username=username, **kwargs)
user.set_password(password) user.set_password(password)
user.save(using=self._db) user.save(using=self._db)
return user return user
def create_or_reset_admin_user(self):
"""
Creates an user with the username admin. If such a user exists, resets
it. The password is (re)set to 'admin'. The user becomes member of the
group 'Staff' (pk=4).
"""
try:
staff = Group.objects.get(pk=4)
except Group.DoesNotExist:
raise UserError("Admin user can not be created or reset because "
"the group 'Staff' is not available.")
admin, created = self.get_or_create(
username='admin',
defaults={'last_name': 'Administrator'})
admin.default_password = 'admin'
admin.password = make_password(admin.default_password, '', 'md5')
admin.save()
admin.groups.add(staff)
return created
def generate_username(self, first_name, last_name):
"""
Generates a username from first name and last name.
"""
first_name = first_name.strip()
last_name = last_name.strip()
if first_name and last_name:
base_name = ' '.join((first_name, last_name))
else:
base_name = first_name or last_name
if not base_name:
raise ValueError("Either 'first_name' or 'last_name' must not be "
"empty")
if not self.filter(username=base_name).exists():
generated_username = base_name
else:
counter = 0
while True:
counter += 1
test_name = '%s %d' % (base_name, counter)
if not self.filter(username=test_name).exists():
generated_username = test_name
break
return generated_username
def generate_password(self):
"""
Generates a random passwort.
"""
chars = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'
size = 8
return ''.join([choice(chars) for i in range(size)])
class User(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, PermissionsMixin, AbstractBaseUser): class User(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, PermissionsMixin, AbstractBaseUser):
"""
Model for users in OpenSlides. A client can login as a user with
credentials. A user can also just be used as representation for a person
in other OpenSlides app like motion submitter or (assignment) election
candidates.
"""
USERNAME_FIELD = 'username' USERNAME_FIELD = 'username'
slide_callback_name = 'user' slide_callback_name = 'user'

View File

@ -1,6 +1,10 @@
from openslides.utils.rest_api import ModelSerializer, RelatedField from django.core.exceptions import ImproperlyConfigured
from django.contrib.auth.hashers import make_password
from django.utils.translation import ugettext as _, ugettext_lazy
from .models import Group, User # TODO: Don't import Group from models but from core.models. from openslides.utils.rest_api import ModelSerializer, PrimaryKeyRelatedField, RelatedField, ValidationError
from .models import Group, User
class UserShortSerializer(ModelSerializer): class UserShortSerializer(ModelSerializer):
@ -45,6 +49,75 @@ class UserFullSerializer(ModelSerializer):
'is_active',) 'is_active',)
class UserCreateUpdateSerializer(ModelSerializer):
"""
Serializer for users.models.User objects.
Serializes data to create new users or update users.
Do not use this for list or retrieve requests.
"""
groups = PrimaryKeyRelatedField(
many=True,
queryset=Group.objects.exclude(pk__in=(1, 2)),
help_text=ugettext_lazy('The groups this user belongs to. A user will '
'get all permissions granted to each of '
'his/her groups.'))
class Meta:
model = User
fields = (
'is_present',
'username',
'title',
'first_name',
'last_name',
'structure_level',
'about_me',
'comment',
'groups',
'default_password',
'is_active',)
def __init__(self, *args, **kwargs):
"""
Overridden to add read_only flag to username field in create requests.
"""
super().__init__(*args, **kwargs)
if self.context['view'].action == 'create':
self.fields['username'].read_only = True
elif self.context['view'].action == 'update':
# Everything is fine. Do nothing.
pass
else: # Other action than 'create' or 'update'.
raise ImproperlyConfigured('This serializer can only be used in create and update requests.')
def validate(self, data):
"""
Checks that first_name or last_name is given.
"""
if not (data.get('username') or data.get('first_name') or data.get('last_name')):
raise ValidationError(_('Username, first name and last name can not all be empty.'))
return data
def create(self, validated_data):
"""
Creates user with generated username and sets the default_password.
Adds the new user to the registered group.
"""
# Generate username if neccessary.
if not validated_data.get('username'):
validated_data['username'] = User.objects.generate_username(
validated_data.get('first_name', ''),
validated_data.get('last_name', ''))
# Prepare setup password.
if not validated_data.get('default_password'):
validated_data['default_password'] = User.objects.generate_password()
validated_data['password'] = make_password(validated_data['default_password'], '', 'md5')
# Perform creation in the database and return new user.
return super().create(validated_data)
class PermissionRelatedField(RelatedField): class PermissionRelatedField(RelatedField):
""" """
A custom field to use for the permission relationship. A custom field to use for the permission relationship.

View File

@ -1,9 +1,12 @@
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable
from .models import Group, Permission, User
def setup_users_config(sender, **kwargs): def setup_users_config(sender, **kwargs):
""" """
@ -99,16 +102,90 @@ def setup_users_config(sender, **kwargs):
groups=(group_general, group_pdf)) groups=(group_general, group_pdf))
def user_post_save(sender, instance, *args, **kwargs): def create_builtin_groups_and_admin(**kwargs):
""" """
Receiver function to add a new user to the registered group. It is Creates the builtin groups: Anonymous, Registered, Delegates and Staff.
connected to the signal django.db.models.signals.post_save during app
loading. Creates the builtin user: admin.
""" """
if not kwargs['created']: # Check whether the group pks 1 to 4 are free
if Group.objects.filter(pk__in=range(1, 5)).exists():
# Do completely nothing if there are already some of our groups in the database.
return return
from openslides.users.api import get_registered_group # TODO: Test, if global import is possible # Anonymous (pk 1) and Registered (pk 2)
registered = get_registered_group() ct_core = ContentType.objects.get(app_label='core', model='customslide')
instance.groups.add(registered) perm_11 = Permission.objects.get(content_type=ct_core, codename='can_see_projector')
instance.save() perm_12 = Permission.objects.get(content_type=ct_core, codename='can_see_dashboard')
ct_agenda = ContentType.objects.get(app_label='agenda', model='item')
ct_speaker = ContentType.objects.get(app_label='agenda', model='speaker')
perm_13 = Permission.objects.get(content_type=ct_agenda, codename='can_see_agenda')
perm_14 = Permission.objects.get(content_type=ct_agenda, codename='can_see_orga_items')
can_speak = Permission.objects.get(content_type=ct_speaker, codename='can_be_speaker')
ct_motion = ContentType.objects.get(app_label='motion', model='motion')
perm_15 = Permission.objects.get(content_type=ct_motion, codename='can_see_motion')
ct_assignment = ContentType.objects.get(app_label='assignment', model='assignment')
perm_16 = Permission.objects.get(content_type=ct_assignment, codename='can_see_assignments')
ct_users = ContentType.objects.get(app_label='users', model='user')
perm_users_can_see_name = Permission.objects.get(content_type=ct_users, codename='can_see_name')
perm_users_can_see_extra_data = Permission.objects.get(content_type=ct_users, codename='can_see_extra_data')
ct_mediafile = ContentType.objects.get(app_label='mediafile', model='mediafile')
perm_18 = Permission.objects.get(content_type=ct_mediafile, codename='can_see')
base_permission_list = (
perm_11,
perm_12,
perm_13,
perm_14,
perm_15,
perm_16,
perm_users_can_see_name,
perm_users_can_see_extra_data,
perm_18)
group_anonymous = Group.objects.create(name=ugettext_noop('Anonymous'), pk=1)
group_anonymous.permissions.add(*base_permission_list)
group_registered = Group.objects.create(name=ugettext_noop('Registered'), pk=2)
group_registered.permissions.add(can_speak, *base_permission_list)
# Delegates (pk 3)
perm_31 = Permission.objects.get(content_type=ct_motion, codename='can_create_motion')
perm_32 = Permission.objects.get(content_type=ct_motion, codename='can_support_motion')
perm_33 = Permission.objects.get(content_type=ct_assignment, codename='can_nominate_other')
perm_34 = Permission.objects.get(content_type=ct_assignment, codename='can_nominate_self')
perm_35 = Permission.objects.get(content_type=ct_mediafile, codename='can_upload')
group_delegates = Group.objects.create(name=ugettext_noop('Delegates'), pk=3)
group_delegates.permissions.add(perm_31, perm_32, perm_33, perm_34, perm_35)
# Staff (pk 4)
perm_41 = Permission.objects.get(content_type=ct_agenda, codename='can_manage_agenda')
perm_42 = Permission.objects.get(content_type=ct_motion, codename='can_manage_motion')
perm_43 = Permission.objects.get(content_type=ct_assignment, codename='can_manage_assignments')
perm_44 = Permission.objects.get(content_type=ct_users, codename='can_manage')
perm_45 = Permission.objects.get(content_type=ct_core, codename='can_manage_projector')
perm_46 = Permission.objects.get(content_type=ct_core, codename='can_use_chat')
perm_47 = Permission.objects.get(content_type=ct_mediafile, codename='can_manage')
ct_config = ContentType.objects.get(app_label='config', model='configstore')
perm_48 = Permission.objects.get(content_type=ct_config, codename='can_manage')
ct_tag = ContentType.objects.get(app_label='core', model='tag')
can_manage_tags = Permission.objects.get(content_type=ct_tag, codename='can_manage_tags')
group_staff = Group.objects.create(name=ugettext_noop('Staff'), pk=4)
# add delegate permissions (without can_support_motion)
group_staff.permissions.add(perm_31, perm_33, perm_34, perm_35)
# add staff permissions
group_staff.permissions.add(perm_41, perm_42, perm_43, perm_44, perm_45, perm_46, perm_47, perm_48, can_manage_tags)
# add can_see_name and can_see_extra_data permissions
# TODO: Remove this redundancy after cleanup of the permission system.
group_staff.permissions.add(perm_users_can_see_name, perm_users_can_see_extra_data)
# Admin user
User.objects.create_or_reset_admin_user()

View File

@ -14,13 +14,13 @@ from openslides.utils.views import (
UpdateView, LoginMixin) UpdateView, LoginMixin)
from openslides.utils.exceptions import OpenSlidesError from openslides.utils.exceptions import OpenSlidesError
from .api import gen_password, gen_username, get_protected_perm from .api import get_protected_perm
from .csv_import import import_users from .csv_import import import_users
from .forms import (GroupForm, UserCreateForm, UserMultipleCreateForm, from .forms import (GroupForm, UserCreateForm, UserMultipleCreateForm,
UsersettingsForm, UserUpdateForm) UsersettingsForm, UserUpdateForm)
from .models import Group, User from .models import Group, User
from .pdf import users_to_pdf, users_passwords_to_pdf from .pdf import users_to_pdf, users_passwords_to_pdf
from .serializers import GroupSerializer, UserFullSerializer, UserShortSerializer from .serializers import GroupSerializer, UserCreateUpdateSerializer, UserFullSerializer, UserShortSerializer
class UserListView(ListView): class UserListView(ListView):
@ -69,25 +69,14 @@ class UserCreateView(CreateView):
url_name_args = [] url_name_args = []
def manipulate_object(self, form): def manipulate_object(self, form):
self.object.username = gen_username( self.object.username = User.objects.generate_username(
form.cleaned_data['first_name'], form.cleaned_data['last_name']) form.cleaned_data['first_name'], form.cleaned_data['last_name'])
if not self.object.default_password: if not self.object.default_password:
self.object.default_password = gen_password() self.object.default_password = User.objects.generate_password()
self.object.set_password(self.object.default_password) self.object.set_password(self.object.default_password)
def post_save(self, form):
super(UserCreateView, self).post_save(form)
# TODO: find a better solution that makes the following lines obsolete
# Background: motion.models.use_post_save adds already the registerd group
# to new user but super(..).post_save(form) removes it and sets only the
# groups selected in the form (without 'registered')
# workaround: add registered group again manually
from openslides.users.api import get_registered_group # TODO: Test, if global import is possible
registered = get_registered_group()
self.object.groups.add(registered)
class UserMultipleCreateView(FormView): class UserMultipleCreateView(FormView):
""" """
@ -108,8 +97,8 @@ class UserMultipleCreateView(FormView):
names_list = line.split() names_list = line.split()
first_name = ' '.join(names_list[:-1]) first_name = ' '.join(names_list[:-1])
last_name = names_list[-1] last_name = names_list[-1]
username = gen_username(first_name, last_name) username = User.objects.generate_username(first_name, last_name)
default_password = gen_password() default_password = User.objects.generate_password()
User.objects.create( User.objects.create(
username=username, username=username,
first_name=first_name, first_name=first_name,
@ -136,17 +125,6 @@ class UserUpdateView(UpdateView):
form_kwargs.update({'request': self.request}) form_kwargs.update({'request': self.request})
return form_kwargs return form_kwargs
def post_save(self, form):
super(UserUpdateView, self).post_save(form)
# TODO: Find a better solution that makes the following lines obsolete
# Background: motion.models.use_post_save adds already the registerd group
# to new user but super(..).post_save(form) removes it and sets only the
# groups selected in the form (without 'registered')
# workaround: add registered group again manually
from openslides.users.api import get_registered_group # TODO: Test, if global import is possible
registered = get_registered_group()
self.object.groups.add(registered)
class UserDeleteView(DeleteView): class UserDeleteView(DeleteView):
""" """
@ -281,9 +259,12 @@ class UserViewSet(ModelViewSet):
def get_serializer_class(self): def get_serializer_class(self):
""" """
Returns different serializer classes with respect to users permissions. Returns different serializer classes with respect to action and user's
permissions.
""" """
if self.request.user.has_perm('users.can_see_extra_data'): if self.action in ('create', 'update'):
serializer_class = UserCreateUpdateSerializer
elif self.request.user.has_perm('users.can_see_extra_data'):
serializer_class = UserFullSerializer serializer_class = UserFullSerializer
else: else:
serializer_class = UserShortSerializer serializer_class = UserShortSerializer

View File

@ -9,7 +9,8 @@ from rest_framework.serializers import ( # noqa
ModelSerializer, ModelSerializer,
PrimaryKeyRelatedField, PrimaryKeyRelatedField,
RelatedField, RelatedField,
SerializerMethodField) SerializerMethodField,
ValidationError)
from rest_framework.response import Response # noqa from rest_framework.response import Response # noqa
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from rest_framework.viewsets import ModelViewSet, ViewSet # noqa from rest_framework.viewsets import ModelViewSet, ViewSet # noqa

View File

View File

@ -0,0 +1,89 @@
from django.core.urlresolvers import reverse
from rest_framework import status
from rest_framework.test import APIClient
from openslides.users.models import User
from openslides.utils.test import TestCase
class UserCreation(TestCase):
"""
Tests creation of users via REST API.
"""
def test_simple_creation(self):
self.client.login(username='admin', password='admin')
response = self.client.post(
reverse('user-list'),
{'last_name': 'Test name keimeiShieX4Aekoe3do'})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(User.objects.filter(username='Test name keimeiShieX4Aekoe3do').exists())
def test_creation_with_group(self):
self.client.login(username='admin', password='admin')
self.client.post(
reverse('user-list'),
{'last_name': 'Test name aedah1iequoof0Ashed4',
'groups': ['3', '4']})
user = User.objects.get(username='Test name aedah1iequoof0Ashed4')
self.assertTrue(user.groups.filter(pk=3).exists())
self.assertTrue(user.groups.filter(pk=4).exists())
def test_creation_with_anonymous_or_registered_group(self):
self.client.login(username='admin', password='admin')
response = self.client.post(
reverse('user-list'),
{'last_name': 'Test name aedah1iequoof0Ashed4',
'groups': ['1', '2']})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'groups': ["Invalid pk '1' - object does not exist."]})
class UserUpdate(TestCase):
"""
Tests update of users via REST API.
"""
def test_simple_update_via_patch(self):
admin_client = APIClient()
admin_client.login(username='admin', password='admin')
response = admin_client.patch(
reverse('user-detail', args=['1']),
{'last_name': 'New name tu3ooh5Iez5Aec2laefo'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
user = User.objects.get(pk=1)
self.assertEqual(user.last_name, 'New name tu3ooh5Iez5Aec2laefo')
self.assertEqual(user.username, 'admin')
def test_simple_update_via_put(self):
admin_client = APIClient()
admin_client.login(username='admin', password='admin')
response = admin_client.put(
reverse('user-detail', args=['1']),
{'last_name': 'New name Ohy4eeyei5Sahzah0Os2'})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'username': ['This field is required.']})
class UserDelete(TestCase):
"""
Tests delete of users via REST API.
"""
def test_delete(self):
admin_client = APIClient()
admin_client.login(username='admin', password='admin')
User.objects.create(username='Test name bo3zieT3iefahng0ahqu')
self.assertTrue(User.objects.filter(username='Test name bo3zieT3iefahng0ahqu').exists())
response = admin_client.delete(reverse('user-detail', args=['2']))
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(User.objects.filter(username='Test name bo3zieT3iefahng0ahqu').exists())

View File

@ -246,7 +246,7 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase):
self.assertMessage(response, 'You were successfully added to the list of speakers.') self.assertMessage(response, 'You were successfully added to the list of speakers.')
perm = Permission.objects.filter(name='Can see agenda').get() perm = Permission.objects.filter(name='Can see agenda').get()
self.speaker2.groups.get(name='Registered').permissions.remove(perm) self.speaker2.groups.model.objects.get(name='Registered').permissions.remove(perm)
response = self.speaker2_client.get('/agenda/list_of_speakers/add/') response = self.speaker2_client.get('/agenda/list_of_speakers/add/')
self.assertMessage(response, 'You were successfully added to the list of speakers.') self.assertMessage(response, 'You were successfully added to the list of speakers.')

View File

@ -208,7 +208,7 @@ class ViewTest(TestCase):
orga_perm = Permission.objects.get( orga_perm = Permission.objects.get(
content_type=ContentType.objects.get_for_model(Item), content_type=ContentType.objects.get_for_model(Item),
codename='can_see_orga_items') codename='can_see_orga_items')
user.groups.get(name='Registered').permissions.remove(orga_perm) user.groups.model.objects.get(name='Registered').permissions.remove(orga_perm)
# Reload user # Reload user
user = User.objects.get(username=user.username) user = User.objects.get(username=user.username)
# Test view without permission # Test view without permission

View File

@ -1,172 +1,7 @@
from unittest import TestCase from unittest import TestCase
from unittest.mock import patch, call, MagicMock from unittest.mock import patch
from openslides.users.api import ( from openslides.users.api import get_protected_perm
gen_username,
gen_password,
get_registered_group,
create_or_reset_admin_user,
get_protected_perm)
@patch('openslides.users.api.User')
class UserGenUsername(TestCase):
"""
Tests for the function gen_username.
"""
def test_clear_strings(self, mock_user):
mock_user.objects.filter().exists.return_value = False
self.assertEqual(
gen_username('foo', 'bar'),
'foo bar')
def test_unstripped_strings(self, mock_user):
mock_user.objects.filter().exists.return_value = False
self.assertEqual(
gen_username('foo ', ' bar\n'),
'foo bar',
"The retuned value should only have one whitespace between the names")
def test_empty_second_string(self, mock_user):
mock_user.objects.filter().exists.return_value = False
self.assertEqual(
gen_username('foobar', ''),
'foobar',
"The returned value should not have whitespaces at the end")
def test_empty_first_string(self, mock_user):
mock_user.objects.filter().exists.return_value = False
self.assertEqual(
gen_username('', 'foobar'),
'foobar',
"The returned value should not have whitespaces at the beginning")
def test_two_empty_strings(self, mock_user):
mock_user.objects.filter().exists.return_value = False
with self.assertRaises(ValueError,
msg="A ValueError should be raised"):
gen_username('', '')
def test_used_username(self, mock_user):
mock_user.objects.filter().exists.side_effect = (True, False)
self.assertEqual(
gen_username('user', 'name'),
'user name 1',
"If the username already exist, a number should be added to the name")
def test_two_used_username(self, mock_user):
mock_user.objects.filter().exists.side_effect = (True, True, False)
self.assertEqual(
gen_username('user', 'name'),
'user name 2',
"If the username with a number already exist, a higher number should "
"be added to the name")
def test_umlauts(self, mock_user):
mock_user.objects.filter().exists.return_value = False
self.assertEqual(
gen_username('äöü', 'ßüäö'),
'äöü ßüäö',
"gen_username has also to work with umlauts")
@patch('openslides.users.api.choice')
class GenPassword(TestCase):
def test_normal(self, mock_choice):
"""
Test normal run of the function
"""
mock_choice.side_effect = tuple('test_password')
self.assertEqual(
gen_password(),
'test_pas')
# choice has to be called 8 times
mock_choice.assert_has_calls(
[call("abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789")
for _ in range(8)])
@patch('openslides.users.api.Group')
class GetRegisteredGroup(TestCase):
def test_normal(self, mock_group):
mock_group.objects.get.return_value = 'test_group'
self.assertEqual(
get_registered_group(),
'test_group')
mock_group.objects.getassert_called_once_with(pk=2)
@patch('openslides.users.api.Group')
@patch('openslides.users.api.User')
class CreateOrResetAdminUser(TestCase):
def test_get_admin_group(self, mock_user, mock_group):
"""
Tests, that the Group with pk4 is added to the admin
"""
admin_user = MagicMock(name='admin_user')
mock_user.objects.get.return_value = admin_user
mock_group.objects.get.return_value = 'admin_group'
create_or_reset_admin_user()
mock_group.objects.get.assert_called_once_with(pk=4)
admin_user.groups.add.assert_called_once_with('admin_group')
def test_password_set_to_admin(self, mock_user, mock_group):
"""
Tests, that the password of the admin is set to 'admin'.
"""
admin_user = MagicMock(name='admin_user')
mock_user.objects.get.return_value = admin_user
create_or_reset_admin_user()
self.assertEqual(
admin_user.default_password,
'admin')
admin_user.set_password.assert_called_once_with('admin')
admin_user.save.assert_called_once_with()
def test_return_value(self, mock_user, mock_group):
"""
Test, that the function retruns True, when a user is created.
"""
mock_user.DoesNotExist = Exception
mock_user.objects.get.side_effect = Exception
self.assertEqual(
create_or_reset_admin_user(),
True,
"create_or_reset_admin_user should return True when a new user is "
" created")
def test_attributes_of_created_user(self, mock_user, mock_group):
admin_user = MagicMock(name='admin_user')
mock_user.return_value = admin_user
mock_user.DoesNotExist = Exception
mock_user.objects.get.side_effect = Exception
create_or_reset_admin_user()
self.assertEqual(
admin_user.username,
'admin',
"The username of a new created admin should be 'admin'")
self.assertEqual(
admin_user.last_name,
'Administrator',
"The last_name of a new created admin should be 'Administrator'")
@patch('openslides.users.api.Permission') @patch('openslides.users.api.Permission')

View File

@ -1,5 +1,5 @@
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, call, patch
from openslides.users.models import User, UserManager from openslides.users.models import User, UserManager
@ -11,12 +11,12 @@ class UserTest(TestCase):
User.get_full_name(). User.get_full_name().
""" """
user = User() user = User()
user.get_full_name = MagicMock(return_value='Test Value') user.get_full_name = MagicMock(return_value='Test Value IJee1yoet1ooGhesh5li')
self.assertEqual( self.assertEqual(
str(user), str(user),
'Test Value', 'Test Value IJee1yoet1ooGhesh5li',
"The str representation of User is not user.get_full_name()") "The str representation of User is not user.get_full_name().")
def test_get_slide_context(self): def test_get_slide_context(self):
""" """
@ -29,7 +29,7 @@ class UserTest(TestCase):
self.assertEqual( self.assertEqual(
user.get_slide_context(), user.get_slide_context(),
{'shown_user': user}, {'shown_user': user},
"User.get_slide_context returns a wrong context") "User.get_slide_context returns a wrong context.")
class UserGetAbsoluteUrlTest(TestCase): class UserGetAbsoluteUrlTest(TestCase):
@ -37,7 +37,7 @@ class UserGetAbsoluteUrlTest(TestCase):
""" """
Tests get_absolute_url() with no argument. Tests get_absolute_url() with no argument.
It should return the url for the url-pattern of user_detail It should return the url for the url-pattern of user_detail.
""" """
user = User(pk=5) user = User(pk=5)
@ -48,12 +48,12 @@ class UserGetAbsoluteUrlTest(TestCase):
self.assertEqual( self.assertEqual(
url, url,
'test url', 'test url',
"User.get_absolute_url() does not return the result of reverse") "User.get_absolute_url() does not return the result of reverse.")
mock_reverse.assert_called_once_with('user_detail', args=['5']) mock_reverse.assert_called_once_with('user_detail', args=['5'])
def test_get_absolute_url_detail(self): def test_get_absolute_url_detail(self):
""" """
Tests get_absolute_url() with 'detail' as argument Tests get_absolute_url() with 'detail' as argument.
""" """
user = User(pk=5) user = User(pk=5)
@ -64,12 +64,12 @@ class UserGetAbsoluteUrlTest(TestCase):
self.assertEqual( self.assertEqual(
url, url,
'test url', 'test url',
"User.get_absolute_url('detail') does not return the result of reverse") "User.get_absolute_url('detail') does not return the result of reverse.")
mock_reverse.assert_called_once_with('user_detail', args=['5']) mock_reverse.assert_called_once_with('user_detail', args=['5'])
def test_get_absolute_url_update(self): def test_get_absolute_url_update(self):
""" """
Tests get_absolute_url() with 'update' as argument Tests get_absolute_url() with 'update' as argument.
""" """
user = User(pk=5) user = User(pk=5)
@ -80,12 +80,12 @@ class UserGetAbsoluteUrlTest(TestCase):
self.assertEqual( self.assertEqual(
url, url,
'test url', 'test url',
"User.get_absolute_url('update') does not return the result of reverse") "User.get_absolute_url('update') does not return the result of reverse.")
mock_reverse.assert_called_once_with('user_update', args=['5']) mock_reverse.assert_called_once_with('user_update', args=['5'])
def test_get_absolute_url_delete(self): def test_get_absolute_url_delete(self):
""" """
Tests get_absolute_url() with 'delete' as argument Tests get_absolute_url() with 'delete' as argument.
""" """
user = User(pk=5) user = User(pk=5)
@ -96,12 +96,12 @@ class UserGetAbsoluteUrlTest(TestCase):
self.assertEqual( self.assertEqual(
url, url,
'test url', 'test url',
"User.get_absolute_url('delete') does not return the result of reverse") "User.get_absolute_url('delete') does not return the result of reverse.")
mock_reverse.assert_called_once_with('user_delete', args=['5']) mock_reverse.assert_called_once_with('user_delete', args=['5'])
def test_get_absolute_url_other(self): def test_get_absolute_url_other(self):
""" """
Tests get_absolute_url() with any other argument Tests get_absolute_url() with any other argument.
""" """
user = User(pk=5) user = User(pk=5)
dummy_argument = MagicMock() dummy_argument = MagicMock()
@ -113,14 +113,14 @@ class UserGetAbsoluteUrlTest(TestCase):
self.assertEqual( self.assertEqual(
url, url,
'test url', 'test url',
"User.get_absolute_url(OTHER) does not return the result of reverse") "User.get_absolute_url(OTHER) does not return the result of reverse.")
mock_super().get_absolute_url.assert_called_once_with(dummy_argument) mock_super().get_absolute_url.assert_called_once_with(dummy_argument)
class UserGetFullName(TestCase): class UserGetFullName(TestCase):
def test_get_full_name_with_structure_level_and_title(self): def test_get_full_name_with_structure_level_and_title(self):
""" """
Tests, that get_full_name returns the write string. Tests that get_full_name returns the write string.
""" """
user = User() user = User()
user.title = 'test_title' user.title = 'test_title'
@ -130,11 +130,12 @@ class UserGetFullName(TestCase):
self.assertEqual( self.assertEqual(
user.get_full_name(), user.get_full_name(),
'test_title test_short_name (test_structure_level)', 'test_title test_short_name (test_structure_level)',
"User.get_full_name() returns wrong string when it has a structure_level and title") "User.get_full_name() returns wrong string when it has a "
"structure_level and title.")
def test_get_full_name_without_structure_level_and_with_title(self): def test_get_full_name_without_structure_level_and_with_title(self):
""" """
Tests, that get_full_name returns the write string. Tests that get_full_name returns the write string.
""" """
user = User() user = User()
user.title = 'test_title' user.title = 'test_title'
@ -144,11 +145,12 @@ class UserGetFullName(TestCase):
self.assertEqual( self.assertEqual(
user.get_full_name(), user.get_full_name(),
'test_title test_short_name', 'test_title test_short_name',
"User.get_full_name() returns wrong string when it has no structure_level but a title") "User.get_full_name() returns wrong string when it has no "
"structure_level but a title.")
def test_get_full_name_without_structure_level_and_without_title(self): def test_get_full_name_without_structure_level_and_without_title(self):
""" """
Tests, that get_full_name returns the write string. Tests that get_full_name returns the write string.
""" """
user = User() user = User()
user.title = '' user.title = ''
@ -158,7 +160,8 @@ class UserGetFullName(TestCase):
self.assertEqual( self.assertEqual(
user.get_full_name(), user.get_full_name(),
'test_short_name', 'test_short_name',
"User.get_full_name() returns wrong string when it has no structure_level and no title") "User.get_full_name() returns wrong string when it has no "
"structure_level and no title.")
class UserGetShortName(TestCase): class UserGetShortName(TestCase):
@ -177,7 +180,7 @@ class UserGetShortName(TestCase):
short_name, short_name,
'test_first_name', 'test_first_name',
"User.get_short_name() returns wrong string when it has only a " "User.get_short_name() returns wrong string when it has only a "
"first_name and is sorted by first_name") "first_name and is sorted by first_name.")
def test_get_short_name_sort_first_name_both_names(self): def test_get_short_name_sort_first_name_both_names(self):
""" """
@ -195,7 +198,7 @@ class UserGetShortName(TestCase):
short_name, short_name,
'test_first_name test_last_name', 'test_first_name test_last_name',
"User.get_short_name() returns wrong string when it has a fist_name " "User.get_short_name() returns wrong string when it has a fist_name "
"and a last_name and is sorted by first_name") "and a last_name and is sorted by first_name.")
def test_get_short_name_sort_last_name_only_first_name(self): def test_get_short_name_sort_last_name_only_first_name(self):
""" """
@ -212,7 +215,7 @@ class UserGetShortName(TestCase):
short_name, short_name,
'test_first_name', 'test_first_name',
"User.get_short_name() returns wrong string when it has only a " "User.get_short_name() returns wrong string when it has only a "
"first_name and is sorted by last_name") "first_name and is sorted by last_name.")
def test_get_short_name_sort_last_name_both_names(self): def test_get_short_name_sort_last_name_both_names(self):
""" """
@ -230,7 +233,7 @@ class UserGetShortName(TestCase):
short_name, short_name,
'test_last_name, test_first_name', 'test_last_name, test_first_name',
"User.get_short_name() returns wrong string when it has a fist_name " "User.get_short_name() returns wrong string when it has a fist_name "
"and a last_name and is sorted by last_name") "and a last_name and is sorted by last_name.")
def test_get_short_name_no_names(self): def test_get_short_name_no_names(self):
""" """
@ -246,7 +249,7 @@ class UserGetShortName(TestCase):
short_name, short_name,
'test_username', 'test_username',
"User.get_short_name() returns wrong string when it has no fist_name " "User.get_short_name() returns wrong string when it has no fist_name "
"and no last_name and is sorted by last_name") "and no last_name and is sorted by last_name.")
def test_while_spaces_in_name_parts(self): def test_while_spaces_in_name_parts(self):
""" """
@ -264,7 +267,7 @@ class UserGetShortName(TestCase):
self.assertEqual( self.assertEqual(
short_name, short_name,
'test_first_name test_last_name', 'test_first_name test_last_name',
"User.get_short_name() has to strip whitespaces from the name parts") "User.get_short_name() has to strip whitespaces from the name parts.")
class UserResetPassword(TestCase): class UserResetPassword(TestCase):
@ -294,7 +297,7 @@ class UserResetPassword(TestCase):
class UserManagerTest(TestCase): class UserManagerTest(TestCase):
def test_create_user(self): def test_create_user(self):
""" """
Tests, that create_user saves a new user with a set password. Tests that create_user saves a new user with a set password.
""" """
user = MagicMock() user = MagicMock()
user_manager = UserManager() user_manager = UserManager()
@ -309,4 +312,175 @@ class UserManagerTest(TestCase):
self.assertEqual( self.assertEqual(
return_user, return_user,
user, user,
"The returned user is not the created user") "The returned user is not the created user.")
class UserManagerGenerateUsername(TestCase):
"""
Tests for the manager method generate_username.
"""
def setUp(self):
self.exists_mock = MagicMock()
self.filter_mock = MagicMock(return_value=self.exists_mock)
self.manager = UserManager()
self.manager.filter = self.filter_mock
def test_clear_strings(self):
self.exists_mock.exists.return_value = False
self.assertEqual(
self.manager.generate_username('wiaf9eecu9mooJiZ3Lah', 'ieHaVe9ci7mooPhe0AuY'),
'wiaf9eecu9mooJiZ3Lah ieHaVe9ci7mooPhe0AuY')
def test_unstripped_strings(self):
self.exists_mock.exists.return_value = False
self.assertEqual(
self.manager.generate_username('ouYeuwai0pheukeeShah ', ' Waefa8gahj8ohRaeroca\n'),
'ouYeuwai0pheukeeShah Waefa8gahj8ohRaeroca',
"The returned value should only have one whitespace between the "
"names.")
def test_empty_second_string(self):
self.exists_mock.exists.return_value = False
self.assertEqual(
self.manager.generate_username('foobar', ''),
'foobar',
"The returned value should not have whitespaces at the end.")
def test_empty_first_string(self):
self.exists_mock.exists.return_value = False
self.assertEqual(
self.manager.generate_username('', 'foobar'),
'foobar',
"The returned value should not have whitespaces at the beginning.")
def test_two_empty_strings(self):
self.exists_mock.exists.return_value = False
with self.assertRaises(ValueError,
msg="A ValueError should be raised."):
self.manager.generate_username('', '')
def test_used_username(self):
self.exists_mock.exists.side_effect = (True, False)
self.assertEqual(
self.manager.generate_username('user', 'name'),
'user name 1',
"If the username already exists, a number should be added to the "
"name.")
def test_two_used_username(self):
self.exists_mock.exists.side_effect = (True, True, False)
self.assertEqual(
self.manager.generate_username('user', 'name'),
'user name 2',
"If the username with a number already exists, a higher number "
"should be added to the name.")
def test_umlauts(self):
self.exists_mock.exists.return_value = False
self.assertEqual(
self.manager.generate_username('äöü', 'ßüäö'),
'äöü ßüäö',
"The method gen_username has also to work with umlauts.")
@patch('openslides.users.models.choice')
class UserManagerGeneratePassword(TestCase):
def test_normal(self, mock_choice):
"""
Test normal run of the method.
"""
mock_choice.side_effect = tuple('test_password')
self.assertEqual(
UserManager().generate_password(),
'test_pas')
# choice has to be called 8 times
mock_choice.assert_has_calls(
[call("abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789")
for _ in range(8)])
@patch('openslides.users.models.Group')
class UserManagerCreateOrResetAdminUser(TestCase):
def test_get_admin_group(self, mock_group):
"""
Tests that the Group with pk=4 is added to the admin.
"""
def mock_side_effect(pk):
if pk == 2:
result = 'mock_registered'
elif pk == 4:
result = 'mock_staff'
else:
result = ''
return result
admin_user = MagicMock()
manager = UserManager()
manager.get_or_create = MagicMock(return_value=(admin_user, False))
mock_group.objects.get.side_effect = mock_side_effect
manager.create_or_reset_admin_user()
mock_group.objects.get.assert_called_once(pk=2)
admin_user.groups.add.assert_called_once('mock_staff')
def test_password_set_to_admin(self, mock_group):
"""
Tests that the password of the admin is set to 'admin'.
"""
admin_user = MagicMock()
manager = UserManager()
manager.get_or_create = MagicMock(return_value=(admin_user, False))
manager.create_or_reset_admin_user()
self.assertEqual(
admin_user.default_password,
'admin')
admin_user.save.assert_called_once_with()
@patch('openslides.users.models.User')
def test_return_value(self, mock_user, mock_group):
"""
Tests that the method returns True when a user is created.
"""
admin_user = MagicMock()
manager = UserManager()
manager.get_or_create = MagicMock(return_value=(admin_user, True))
manager.model = mock_user
self.assertEqual(
manager.create_or_reset_admin_user(),
True,
"The method create_or_reset_admin_user should return True when a "
"new user is created.")
@patch('openslides.users.models.User')
def test_attributes_of_created_user(self, mock_user, mock_group):
"""
Tests username and last_name of the created admin user.
"""
admin_user = MagicMock(username='admin', last_name='Administrator')
manager = UserManager()
manager.get_or_create = MagicMock(return_value=(admin_user, True))
manager.model = mock_user
manager.create_or_reset_admin_user()
self.assertEqual(
admin_user.username,
'admin',
"The username of a new created admin should be 'admin'.")
self.assertEqual(
admin_user.last_name,
'Administrator',
"The last_name of a new created admin should be 'Administrator'.")

View File

@ -0,0 +1,83 @@
from unittest import TestCase
from unittest.mock import MagicMock, patch
from django.core.exceptions import ImproperlyConfigured
from rest_framework import status
from rest_framework.viewsets import ModelViewSet
from rest_framework.test import APIRequestFactory
class UserCreateUpdateSerializer(TestCase):
def test_improperly_configured_exception_list_request(self):
"""
Tests that ImproperlyConfigured is raised if one tries to use the
UserCreateUpdateSerializer with a list request.
"""
# Global import is not possible for some unknown magic.
from openslides.users.serializers import UserCreateUpdateSerializer
factory = APIRequestFactory()
request = factory.get('/')
view_class = ModelViewSet
view_class.queryset = MagicMock()
view_class.serializer_class = UserCreateUpdateSerializer
view = view_class.as_view({'get': 'list'})
with self.assertRaises(ImproperlyConfigured):
view(request)
@patch('rest_framework.generics.get_object_or_404')
def test_improperly_configured_exception_retrieve_request(self, mock_get_object_or_404):
"""
Tests that ImproperlyConfigured is raised if one tries to use the
UserCreateUpdateSerializer with a retrieve request.
"""
# Global import is not possible for some unknown magic.
from openslides.users.serializers import UserCreateUpdateSerializer
factory = APIRequestFactory()
request = factory.get('/')
view_class = ModelViewSet
view_class.queryset = MagicMock()
view_class.serializer_class = UserCreateUpdateSerializer
view = view_class.as_view({'get': 'retrieve'})
mock_get_object_or_404.return_value = MagicMock()
with self.assertRaises(ImproperlyConfigured):
view(request, pk='1')
def test_no_improperly_configured_exception_create_request(self):
"""
Tests that ImproperlyConfigured is not raised if one tries to use the
UserCreateUpdateSerializer with a create request.
"""
# Global import is not possible for some unknown magic.
from openslides.users.serializers import UserCreateUpdateSerializer
factory = APIRequestFactory()
request = factory.get('/')
view_class = ModelViewSet
view_class.queryset = MagicMock()
view_class.serializer_class = UserCreateUpdateSerializer
view = view_class.as_view({'get': 'create'})
response = view(request)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
@patch('rest_framework.generics.get_object_or_404')
def test_no_improperly_configured_exception_update_request(self, mock_get_object_or_404):
"""
Tests that ImproperlyConfigured is not raised if one tries to use the
UserCreateUpdateSerializer with a update request.
"""
# Global import is not possible for some unknown magic.
from openslides.users.serializers import UserCreateUpdateSerializer
factory = APIRequestFactory()
request = factory.get('/')
view_class = ModelViewSet
view_class.queryset = MagicMock()
view_class.serializer_class = UserCreateUpdateSerializer
view = view_class.as_view({'get': 'update'})
mock_get_object_or_404.return_value = MagicMock()
response = view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)