From 3a19218bd5d20ace79364b5c66238838b75ed034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Thu, 12 Feb 2015 20:57:05 +0100 Subject: [PATCH] Refactored parts of users app. Refactored user creation and update via REST API. Used new serializer. Cleaned up management commands, signals and imports. Moved code from 'api.py' to 'models.py'. Changed usage of group 'Registered'. Now the users don't have to be members to gain its permissions. Used customized auth backend for this. Added and changed some tests. --- .../core/management/commands/migrate.py | 22 +- openslides/core/signals.py | 6 + openslides/global_settings.py | 4 +- openslides/users/api.py | 158 +----------- openslides/users/apps.py | 8 +- openslides/users/auth.py | 35 ++- openslides/users/csv_import.py | 5 +- openslides/users/exceptions.py | 5 + openslides/users/forms.py | 3 +- openslides/users/management/__init__.py | 0 .../users/management/commands/__init__.py | 0 .../management/commands/createsuperuser.py | 8 +- openslides/users/models.py | 76 +++++- openslides/users/serializers.py | 77 +++++- openslides/users/signals.py | 95 ++++++- openslides/users/views.py | 41 +--- openslides/utils/rest_api.py | 3 +- tests/integration/users/__init__.py | 0 tests/integration/users/test_viewset.py | 89 +++++++ tests/old/agenda/test_list_of_speakers.py | 2 +- tests/old/agenda/tests.py | 2 +- tests/unit/users/test_api.py | 169 +------------ tests/unit/users/test_models.py | 232 +++++++++++++++--- tests/unit/users/test_serializers.py | 83 +++++++ 24 files changed, 695 insertions(+), 428 deletions(-) create mode 100644 openslides/users/exceptions.py create mode 100644 openslides/users/management/__init__.py create mode 100644 openslides/users/management/commands/__init__.py rename openslides/{core => users}/management/commands/createsuperuser.py (63%) create mode 100644 tests/integration/users/__init__.py create mode 100644 tests/integration/users/test_viewset.py create mode 100644 tests/unit/users/test_serializers.py diff --git a/openslides/core/management/commands/migrate.py b/openslides/core/management/commands/migrate.py index 9219b5d51..d6766ab47 100644 --- a/openslides/core/management/commands/migrate.py +++ b/openslides/core/management/commands/migrate.py @@ -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) diff --git a/openslides/core/signals.py b/openslides/core/signals.py index 54407e902..f549468a3 100644 --- a/openslides/core/signals.py +++ b/openslides/core/signals.py @@ -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): """ diff --git a/openslides/global_settings.py b/openslides/global_settings.py index 3f708a3f7..77c3d1798 100644 --- a/openslides/global_settings.py +++ b/openslides/global_settings.py @@ -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', ) diff --git a/openslides/users/api.py b/openslides/users/api.py index 8d29151ca..2fe0d3106 100644 --- a/openslides/users/api.py +++ b/openslides/users/api.py @@ -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(): diff --git a/openslides/users/apps.py b/openslides/users/apps.py index 10d4a2ec9..20d20d2bd 100644 --- a/openslides/users/apps.py +++ b/openslides/users/apps.py @@ -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') diff --git a/openslides/users/auth.py b/openslides/users/auth.py index 00b94c8dc..2dc039d43 100644 --- a/openslides/users/auth.py +++ b/openslides/users/auth.py @@ -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. diff --git a/openslides/users/csv_import.py b/openslides/users/csv_import.py index 1bdd417c4..e9055fbdb 100644 --- a/openslides/users/csv_import.py +++ b/openslides/users/csv_import.py @@ -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: diff --git a/openslides/users/exceptions.py b/openslides/users/exceptions.py new file mode 100644 index 000000000..6f0d8ca80 --- /dev/null +++ b/openslides/users/exceptions.py @@ -0,0 +1,5 @@ +from openslides.utils.exceptions import OpenSlidesError + + +class UserError(OpenSlidesError): + pass diff --git a/openslides/users/forms.py b/openslides/users/forms.py index fa504830e..967f94969 100644 --- a/openslides/users/forms.py +++ b/openslides/users/forms.py @@ -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 diff --git a/openslides/users/management/__init__.py b/openslides/users/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openslides/users/management/commands/__init__.py b/openslides/users/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openslides/core/management/commands/createsuperuser.py b/openslides/users/management/commands/createsuperuser.py similarity index 63% rename from openslides/core/management/commands/createsuperuser.py rename to openslides/users/management/commands/createsuperuser.py index b65082189..79b43d847 100644 --- a/openslides/core/management/commands/createsuperuser.py +++ b/openslides/users/management/commands/createsuperuser.py @@ -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.') diff --git a/openslides/users/models.py b/openslides/users/models.py index fd4c01c81..265d1548f 100644 --- a/openslides/users/models.py +++ b/openslides/users/models.py @@ -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' diff --git a/openslides/users/serializers.py b/openslides/users/serializers.py index 0e8e12983..1e9f3e173 100644 --- a/openslides/users/serializers.py +++ b/openslides/users/serializers.py @@ -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. diff --git a/openslides/users/signals.py b/openslides/users/signals.py index 722ea542e..c61ff61b9 100644 --- a/openslides/users/signals.py +++ b/openslides/users/signals.py @@ -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() diff --git a/openslides/users/views.py b/openslides/users/views.py index b20090a32..e9d82fd01 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -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 diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index a01657a4d..e5663da14 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -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 diff --git a/tests/integration/users/__init__.py b/tests/integration/users/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/users/test_viewset.py b/tests/integration/users/test_viewset.py new file mode 100644 index 000000000..ef1537653 --- /dev/null +++ b/tests/integration/users/test_viewset.py @@ -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()) diff --git a/tests/old/agenda/test_list_of_speakers.py b/tests/old/agenda/test_list_of_speakers.py index 54d0dcb98..04abdff78 100644 --- a/tests/old/agenda/test_list_of_speakers.py +++ b/tests/old/agenda/test_list_of_speakers.py @@ -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.') diff --git a/tests/old/agenda/tests.py b/tests/old/agenda/tests.py index 5a5fa3d64..649c685c9 100644 --- a/tests/old/agenda/tests.py +++ b/tests/old/agenda/tests.py @@ -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 diff --git a/tests/unit/users/test_api.py b/tests/unit/users/test_api.py index 44ed1ecd0..f82beac58 100644 --- a/tests/unit/users/test_api.py +++ b/tests/unit/users/test_api.py @@ -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') diff --git a/tests/unit/users/test_models.py b/tests/unit/users/test_models.py index b47a0e211..bae7e12af 100644 --- a/tests/unit/users/test_models.py +++ b/tests/unit/users/test_models.py @@ -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'.") diff --git a/tests/unit/users/test_serializers.py b/tests/unit/users/test_serializers.py new file mode 100644 index 000000000..7fd7ab658 --- /dev/null +++ b/tests/unit/users/test_serializers.py @@ -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)