Merge pull request #1458 from normanjaeckel/feature/rest-api/users
Refactoring users app.
This commit is contained in:
commit
411213b1ce
@ -2,28 +2,26 @@ import os
|
||||
|
||||
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):
|
||||
"""
|
||||
Migration command that does the same like the django migration command but
|
||||
calles also creates the default groups
|
||||
Migration command that does nearly the same as Django's migration command
|
||||
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):
|
||||
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':
|
||||
try:
|
||||
os.makedirs(settings.OPENSLIDES_USER_DATA_PATH)
|
||||
except (FileExistsError, AttributeError):
|
||||
# If the folder already exist or the settings OPENSLIDES_USER_DATA_PATH
|
||||
# is unknown, then do nothing
|
||||
# If the folder already exists or the settings
|
||||
# OPENSLIDES_USER_DATA_PATH is unknown, just do nothing.
|
||||
pass
|
||||
|
||||
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)
|
||||
|
@ -1,9 +1,15 @@
|
||||
from django import forms
|
||||
from django.dispatch import Signal
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy, ugettext_noop
|
||||
|
||||
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):
|
||||
"""
|
||||
|
@ -9,6 +9,8 @@ SITE_ROOT = os.path.realpath(os.path.dirname(__file__))
|
||||
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
|
||||
AUTHENTICATION_BACKENDS = ('openslides.users.auth.CustomizedModelBackend',)
|
||||
|
||||
LOGIN_URL = '/login/'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
|
||||
@ -76,6 +78,7 @@ ROOT_URLCONF = 'openslides.urls'
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'openslides.core',
|
||||
'openslides.users',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
@ -92,7 +95,6 @@ INSTALLED_APPS = (
|
||||
'openslides.agenda',
|
||||
'openslides.motion',
|
||||
'openslides.assignment',
|
||||
'openslides.users',
|
||||
'openslides.mediafile',
|
||||
'openslides.config',
|
||||
)
|
||||
|
@ -1,160 +1,4 @@
|
||||
from random import choice
|
||||
|
||||
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
|
||||
from .models import Permission
|
||||
|
||||
|
||||
def get_protected_perm():
|
||||
|
@ -11,11 +11,11 @@ class UsersAppConfig(AppConfig):
|
||||
from . import main_menu, widgets # noqa
|
||||
|
||||
# Import all required stuff.
|
||||
from django.db.models.signals import post_save
|
||||
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.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
|
||||
|
||||
# Load User model.
|
||||
@ -23,7 +23,9 @@ class UsersAppConfig(AppConfig):
|
||||
|
||||
# Connect signals.
|
||||
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_slide_model(User, 'participant/user_slide.html')
|
||||
|
@ -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 import get_user as _get_user
|
||||
from django.db.models import Q
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
|
||||
from openslides.config.api import config
|
||||
|
||||
from .models import Permission
|
||||
|
||||
|
||||
class AnonymousUser(DjangoAnonymousUser):
|
||||
"""
|
||||
@ -41,6 +46,34 @@ class AnonymousUser(DjangoAnonymousUser):
|
||||
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):
|
||||
"""
|
||||
Middleware to get the logged in user in users.
|
||||
|
@ -6,7 +6,6 @@ from django.utils.translation import ugettext as _
|
||||
from openslides.utils import csv_ext
|
||||
from openslides.utils.utils import html_strong
|
||||
|
||||
from .api import gen_password, gen_username
|
||||
from .models import Group, User
|
||||
|
||||
|
||||
@ -39,7 +38,7 @@ def import_users(csvfile):
|
||||
user.title = title
|
||||
user.last_name = last_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.email = email
|
||||
user.structure_level = structure_level
|
||||
@ -50,7 +49,7 @@ def import_users(csvfile):
|
||||
user.is_active = True
|
||||
else:
|
||||
user.is_active = False
|
||||
user.default_password = gen_password()
|
||||
user.default_password = User.objects.generate_password()
|
||||
user.save()
|
||||
for groupid in groups.split(','):
|
||||
try:
|
||||
|
5
openslides/users/exceptions.py
Normal file
5
openslides/users/exceptions.py
Normal file
@ -0,0 +1,5 @@
|
||||
from openslides.utils.exceptions import OpenSlidesError
|
||||
|
||||
|
||||
class UserError(OpenSlidesError):
|
||||
pass
|
@ -1,13 +1,12 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from openslides.config.api import config
|
||||
from openslides.utils.forms import (CssClassMixin,
|
||||
LocalizedModelMultipleChoiceField)
|
||||
|
||||
from .models import Group, User
|
||||
from .models import Group, Permission, User
|
||||
from .api import get_protected_perm
|
||||
|
||||
|
||||
|
0
openslides/users/management/__init__.py
Normal file
0
openslides/users/management/__init__.py
Normal file
0
openslides/users/management/commands/__init__.py
Normal file
0
openslides/users/management/commands/__init__.py
Normal file
@ -1,15 +1,15 @@
|
||||
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):
|
||||
"""
|
||||
Commands to create or reset the adminuser
|
||||
Command to create or reset the admin user.
|
||||
"""
|
||||
|
||||
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.')
|
||||
else:
|
||||
self.stdout.write('Admin user successfully reset.')
|
@ -1,10 +1,13 @@
|
||||
# TODO: Check every app, that they do not import Group or User from here.
|
||||
|
||||
from django.contrib.auth.models import (PermissionsMixin, AbstractBaseUser,
|
||||
BaseUserManager)
|
||||
from random import choice
|
||||
|
||||
# TODO: Do not import the Group in here, but in core.models (if necessary)
|
||||
from django.contrib.auth.models import Group # noqa
|
||||
from django.contrib.auth.hashers import make_password
|
||||
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.db import models
|
||||
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.rest_api import RESTModelMixin
|
||||
|
||||
from .exceptions import UserError
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
"""
|
||||
UserManager that creates new users only with a password and a username.
|
||||
"""
|
||||
|
||||
def create_user(self, username, password, **kwargs):
|
||||
user = self.model(username=username, **kwargs)
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
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):
|
||||
"""
|
||||
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'
|
||||
slide_callback_name = 'user'
|
||||
|
||||
|
@ -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):
|
||||
@ -45,6 +49,75 @@ class UserFullSerializer(ModelSerializer):
|
||||
'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):
|
||||
"""
|
||||
A custom field to use for the permission relationship.
|
||||
|
@ -1,9 +1,12 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy, ugettext_noop
|
||||
|
||||
from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable
|
||||
|
||||
from .models import Group, Permission, User
|
||||
|
||||
|
||||
def setup_users_config(sender, **kwargs):
|
||||
"""
|
||||
@ -99,16 +102,90 @@ def setup_users_config(sender, **kwargs):
|
||||
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
|
||||
connected to the signal django.db.models.signals.post_save during app
|
||||
loading.
|
||||
Creates the builtin groups: Anonymous, Registered, Delegates and Staff.
|
||||
|
||||
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
|
||||
|
||||
from openslides.users.api import get_registered_group # TODO: Test, if global import is possible
|
||||
registered = get_registered_group()
|
||||
instance.groups.add(registered)
|
||||
instance.save()
|
||||
# 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
|
||||
User.objects.create_or_reset_admin_user()
|
||||
|
@ -14,13 +14,13 @@ from openslides.utils.views import (
|
||||
UpdateView, LoginMixin)
|
||||
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 .forms import (GroupForm, UserCreateForm, UserMultipleCreateForm,
|
||||
UsersettingsForm, UserUpdateForm)
|
||||
from .models import Group, User
|
||||
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):
|
||||
@ -69,25 +69,14 @@ class UserCreateView(CreateView):
|
||||
url_name_args = []
|
||||
|
||||
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'])
|
||||
|
||||
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)
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -108,8 +97,8 @@ class UserMultipleCreateView(FormView):
|
||||
names_list = line.split()
|
||||
first_name = ' '.join(names_list[:-1])
|
||||
last_name = names_list[-1]
|
||||
username = gen_username(first_name, last_name)
|
||||
default_password = gen_password()
|
||||
username = User.objects.generate_username(first_name, last_name)
|
||||
default_password = User.objects.generate_password()
|
||||
User.objects.create(
|
||||
username=username,
|
||||
first_name=first_name,
|
||||
@ -136,17 +125,6 @@ class UserUpdateView(UpdateView):
|
||||
form_kwargs.update({'request': self.request})
|
||||
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):
|
||||
"""
|
||||
@ -281,9 +259,12 @@ class UserViewSet(ModelViewSet):
|
||||
|
||||
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
|
||||
else:
|
||||
serializer_class = UserShortSerializer
|
||||
|
@ -9,7 +9,8 @@ from rest_framework.serializers import ( # noqa
|
||||
ModelSerializer,
|
||||
PrimaryKeyRelatedField,
|
||||
RelatedField,
|
||||
SerializerMethodField)
|
||||
SerializerMethodField,
|
||||
ValidationError)
|
||||
from rest_framework.response import Response # noqa
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.viewsets import ModelViewSet, ViewSet # noqa
|
||||
|
0
tests/integration/users/__init__.py
Normal file
0
tests/integration/users/__init__.py
Normal file
89
tests/integration/users/test_viewset.py
Normal file
89
tests/integration/users/test_viewset.py
Normal 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())
|
@ -246,7 +246,7 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase):
|
||||
self.assertMessage(response, 'You were successfully added to the list of speakers.')
|
||||
|
||||
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/')
|
||||
self.assertMessage(response, 'You were successfully added to the list of speakers.')
|
||||
|
||||
|
@ -208,7 +208,7 @@ class ViewTest(TestCase):
|
||||
orga_perm = Permission.objects.get(
|
||||
content_type=ContentType.objects.get_for_model(Item),
|
||||
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
|
||||
user = User.objects.get(username=user.username)
|
||||
# Test view without permission
|
||||
|
@ -1,172 +1,7 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch, call, MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from openslides.users.api import (
|
||||
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'")
|
||||
from openslides.users.api import get_protected_perm
|
||||
|
||||
|
||||
@patch('openslides.users.api.Permission')
|
||||
|
@ -1,5 +1,5 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from openslides.users.models import User, UserManager
|
||||
|
||||
@ -11,12 +11,12 @@ class UserTest(TestCase):
|
||||
User.get_full_name().
|
||||
"""
|
||||
user = User()
|
||||
user.get_full_name = MagicMock(return_value='Test Value')
|
||||
user.get_full_name = MagicMock(return_value='Test Value IJee1yoet1ooGhesh5li')
|
||||
|
||||
self.assertEqual(
|
||||
str(user),
|
||||
'Test Value',
|
||||
"The str representation of User is not user.get_full_name()")
|
||||
'Test Value IJee1yoet1ooGhesh5li',
|
||||
"The str representation of User is not user.get_full_name().")
|
||||
|
||||
def test_get_slide_context(self):
|
||||
"""
|
||||
@ -29,7 +29,7 @@ class UserTest(TestCase):
|
||||
self.assertEqual(
|
||||
user.get_slide_context(),
|
||||
{'shown_user': user},
|
||||
"User.get_slide_context returns a wrong context")
|
||||
"User.get_slide_context returns a wrong context.")
|
||||
|
||||
|
||||
class UserGetAbsoluteUrlTest(TestCase):
|
||||
@ -37,7 +37,7 @@ class UserGetAbsoluteUrlTest(TestCase):
|
||||
"""
|
||||
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)
|
||||
|
||||
@ -48,12 +48,12 @@ class UserGetAbsoluteUrlTest(TestCase):
|
||||
self.assertEqual(
|
||||
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'])
|
||||
|
||||
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)
|
||||
|
||||
@ -64,12 +64,12 @@ class UserGetAbsoluteUrlTest(TestCase):
|
||||
self.assertEqual(
|
||||
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'])
|
||||
|
||||
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)
|
||||
|
||||
@ -80,12 +80,12 @@ class UserGetAbsoluteUrlTest(TestCase):
|
||||
self.assertEqual(
|
||||
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'])
|
||||
|
||||
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)
|
||||
|
||||
@ -96,12 +96,12 @@ class UserGetAbsoluteUrlTest(TestCase):
|
||||
self.assertEqual(
|
||||
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'])
|
||||
|
||||
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)
|
||||
dummy_argument = MagicMock()
|
||||
@ -113,14 +113,14 @@ class UserGetAbsoluteUrlTest(TestCase):
|
||||
self.assertEqual(
|
||||
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)
|
||||
|
||||
|
||||
class UserGetFullName(TestCase):
|
||||
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.title = 'test_title'
|
||||
@ -130,11 +130,12 @@ class UserGetFullName(TestCase):
|
||||
self.assertEqual(
|
||||
user.get_full_name(),
|
||||
'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):
|
||||
"""
|
||||
Tests, that get_full_name returns the write string.
|
||||
Tests that get_full_name returns the write string.
|
||||
"""
|
||||
user = User()
|
||||
user.title = 'test_title'
|
||||
@ -144,11 +145,12 @@ class UserGetFullName(TestCase):
|
||||
self.assertEqual(
|
||||
user.get_full_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):
|
||||
"""
|
||||
Tests, that get_full_name returns the write string.
|
||||
Tests that get_full_name returns the write string.
|
||||
"""
|
||||
user = User()
|
||||
user.title = ''
|
||||
@ -158,7 +160,8 @@ class UserGetFullName(TestCase):
|
||||
self.assertEqual(
|
||||
user.get_full_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):
|
||||
@ -177,7 +180,7 @@ class UserGetShortName(TestCase):
|
||||
short_name,
|
||||
'test_first_name',
|
||||
"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):
|
||||
"""
|
||||
@ -195,7 +198,7 @@ class UserGetShortName(TestCase):
|
||||
short_name,
|
||||
'test_first_name test_last_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):
|
||||
"""
|
||||
@ -212,7 +215,7 @@ class UserGetShortName(TestCase):
|
||||
short_name,
|
||||
'test_first_name',
|
||||
"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):
|
||||
"""
|
||||
@ -230,7 +233,7 @@ class UserGetShortName(TestCase):
|
||||
short_name,
|
||||
'test_last_name, test_first_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):
|
||||
"""
|
||||
@ -246,7 +249,7 @@ class UserGetShortName(TestCase):
|
||||
short_name,
|
||||
'test_username',
|
||||
"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):
|
||||
"""
|
||||
@ -264,7 +267,7 @@ class UserGetShortName(TestCase):
|
||||
self.assertEqual(
|
||||
short_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):
|
||||
@ -294,7 +297,7 @@ class UserResetPassword(TestCase):
|
||||
class UserManagerTest(TestCase):
|
||||
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_manager = UserManager()
|
||||
@ -309,4 +312,175 @@ class UserManagerTest(TestCase):
|
||||
self.assertEqual(
|
||||
return_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'.")
|
||||
|
83
tests/unit/users/test_serializers.py
Normal file
83
tests/unit/users/test_serializers.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user