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)