From 6a4816b33f19686f2bafb0d158bc530209a631e9 Mon Sep 17 00:00:00 2001 From: Hugo Smett Date: Fri, 10 Jul 2020 14:30:43 +0200 Subject: [PATCH 01/12] Creme Api --- creme/creme_api/README | 52 +++ creme/creme_api/__init__.py | 1 + creme/creme_api/api/__init__.py | 0 creme/creme_api/api/auth/__init__.py | 0 creme/creme_api/api/auth/serializers.py | 370 ++++++++++++++++++ creme/creme_api/api/auth/viewsets.py | 204 ++++++++++ creme/creme_api/api/authentication.py | 64 +++ creme/creme_api/api/pagination.py | 5 + creme/creme_api/api/permissions.py | 12 + creme/creme_api/api/routes.py | 29 ++ creme/creme_api/api/schemas.py | 10 + creme/creme_api/api/tokens/__init__.py | 0 creme/creme_api/api/tokens/serializers.py | 39 ++ creme/creme_api/api/tokens/views.py | 41 ++ creme/creme_api/apps.py | 26 ++ creme/creme_api/bricks.py | 27 ++ creme/creme_api/forms.py | 13 + creme/creme_api/migrations/0001_initial.py | 50 +++ creme/creme_api/migrations/__init__.py | 0 creme/creme_api/models.py | 127 ++++++ creme/creme_api/templates/creme_api/base.html | 68 ++++ .../creme_api/bricks/applications.html | 57 +++ .../templates/creme_api/configuration.html | 8 + .../templates/creme_api/description.md | 5 + .../templates/creme_api/documentation.html | 51 +++ creme/creme_api/tests/__init__.py | 0 creme/creme_api/tests/functional/__init__.py | 0 .../tests/functional/test_authentication.py | 91 +++++ .../tests/functional/test_credentials.py | 232 +++++++++++ .../tests/functional/test_documentation.py | 61 +++ .../creme_api/tests/functional/test_roles.py | 286 ++++++++++++++ .../creme_api/tests/functional/test_teams.py | 215 ++++++++++ .../creme_api/tests/functional/test_tokens.py | 47 +++ .../creme_api/tests/functional/test_users.py | 350 +++++++++++++++++ creme/creme_api/tests/test_models.py | 109 ++++++ creme/creme_api/tests/utils.py | 162 ++++++++ creme/creme_api/urls.py | 36 ++ creme/creme_api/views.py | 112 ++++++ creme/creme_core/models/auth.py | 40 ++ creme/creme_core/templatetags/creme_bricks.py | 4 +- creme/settings.py | 34 ++ setup.cfg | 3 + 42 files changed, 3039 insertions(+), 2 deletions(-) create mode 100644 creme/creme_api/README create mode 100644 creme/creme_api/__init__.py create mode 100644 creme/creme_api/api/__init__.py create mode 100644 creme/creme_api/api/auth/__init__.py create mode 100644 creme/creme_api/api/auth/serializers.py create mode 100644 creme/creme_api/api/auth/viewsets.py create mode 100644 creme/creme_api/api/authentication.py create mode 100644 creme/creme_api/api/pagination.py create mode 100644 creme/creme_api/api/permissions.py create mode 100644 creme/creme_api/api/routes.py create mode 100644 creme/creme_api/api/schemas.py create mode 100644 creme/creme_api/api/tokens/__init__.py create mode 100644 creme/creme_api/api/tokens/serializers.py create mode 100644 creme/creme_api/api/tokens/views.py create mode 100644 creme/creme_api/apps.py create mode 100644 creme/creme_api/bricks.py create mode 100644 creme/creme_api/forms.py create mode 100644 creme/creme_api/migrations/0001_initial.py create mode 100644 creme/creme_api/migrations/__init__.py create mode 100644 creme/creme_api/models.py create mode 100644 creme/creme_api/templates/creme_api/base.html create mode 100644 creme/creme_api/templates/creme_api/bricks/applications.html create mode 100644 creme/creme_api/templates/creme_api/configuration.html create mode 100644 creme/creme_api/templates/creme_api/description.md create mode 100644 creme/creme_api/templates/creme_api/documentation.html create mode 100644 creme/creme_api/tests/__init__.py create mode 100644 creme/creme_api/tests/functional/__init__.py create mode 100644 creme/creme_api/tests/functional/test_authentication.py create mode 100644 creme/creme_api/tests/functional/test_credentials.py create mode 100644 creme/creme_api/tests/functional/test_documentation.py create mode 100644 creme/creme_api/tests/functional/test_roles.py create mode 100644 creme/creme_api/tests/functional/test_teams.py create mode 100644 creme/creme_api/tests/functional/test_tokens.py create mode 100644 creme/creme_api/tests/functional/test_users.py create mode 100644 creme/creme_api/tests/test_models.py create mode 100644 creme/creme_api/tests/utils.py create mode 100644 creme/creme_api/urls.py create mode 100644 creme/creme_api/views.py diff --git a/creme/creme_api/README b/creme/creme_api/README new file mode 100644 index 0000000000..9ab7adbb49 --- /dev/null +++ b/creme/creme_api/README @@ -0,0 +1,52 @@ +A way to use a specific block for the creme config - creme model administration ? + +ajouter une application + créer un token pour une application donnée (app id app secret) + -> génération d'un token + +appel api avec le app id et le token + + + + + +################################### +# Configuration +- Une page spécifique à l'api pour la gestion des applications, des tokens ? +Pour l'accès à la documentation? + +## application +applications serveurs / application user +filtre / IP +utilisateurs autorisés +configuration de la durée max des tokens +restriction par scopes (resources) +##tokens +restriction par scopes (resources) +durée de validité + + +# Documentation +- Une page custom pour la documentation ? (Charte graphique Creme) + +# Restapi +- Erreurs 400 format custom {errors: [{code, field_name, message}, ...]} +- pagination headers au lieu de la réponse ? +- versionning + + + +# TODO: ordering, filtering +https://www.django-rest-framework.org/api-guide/filtering/ + +# TODO: applications scopes (for per resource restrictions) +# {tokens scopes} € {token.application scopes} + +# TODO: SetCredentials.efilter; require entityfilters endpoints + +# TODO: Swagger documentation +# - authentication scheme +# https://swagger.io/docs/specification/authentication/ +# Swagger.persistAuthorization ? +# - embed swagger lib dist +# - documentation markdown description diff --git a/creme/creme_api/__init__.py b/creme/creme_api/__init__.py new file mode 100644 index 0000000000..3277f64c24 --- /dev/null +++ b/creme/creme_api/__init__.py @@ -0,0 +1 @@ +VERSION = "1.0.0" diff --git a/creme/creme_api/api/__init__.py b/creme/creme_api/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/creme/creme_api/api/auth/__init__.py b/creme/creme_api/api/auth/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/creme/creme_api/api/auth/serializers.py b/creme/creme_api/api/auth/serializers.py new file mode 100644 index 0000000000..257d60ef69 --- /dev/null +++ b/creme/creme_api/api/auth/serializers.py @@ -0,0 +1,370 @@ +from collections import defaultdict + +from django.contrib.auth import get_user_model, password_validation +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from creme.creme_config.forms.user_role import filtered_entity_ctypes +from creme.creme_core.apps import CremeAppConfig, creme_app_configs +from creme.creme_core.models import SetCredentials, UserRole +from creme.creme_core.models.fields import CremeUserForeignKey +from creme.creme_core.registry import creme_registry + +CremeUser = get_user_model() + + +class PasswordSerializer(serializers.Serializer): + password = serializers.CharField( + label=_('Password'), trim_whitespace=False, write_only=True, required=True) + + def validate_password(self, password): + password_validation.validate_password(password, self.instance) + return password + + def save(self): + self.instance.set_password(self.validated_data['password']) + self.instance.save() + return self.instance + + +class UserSerializer(serializers.ModelSerializer): + default_error_messages = { + 'is_superuser_xor_role': _("A user must either have a role, or be a superuser.") + } + + class Meta: + model = CremeUser + fields = [ + 'id', + 'username', + 'last_name', + 'first_name', + 'email', + + 'date_joined', + 'last_login', + 'is_active', + # 'is_staff', + 'is_superuser', + 'role', + # 'is_team', + # 'teammates_set', + 'time_zone', + 'theme', + # 'settings', + ] + read_only_fields = [ + 'date_joined', + 'last_login', + ] + extra_kwargs = { + 'first_name': {'required': True}, + 'last_name': {'required': True}, + 'email': {'required': True}, + } + + def validate(self, attrs): + is_superuser = None + role_id = None + + if self.instance is not None: + is_superuser = self.instance.is_superuser + role_id = self.instance.role_id + + has_is_superuser = bool(attrs.get('is_superuser', is_superuser)) + has_role = bool(attrs.get('role', role_id)) + if not (has_is_superuser ^ has_role): + self.fail("is_superuser_xor_role") + return attrs + + +class TeamSerializer(serializers.ModelSerializer): + teammates = serializers.PrimaryKeyRelatedField( + queryset=CremeUser.objects.filter(is_team=False, is_staff=False), + many=True, + label=_('Teammates'), + required=True, + source='teammates_set', + ) + + class Meta: + model = CremeUser + fields = [ + 'id', + 'username', + 'teammates', + ] + extra_kwargs = { + 'username': {"label": _("Team name")} + } + + def __init__(self, *args, **kwargs): + super(TeamSerializer, self).__init__(*args, **kwargs) + username_field = self.fields.pop("username") + # username_field.source = "username" + self.fields["name"] = username_field + + def save(self, **kwargs): + kwargs['is_team'] = True + team = super().save(**kwargs) + return team + + +class DeleteUserSerializer(serializers.ModelSerializer): + """ + Serializer which assigns the fields with type CremeUserForeignKey + referencing a given user A to another user B, then deletes A. + """ + transfer_to = serializers.PrimaryKeyRelatedField( + queryset=CremeUser.objects.none(), + required=True + ) + + class Meta: + model = CremeUser + fields = ['transfer_to'] + + def __init__(self, instance=None, **kwargs): + super().__init__(instance=instance, **kwargs) + users = CremeUser.objects.exclude(is_staff=True) + if instance is not None: + users = users.exclude(pk=instance.pk) + self.fields['transfer_to'].queryset = users + + def save(self, **kwargs): + CremeUserForeignKey._TRANSFER_TO_USER = self.validated_data['transfer_to'] + + try: + self.instance.delete() + finally: + CremeUserForeignKey._TRANSFER_TO_USER = None + + +class UserRoleSerializer(serializers.ModelSerializer): + allowed_apps = serializers.MultipleChoiceField( + label=_('Allowed applications'), + choices=(), + ) + admin_4_apps = serializers.MultipleChoiceField( + label=_('Administrated applications'), + choices=(), + help_text=_( + 'These applications can be configured. ' + 'For example, the concerned users can create new choices ' + 'available in forms (eg: position for contacts).' + ), + ) + creatable_ctypes = serializers.PrimaryKeyRelatedField( + label=_('Creatable resources'), + many=True, + queryset=ContentType.objects.none(), + ) + exportable_ctypes = serializers.PrimaryKeyRelatedField( + label=_('Exportable resources'), + many=True, + queryset=ContentType.objects.none(), + help_text=_( + 'This types of entities can be downloaded as CSV/XLS ' + 'files (in the corresponding list-views).' + ), + ) + + default_error_messages = { + 'admin_4_not_allowed_app': _('App "{app}" is not an allowed app for this role.'), + 'not_allowed_ctype': _( + 'Content type "{ct}" ({id}) is part of the app "{app}" ' + 'which is not an allowed app for this role.'), + } + + class Meta: + model = UserRole + fields = [ + "id", + "name", + "allowed_apps", + "admin_4_apps", + "creatable_ctypes", + "exportable_ctypes", + "credentials" + ] + read_only_fields = [ + "credentials", + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + apps = list(creme_app_configs()) + + CRED_REGULAR = CremeAppConfig.CRED_REGULAR + allowed_apps_f = self.fields['allowed_apps'] + allowed_apps_f.choices = ( + (app.label, str(app.verbose_name)) for app in apps if app.credentials & CRED_REGULAR + ) + + CRED_ADMIN = CremeAppConfig.CRED_ADMIN + admin_4_apps_f = self.fields['admin_4_apps'] + admin_4_apps_f.choices = ( + (app.label, str(app.verbose_name)) for app in apps if app.credentials & CRED_ADMIN + ) + + models = list(creme_registry.iter_entity_models()) + content_types = ContentType.objects.get_for_models(*models).values() + ct_queryset = ContentType.objects.filter(pk__in=[ct.id for ct in content_types]) + + creatable_ctypes_f = self.fields['creatable_ctypes'] + creatable_ctypes_f.child_relation.queryset = ct_queryset.all() + + exportable_ctypes_f = self.fields['exportable_ctypes'] + exportable_ctypes_f.child_relation.queryset = ct_queryset.all() + + def build_error_detail(self, error_code, **kwargs): + msg = self.error_messages[error_code].format(**kwargs) + return serializers.ErrorDetail(msg, code=error_code) + + def validate(self, attrs): + # TODO: move in UserRole.clean ? + errors = defaultdict(list) + if self.instance is not None: + allowed_apps = self.instance.allowed_apps + admin_4_apps = self.instance.admin_4_apps + creatable_ctypes = self.instance.creatable_ctypes.all() + exportable_ctypes = self.instance.exportable_ctypes.all() + else: + allowed_apps = [] + admin_4_apps = [] + creatable_ctypes = [] + exportable_ctypes = [] + + allowed_apps = set(attrs.get("allowed_apps", allowed_apps)) + admin_4_apps = set(attrs.get("admin_4_apps", admin_4_apps)) + creatable_ctypes = set(attrs.get("creatable_ctypes", creatable_ctypes)) + exportable_ctypes = set(attrs.get("exportable_ctypes", exportable_ctypes)) + + allowed_ctypes = set(filtered_entity_ctypes(allowed_apps)) + + for admin_not_allowed_app in (admin_4_apps - allowed_apps): + errors["admin_4_apps"].append( + self.build_error_detail( + "admin_4_not_allowed_app", + app=admin_not_allowed_app, + ) + ) + + for create_not_allowed_ctype in (creatable_ctypes - allowed_ctypes): + errors["creatable_ctypes"].append( + self.build_error_detail( + "not_allowed_ctype", + id=create_not_allowed_ctype.id, + ct=create_not_allowed_ctype.model, + app=create_not_allowed_ctype.app_label, + ) + ) + + for export_not_allowed_ctype in (exportable_ctypes - allowed_ctypes): + errors["exportable_ctypes"].append( + self.build_error_detail( + "not_allowed_ctype", + id=export_not_allowed_ctype.id, + ct=export_not_allowed_ctype.model, + app=export_not_allowed_ctype.app_label) + ) + + if errors: + raise serializers.ValidationError(dict(errors)) + + return attrs + + +class SetCredentialsSerializer(serializers.ModelSerializer): + can_view = serializers.BooleanField( + label=_('View'), + required=True, + ) + can_change = serializers.BooleanField( + label=_('Change'), + required=True, + ) + can_delete = serializers.BooleanField( + label=_('Delete'), + required=True, + ) + + can_link = serializers.BooleanField( + label=_('Link'), + required=True, + help_text=_( + "You must have the permission to link on 2 entities " + "to create a relationship between them. " + "Beware: if you use «Filtered entities», you won't " + "be able to add relationships in the creation forms " + "(you'll have to add them later).", + ), + ) + can_unlink = serializers.BooleanField( + label=_('Unlink'), + required=True, + help_text=_( + 'You must have the permission to unlink on ' + '2 entities to delete a relationship between them.' + ), + ) + + class Meta: + model = SetCredentials + fields = [ + "id", + "role", + "set_type", + "ctype", + "can_view", + "can_change", + "can_delete", + "can_link", + "can_unlink", + "forbidden", + "efilter", + ] + read_only_fields = [ + 'efilter', + ] + extra_kwargs = { + 'set_type': {'required': True}, + 'ctype': {'required': True}, + 'forbidden': {'required': True}, + } + + def update(self, instance, validated_data): + instance.set_value( + can_view=validated_data.pop('can_view', instance.can_view), + can_change=validated_data.pop('can_change', instance.can_change), + can_delete=validated_data.pop('can_delete', instance.can_delete), + can_link=validated_data.pop('can_link', instance.can_link), + can_unlink=validated_data.pop('can_unlink', instance.can_unlink), + ) + return super().update(instance, validated_data) + + +class SetCredentialsCreateSerializer(SetCredentialsSerializer): + role = serializers.PrimaryKeyRelatedField( + queryset=UserRole.objects.all(), + required=True, + ) + + def create(self, validated_data): + instance = SetCredentials( + role=validated_data['role'], + set_type=validated_data['set_type'], + ctype=validated_data['ctype'], + forbidden=validated_data['forbidden'], + # efilter=validated_data['efilter'], + ) + instance.set_value( + can_view=validated_data['can_view'], + can_change=validated_data['can_change'], + can_delete=validated_data['can_delete'], + can_link=validated_data['can_link'], + can_unlink=validated_data['can_unlink'], + ) + instance.save() + return instance diff --git a/creme/creme_api/api/auth/viewsets.py b/creme/creme_api/api/auth/viewsets.py new file mode 100644 index 0000000000..a5618a6ab1 --- /dev/null +++ b/creme/creme_api/api/auth/viewsets.py @@ -0,0 +1,204 @@ +from django.contrib.auth import get_user_model +from django.db.models import ProtectedError +from rest_framework import mixins, status, viewsets +from rest_framework.decorators import action +from rest_framework.exceptions import PermissionDenied +from rest_framework.response import Response + +from creme.creme_api.api.schemas import CremeSchema +from creme.creme_core.models import SetCredentials, UserRole + +from .serializers import ( + DeleteUserSerializer, + PasswordSerializer, + SetCredentialsCreateSerializer, + SetCredentialsSerializer, + TeamSerializer, + UserRoleSerializer, + UserSerializer, +) + +CremeUser = get_user_model() + + +class UserViewSet(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + # mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + """ + create: + POST /users + + retrieve: + GET /users/{userId} + + update: + PUT /users/{userId} + + partial_update: + PATCH /users/{userId} + + list: + GET /users + + """ + queryset = CremeUser.objects.filter(is_team=False, is_staff=False) + serializer_class = UserSerializer + schema = CremeSchema(tags=["Users"], operation_id_base="Users") + + @action(methods=['post'], detail=True, serializer_class=PasswordSerializer) + def set_password(self, request, pk): + """ + post: + POST /users/{userId}/set_password + """ + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + + serializer = self.get_serializer(instance) + return Response(serializer.data) + + @action( + methods=['post'], + detail=True, + serializer_class=DeleteUserSerializer, + url_path="delete", + url_name="delete", + name="delete", + ) + def _delete(self, request, pk): + """ + post: + POST /users/{userId}/delete + """ + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class TeamViewSet(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + # mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + """ + create: + POST /teams + + retrieve: + GET /teams/{teamId} + + update: + PUT /teams/{teamId} + + partial_update: + PATCH /teams/{teamId} + + list: + GET /teams + + """ + queryset = CremeUser.objects.filter(is_team=True, is_staff=False) + serializer_class = TeamSerializer + schema = CremeSchema(tags=["Teams"], operation_id_base="Teams") + + @action( + methods=['post'], + detail=True, + serializer_class=DeleteUserSerializer, + url_path="delete", + url_name="delete", + name="delete", + ) + def _delete(self, request, pk): + """ + post: + POST /teams/{teamId}/delete + """ + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class UserRoleViewSet(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + """ + + create: + POST /roles + + retrieve: + GET /roles/{roleId} + + update: + PUT /roles/{roleId} + + partial_update: + PATCH /roles/{roleId} + + destroy: + DELETE /roles/{roleId} + + list: + GET /roles + + """ + queryset = UserRole.objects.all() + serializer_class = UserRoleSerializer + schema = CremeSchema(tags=["Roles"]) + + def perform_destroy(self, instance): + try: + instance.delete() + except ProtectedError as e: + raise PermissionDenied(e.args[0]) from e + + +class SetCredentialsViewSet(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + """ + + create: + POST /credentials + + retrieve: + GET /credentials/{credentialId} + + update: + PUT /credentials/{credentialId} + + partial_update: + PATCH /credentials/{credentialId} + + destroy: + DELETE /credentials/{credentialId} + + list: + GET /credentials + + """ + queryset = SetCredentials.objects.all() + serializer_class = SetCredentialsSerializer + schema = CremeSchema(tags=["Credentials"]) + + def get_serializer_class(self): + if self.action == 'create': + return SetCredentialsCreateSerializer + return super().get_serializer_class() diff --git a/creme/creme_api/api/authentication.py b/creme/creme_api/api/authentication.py new file mode 100644 index 0000000000..c832b0678a --- /dev/null +++ b/creme/creme_api/api/authentication.py @@ -0,0 +1,64 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import exceptions +from rest_framework.authentication import ( + BaseAuthentication, + get_authorization_header, +) + +from creme.creme_api.models import Token + + +class TokenAuthentication(BaseAuthentication): + """ + Token based authentication + """ + + keyword = 'Token' + errors = { + 'empty': _('Invalid token header. No credentials provided.'), + 'too_long': _('Invalid token header. Token string should not contain spaces.'), + 'encoding': _('Invalid token header. Token string should not contain invalid characters.'), + 'invalid': _('Incorrect authentication credentials.'), + 'expired': _('Incorrect authentication credentials. Token has expired.'), + } + + def authentication_failure(self, code): + return exceptions.AuthenticationFailed(detail=self.errors[code], code=code) + + def authenticate(self, request): + request.token = None + request.application = None + + auth = get_authorization_header(request).split() + + if not auth or auth[0].lower() != self.keyword.lower().encode(): + return None + + if len(auth) == 1: + raise self.authentication_failure('empty') + elif len(auth) > 2: + raise self.authentication_failure('too_long') + + try: + token_code = auth[1].decode() + except UnicodeError: + raise self.authentication_failure('encoding') + + try: + token = Token.objects.select_related('application').get(code=token_code) + except Token.DoesNotExist: + raise self.authentication_failure('invalid') + + if not token.application.can_authenticate(request=request): + raise self.authentication_failure('invalid') + + if token.is_expired(): + raise self.authentication_failure('expired') + + request.token = token + request.application = token.application + + return token.user, token + + def authenticate_header(self, request): + return self.keyword diff --git a/creme/creme_api/api/pagination.py b/creme/creme_api/api/pagination.py new file mode 100644 index 0000000000..6f6fddcfe5 --- /dev/null +++ b/creme/creme_api/api/pagination.py @@ -0,0 +1,5 @@ +from rest_framework.pagination import CursorPagination + + +class CremeCursorPagination(CursorPagination): + ordering = 'id' diff --git a/creme/creme_api/api/permissions.py b/creme/creme_api/api/permissions.py new file mode 100644 index 0000000000..e8f23a75e1 --- /dev/null +++ b/creme/creme_api/api/permissions.py @@ -0,0 +1,12 @@ +from rest_framework.permissions import BasePermission, IsAuthenticated + + +class TokenPermission(BasePermission): + def has_permission(self, request, view): + return bool(request.token) + + +class CremeApiPermission(IsAuthenticated): + def has_permission(self, request, view): + is_authenticated = super().has_permission(request, view) + return is_authenticated and request.user.has_perm("creme_api") diff --git a/creme/creme_api/api/routes.py b/creme/creme_api/api/routes.py new file mode 100644 index 0000000000..42d1e5ca4d --- /dev/null +++ b/creme/creme_api/api/routes.py @@ -0,0 +1,29 @@ +from rest_framework import routers + +import creme.creme_api.api.auth.viewsets + + +class CremeRouter(routers.DefaultRouter): + include_root_view = False + include_format_suffixes = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.resources_list = set() + + def register_viewset(self, resource_name, viewset): + self.resources_list.add(resource_name) + + return self.register( + resource_name, + viewset, + basename=f'creme_api__{resource_name}' + ) + + +router = CremeRouter() + +router.register_viewset("users", creme.creme_api.api.auth.viewsets.UserViewSet) +router.register_viewset("teams", creme.creme_api.api.auth.viewsets.TeamViewSet) +router.register_viewset("roles", creme.creme_api.api.auth.viewsets.UserRoleViewSet) +router.register_viewset("credentials", creme.creme_api.api.auth.viewsets.SetCredentialsViewSet) diff --git a/creme/creme_api/api/schemas.py b/creme/creme_api/api/schemas.py new file mode 100644 index 0000000000..81d985d725 --- /dev/null +++ b/creme/creme_api/api/schemas.py @@ -0,0 +1,10 @@ +from rest_framework.schemas.openapi import AutoSchema + + +class CremeSchema(AutoSchema): + + def get_operation_id(self, path, method): + method_name = getattr(self.view, 'action', method.lower()) + action = self._to_camel_case(method_name) + name = self.get_operation_id_base(path, method, action) + return action + name diff --git a/creme/creme_api/api/tokens/__init__.py b/creme/creme_api/api/tokens/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/creme/creme_api/api/tokens/serializers.py b/creme/creme_api/api/tokens/serializers.py new file mode 100644 index 0000000000..3bc117cdf1 --- /dev/null +++ b/creme/creme_api/api/tokens/serializers.py @@ -0,0 +1,39 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from creme.creme_api.models import Application + + +class TokenSerializer(serializers.Serializer): + default_error_messages = { + 'authentication_failure': _('Unable to log in with provided credentials.') + } + + client_id = serializers.UUIDField( + label=_("Client ID"), + write_only=True + ) + client_secret = serializers.CharField( + label=_("Client secret"), + style={'input_type': 'password'}, + write_only=True + ) + token = serializers.CharField( + label=_("Token"), + read_only=True + ) + + def validate(self, attrs): + client_id = attrs["client_id"] + client_secret = attrs["client_secret"] + + application = Application.authenticate( + client_id, client_secret, + request=self.context['request'], + ) + if not application: + self.fail('authentication_failure') + + attrs['application'] = application + + return attrs diff --git a/creme/creme_api/api/tokens/views.py b/creme/creme_api/api/tokens/views.py new file mode 100644 index 0000000000..b76bb4a997 --- /dev/null +++ b/creme/creme_api/api/tokens/views.py @@ -0,0 +1,41 @@ +from rest_framework import parsers, renderers +from rest_framework.response import Response +from rest_framework.views import APIView + +from creme.creme_api.api.schemas import CremeSchema +from creme.creme_api.models import Token + +from .serializers import TokenSerializer + + +class TokenView(APIView): + throttle_classes = [] + permission_classes = [] + parser_classes = [ + parsers.FormParser, + parsers.MultiPartParser, + parsers.JSONParser, + ] + renderer_classes = [ + renderers.JSONRenderer, + ] + serializer_class = TokenSerializer + schema = CremeSchema(tags=["Tokens"]) + + def get_serializer_context(self): + return { + 'request': self.request, + 'format': self.format_kwarg, + 'view': self + } + + def get_serializer(self, *args, **kwargs): + kwargs['context'] = self.get_serializer_context() + return self.serializer_class(*args, **kwargs) + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + application = serializer.validated_data['application'] + token = Token.objects.create(application=application) + return Response({'token': token.code}) diff --git a/creme/creme_api/apps.py b/creme/creme_api/apps.py new file mode 100644 index 0000000000..9c0670621a --- /dev/null +++ b/creme/creme_api/apps.py @@ -0,0 +1,26 @@ +from django.utils.translation import gettext_lazy as _ + +from creme.creme_core.apps import CremeAppConfig + + +class CremeApiConfig(CremeAppConfig): + default = True + name = 'creme.creme_api' + verbose_name = _('Creme Api') + dependencies = ["creme.creme_core"] + + def register_creme_config(self, config_registry): + from creme.creme_api import models + + from .forms import ApplicationForm + + register_model = config_registry.register_model + register_model(models.Application, 'application').creation( + form_class=ApplicationForm, + ).edition( + form_class=ApplicationForm, + ) + + def register_bricks(self, brick_registry): + from .bricks import ApplicationsBrick + brick_registry.register(ApplicationsBrick) diff --git a/creme/creme_api/bricks.py b/creme/creme_api/bricks.py new file mode 100644 index 0000000000..1c08eed2d3 --- /dev/null +++ b/creme/creme_api/bricks.py @@ -0,0 +1,27 @@ +from django.contrib.messages import get_messages +from django.utils.translation import gettext_lazy as _ + +from creme.creme_api.models import Application +from creme.creme_core.gui.bricks import QuerysetBrick + + +class ApplicationsBrick(QuerysetBrick): + id_ = QuerysetBrick.generate_id('creme_api', 'applications') + verbose_name = _('Applications') + description = _( + 'Displays the list of the applications that are allowed ' + 'to access Creme CRM web services.\n' + ) + dependencies = (Application, ) + template_name = 'creme_api/bricks/applications.html' + order_by = 'name' + + def detailview_display(self, context): + messages = list(get_messages(context['request'])) + secret_application_message = messages[0] if messages else None + btc = self.get_template_context( + context, + Application.objects.all(), + secret_application_message=secret_application_message + ) + return self._render(btc) diff --git a/creme/creme_api/forms.py b/creme/creme_api/forms.py new file mode 100644 index 0000000000..9e12c3682e --- /dev/null +++ b/creme/creme_api/forms.py @@ -0,0 +1,13 @@ +from creme.creme_core.forms import CremeModelForm + +from .models import Application + + +class ApplicationForm(CremeModelForm): + class Meta(CremeModelForm.Meta): + model = Application + fields = [ + "name", + "enabled", + "token_duration", + ] diff --git a/creme/creme_api/migrations/0001_initial.py b/creme/creme_api/migrations/0001_initial.py new file mode 100644 index 0000000000..afa2199657 --- /dev/null +++ b/creme/creme_api/migrations/0001_initial.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.12 on 2022-02-08 16:07 + +import uuid + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + +import creme.creme_api.models +import creme.creme_core.models.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Application', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True, max_length=255, unique=True, verbose_name='Name')), + ('client_id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True, verbose_name='Client ID')), + ('client_secret', models.CharField(blank=True, max_length=255, verbose_name='Client secret')), + ('enabled', models.BooleanField(default=True, verbose_name='Enabled')), + ('token_duration', models.IntegerField(default=3600, help_text='Number of seconds during which tokens will be valid. It will only affect newly created tokens.', verbose_name='Tokens duration')), + ('created', creme.creme_core.models.fields.CreationDateTimeField(blank=True, default=django.utils.timezone.now, editable=False, verbose_name='Creation date')), + ('modified', creme.creme_core.models.fields.ModificationDateTimeField(blank=True, default=django.utils.timezone.now, editable=False, verbose_name='Last modification')), + ], + options={ + 'verbose_name': 'Application', + 'verbose_name_plural': 'Applications', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Token', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(db_index=True, default=creme.creme_api.models.default_token_code, max_length=255, unique=True)), + ('expires', models.DateTimeField()), + ('created', creme.creme_core.models.fields.CreationDateTimeField(blank=True, default=django.utils.timezone.now, editable=False)), + ('modified', creme.creme_core.models.fields.ModificationDateTimeField(blank=True, default=django.utils.timezone.now, editable=False)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='creme_api.application')), + ], + ), + ] diff --git a/creme/creme_api/migrations/__init__.py b/creme/creme_api/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/creme/creme_api/models.py b/creme/creme_api/models.py new file mode 100644 index 0000000000..c65128c6a6 --- /dev/null +++ b/creme/creme_api/models.py @@ -0,0 +1,127 @@ +import secrets +import string +import uuid + +from django.contrib.auth.hashers import check_password, make_password +from django.core.exceptions import ValidationError +from django.db import models +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from creme.creme_core.models import CremeModel +from creme.creme_core.models.fields import ( + CreationDateTimeField, + ModificationDateTimeField, +) + + +def generate_secret(length, chars=(string.ascii_letters + string.digits)): + return ''.join(secrets.choice(chars) for i in range(length)) + + +def default_application_client_secret(): + return generate_secret(40) + + +class Application(CremeModel): + # IP restriction capabilities ? + # Allow to restrict this application to a subset of resources ? + + name = models.CharField( + verbose_name=_("Name"), max_length=255, unique=True, db_index=True, + ) + + client_id = models.UUIDField( + verbose_name=_("Client ID"), + max_length=100, + unique=True, + db_index=True, + default=uuid.uuid4, + editable=False, + ) + client_secret = models.CharField( + verbose_name=_("Client secret"), max_length=255, blank=True, + ) + _client_secret = None + + enabled = models.BooleanField( + verbose_name=_("Enabled"), default=True, + ) + token_duration = models.IntegerField( + verbose_name=_('Tokens duration'), default=3600, + help_text=_("Number of seconds during which tokens will be valid. " + "It will only affect newly created tokens."), + ) + + created = CreationDateTimeField( + verbose_name=_('Creation date'), editable=False + ) + modified = ModificationDateTimeField( + verbose_name=_('Last modification'), editable=False + ) + + class Meta: + verbose_name = _("Application") + verbose_name_plural = _("Applications") + app_label = 'creme_api' + ordering = ['name'] + + def __str__(self): + return self.name + + def set_client_secret(self, raw_client_secret): + self.client_secret = make_password(raw_client_secret) + self._client_secret = raw_client_secret + + def check_client_secret(self, raw_client_secret): + def setter(rcs): + self.set_client_secret(rcs) + self.save(update_fields=["client_secret"]) + return check_password(raw_client_secret, self.client_secret, setter) + + def save(self, **kwargs): + if self.pk is None: + self.set_client_secret(default_application_client_secret()) + return super().save(**kwargs) + + def can_authenticate(self, request=None): + return self.enabled + + @staticmethod + def authenticate(client_id, client_secret, request=None): + try: + application = Application.objects.get(client_id=client_id) + except (Application.DoesNotExist, ValidationError): + Application().set_client_secret(client_secret) + else: + if application.check_client_secret(client_secret) \ + and application.can_authenticate(request=request): + return application + + +def default_token_code(): + return generate_secret(128) + + +class Token(models.Model): + # Allow to restrict this token to a subset of resources ? + # Allow create token with a duration <= application token duration + + application = models.ForeignKey(Application, on_delete=models.CASCADE) + + code = models.CharField(max_length=255, unique=True, db_index=True, default=default_token_code) + expires = models.DateTimeField() + + created = CreationDateTimeField(editable=False) + modified = ModificationDateTimeField(editable=False) + + user = None + + def is_expired(self): + return timezone.now() >= self.expires + + def save(self, **kwargs): + if self.expires is None: + delta = timezone.timedelta(seconds=self.application.token_duration) + self.expires = timezone.now() + delta + return super().save(**kwargs) diff --git a/creme/creme_api/templates/creme_api/base.html b/creme/creme_api/templates/creme_api/base.html new file mode 100644 index 0000000000..11068cdb36 --- /dev/null +++ b/creme/creme_api/templates/creme_api/base.html @@ -0,0 +1,68 @@ +{% extends 'creme_core/base.html' %} +{% load i18n creme_widgets creme_bricks %} + +{% block page_title %}{{page_title}} - {% endblock %} + +{% block extrahead %} + {{ block.super }} + +{% endblock %} + +{% block content %} +
+
+ {% block content-left-panel %} + + {% endblock %} +
+
+ {% block content-right-panel %} + {% endblock %} +
+
+{% endblock %} diff --git a/creme/creme_api/templates/creme_api/bricks/applications.html b/creme/creme_api/templates/creme_api/bricks/applications.html new file mode 100644 index 0000000000..01d06b4a55 --- /dev/null +++ b/creme/creme_api/templates/creme_api/bricks/applications.html @@ -0,0 +1,57 @@ +{% extends 'creme_core/bricks/base/paginated-table.html' %} +{% load i18n creme_bricks creme_widgets %} +{% load has_perm_to print_field url from creme_core_tags %} + +{% block brick_extra_class %}{{block.super}} creme_api-applications-brick{% endblock %} + +{% block brick_content %} + {% if secret_application_message %} +
+ {{ secret_application_message|linebreaksbr }} +
+ {% endif %} + + {{ block.super }} +{% endblock %} +{% block brick_header_title %} + {% brick_header_title title=_('{count} Application') plural=_('{count} Applications') empty=verbose_name icon='relations' %} +{% endblock %} + +{% block brick_header_actions %} + {% brick_header_action id='add' url='creme_api__create_application'|url label=_('New application') %} +{% endblock %} + +{% block brick_table_columns %} + {% brick_table_column_for_field ctype=objects_ctype field='name' status='primary' %} + {% brick_table_column_for_field ctype=objects_ctype field='client_id' %} + {% brick_table_column_for_field ctype=objects_ctype field='enabled' %} + {% brick_table_column_for_field ctype=objects_ctype field='token_duration' %} +{# {% brick_table_column_for_field ctype=objects_ctype field='_client_secret' %}#} + {% brick_table_column_for_field ctype=objects_ctype field='created' data_type='date' %} + {% brick_table_column_for_field ctype=objects_ctype field='modified' data_type='date' %} + {% brick_table_column title=_('Action') status='action' %} +{% endblock %} + +{% block brick_table_rows %}{% url 'creme_api__delete_application' as delete_url %} + {% for application in page.object_list %} + + {% print_field object=application field='name' %} + {% print_field object=application field='client_id' %} + {% print_field object=application field='enabled' %} + {% print_field object=application field='token_duration' %} {% trans 'seconds' %} +{# {% print_field object=application field='_client_secret' %}#} + {{application.created|date:"DATE_FORMAT"}} + {{application.modified|date:"DATE_FORMAT"}} + {% url 'creme_api__edit_application' application.id as edit_url %} + {% brick_table_action id='edit' url=edit_url label=_('Edit this application') %} + + {% url 'creme_api__delete_application' application.id as delete_url %} + {% brick_table_action id='edit' url=delete_url label=_('Delete this application') icon='delete' %} + + + {% endfor %} +{% endblock %} + +{% block brick_table_empty %} + {% translate 'No application for the moment' %} +{% endblock %} diff --git a/creme/creme_api/templates/creme_api/configuration.html b/creme/creme_api/templates/creme_api/configuration.html new file mode 100644 index 0000000000..05c3bc5a1f --- /dev/null +++ b/creme/creme_api/templates/creme_api/configuration.html @@ -0,0 +1,8 @@ +{% extends 'creme_api/base.html' %} + +{% load i18n creme_widgets creme_bricks %} + +{% block content-right-panel %} + {% brick_declare bricks %} + {% brick_display bricks %} +{% endblock %} diff --git a/creme/creme_api/templates/creme_api/description.md b/creme/creme_api/templates/creme_api/description.md new file mode 100644 index 0000000000..f07643f4e2 --- /dev/null +++ b/creme/creme_api/templates/creme_api/description.md @@ -0,0 +1,5 @@ +# Drive Creme CRM from the outside + +## Authentication + +## Pagination diff --git a/creme/creme_api/templates/creme_api/documentation.html b/creme/creme_api/templates/creme_api/documentation.html new file mode 100644 index 0000000000..7e19680c7c --- /dev/null +++ b/creme/creme_api/templates/creme_api/documentation.html @@ -0,0 +1,51 @@ +{% extends 'creme_api/base.html' %} + +{% block extrahead %} + {{ block.super }} + +{% endblock %} + +{% block content-right-panel %} +
+ + + + + +{% endblock %} diff --git a/creme/creme_api/tests/__init__.py b/creme/creme_api/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/creme/creme_api/tests/functional/__init__.py b/creme/creme_api/tests/functional/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/creme/creme_api/tests/functional/test_authentication.py b/creme/creme_api/tests/functional/test_authentication.py new file mode 100644 index 0000000000..72eac1570a --- /dev/null +++ b/creme/creme_api/tests/functional/test_authentication.py @@ -0,0 +1,91 @@ +from django.urls import path, reverse_lazy +from django.utils import timezone +from rest_framework import status +from rest_framework.exceptions import NotAuthenticated +from rest_framework.response import Response +from rest_framework.test import APITestCase, URLPatternsTestCase +from rest_framework.views import APIView + +from creme.creme_api.api.authentication import TokenAuthentication +from creme.creme_api.models import Application, Token + + +class TestAuthenticationView(APIView): + authentication_classes = [TokenAuthentication] + + def get(self, request): + return Response(data={'ok': True}) + + +class TokenAuthenticationAPITestCase(APITestCase, URLPatternsTestCase): + urlpatterns = [ + path("test-authentication/", + TestAuthenticationView.as_view(), + name="api_tests__test-authentication") + ] + url = reverse_lazy("api_tests__test-authentication") + + def request(self): + return self.client.get(self.url) + + def assert401(self, error_code=None): + response = self.request() + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + error_detail = response.data['detail'] + if error_code: + code = error_code + message = TokenAuthentication.errors[error_code] + else: + code = NotAuthenticated.default_code + message = NotAuthenticated.default_detail + + self.assertEqual(error_detail.code, code) + self.assertEqual(error_detail, message, error_detail) + + def test_authenticate01(self): + self.assert401() + + def test_authenticate02(self): + self.client.credentials(HTTP_AUTHORIZATION='') + self.assert401() + + def test_authenticate03(self): + self.client.credentials(HTTP_AUTHORIZATION='Bearer') + self.assert401() + + def test_authenticate04(self): + self.client.credentials(HTTP_AUTHORIZATION='Token') + self.assert401(error_code='empty') + + def test_authenticate05(self): + self.client.credentials(HTTP_AUTHORIZATION='Token T1 T2') + self.assert401(error_code='too_long') + + def test_authenticate06(self): + self.client.credentials(HTTP_AUTHORIZATION=b'Token \xa1') + self.assert401(error_code='encoding') + + def test_authenticate07(self): + application = Application.objects.create(name="APITestCase") + Token.objects.create(application=application) + self.client.credentials(HTTP_AUTHORIZATION=b'Token TEST') + self.assert401(error_code='invalid') + + def test_authenticate08(self): + application = Application.objects.create(name="APITestCase", enabled=False) + token = Token.objects.create(application=application) + self.client.credentials(HTTP_AUTHORIZATION=f'Token {token.code}') + self.assert401(error_code='invalid') + + def test_authenticate09(self): + application = Application.objects.create(name="APITestCase") + token = Token.objects.create(application=application, expires=timezone.now()) + self.client.credentials(HTTP_AUTHORIZATION=f'Token {token.code}') + self.assert401(error_code='expired') + + def test_authenticate10(self): + application = Application.objects.create(name="APITestCase") + token = Token.objects.create(application=application) + self.client.credentials(HTTP_AUTHORIZATION=f'Token {token.code}') + response = self.request() + self.assertEqual(status.HTTP_200_OK, response.status_code) diff --git a/creme/creme_api/tests/functional/test_credentials.py b/creme/creme_api/tests/functional/test_credentials.py new file mode 100644 index 0000000000..caf007edd0 --- /dev/null +++ b/creme/creme_api/tests/functional/test_credentials.py @@ -0,0 +1,232 @@ +from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType + +from creme.creme_api.tests.utils import CremeAPITestCase +from creme.creme_core.models import SetCredentials +from creme.persons import get_contact_model, get_organisation_model + +CremeUser = get_user_model() +Contact = get_contact_model() +Organisation = get_organisation_model() + + +class CreateSetCredentialTestCase(CremeAPITestCase): + url_name = 'creme_api__credentials-list' + method = 'post' + + def test_validation__required(self): + response = self.make_request(data={}) + self.assertValidationErrors(response, { + 'role': ['required'], + 'set_type': ['required'], + 'ctype': ['required'], + 'can_view': ['required'], + 'can_change': ['required'], + 'can_delete': ['required'], + 'can_link': ['required'], + 'can_unlink': ['required'], + 'forbidden': ['required'], + }) + + def test_create_setcredentials(self): + contact_ct = ContentType.objects.get_for_model(Contact) + role = self.factory.role() + data = { + "role": role.id, + "set_type": SetCredentials.ESET_ALL, + "ctype": contact_ct.id, + "can_view": True, + "can_change": True, + "can_delete": False, + "can_link": True, + "can_unlink": True, + "forbidden": False, + } + response = self.make_request(data=data) + creds = SetCredentials.objects.get(id=response.data['id']) + self.assertResponseEqual(response, 201, { + 'id': creds.id, + "role": role.id, + "set_type": SetCredentials.ESET_ALL, + "ctype": contact_ct.id, + "can_view": True, + "can_change": True, + "can_delete": False, + "can_link": True, + "can_unlink": True, + "forbidden": False, + "efilter": None, + }) + self.assertEqual(creds.role, role) + self.assertEqual(creds.set_type, SetCredentials.ESET_ALL) + self.assertEqual(creds.ctype, contact_ct) + self.assertEqual(creds.value, 2 + 4 + 16 + 32) + self.assertFalse(creds.forbidden) + + +class RetrieveSetCredentialTestCase(CremeAPITestCase): + url_name = 'creme_api__credentials-detail' + method = 'get' + + def test_retrieve_setcredentials(self): + contact_ct = ContentType.objects.get_for_model(Contact) + creds = self.factory.credential() + response = self.make_request(to=creds.id) + self.assertResponseEqual(response, 200, { + 'id': creds.id, + "role": creds.role_id, + "set_type": SetCredentials.ESET_OWN, + "ctype": contact_ct.id, + "can_view": True, + "can_change": True, + "can_delete": True, + "can_link": True, + "can_unlink": True, + "forbidden": False, + "efilter": None, + }) + + +class UpdateSetCredentialTestCase(CremeAPITestCase): + url_name = 'creme_api__credentials-detail' + method = 'put' + + def test_validation__required(self): + creds = self.factory.credential() + response = self.make_request(to=creds.id, data={}) + self.assertValidationErrors(response, { + 'set_type': ['required'], + 'ctype': ['required'], + 'can_view': ['required'], + 'can_change': ['required'], + 'can_delete': ['required'], + 'can_link': ['required'], + 'can_unlink': ['required'], + 'forbidden': ['required'], + }) + + def test_update_creds(self): + orga_ct = ContentType.objects.get_for_model(Organisation) + creds = self.factory.credential() + role_id = creds.role_id + data = { + "role": 123456, + "set_type": SetCredentials.ESET_ALL, + "ctype": orga_ct.id, + "can_view": True, + "can_change": True, + "can_delete": False, + "can_link": True, + "can_unlink": True, + "forbidden": False, + } + response = self.make_request(to=creds.id, data=data) + self.assertResponseEqual(response, 200, { + 'id': creds.id, + "role": creds.role_id, + "set_type": SetCredentials.ESET_ALL, + "ctype": orga_ct.id, + "can_view": True, + "can_change": True, + "can_delete": False, + "can_link": True, + "can_unlink": True, + "forbidden": False, + "efilter": None, + }) + creds.refresh_from_db() + self.assertEqual(creds.role_id, role_id) + self.assertEqual(creds.set_type, SetCredentials.ESET_ALL) + self.assertEqual(creds.ctype, orga_ct) + self.assertEqual(creds.value, 2 + 4 + 16 + 32) + self.assertFalse(creds.forbidden) + + +class PartialUpdateSetCredentialTestCase(CremeAPITestCase): + url_name = 'creme_api__credentials-detail' + method = 'patch' + + def test_partial_update_creds(self): + orga_ct = ContentType.objects.get_for_model(Organisation) + creds = self.factory.credential() + role_id = creds.role_id + data = { + "role": 123456, + "set_type": SetCredentials.ESET_ALL, + "ctype": orga_ct.id, + "can_delete": False, + "forbidden": False, + } + response = self.make_request(to=creds.id, data=data) + self.assertResponseEqual(response, 200, { + 'id': creds.id, + "role": creds.role_id, + "set_type": SetCredentials.ESET_ALL, + "ctype": orga_ct.id, + "can_view": True, + "can_change": True, + "can_delete": False, + "can_link": True, + "can_unlink": True, + "forbidden": False, + "efilter": None, + }) + creds.refresh_from_db() + self.assertEqual(creds.role_id, role_id) + self.assertEqual(creds.set_type, SetCredentials.ESET_ALL) + self.assertEqual(creds.ctype, orga_ct) + self.assertEqual(creds.value, 2 + 4 + 16 + 32) + self.assertFalse(creds.forbidden) + + +class ListSetCredentialTestCase(CremeAPITestCase): + url_name = 'creme_api__credentials-list' + method = 'get' + + def test_list_setcredentials(self): + contact_ct = ContentType.objects.get_for_model(Contact) + orga_ct = ContentType.objects.get_for_model(Organisation) + role = self.factory.role() + creds1 = self.factory.credential(role=role, ctype=contact_ct) + creds2 = self.factory.credential(role=role, ctype=orga_ct, can_delete=False) + + response = self.make_request() + self.assertResponseEqual(response, 200, [ + { + 'id': creds1.id, + "role": role.id, + "set_type": SetCredentials.ESET_OWN, + "ctype": contact_ct.id, + "can_view": True, + "can_change": True, + "can_delete": True, + "can_link": True, + "can_unlink": True, + "forbidden": False, + "efilter": None, + }, + { + 'id': creds2.id, + "role": role.id, + "set_type": SetCredentials.ESET_OWN, + "ctype": orga_ct.id, + "can_view": True, + "can_change": True, + "can_delete": False, + "can_link": True, + "can_unlink": True, + "forbidden": False, + "efilter": None, + } + ]) + + +class DeleteSetCredentialTestCase(CremeAPITestCase): + url_name = 'creme_api__credentials-detail' + method = 'delete' + + def test_delete(self): + creds = self.factory.credential() + response = self.make_request(to=creds.id) + self.assertResponseEqual(response, 204) + self.assertFalse(SetCredentials.objects.filter(id=creds.id).exists()) diff --git a/creme/creme_api/tests/functional/test_documentation.py b/creme/creme_api/tests/functional/test_documentation.py new file mode 100644 index 0000000000..80813feed4 --- /dev/null +++ b/creme/creme_api/tests/functional/test_documentation.py @@ -0,0 +1,61 @@ +import yaml +from django.urls import reverse_lazy + +import creme.creme_api +from creme.creme_api.views import ( + documentation_description, + documentation_title, +) +from creme.creme_core.tests.base import CremeTestCase + + +class SchemaViewTestCase(CremeTestCase): + url = reverse_lazy('creme_api__openapi_schema') + + def test_permissions__permission_denied(self): + self.login(is_superuser=False, allowed_apps=('creme_core',)) + self.assertGET403(self.url) + + def test_permissions__allowed_app(self): + self.login(is_superuser=False, allowed_apps=('creme_core', 'creme_api')) + self.assertGET200(self.url) + + def test_permissions__superuser(self): + self.login(is_superuser=True, allowed_apps=('creme_core',)) + self.assertGET200(self.url) + + def test_context(self): + self.login() + response = self.assertGET200(self.url) + self.assertEqual(response['content-type'], 'application/vnd.oai.openapi') + openapi_schema = yaml.safe_load(response.content) + self.assertEqual( + openapi_schema['info'], + {'title': documentation_title, + 'version': creme.creme_api.VERSION, + 'description': documentation_description}) + + +class DocumentationViewTestCase(CremeTestCase): + url = reverse_lazy('creme_api__documentation') + + def test_permissions__permission_denied(self): + self.login(is_superuser=False, allowed_apps=('creme_core',)) + self.assertGET403(self.url) + + def test_permissions__allowed_app(self): + self.login(is_superuser=False, allowed_apps=('creme_core', 'creme_api')) + self.assertGET200(self.url) + + def test_permissions__superuser(self): + self.login(is_superuser=True, allowed_apps=('creme_core',)) + self.assertGET200(self.url) + + def test_context(self): + self.login() + response = self.assertGET200(self.url) + self.assertTemplateUsed(response, 'creme_api/documentation.html') + self.assertEqual(response.context["schema_url"], 'creme_api__openapi_schema') + self.assertEqual( + response.context["creme_api__tokens_url"], 'http://testserver/creme_api/tokens') + self.assertEqual(response.context["token_type"], 'Token') diff --git a/creme/creme_api/tests/functional/test_roles.py b/creme/creme_api/tests/functional/test_roles.py new file mode 100644 index 0000000000..59464cbe00 --- /dev/null +++ b/creme/creme_api/tests/functional/test_roles.py @@ -0,0 +1,286 @@ +from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType + +from creme.creme_api.tests.utils import CremeAPITestCase +from creme.creme_core.models import UserRole +from creme.persons import get_contact_model, get_organisation_model + +CremeUser = get_user_model() +Contact = get_contact_model() +Organisation = get_organisation_model() + + +class CreateRoleTestCase(CremeAPITestCase): + url_name = 'creme_api__roles-list' + method = 'post' + + def test_validation__required(self): + response = self.make_request(data={}) + self.assertValidationErrors(response, { + 'name': ['required'], + 'allowed_apps': ['required'], + 'admin_4_apps': ['required'], + 'creatable_ctypes': ['required'], + 'exportable_ctypes': ['required'], + }) + + def test_validation__name_unique(self): + self.factory.role(name="UniqueRoleName") + data = { + 'name': "UniqueRoleName", + 'allowed_apps': [], + 'admin_4_apps': [], + 'creatable_ctypes': [], + 'exportable_ctypes': [], + } + response = self.make_request(data=data) + self.assertValidationError(response, 'name', ['unique']) + + def test_validation(self): + contact_ct = ContentType.objects.get_for_model(Contact) + orga_ct = ContentType.objects.get_for_model(Organisation) + data = { + 'name': "CEO", + 'allowed_apps': ['creme_core'], + 'admin_4_apps': ['creme_core', 'creme_api', 'persons'], + 'creatable_ctypes': [contact_ct.id, orga_ct.id], + 'exportable_ctypes': [contact_ct.id, orga_ct.id], + } + response = self.make_request(data=data) + self.assertValidationErrors(response, { + 'admin_4_apps': ["admin_4_not_allowed_app", "admin_4_not_allowed_app"], + 'creatable_ctypes': ["not_allowed_ctype", "not_allowed_ctype"], + 'exportable_ctypes': ["not_allowed_ctype", "not_allowed_ctype"], + }) + + def test_create_role(self): + contact_ct = ContentType.objects.get_for_model(Contact) + orga_ct = ContentType.objects.get_for_model(Organisation) + data = { + 'name': "CEO", + 'allowed_apps': ['creme_core', 'creme_api', 'persons'], + 'admin_4_apps': ['creme_core', 'creme_api'], + 'creatable_ctypes': [contact_ct.id, orga_ct.id], + 'exportable_ctypes': [], + } + response = self.make_request(data=data) + role = UserRole.objects.get(id=response.data['id']) + self.assertResponseEqual(response, 201, { + 'id': role.id, + 'name': "CEO", + 'allowed_apps': {'creme_core', 'creme_api', 'persons'}, + 'admin_4_apps': {'creme_core', 'creme_api'}, + 'creatable_ctypes': [contact_ct.id, orga_ct.id], + 'exportable_ctypes': [], + 'credentials': [], + }) + self.assertEqual(role.name, "CEO") + self.assertEqual(role.allowed_apps, {'creme_core', 'persons', 'creme_api'}) + self.assertEqual(role.admin_4_apps, {'creme_core', 'creme_api'}) + self.assertEqual(list(role.creatable_ctypes.all()), [contact_ct, orga_ct]) + self.assertEqual(list(role.exportable_ctypes.all()), []) + + +class RetrieveRoleTestCase(CremeAPITestCase): + url_name = 'creme_api__roles-detail' + method = 'get' + + def test_retrieve_role(self): + contact_ct = ContentType.objects.get_for_model(Contact) + orga_ct = ContentType.objects.get_for_model(Organisation) + role = self.factory.role() + + response = self.make_request(to=role.id) + self.assertResponseEqual(response, 200, { + 'id': role.id, + 'name': "Basic", + 'allowed_apps': {'creme_core', 'creme_api', 'persons'}, + 'admin_4_apps': {'creme_core', 'creme_api'}, + 'creatable_ctypes': [contact_ct.id, orga_ct.id], + 'exportable_ctypes': [contact_ct.id], + 'credentials': [], + }) + + +class UpdateRoleTestCase(CremeAPITestCase): + url_name = 'creme_api__roles-detail' + method = 'put' + + def test_validation__required(self): + role = self.factory.role() + response = self.make_request(to=role.id, data={}) + self.assertValidationErrors(response, { + 'name': ['required'], + 'allowed_apps': ['required'], + 'admin_4_apps': ['required'], + 'creatable_ctypes': ['required'], + 'exportable_ctypes': ['required'], + }) + + def test_validation(self): + contact_ct = ContentType.objects.get_for_model(Contact) + orga_ct = ContentType.objects.get_for_model(Organisation) + role = self.factory.role() + data = { + 'name': "CEO", + 'allowed_apps': ['creme_core'], + 'admin_4_apps': ['creme_core', 'persons'], + 'creatable_ctypes': [contact_ct.id], + 'exportable_ctypes': [orga_ct.id], + } + response = self.make_request(to=role.id, data=data) + self.assertValidationErrors(response, { + 'admin_4_apps': ["admin_4_not_allowed_app"], + 'creatable_ctypes': ["not_allowed_ctype"], + 'exportable_ctypes': ["not_allowed_ctype"], + }) + + def test_update_role(self): + contact_ct = ContentType.objects.get_for_model(Contact) + orga_ct = ContentType.objects.get_for_model(Organisation) + role = self.factory.role() + data = { + 'name': "CEO", + 'allowed_apps': ['creme_core', 'persons'], + 'admin_4_apps': ['creme_core', 'persons'], + 'creatable_ctypes': [contact_ct.id], + 'exportable_ctypes': [orga_ct.id], + } + response = self.make_request(to=role.id, data=data) + self.assertResponseEqual(response, 200, { + 'id': role.id, + 'name': "CEO", + 'allowed_apps': {'creme_core', 'persons'}, + 'admin_4_apps': {'creme_core', 'persons'}, + 'creatable_ctypes': [contact_ct.id], + 'exportable_ctypes': [orga_ct.id], + 'credentials': [], + }) + role.refresh_from_db() + self.assertEqual(role.name, "CEO") + self.assertEqual(role.allowed_apps, {'creme_core', 'persons'}) + self.assertEqual(role.admin_4_apps, {'creme_core', 'persons'}) + self.assertEqual(list(role.creatable_ctypes.all()), [contact_ct]) + self.assertEqual(list(role.exportable_ctypes.all()), [orga_ct]) + + +class PartialUpdateRoleTestCase(CremeAPITestCase): + url_name = 'creme_api__roles-detail' + method = 'patch' + + def test_validation__name_unique(self): + self.factory.role(name="UniqueRoleName") + role = self.factory.role(name="OtherName") + data = { + 'name': "UniqueRoleName", + } + response = self.make_request(to=role.id, data=data) + self.assertValidationError(response, 'name', ['unique']) + + def test_validation(self): + role = self.factory.role() + data = { + 'allowed_apps': ['creme_core'], + } + response = self.make_request(to=role.id, data=data) + self.assertValidationErrors(response, { + 'admin_4_apps': ["admin_4_not_allowed_app"], + 'creatable_ctypes': ["not_allowed_ctype", "not_allowed_ctype"], + 'exportable_ctypes': ["not_allowed_ctype"], + }) + + def test_partial_update_role(self): + contact_ct = ContentType.objects.get_for_model(Contact) + orga_ct = ContentType.objects.get_for_model(Organisation) + role = self.factory.role() + + data = { + 'name': "CEO", + } + response = self.make_request(to=role.id, data=data) + self.assertResponseEqual(response, 200, { + 'id': role.id, + 'name': "CEO", + 'allowed_apps': {'creme_core', 'persons', 'creme_api'}, + 'admin_4_apps': {'creme_core', 'creme_api'}, + 'creatable_ctypes': [contact_ct.id, orga_ct.id], + 'exportable_ctypes': [contact_ct.id], + 'credentials': [], + }) + role.refresh_from_db() + self.assertEqual(role.name, "CEO") + self.assertEqual(role.allowed_apps, {'creme_core', 'persons', 'creme_api'}) + self.assertEqual(role.admin_4_apps, {'creme_core', 'creme_api'}) + self.assertEqual(list(role.creatable_ctypes.all()), [contact_ct, orga_ct]) + self.assertEqual(list(role.exportable_ctypes.all()), [contact_ct]) + + data = { + 'allowed_apps': ['creme_core', 'persons', 'creme_api'], + 'exportable_ctypes': [contact_ct.id, orga_ct.id], + } + response = self.make_request(to=role.id, data=data) + self.assertResponseEqual(response, 200, { + 'id': role.id, + 'name': "CEO", + 'allowed_apps': {'creme_core', 'persons', 'creme_api'}, + 'admin_4_apps': {'creme_core', 'creme_api'}, + 'creatable_ctypes': [contact_ct.id, orga_ct.id], + 'exportable_ctypes': [contact_ct.id, orga_ct.id], + 'credentials': [], + }) + role.refresh_from_db() + self.assertEqual(role.name, "CEO") + self.assertEqual(role.allowed_apps, {'creme_core', 'persons', 'creme_api'}) + self.assertEqual(role.admin_4_apps, {'creme_core', 'creme_api'}) + self.assertEqual(list(role.creatable_ctypes.all()), [contact_ct, orga_ct]) + self.assertEqual(list(role.exportable_ctypes.all()), [contact_ct, orga_ct]) + + +class ListRoleTestCase(CremeAPITestCase): + url_name = 'creme_api__roles-list' + method = 'get' + + def test_list_roles(self): + contact_ct = ContentType.objects.get_for_model(Contact) + orga_ct = ContentType.objects.get_for_model(Organisation) + role1 = self.factory.role(name='Role #1') + role2 = self.factory.role(name='Role #2') + + response = self.make_request() + self.assertResponseEqual(response, 200, [ + { + 'id': role1.id, + 'name': "Role #1", + 'allowed_apps': {'creme_core', 'persons', 'creme_api'}, + 'admin_4_apps': {'creme_core', 'creme_api'}, + 'creatable_ctypes': [contact_ct.id, orga_ct.id], + 'exportable_ctypes': [contact_ct.id], + 'credentials': [], + }, + { + 'id': role2.id, + 'name': "Role #2", + 'allowed_apps': {'creme_core', 'persons', 'creme_api'}, + 'admin_4_apps': {'creme_core', 'creme_api'}, + 'creatable_ctypes': [contact_ct.id, orga_ct.id], + 'exportable_ctypes': [contact_ct.id], + 'credentials': [], + } + ]) + + +class DeleteRoleTestCase(CremeAPITestCase): + url_name = 'creme_api__roles-detail' + method = 'delete' + + def test_delete_role__protected(self): + role = self.factory.role() + self.factory.user(role=role) + response = self.make_request(to=role.id) + self.assertResponseEqual(response, 403) + + def test_delete_role(self): + role = self.factory.role() + response = self.make_request(to=role.id) + self.assertResponseEqual(response, 204) + self.assertFalse(UserRole.objects.filter(id=role.id).exists()) diff --git a/creme/creme_api/tests/functional/test_teams.py b/creme/creme_api/tests/functional/test_teams.py new file mode 100644 index 0000000000..53c4a35242 --- /dev/null +++ b/creme/creme_api/tests/functional/test_teams.py @@ -0,0 +1,215 @@ +from django.contrib.auth import get_user_model +from django.urls import reverse + +from creme.creme_api.tests.utils import CremeAPITestCase +from creme.persons import get_contact_model + +CremeUser = get_user_model() +Contact = get_contact_model() + + +class CreateTeamTestCase(CremeAPITestCase): + url_name = 'creme_api__teams-list' + method = 'post' + + def test_validation__required(self): + response = self.make_request(data={}) + self.assertValidationErrors(response, { + 'name': ['required'], + 'teammates': ['required'], + }) + + def test_validation__name_max_length(self): + data = {'name': "a" * (CremeUser._meta.get_field('username').max_length + 1)} + response = self.make_request(data=data) + self.assertValidationError(response, 'name', ['max_length']) + + def test_validation__name_invalid_chars(self): + data = {'name': "*********"} + response = self.make_request(data=data) + self.assertValidationError(response, 'name', ['invalid']) + + def test_validation__teammates(self): + data = {'name': "TEAM", 'teammates': [9999]} + response = self.make_request(data=data) + self.assertValidationError(response, 'teammates', ['does_not_exist']) + + def test_create_team(self): + user1 = self.factory.user(username="user1") + user2 = self.factory.user(username="user2") + + data = {'name': "creme-team", 'teammates': [user1.id, user2.id]} + response = self.make_request(data=data) + team = CremeUser.objects.get(id=response.data['id']) + + self.assertResponseEqual(response, 201, { + 'id': team.id, + 'teammates': [user1.id, user2.id], + 'name': "creme-team", + }) + + self.assertTrue(team.is_team) + self.assertEqual(team.username, "creme-team") + self.assertEqual(team.teammates, {user1.id: user1, user2.id: user2}) + + +class RetrieveTeamTestCase(CremeAPITestCase): + url_name = 'creme_api__teams-detail' + method = 'get' + + def test_get_team(self): + user = self.factory.user() + team = self.factory.team(teammates=[user]) + + response = self.make_request(to=team.id) + self.assertResponseEqual(response, 200, { + 'id': team.id, + 'teammates': [user.id], + 'name': 'Team #1', + }) + + +class UpdateTeamTestCase(CremeAPITestCase): + url_name = 'creme_api__teams-detail' + method = 'put' + + def test_validation__required(self): + team = self.factory.team() + response = self.make_request(to=team.id, data={}) + self.assertValidationErrors(response, { + 'name': ['required'], + 'teammates': ['required'], + }) + + def test_update_team(self): + user = self.factory.user() + team = self.factory.team(teammates=[user]) + + user2 = self.factory.user(username="user2") + data = {'name': "Sales", 'teammates': [user2.id]} + response = self.make_request(to=team.id, data=data) + + self.assertResponseEqual(response, 200, { + 'id': team.id, + 'teammates': [user2.id], + 'name': 'Sales', + }) + + team.refresh_from_db() + self.assertTrue(team.is_team) + self.assertEqual(team.username, "Sales") + self.assertEqual(team.teammates, {user2.id: user2}) + + +class PartialUpdateTeamTestCase(CremeAPITestCase): + url_name = 'creme_api__teams-detail' + method = 'patch' + + def test_partial_update_team__name(self): + user = self.factory.user() + team = self.factory.team(teammates=[user]) + + data = {'name': "Sales"} + response = self.make_request(to=team.id, data=data) + self.assertResponseEqual(response, 200, { + 'id': team.id, + 'teammates': [user.id], + 'name': 'Sales', + }) + + team.refresh_from_db() + self.assertTrue(team.is_team) + self.assertEqual(team.username, "Sales") + self.assertEqual(team.teammates, {user.id: user}) + + def test_partial_update_team__teammates(self): + user = self.factory.user() + team = self.factory.team(teammates=[user]) + + # change + user2 = self.factory.user(username='user2') + data = {'teammates': [user2.id]} + response = self.make_request(to=team.id, data=data) + self.assertResponseEqual(response, 200, { + 'id': team.id, + 'teammates': [user2.id], + 'name': 'Team #1', + }) + + team.refresh_from_db() + self.assertTrue(team.is_team) + self.assertEqual(team.username, "Team #1") + self.assertEqual(team.teammates, {user2.id: user2}) + + # empty + data = {'teammates': []} + response = self.make_request(to=team.id, data=data) + self.assertResponseEqual(response, 200, { + 'id': team.id, + 'teammates': [], + 'name': 'Team #1', + }) + + team.refresh_from_db() + self.assertTrue(team.is_team) + self.assertEqual(team.username, "Team #1") + self.assertEqual(team.teammates, {}) + + +class ListTeamTestCase(CremeAPITestCase): + url_name = 'creme_api__teams-list' + method = 'get' + + def test_list_teams(self): + user1 = self.factory.user(username="user1") + team1 = self.factory.team(name='test1', teammates=[user1]) + user2 = self.factory.user(username="user2") + team2 = self.factory.team(name='test2', teammates=[user1, user2]) + teams = CremeUser.objects.filter(is_team=True) + self.assertEqual(teams.count(), 2, teams) + + response = self.make_request() + self.assertResponseEqual(response, 200, [ + { + 'id': team1.id, + 'teammates': [user1.id], + 'name': 'test1', + }, + { + 'id': team2.id, + 'teammates': [user1.id, user2.id], + 'name': 'test2', + }, + ]) + + +class DeleteTeamTestCase(CremeAPITestCase): + url_name = 'creme_api__teams-delete' + method = 'post' + + def test_delete(self): + url = reverse('creme_api__teams-detail', args=[1]) + response = self.client.delete(url, format='json') + self.assertResponseEqual(response, 405) + + def test_delete_team(self): + user = self.factory.user() + team1 = self.factory.team(name='team1') + team2 = self.factory.team(name='team2') + contact = self.factory.contact(user=team2) + + data = {'transfer_to': team1.id} + response = self.make_request(to=team2.id, data=data) + self.assertResponseEqual(response, 204) + + self.assertFalse(CremeUser.objects.filter(username='team2').exists()) + contact.refresh_from_db() + self.assertEqual(contact.user, team1) + + data = {'transfer_to': user.id} + response = self.make_request(to=team1.id, data=data) + self.assertResponseEqual(response, 204) + + self.assertFalse(CremeUser.objects.filter(username='team1').exists()) + contact.refresh_from_db() + self.assertEqual(contact.user, user) diff --git a/creme/creme_api/tests/functional/test_tokens.py b/creme/creme_api/tests/functional/test_tokens.py new file mode 100644 index 0000000000..6cde61d240 --- /dev/null +++ b/creme/creme_api/tests/functional/test_tokens.py @@ -0,0 +1,47 @@ +from uuid import uuid4 + +from creme.creme_api.models import Token +from creme.creme_api.tests.utils import CremeAPITestCase + + +class TokensTestCase(CremeAPITestCase): + auto_login = False + url_name = 'creme_api__tokens' + method = 'post' + + def test_create_token__missing(self): + response = self.make_request(data={}) + self.assertValidationErrors(response, { + 'client_id': ['required'], + 'client_secret': ['required'], + }) + + def test_create_token__empty(self): + data = { + "client_id": "", # trim + "client_secret": "", + } + response = self.make_request(data=data) + self.assertValidationErrors(response, { + 'client_id': ['invalid'], # Must be a valid UUID. + 'client_secret': ['blank'], + }) + + def test_create_token__no_application(self): + data = { + "client_id": uuid4().hex, + "client_secret": "Secret", + } + response = self.make_request(data=data) + self.assertValidationErrors(response, { + '': ['authentication_failure'], + }) + + def test_create_token(self): + data = { + "client_id": self.application.client_id, + "client_secret": self.application._client_secret, + } + response = self.make_request(data=data) + token = Token.objects.get(application=self.application) + self.assertResponseEqual(response, 200, {"token": token.code}) diff --git a/creme/creme_api/tests/functional/test_users.py b/creme/creme_api/tests/functional/test_users.py new file mode 100644 index 0000000000..9797bca7dd --- /dev/null +++ b/creme/creme_api/tests/functional/test_users.py @@ -0,0 +1,350 @@ +from django.contrib.auth import get_user_model +from django.urls import reverse + +from creme.creme_api.tests.utils import CremeAPITestCase + +CremeUser = get_user_model() + + +class CreateUserTestCase(CremeAPITestCase): + url_name = 'creme_api__users-list' + method = 'post' + + def test_validation__required(self): + response = self.make_request(data={}) + self.assertValidationErrors(response, { + 'username': ['required'], + 'first_name': ['required'], + 'last_name': ['required'], + 'email': ['required'], + }) + + def test_validation__username_max_length(self): + data = {'username': "a" * (CremeUser._meta.get_field('username').max_length + 1)} + response = self.make_request(data=data) + self.assertValidationError(response, 'username', ['max_length']) + + def test_validation__username_invalid_chars(self): + data = {'username': "*********"} + response = self.make_request(data=data) + self.assertValidationError(response, 'username', ['invalid']) + + def test_validation__is_superuser_xor_role(self): + role = self.factory.role() + + data = self.factory.user_data(is_superuser=False, role=None) + response = self.make_request(data=data) + self.assertValidationError(response, '', ['is_superuser_xor_role']) + + data = self.factory.user_data(is_superuser=True, role=role.id) + response = self.make_request(data=data) + self.assertValidationError(response, '', ['is_superuser_xor_role']) + + def test_create_superuser(self): + data = self.factory.user_data(is_superuser=True, role=None) + response = self.make_request(data=data) + user = CremeUser.objects.get(id=response.data['id']) + self.assertResponseEqual(response, 201, { + 'id': user.id, + 'username': 'john.doe', + 'last_name': 'Doe', + 'first_name': 'John', + 'email': 'john.doe@provider.com', + 'date_joined': self.to_iso8601(user.date_joined), + 'last_login': None, + 'is_active': True, + 'is_superuser': True, + 'role': None, + 'time_zone': 'Europe/Paris', + 'theme': 'icecream' + }) + self.assertEqual(user.username, "john.doe") + self.assertTrue(user.is_superuser) + + def test_create_user(self): + role = self.factory.role() + data = self.factory.user_data(is_superuser=False, role=role.id) + response = self.make_request(data=data) + user = CremeUser.objects.get(id=response.data['id']) + self.assertResponseEqual(response, 201, { + 'id': user.id, + 'username': 'john.doe', + 'last_name': 'Doe', + 'first_name': 'John', + 'email': 'john.doe@provider.com', + 'date_joined': self.to_iso8601(user.date_joined), + 'last_login': None, + 'is_active': True, + 'is_superuser': False, + 'role': role.id, + 'time_zone': 'Europe/Paris', + 'theme': 'icecream' + }) + self.assertEqual(user.username, "john.doe") + + +class RetrieveUserTestCase(CremeAPITestCase): + url_name = 'creme_api__users-detail' + method = 'get' + + def test_get_user(self): + user = self.factory.user() + response = self.make_request(to=user.id) + self.assertResponseEqual(response, 200, { + 'id': user.id, + 'username': user.username, + 'last_name': user.last_name, + 'first_name': user.first_name, + 'email': user.email, + 'date_joined': self.to_iso8601(user.date_joined), + 'last_login': None, + 'is_active': True, + 'is_superuser': True, + 'role': None, + 'time_zone': 'Europe/Paris', + 'theme': 'icecream' + }) + + +class UpdateUserTestCase(CremeAPITestCase): + url_name = 'creme_api__users-detail' + method = 'put' + + def test_validation__required(self): + user = self.factory.user() + response = self.make_request(to=user.id, data={}) + self.assertValidationErrors(response, { + 'username': ['required'], + 'first_name': ['required'], + 'last_name': ['required'], + 'email': ['required'], + }) + + def test_validation__is_superuser_xor_role(self): + role = self.factory.role() + user = self.factory.user(is_superuser=True, role=None) + + data = self.factory.user_data(is_superuser=False, role=None) + response = self.make_request(to=user.id, data=data) + self.assertValidationError(response, '', ['is_superuser_xor_role']) + + data = self.factory.user_data(is_superuser=True, role=role.id) + response = self.make_request(to=user.id, data=data) + self.assertValidationError(response, '', ['is_superuser_xor_role']) + + def test_update_user(self): + user = self.factory.user() + + data = self.factory.user_data(last_name="Smith", username="Nick") + response = self.make_request(to=user.id, data=data) + self.assertResponseEqual(response, 200, { + 'id': user.id, + 'username': 'Nick', + 'last_name': 'Smith', + 'first_name': 'John', + 'email': 'john.doe@provider.com', + 'date_joined': self.to_iso8601(user.date_joined), + 'last_login': None, + 'is_active': True, + 'is_superuser': True, + 'role': None, + 'time_zone': 'Europe/Paris', + 'theme': 'icecream' + }) + user.refresh_from_db() + self.assertEqual(user.username, "Nick") + self.assertEqual(user.last_name, "Smith") + + +class PartialUpdateUserTestCase(CremeAPITestCase): + url_name = 'creme_api__users-detail' + method = 'patch' + + def test_validation__is_superuser_xor_role__superuser(self): + role = self.factory.role() + user = self.factory.user(username='user1', is_superuser=True, role=None) + + data = {'role': role.id} + response = self.make_request(to=user.id, data=data) + self.assertValidationError(response, '', ['is_superuser_xor_role']) + + data = {'is_superuser': False} + response = self.make_request(to=user.id, data=data) + self.assertValidationError(response, '', ['is_superuser_xor_role']) + + def test_validation__is_superuser_xor_role__role(self): + role = self.factory.role() + user = self.factory.user(username='user2', is_superuser=False, role=role) + + data = {'role': None} + response = self.make_request(to=user.id, data=data) + self.assertValidationError(response, '', ['is_superuser_xor_role']) + + data = {'is_superuser': True} + response = self.make_request(to=user.id, data=data) + self.assertValidationError(response, '', ['is_superuser_xor_role']) + + def test_partial_update_user(self): + user = self.factory.user() + data = {'theme': "chantilly"} + response = self.make_request(to=user.id, data=data) + self.assertResponseEqual(response, 200, { + 'id': user.id, + 'username': 'john.doe', + 'last_name': 'Doe', + 'first_name': 'John', + 'email': 'john.doe@provider.com', + 'date_joined': self.to_iso8601(user.date_joined), + 'last_login': None, + 'is_active': True, + 'is_superuser': True, + 'role': None, + 'time_zone': 'Europe/Paris', + 'theme': 'chantilly' + }) + user.refresh_from_db() + self.assertEqual(user.theme, "chantilly") + + +class ListUserTestCase(CremeAPITestCase): + url_name = 'creme_api__users-list' + method = 'get' + + def test_list_users(self): + fulbert = CremeUser.objects.get() + user = self.factory.user(username="user", theme='chantilly') + self.assertEqual(CremeUser.objects.count(), 2) + + response = self.make_request() + self.assertResponseEqual(response, 200, [ + { + 'id': fulbert.id, + 'username': 'root', + 'last_name': 'Creme', + 'first_name': 'Fulbert', + 'email': fulbert.email, + 'date_joined': self.to_iso8601(fulbert.date_joined), + 'last_login': None, + 'is_active': True, + 'is_superuser': True, + 'role': None, + 'time_zone': 'Europe/Paris', + 'theme': 'icecream' + }, + { + 'id': user.id, + 'username': 'user', + 'last_name': 'Doe', + 'first_name': 'John', + 'email': 'john.doe@provider.com', + 'date_joined': self.to_iso8601(user.date_joined), + 'last_login': None, + 'is_active': True, + 'is_superuser': True, + 'role': None, + 'time_zone': 'Europe/Paris', + 'theme': 'chantilly' + }, + ]) + + +class SetPasswordUserTestCase(CremeAPITestCase): + url_name = 'creme_api__users-set-password' + method = 'post' + + def test_password_validation__required(self): + user = self.factory.user() + response = self.make_request(to=user.id, data={}) + self.assertValidationErrors(response, { + 'password': ['required'] + }) + + def test_password_validation__blank(self): + user = self.factory.user() + + data = {'password': ''} + response = self.make_request(to=user.id, data=data) + self.assertValidationError(response, 'password', ['blank']) + + def test_password_validation__no_trim(self): + user = self.factory.user() + + data = {'password': " StrongPassword "} + response = self.make_request(to=user.id, data=data) + self.assertResponseEqual(response, 200, {}) + + user.refresh_from_db() + self.assertTrue(user.check_password(" StrongPassword ")) + + def test_password_validation__similarity(self): + user = self.factory.user( + username="76aa224e-056a", + first_name="4816-ac3e", + last_name="ffe6e2c0748c", + email='df8e4b1a4f39@provider.com' + ) + + data = {'password': user.username} + response = self.make_request(to=user.id, data=data) + self.assertValidationError(response, 'password', ['password_too_similar']) + + data = {'password': user.first_name} + response = self.make_request(to=user.id, data=data) + self.assertValidationError(response, 'password', ['password_too_similar']) + + data = {'password': user.last_name} + response = self.make_request(to=user.id, data=data) + self.assertValidationError(response, 'password', ['password_too_similar']) + + data = {'password': user.email.split("@")[0]} + response = self.make_request(to=user.id, data=data) + self.assertValidationError(response, 'password', ['password_too_similar']) + + def test_set_password_user(self): + user = self.factory.user() + + data = {'password': "StrongPassword"} + response = self.make_request(to=user.id, data=data) + self.assertResponseEqual(response, 200, {}) + + user.refresh_from_db() + self.assertTrue(user.check_password("StrongPassword")) + + +class DeleteUserTestCase(CremeAPITestCase): + url_name = 'creme_api__users-delete' + method = 'post' + + def test_delete(self): + url = reverse('creme_api__users-detail', args=[1]) + response = self.client.delete(url, format='json') + self.assertResponseEqual(response, 405) + + def test_validation__required(self): + user = self.factory.user() + response = self.make_request(to=user.id, data={}) + self.assertValidationErrors(response, { + 'transfer_to': ['required'] + }) + + def test_delete_user(self): + team = self.factory.team() + user1 = self.factory.user(username='user1') + user2 = self.factory.user(username='user2') + contact = self.factory.contact(user=user2) + + data = {'transfer_to': user1.id} + response = self.make_request(to=user2.id, data=data) + self.assertResponseEqual(response, 204) + + self.assertFalse(CremeUser.objects.filter(username='user2').exists()) + contact.refresh_from_db() + self.assertEqual(contact.user, user1) + + data = {'transfer_to': team.id} + response = self.make_request(to=user1.id, data=data) + self.assertResponseEqual(response, 204) + + self.assertFalse(CremeUser.objects.filter(username='user1').exists()) + contact.refresh_from_db() + self.assertEqual(contact.user, team) diff --git a/creme/creme_api/tests/test_models.py b/creme/creme_api/tests/test_models.py new file mode 100644 index 0000000000..11849f4d72 --- /dev/null +++ b/creme/creme_api/tests/test_models.py @@ -0,0 +1,109 @@ +from uuid import uuid4 + +from django.contrib.auth.hashers import check_password +from django.test import TestCase +from django.utils import timezone + +from creme.creme_api.models import Application, Token + + +class ApplicationTestCase(TestCase): + def test_init(self): + application = Application(name="TestCase") + self.assertTrue(application.client_id) + self.assertEqual(len(application.client_secret), 0) + self.assertTrue(application.enabled) + self.assertEqual(application.token_duration, 3600) + self.assertEqual(application.client_secret, "") + self.assertIsNone(application._client_secret) + + def test_str(self): + application = Application(name="TestCase") + self.assertEqual(str(application), "TestCase") + + def test_set_client_secret(self): + application = Application(name="TestCase") + application.set_client_secret("Password") + self.assertEqual(application._client_secret, "Password") + self.assertIsNotNone(application.client_secret) + self.assertTrue(check_password("Password", application.client_secret)) + + def test_save01(self): + application = Application(name="TestCase") + application.save() + self.assertIsNotNone(application._client_secret) + self.assertIsNotNone(application.client_secret) + self.assertTrue(check_password(application._client_secret, application.client_secret)) + + application.save() + self.assertTrue(check_password(application._client_secret, application.client_secret)) + + def test_save02(self): + application = Application.objects.create(name="TestCase") + self.assertIsNotNone(application._client_secret) + self.assertIsNotNone(application.client_secret) + self.assertTrue(check_password(application._client_secret, application.client_secret)) + + def test_check_client_secret(self): + application = Application(name="TestCase") + application.set_client_secret("Password") + self.assertTrue(application.check_client_secret("Password")) + self.assertFalse(application.check_client_secret("WrongPassword")) + + def test_can_authenticate(self): + application = Application(name="TestCase", enabled=True) + self.assertTrue(application.can_authenticate()) + application = Application(name="TestCase", enabled=False) + self.assertFalse(application.can_authenticate()) + + def test_authenticate01(self): + self.assertIsNone(Application.authenticate("client_id", "Secret")) + + def test_authenticate02(self): + self.assertIsNone(Application.authenticate(uuid4().hex, "WrongSecret")) + + def test_authenticate03(self): + application = Application.objects.create(name="TestCase", enabled=False) + self.assertIsNone( + Application.authenticate(application.client_id, application._client_secret) + ) + + def test_authenticate04(self): + application = Application.objects.create(name="TestCase") + authenticated_application = Application.authenticate( + application.client_id, application._client_secret) + self.assertEqual(authenticated_application.pk, application.pk) + + +class TokenTestCase(TestCase): + def test_init(self): + application = Application.objects.create(name="TestCase") + token = Token(application=application) + self.assertEqual(len(token.code), 128) + + def test_save01(self): + application = Application.objects.create(name="TestCase", token_duration=20) + token = Token(application=application) + self.assertIsNone(token.expires) + token.save() + expected_expires = timezone.now() + timezone.timedelta(seconds=20) + self.assertAlmostEqual( + token.expires, expected_expires, delta=timezone.timedelta(seconds=1)) + + def test_save02(self): + application = Application.objects.create(name="TestCase", token_duration=20) + expires = timezone.now() + timezone.timedelta(seconds=10) + token = Token.objects.create(application=application, expires=expires) + self.assertEqual(token.expires, expires) + + def test_is_expired01(self): + application = Application.objects.create(name="TestCase") + expires = timezone.now() + timezone.timedelta(seconds=10) + token = Token.objects.create(application=application, expires=expires) + self.assertFalse(token.is_expired()) + + def test_is_expired02(self): + application = Application.objects.create(name="TestCase") + expires = timezone.now() - timezone.timedelta(seconds=10) + token = Token.objects.create(application=application, expires=expires) + self.assertTrue(token.is_expired()) diff --git a/creme/creme_api/tests/utils.py b/creme/creme_api/tests/utils.py new file mode 100644 index 0000000000..2e209fd04f --- /dev/null +++ b/creme/creme_api/tests/utils.py @@ -0,0 +1,162 @@ +from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType +from django.urls import reverse +from rest_framework.fields import DateTimeField +from rest_framework.test import APITestCase + +from creme.creme_api.api.authentication import TokenAuthentication +from creme.creme_api.models import Application, Token +from creme.creme_core.models import SetCredentials, UserRole +from creme.persons import get_contact_model, get_organisation_model + +Contact = get_contact_model() +CremeUser = get_user_model() +Organisation = get_organisation_model() + + +class Factory: + def user(self, **kwargs): + data = { + 'username': 'john.doe', + 'first_name': 'John', + 'last_name': 'Doe', + 'email': 'john.doe@provider.com', + 'is_active': True, + 'is_superuser': True, + 'role': None, + } + data.update(**kwargs) + return CremeUser.objects.create(**data) + + def user_data(self, **kwargs): + data = { + 'username': "john.doe", + 'first_name': "John", + 'last_name': "Doe", + 'email': "john.doe@provider.com", + 'is_active': True, + "is_superuser": True, + 'role': None, + } + data.update(**kwargs) + return data + + def team(self, **kwargs): + data = { + 'username': 'Team #1', + } + data.update(**kwargs) + if 'name' in data: + data['username'] = data.pop('name') + data['is_team'] = True + teammates = data.pop('teammates', []) + + team = CremeUser.objects.create(**data) + team.teammates = teammates + + return team + + def contact(self, **kwargs): + return Contact.objects.create(**kwargs) + + def role(self, **kwargs): + contact_ct = ContentType.objects.get_for_model(Contact) + orga_ct = ContentType.objects.get_for_model(Organisation) + data = { + 'name': "Basic", + 'allowed_apps': ['creme_core', 'creme_api', 'persons'], + 'admin_4_apps': ['creme_core', 'creme_api'], + 'creatable_ctypes': [contact_ct.id, orga_ct.id], + 'exportable_ctypes': [contact_ct.id], + } + data.update(**kwargs) + role = UserRole(name=data['name']) + role.allowed_apps = data['allowed_apps'] + role.admin_4_apps = data['admin_4_apps'] + role.save() + role.creatable_ctypes.set(data['creatable_ctypes']) + role.exportable_ctypes.set(data['exportable_ctypes']) + return role + + def credential(self, **kwargs): + contact_ct = ContentType.objects.get_for_model(Contact) + perms = {'can_view', 'can_change', 'can_delete', 'can_link', 'can_unlink'} + data = { + 'set_type': SetCredentials.ESET_OWN, + 'ctype': contact_ct, + 'forbidden': False, + 'efilter': None, + **{p: True for p in perms} + } + data.update(**kwargs) + if 'role' not in data: + data['role'] = self.role() + value = {k: data.pop(k) for k in perms} + creds = SetCredentials(**data) + creds.set_value(**value) + creds.save() + return creds + + +class CremeAPITestCase(APITestCase): + auto_login = True + url_name = None + method = None + maxDiff = None + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.application = Application.objects.create(name="APITestCase") + cls.factory = Factory() + + def login(self, application): + self.token = Token.objects.create(application=application) + self.client.credentials( + HTTP_AUTHORIZATION=f"{TokenAuthentication.keyword} {self.token.code}" + ) + + def setUp(self) -> None: + super().setUp() + if self.auto_login: + self.login(self.application) + + def assertValidationError(self, response, field_name, error_codes): + self.assertEqual(response.status_code, 400) + self.assertIn(field_name, response.data, response.data) + codes = [error.code for error in response.data[field_name]] + self.assertEqual(error_codes, codes, response.data) + + def assertValidationErrors(self, response, errors): + self.assertEqual(response.status_code, 400) + current_errors = { + field_name: [error.code for error in errors] + for (field_name, errors) in response.data.items() + } + self.assertEqual(current_errors, errors, response.data) + + def assertResponseEqual(self, response, status_code, data=None): + self.assertEqual(response.status_code, status_code, response.data) + if data is None: + return + if isinstance(data, dict): + self.assertDictEqual(dict(response.data), data) + elif isinstance(data, list): + self.assertEqual(len(response.data['results']), len(data)) + for i, (obj1, obj2) in enumerate(zip(response.data['results'], data)): + self.assertDictEqual( + dict(obj1), obj2, msg=f"Elements response.data['results'][{i}] differ.") + else: + self.assertEqual(response.data, data) + + @staticmethod + def to_iso8601(value): + return DateTimeField().to_representation(value) + + def make_request(self, *, to=None, data=None): + assert self.url_name is not None + assert self.method is not None + args = [to] if to is not None else None + url = reverse(self.url_name, args=args) + method = getattr(self.client, self.method) + return method(url, data=data, format='json') diff --git a/creme/creme_api/urls.py b/creme/creme_api/urls.py new file mode 100644 index 0000000000..91637d4e26 --- /dev/null +++ b/creme/creme_api/urls.py @@ -0,0 +1,36 @@ +from django.urls import include, re_path + +from creme.creme_api.api.routes import router +from creme.creme_api.api.tokens.views import TokenView +from creme.creme_api.views import ( + ApplicationCreation, + ApplicationEdition, + ConfigurationView, + DocumentationView, + SchemaView, +) + +urlpatterns = [ + re_path(r"^tokens[/]?$", TokenView.as_view(), name="creme_api__tokens"), + # re_path(r"^revoke_token/$", oauth_views.RevokeTokenView.as_view(), name="revoke-token"), + # re_path(r"^introspect/$", oauth_views.IntrospectTokenView.as_view(), name="introspect"), + + re_path(r'^openapi[/]?$', SchemaView.as_view(), name='creme_api__openapi_schema'), + re_path(r'^documentation[/]?$', DocumentationView.as_view(), name='creme_api__documentation'), + re_path(r'^configuration[/]?$', ConfigurationView.as_view(), name='creme_api__configuration'), + re_path( + r'^configuration/applications/', + include([ + re_path( + r'^add[/]?$', + ApplicationCreation.as_view(), + name='creme_api__create_application', + ), + re_path( + r'^edit/(?P\d+)[/]?$', + ApplicationEdition.as_view(), + name='creme_api__edit_application', + ), + ]), + ), +] + router.urls diff --git a/creme/creme_api/views.py b/creme/creme_api/views.py new file mode 100644 index 0000000000..144fb087df --- /dev/null +++ b/creme/creme_api/views.py @@ -0,0 +1,112 @@ +from django.contrib import messages +from django.template.loader import render_to_string +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from rest_framework.authentication import SessionAuthentication +from rest_framework.schemas import openapi +from rest_framework.schemas.views import SchemaView as DRFSchemaView + +from creme.creme_api import VERSION +from creme.creme_api.api.authentication import TokenAuthentication +from creme.creme_api.api.permissions import CremeApiPermission +from creme.creme_core.views import generic +from creme.creme_core.views.generic import base + +from .bricks import ApplicationsBrick +from .forms import ApplicationForm +from .models import Application + + +class SchemaView(DRFSchemaView): + title = _("Creme CRM API") + description_template = "creme_api/description.md" + version = VERSION + authentication_classes = [SessionAuthentication] + permission_classes = [CremeApiPermission] + public = True + generator_class = openapi.SchemaGenerator + + def get_description(self, context=None, request=None): + description = render_to_string( + self.description_template, context=context, request=request) + # Force django safestring into builtin string + return description + "" + + def get_title(self): + return str(self.title) + + def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) + title = self.get_title() + description = self.get_description(request=request) + self.schema_generator = self.generator_class( + title=title, + description=description, + version=self.version, + ) + + +class CremeApiView(base.BricksView): + title = _("Creme CRM API") + permissions = "creme_api" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['page_title'] = self.title + return context + + +class DocumentationView(CremeApiView): + template_name = 'creme_api/documentation.html' + extra_context = {'schema_url': 'creme_api__openapi_schema'} + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['creme_api__tokens_url'] = self.request.build_absolute_uri( + reverse("creme_api__tokens")) + context['token_type'] = TokenAuthentication.keyword + return context + + +class ConfigurationView(CremeApiView): + template_name = 'creme_api/configuration.html' + + def get_brick_ids(self): + return [ApplicationsBrick.id_] + + +class ApplicationCreation(generic.CremeModelCreationPopup): + model = Application + form_class = ApplicationForm + title = _('New Application') + success_message = _( + "The application «{application_name}» has been created. " + "Identifiers have been generated, here they are: \n\n" + "Client ID : {client_id}\n" + "Client Secret : {client_secret}\n\n" + "This is the first and last time this secret displayed!" + ) + + def get_success_message(self): + return self.success_message.format( + application_name=self.object.name, + client_id=self.object.client_id, + client_secret=self.object._client_secret, + ) + + def form_valid(self, form): + response = super(ApplicationCreation, self).form_valid(form) + message = self.get_success_message() + messages.success(self.request, message) + return response + + +class ApplicationEdition(generic.CremeModelEditionPopup): + model = Application + form_class = ApplicationForm + pk_url_kwarg = 'application_id' + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['request'] = self.request + return kwargs diff --git a/creme/creme_core/models/auth.py b/creme/creme_core/models/auth.py index 399df4d51e..76d8660be0 100644 --- a/creme/creme_core/models/auth.py +++ b/creme/creme_core/models/auth.py @@ -326,6 +326,20 @@ def filter_entities(self, as_model=as_model, ) + def refresh_from_db(self, *args, **kwargs): + self._allowed_apps = None + self._extended_allowed_apps = None + + self._admin_4_apps = None + self._extended_admin_4_apps = None + + self._creatable_ctypes_set = None + self._exportable_ctypes_set = None + + self._setcredentials = None + + super().refresh_from_db(*args, **kwargs) + class SetCredentials(models.Model): # 'ESET' means 'Entities SET' @@ -801,6 +815,26 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) + @property + def can_view(self): + return bool(self.value & EntityCredentials.VIEW) + + @property + def can_change(self): + return bool(self.value & EntityCredentials.CHANGE) + + @property + def can_delete(self): + return bool(self.value & EntityCredentials.DELETE) + + @property + def can_link(self): + return bool(self.value & EntityCredentials.LINK) + + @property + def can_unlink(self): + return bool(self.value & EntityCredentials.UNLINK) + def set_value(self, *, can_view: bool, can_change: bool, @@ -1273,6 +1307,12 @@ def has_perm_to_view_or_die(self, entity: 'CremeEntity') -> None: ) ) + def refresh_from_db(self, *args, **kwargs): + self._teams = None + self._teammates = None + self._settings = None + super().refresh_from_db(*args, **kwargs) + get_user_field = CremeUser._meta.get_field for fname in ('password', 'last_login'): diff --git a/creme/creme_core/templatetags/creme_bricks.py b/creme/creme_core/templatetags/creme_bricks.py index a8ef7cf2db..67e4296dfc 100644 --- a/creme/creme_core/templatetags/creme_bricks.py +++ b/creme/creme_core/templatetags/creme_bricks.py @@ -939,7 +939,7 @@ def my_view(request): for brick_or_seq in bricks: if brick_or_seq == '': raise ValueError( - '{% brick_declare %}, "bricks" seems empty. Is you variable valid ?' + '{% brick_declare %}, "bricks" seems empty. Is your variable valid ?' ) if hasattr(brick_or_seq, '__iter__'): @@ -1037,7 +1037,7 @@ def pop_group(brick_id): for brick_or_seq in bricks: if brick_or_seq == '': raise ValueError( - '{% brick_display %}: "bricks" seems empty. Is you variable valid ?' + '{% brick_display %}: "bricks" seems empty. Is your variable valid ?' ) # We avoid generator, because we need to iterate twice (import & display) diff --git a/creme/settings.py b/creme/settings.py index 12d59e7e6c..49cb0feb1d 100644 --- a/creme/settings.py +++ b/creme/settings.py @@ -322,6 +322,8 @@ # EXTERNAL APPS 'formtools', 'creme.creme_core.apps.MediaGeneratorConfig', # It manages JS, CSS & static images + + 'rest_framework', ] INSTALLED_CREME_APPS = [ # ---------------------- @@ -454,6 +456,9 @@ # display maps (Google Maps, Open Street Map) using the address information. # I can be useful to plan a business itinerary. 'creme.geolocation', + + # Enable creme API + 'creme.creme_api', ] INSTALLED_APPS = INSTALLED_DJANGO_APPS + INSTALLED_CREME_APPS @@ -1445,6 +1450,35 @@ GEOLOCATION_OSM_COPYRIGHT_URL = 'https://www.openstreetmap.org/copyright' GEOLOCATION_OSM_COPYRIGHT_TITLE = 'OpenStreetMap contributors' +# API +REST_FRAMEWORK = { + # Remove BrowsableAPIRenderer + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + ], + # 'DEFAULT_PARSER_CLASSES': Defaults + + # Authenticate with header Authorization: Token {token} + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'creme.creme_api.api.authentication.TokenAuthentication', + ], + + # TODO: token scopes + 'DEFAULT_PERMISSION_CLASSES': [ + 'creme.creme_api.api.permissions.TokenPermission', + ], + + # Cursor pagination + 'DEFAULT_PAGINATION_CLASS': "creme.creme_api.api.pagination.CremeCursorPagination", + 'PAGE_SIZE': 25, + + # Todo: custom exception handler + # {'errors': [{'code', 'field_name', 'message'}, ...]} + 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler', + 'NON_FIELD_ERRORS_KEY': '', + +} + # APPS CONFIGURATION [END]###################################################### # try: diff --git a/setup.cfg b/setup.cfg index 9d886e99ec..c518aec28a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,9 @@ install_requires = csscompressor ~=0.9 rjsmin ~=1.2 xhtml2pdf ~=0.2.6 + djangorestframework ~=3.13.1 + pyyaml ~=6.0 + uritemplate ~=4.1.1 [options.extras_require] dev= From b3acbb82c4ccd84851cea65293f5f460416e80fc Mon Sep 17 00:00:00 2001 From: Hugo Smett Date: Thu, 10 Feb 2022 12:19:43 +0100 Subject: [PATCH 02/12] wip documentation --- .../creme_api/templates/creme_api/description.md | 8 ++++++++ creme/creme_api/urls.py | 15 ++++++++++----- creme/creme_api/views.py | 14 ++++++++++---- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/creme/creme_api/templates/creme_api/description.md b/creme/creme_api/templates/creme_api/description.md index f07643f4e2..f15ec31c70 100644 --- a/creme/creme_api/templates/creme_api/description.md +++ b/creme/creme_api/templates/creme_api/description.md @@ -1,5 +1,13 @@ # Drive Creme CRM from the outside +Some general description +[Base url]({{ creme_api__base_url }}) + ## Authentication +How to authenticate + ## Pagination + +A note about the pagination system + diff --git a/creme/creme_api/urls.py b/creme/creme_api/urls.py index 91637d4e26..44f17fb243 100644 --- a/creme/creme_api/urls.py +++ b/creme/creme_api/urls.py @@ -1,4 +1,5 @@ from django.urls import include, re_path +from django.views.decorators.cache import cache_page from creme.creme_api.api.routes import router from creme.creme_api.api.tokens.views import TokenView @@ -11,11 +12,11 @@ ) urlpatterns = [ - re_path(r"^tokens[/]?$", TokenView.as_view(), name="creme_api__tokens"), - # re_path(r"^revoke_token/$", oauth_views.RevokeTokenView.as_view(), name="revoke-token"), - # re_path(r"^introspect/$", oauth_views.IntrospectTokenView.as_view(), name="introspect"), - - re_path(r'^openapi[/]?$', SchemaView.as_view(), name='creme_api__openapi_schema'), + re_path( + r'^openapi[/]?$', + cache_page(60 * 15)(SchemaView.as_view()), + name='creme_api__openapi_schema', + ), re_path(r'^documentation[/]?$', DocumentationView.as_view(), name='creme_api__documentation'), re_path(r'^configuration[/]?$', ConfigurationView.as_view(), name='creme_api__configuration'), re_path( @@ -33,4 +34,8 @@ ), ]), ), +] + [ + re_path(r"^tokens/$", TokenView.as_view(), name="creme_api__tokens"), + # re_path(r"^revoke_token/$", oauth_views.RevokeTokenView.as_view(), name="revoke-token"), + # re_path(r"^introspect/$", oauth_views.IntrospectTokenView.as_view(), name="introspect"), ] + router.urls diff --git a/creme/creme_api/views.py b/creme/creme_api/views.py index 144fb087df..136f0eb439 100644 --- a/creme/creme_api/views.py +++ b/creme/creme_api/views.py @@ -1,3 +1,4 @@ +from django.apps import apps from django.contrib import messages from django.template.loader import render_to_string from django.urls import reverse @@ -26,7 +27,12 @@ class SchemaView(DRFSchemaView): public = True generator_class = openapi.SchemaGenerator - def get_description(self, context=None, request=None): + def get_description(self, request=None): + creme_api_app_config = apps.get_app_config('creme_api') + context = { + 'creme_api__base_url': self.request.build_absolute_uri( + creme_api_app_config.url_root) + } description = render_to_string( self.description_template, context=context, request=request) # Force django safestring into builtin string @@ -46,7 +52,7 @@ def initial(self, request, *args, **kwargs): ) -class CremeApiView(base.BricksView): +class _DocumentationBaseView(base.BricksView): title = _("Creme CRM API") permissions = "creme_api" @@ -56,7 +62,7 @@ def get_context_data(self, **kwargs): return context -class DocumentationView(CremeApiView): +class DocumentationView(_DocumentationBaseView): template_name = 'creme_api/documentation.html' extra_context = {'schema_url': 'creme_api__openapi_schema'} @@ -68,7 +74,7 @@ def get_context_data(self, **kwargs): return context -class ConfigurationView(CremeApiView): +class ConfigurationView(_DocumentationBaseView): template_name = 'creme_api/configuration.html' def get_brick_ids(self): From 771158200afcbf6fb3935221bcd6c55f7bd7405c Mon Sep 17 00:00:00 2001 From: Hugo Smett Date: Thu, 10 Feb 2022 12:19:55 +0100 Subject: [PATCH 03/12] wip persons --- creme/creme_api/api/persons/__init__.py | 0 creme/creme_api/api/persons/serializers.py | 21 +++++++++++++ creme/creme_api/api/persons/viewsets.py | 36 ++++++++++++++++++++++ creme/creme_api/api/routes.py | 2 ++ creme/creme_api/apps.py | 12 -------- 5 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 creme/creme_api/api/persons/__init__.py create mode 100644 creme/creme_api/api/persons/serializers.py create mode 100644 creme/creme_api/api/persons/viewsets.py diff --git a/creme/creme_api/api/persons/__init__.py b/creme/creme_api/api/persons/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/creme/creme_api/api/persons/serializers.py b/creme/creme_api/api/persons/serializers.py new file mode 100644 index 0000000000..3ff02527d9 --- /dev/null +++ b/creme/creme_api/api/persons/serializers.py @@ -0,0 +1,21 @@ +from rest_framework import serializers + +from creme import persons + +Contact = persons.get_contact_model() + + +class ContactSerializer(serializers.ModelSerializer): + class Meta: + model = Contact + fields = [ + 'id', + 'last_name', + 'first_name', + 'email', + ] + extra_kwargs = { + 'first_name': {'required': True}, + 'last_name': {'required': True}, + 'email': {'required': True}, + } diff --git a/creme/creme_api/api/persons/viewsets.py b/creme/creme_api/api/persons/viewsets.py new file mode 100644 index 0000000000..4d65bfa0e3 --- /dev/null +++ b/creme/creme_api/api/persons/viewsets.py @@ -0,0 +1,36 @@ +from rest_framework import mixins, viewsets + +from creme import persons +from creme.creme_api.api.schemas import CremeSchema + +from .serializers import ContactSerializer + +Contact = persons.get_contact_model() + + +class ContactViewSet(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + """ + create: + POST /contacts + + retrieve: + GET /contacts/{userId} + + update: + PUT /contacts/{userId} + + partial_update: + PATCH /contacts/{userId} + + list: + GET /contacts + + """ + queryset = Contact.objects.all() + serializer_class = ContactSerializer + schema = CremeSchema(tags=["Contacts"], operation_id_base="Contacts") diff --git a/creme/creme_api/api/routes.py b/creme/creme_api/api/routes.py index 42d1e5ca4d..73100cd3e0 100644 --- a/creme/creme_api/api/routes.py +++ b/creme/creme_api/api/routes.py @@ -1,6 +1,7 @@ from rest_framework import routers import creme.creme_api.api.auth.viewsets +import creme.creme_api.api.persons.viewsets class CremeRouter(routers.DefaultRouter): @@ -27,3 +28,4 @@ def register_viewset(self, resource_name, viewset): router.register_viewset("teams", creme.creme_api.api.auth.viewsets.TeamViewSet) router.register_viewset("roles", creme.creme_api.api.auth.viewsets.UserRoleViewSet) router.register_viewset("credentials", creme.creme_api.api.auth.viewsets.SetCredentialsViewSet) +router.register_viewset("contacts", creme.creme_api.api.persons.viewsets.ContactViewSet) diff --git a/creme/creme_api/apps.py b/creme/creme_api/apps.py index 9c0670621a..e657af884a 100644 --- a/creme/creme_api/apps.py +++ b/creme/creme_api/apps.py @@ -9,18 +9,6 @@ class CremeApiConfig(CremeAppConfig): verbose_name = _('Creme Api') dependencies = ["creme.creme_core"] - def register_creme_config(self, config_registry): - from creme.creme_api import models - - from .forms import ApplicationForm - - register_model = config_registry.register_model - register_model(models.Application, 'application').creation( - form_class=ApplicationForm, - ).edition( - form_class=ApplicationForm, - ) - def register_bricks(self, brick_registry): from .bricks import ApplicationsBrick brick_registry.register(ApplicationsBrick) From 2beae6df954f726c1380c03cdfdf1b36e8f020a2 Mon Sep 17 00:00:00 2001 From: Hugo Smett Date: Thu, 10 Feb 2022 15:19:19 +0100 Subject: [PATCH 04/12] persons --- creme/creme_api/api/auth/serializers.py | 8 +- creme/creme_api/api/auth/viewsets.py | 71 +++-- creme/creme_api/api/contenttypes/__init__.py | 0 .../creme_api/api/contenttypes/serializers.py | 14 + creme/creme_api/api/contenttypes/utils.py | 10 + creme/creme_api/api/contenttypes/viewsets.py | 18 ++ creme/creme_api/api/core/__init__.py | 0 creme/creme_api/api/core/serializers.py | 53 ++++ creme/creme_api/api/core/viewsets.py | 51 ++++ creme/creme_api/api/persons/serializers.py | 122 ++++++++- creme/creme_api/api/persons/viewsets.py | 244 ++++++++++++++++-- creme/creme_api/api/routes.py | 10 + .../tests/functional/test_documentation.py | 32 ++- creme/creme_api/urls.py | 10 +- creme/creme_api/views.py | 5 +- 15 files changed, 560 insertions(+), 88 deletions(-) create mode 100644 creme/creme_api/api/contenttypes/__init__.py create mode 100644 creme/creme_api/api/contenttypes/serializers.py create mode 100644 creme/creme_api/api/contenttypes/utils.py create mode 100644 creme/creme_api/api/contenttypes/viewsets.py create mode 100644 creme/creme_api/api/core/__init__.py create mode 100644 creme/creme_api/api/core/serializers.py create mode 100644 creme/creme_api/api/core/viewsets.py diff --git a/creme/creme_api/api/auth/serializers.py b/creme/creme_api/api/auth/serializers.py index 257d60ef69..603a7142b2 100644 --- a/creme/creme_api/api/auth/serializers.py +++ b/creme/creme_api/api/auth/serializers.py @@ -5,11 +5,13 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from creme.creme_api.api.contenttypes.utils import ( + get_cremeentity_contenttype_queryset, +) from creme.creme_config.forms.user_role import filtered_entity_ctypes from creme.creme_core.apps import CremeAppConfig, creme_app_configs from creme.creme_core.models import SetCredentials, UserRole from creme.creme_core.models.fields import CremeUserForeignKey -from creme.creme_core.registry import creme_registry CremeUser = get_user_model() @@ -208,9 +210,7 @@ def __init__(self, *args, **kwargs): (app.label, str(app.verbose_name)) for app in apps if app.credentials & CRED_ADMIN ) - models = list(creme_registry.iter_entity_models()) - content_types = ContentType.objects.get_for_models(*models).values() - ct_queryset = ContentType.objects.filter(pk__in=[ct.id for ct in content_types]) + ct_queryset = get_cremeentity_contenttype_queryset() creatable_ctypes_f = self.fields['creatable_ctypes'] creatable_ctypes_f.child_relation.queryset = ct_queryset.all() diff --git a/creme/creme_api/api/auth/viewsets.py b/creme/creme_api/api/auth/viewsets.py index a5618a6ab1..ecfceacf9e 100644 --- a/creme/creme_api/api/auth/viewsets.py +++ b/creme/creme_api/api/auth/viewsets.py @@ -29,19 +29,25 @@ class UserViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): """ create: - POST /users + Create a user. retrieve: - GET /users/{userId} + Retrieve a user. update: - PUT /users/{userId} + Update a user. partial_update: - PATCH /users/{userId} + Partially update a user. list: - GET /users + List users. + + set_password: + Change a user's password. + + delete_user: + Delete a user. """ queryset = CremeUser.objects.filter(is_team=False, is_staff=False) @@ -50,10 +56,6 @@ class UserViewSet(mixins.CreateModelMixin, @action(methods=['post'], detail=True, serializer_class=PasswordSerializer) def set_password(self, request, pk): - """ - post: - POST /users/{userId}/set_password - """ instance = self.get_object() serializer = self.get_serializer(instance, data=request.data) serializer.is_valid(raise_exception=True) @@ -70,11 +72,7 @@ def set_password(self, request, pk): url_name="delete", name="delete", ) - def _delete(self, request, pk): - """ - post: - POST /users/{userId}/delete - """ + def delete_user(self, request, pk): instance = self.get_object() serializer = self.get_serializer(instance, data=request.data) serializer.is_valid(raise_exception=True) @@ -90,19 +88,22 @@ class TeamViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): """ create: - POST /teams + Create a team. retrieve: - GET /teams/{teamId} + Retrieve a team. update: - PUT /teams/{teamId} + Update a team. partial_update: - PATCH /teams/{teamId} + Partially update a team. list: - GET /teams + List teams. + + delete_team: + Delete a team. """ queryset = CremeUser.objects.filter(is_team=True, is_staff=False) @@ -117,11 +118,7 @@ class TeamViewSet(mixins.CreateModelMixin, url_name="delete", name="delete", ) - def _delete(self, request, pk): - """ - post: - POST /teams/{teamId}/delete - """ + def delete_team(self, request, pk): instance = self.get_object() serializer = self.get_serializer(instance, data=request.data) serializer.is_valid(raise_exception=True) @@ -138,22 +135,22 @@ class UserRoleViewSet(mixins.CreateModelMixin, """ create: - POST /roles + Create a role. retrieve: - GET /roles/{roleId} + Retrieve a role. update: - PUT /roles/{roleId} + Update a role partial_update: - PATCH /roles/{roleId} + Partially update a role destroy: - DELETE /roles/{roleId} + Delete a role. list: - GET /roles + List roles. """ queryset = UserRole.objects.all() @@ -176,27 +173,27 @@ class SetCredentialsViewSet(mixins.CreateModelMixin, """ create: - POST /credentials + Create a credential set. retrieve: - GET /credentials/{credentialId} + Retrieve a credential set. update: - PUT /credentials/{credentialId} + Update a credential set. partial_update: - PATCH /credentials/{credentialId} + Partially update a credential set. destroy: - DELETE /credentials/{credentialId} + Delete a credential set. list: - GET /credentials + List credential sets. """ queryset = SetCredentials.objects.all() serializer_class = SetCredentialsSerializer - schema = CremeSchema(tags=["Credentials"]) + schema = CremeSchema(tags=["Credential Sets"]) def get_serializer_class(self): if self.action == 'create': diff --git a/creme/creme_api/api/contenttypes/__init__.py b/creme/creme_api/api/contenttypes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/creme/creme_api/api/contenttypes/serializers.py b/creme/creme_api/api/contenttypes/serializers.py new file mode 100644 index 0000000000..9ce1331e0e --- /dev/null +++ b/creme/creme_api/api/contenttypes/serializers.py @@ -0,0 +1,14 @@ + +from django.contrib.contenttypes.models import ContentType +from rest_framework import serializers + + +class ContentTypeSerializer(serializers.ModelSerializer): + """Readonly""" + class Meta: + model = ContentType + fields = [ + "id", + "app_label", + "model", + ] diff --git a/creme/creme_api/api/contenttypes/utils.py b/creme/creme_api/api/contenttypes/utils.py new file mode 100644 index 0000000000..dcbea559de --- /dev/null +++ b/creme/creme_api/api/contenttypes/utils.py @@ -0,0 +1,10 @@ +from django.contrib.contenttypes.models import ContentType + +from creme.creme_core.registry import creme_registry + + +def get_cremeentity_contenttype_queryset(): + models = list(creme_registry.iter_entity_models()) + content_types = ContentType.objects.get_for_models(*models).values() + ct_queryset = ContentType.objects.filter(pk__in=[ct.id for ct in content_types]) + return ct_queryset diff --git a/creme/creme_api/api/contenttypes/viewsets.py b/creme/creme_api/api/contenttypes/viewsets.py new file mode 100644 index 0000000000..491d9b21b3 --- /dev/null +++ b/creme/creme_api/api/contenttypes/viewsets.py @@ -0,0 +1,18 @@ +from rest_framework import viewsets +from rest_framework.schemas.openapi import AutoSchema + +from .serializers import ContentTypeSerializer +from .utils import get_cremeentity_contenttype_queryset + + +class ContentTypeViewSet(viewsets.ReadOnlyModelViewSet): + """ + retrieve: + Retrieve a content type. + + list: + List content types. + """ + queryset = get_cremeentity_contenttype_queryset() + serializer_class = ContentTypeSerializer + schema = AutoSchema(tags=['Content Types']) diff --git a/creme/creme_api/api/core/__init__.py b/creme/creme_api/api/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/creme/creme_api/api/core/serializers.py b/creme/creme_api/api/core/serializers.py new file mode 100644 index 0000000000..c9cfce3a9c --- /dev/null +++ b/creme/creme_api/api/core/serializers.py @@ -0,0 +1,53 @@ +from django.contrib.auth import get_user_model +from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from creme.creme_core.models import CremeEntity + + +class CremeEntityRelatedField(serializers.RelatedField): + queryset = CremeEntity.objects.all() + default_error_messages = { + 'required': _('This field is required.'), + 'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'), + 'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'), + } + + def use_pk_only_optimization(self): + return True + + def to_internal_value(self, data): + try: + creme_entity = self.get_queryset().get(pk=data) + return creme_entity.get_real_entity() + except ObjectDoesNotExist: + self.fail('does_not_exist', pk_value=data) + except (TypeError, ValueError): + self.fail('incorrect_type', data_type=type(data).__name__) + + def to_representation(self, value): + return value.pk + + +class SimpleCremeEntitySerializer(serializers.ModelSerializer): + class Meta: + model = CremeEntity + fields = [ + 'id', + 'uuid', + 'created', + 'modified', + 'is_deleted', + ] + + +class CremeEntitySerializer(SimpleCremeEntitySerializer): + user = serializers.PrimaryKeyRelatedField(queryset=get_user_model().objects.all()) + + class Meta(SimpleCremeEntitySerializer.Meta): + model = CremeEntity + fields = SimpleCremeEntitySerializer.Meta.fields + [ + 'user', + 'description', + ] diff --git a/creme/creme_api/api/core/viewsets.py b/creme/creme_api/api/core/viewsets.py new file mode 100644 index 0000000000..7a8d9c54e1 --- /dev/null +++ b/creme/creme_api/api/core/viewsets.py @@ -0,0 +1,51 @@ +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response + +from creme.creme_core.core.exceptions import SpecificProtectedError + +from .serializers import SimpleCremeEntitySerializer + + +class CremeModelViewSet(viewsets.ModelViewSet): + LOCK_METHODS = {'POST', 'PUT' 'PATCH'} + + def perform_destroy(self, instance): + try: + instance.delete() + except SpecificProtectedError as exc: + raise ValidationError(str(exc)) + + def get_queryset(self): + queryset = super().get_queryset() + if self.request.method in self.LOCK_METHODS: + return queryset.select_for_update() + return queryset + + +class CremeEntityViewSet(CremeModelViewSet): + + @action(methods=['post'], detail=True, serializer_class=SimpleCremeEntitySerializer) + def trash(self, request, *args, **kwargs): + instance = self.get_object() + + try: + instance.trash() + except SpecificProtectedError as exc: + raise ValidationError(str(exc)) + + serializer = self.get_serializer(instance) + return Response(serializer.data) + + @action(methods=['post'], detail=True, serializer_class=SimpleCremeEntitySerializer) + def restore(self, request, *args, **kwargs): + instance = self.get_object() + + try: + instance.restore() + except SpecificProtectedError as exc: + raise ValidationError(str(exc)) + + serializer = self.get_serializer(instance) + return Response(serializer.data) diff --git a/creme/creme_api/api/persons/serializers.py b/creme/creme_api/api/persons/serializers.py index 3ff02527d9..6606fe75e7 100644 --- a/creme/creme_api/api/persons/serializers.py +++ b/creme/creme_api/api/persons/serializers.py @@ -1,21 +1,119 @@ from rest_framework import serializers -from creme import persons +from creme.creme_api.api.core.serializers import ( + CremeEntityRelatedField, + CremeEntitySerializer, +) +from creme.persons import ( + get_address_model, + get_contact_model, + get_organisation_model, +) +from creme.persons.models.other_models import ( + Civility, + LegalForm, + Position, + Sector, + StaffSize, +) -Contact = persons.get_contact_model() +class ContactSerializer(CremeEntitySerializer): + class Meta(CremeEntitySerializer.Meta): + model = get_contact_model() + fields = CremeEntitySerializer.Meta.fields + [ + 'billing_address', + 'shipping_address', + 'civility', + 'last_name', + 'first_name', + 'skype', + 'phone', + 'mobile', + 'fax', + 'email', + 'url_site', + 'position', + 'full_position', + 'sector', + 'is_user', + 'birthday', + 'image', + ] -class ContactSerializer(serializers.ModelSerializer): + +class OrganisationSerializer(CremeEntitySerializer): class Meta: - model = Contact + model = get_organisation_model() + fields = CremeEntitySerializer.Meta.fields + [ + 'billing_address', + 'shipping_address', + 'name', + 'is_managed', + 'phone', + 'fax', + 'email', + 'url_site', + 'sector', + 'legal_form', + 'staff_size', + 'capital', + 'annual_revenue', + 'siren', + 'naf', + 'siret', + 'rcs', + 'tvaintra', + 'subject_to_vat', + 'creation_date', + 'image', + ] + + +class AddressSerializer(serializers.ModelSerializer): + owner = CremeEntityRelatedField() + + class Meta: + model = get_address_model() fields = [ 'id', - 'last_name', - 'first_name', - 'email', + 'name', + 'address', + 'po_box', + 'zipcode', + 'city', + 'department', + 'state', + 'country', + 'owner', ] - extra_kwargs = { - 'first_name': {'required': True}, - 'last_name': {'required': True}, - 'email': {'required': True}, - } + + +class CivilitySerializer(serializers.ModelSerializer): + class Meta: + model = Civility + fields = ['id', 'title', 'shortcut'] + + +class PositionSerializer(serializers.ModelSerializer): + class Meta: + model = Position + fields = ['id', 'title'] + + +class StaffSizeSerializer(serializers.ModelSerializer): + class Meta: + model = StaffSize + fields = ['id', 'size'] + + +class LegalFormSerializer(serializers.ModelSerializer): + class Meta: + model = LegalForm + fields = ['id', 'title'] + + +class SectorSerializer(serializers.ModelSerializer): + class Meta: + model = Sector + fields = ['id', 'title'] diff --git a/creme/creme_api/api/persons/viewsets.py b/creme/creme_api/api/persons/viewsets.py index 4d65bfa0e3..d2224aaed0 100644 --- a/creme/creme_api/api/persons/viewsets.py +++ b/creme/creme_api/api/persons/viewsets.py @@ -1,36 +1,244 @@ -from rest_framework import mixins, viewsets - from creme import persons +from creme.creme_api.api.core.viewsets import ( + CremeEntityViewSet, + CremeModelViewSet, +) from creme.creme_api.api.schemas import CremeSchema +from creme.persons.models.other_models import ( + Civility, + LegalForm, + Position, + Sector, + StaffSize, +) -from .serializers import ContactSerializer - -Contact = persons.get_contact_model() +from .serializers import ( + AddressSerializer, + CivilitySerializer, + ContactSerializer, + LegalFormSerializer, + OrganisationSerializer, + PositionSerializer, + SectorSerializer, + StaffSizeSerializer, +) -class ContactViewSet(mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet): +class ContactViewSet(CremeEntityViewSet): """ create: - POST /contacts + Create a contact. retrieve: - GET /contacts/{userId} + Retrieve a contact. update: - PUT /contacts/{userId} + Update a contact. partial_update: - PATCH /contacts/{userId} + Partially update a contact. list: - GET /contacts + List contacts. + + delete: + Delete a contact. + + trash: + Move a contact to the trash. + + restore: + Restore a contact from the trash. """ - queryset = Contact.objects.all() + queryset = persons.get_contact_model().objects.all() serializer_class = ContactSerializer - schema = CremeSchema(tags=["Contacts"], operation_id_base="Contacts") + schema = CremeSchema(tags=["Contacts"]) + + +class OrganisationViewSet(CremeEntityViewSet): + """ + create: + Create an organisation. + + retrieve: + Retrieve an organisation. + + update: + Update an organisation. + + partial_update: + Partially update an organisation. + + list: + List organisations. + + delete: + Delete an organisation. + + trash: + Move an organisation to the trash. + + restore: + Restore an organisation from the trash. + + """ + queryset = persons.get_organisation_model().objects.all() + serializer_class = OrganisationSerializer + schema = CremeSchema(tags=["Organisations"]) + + +class AddressViewSet(CremeModelViewSet): + """ + create: + Create an address. + + retrieve: + Retrieve an address. + + update: + Update an address. + + partial_update: + Partially update an address. + + list: + List addresses. + + delete: + Delete an address. + + """ + queryset = persons.get_address_model().objects.all() + serializer_class = AddressSerializer + schema = CremeSchema(tags=["Addresses"]) + + +class CivilityViewSet(CremeModelViewSet): + """ + create: + Create a civility. + + retrieve: + Retrieve a civility. + + update: + Update a civility. + + partial_update: + Partially update a civility. + + list: + List civilities. + + delete: + Delete a civility + + """ + queryset = Civility.objects.all() + serializer_class = CivilitySerializer + schema = CremeSchema(tags=["Civilities"]) + + +class PositionViewSet(CremeModelViewSet): + """ + create: + Create a position. + + retrieve: + Retrieve a position. + + update: + Update a position. + + partial_update: + Partially update a position. + + list: + List positions. + + delete: + Delete a position + + """ + queryset = Position.objects.all() + serializer_class = PositionSerializer + schema = CremeSchema(tags=["Positions"]) + + +class StaffSizeViewSet(CremeModelViewSet): + """ + create: + Create a staff size. + + retrieve: + Retrieve a staff size. + + update: + Update a staff size. + + partial_update: + Partially update a staff size. + + list: + List staff sizes. + + delete: + Delete a staff size + + """ + queryset = StaffSize.objects.all() + serializer_class = StaffSizeSerializer + schema = CremeSchema(tags=["Staff sizes"]) + + +class LegalFormViewSet(CremeModelViewSet): + """ + create: + Create a legal form. + + retrieve: + Retrieve a legal form. + + update: + Update a legal form. + + partial_update: + Partially update a legal form. + + list: + List legal forms. + + delete: + Delete a legal form + + """ + queryset = LegalForm.objects.all() + serializer_class = LegalFormSerializer + schema = CremeSchema(tags=["Legal forms"]) + + +class SectorViewSet(CremeModelViewSet): + """ + create: + Create a sector. + + retrieve: + Retrieve a sector. + + update: + Update a sector. + + partial_update: + Partially update a sector. + + list: + List sectors. + + delete: + Delete a sector + + """ + queryset = Sector.objects.all() + serializer_class = SectorSerializer + schema = CremeSchema(tags=["Sectors"]) diff --git a/creme/creme_api/api/routes.py b/creme/creme_api/api/routes.py index 73100cd3e0..8207c2fa1c 100644 --- a/creme/creme_api/api/routes.py +++ b/creme/creme_api/api/routes.py @@ -1,6 +1,7 @@ from rest_framework import routers import creme.creme_api.api.auth.viewsets +import creme.creme_api.api.contenttypes.viewsets import creme.creme_api.api.persons.viewsets @@ -24,8 +25,17 @@ def register_viewset(self, resource_name, viewset): router = CremeRouter() +router.register_viewset( + "contenttypes", creme.creme_api.api.contenttypes.viewsets.ContentTypeViewSet) router.register_viewset("users", creme.creme_api.api.auth.viewsets.UserViewSet) router.register_viewset("teams", creme.creme_api.api.auth.viewsets.TeamViewSet) router.register_viewset("roles", creme.creme_api.api.auth.viewsets.UserRoleViewSet) router.register_viewset("credentials", creme.creme_api.api.auth.viewsets.SetCredentialsViewSet) router.register_viewset("contacts", creme.creme_api.api.persons.viewsets.ContactViewSet) +router.register_viewset("organisations", creme.creme_api.api.persons.viewsets.OrganisationViewSet) +router.register_viewset("addresses", creme.creme_api.api.persons.viewsets.AddressViewSet) +router.register_viewset("civilities", creme.creme_api.api.persons.viewsets.CivilityViewSet) +router.register_viewset("positions", creme.creme_api.api.persons.viewsets.PositionViewSet) +router.register_viewset("staff_sizes", creme.creme_api.api.persons.viewsets.StaffSizeViewSet) +router.register_viewset("legal_forms", creme.creme_api.api.persons.viewsets.LegalFormViewSet) +router.register_viewset("sectors", creme.creme_api.api.persons.viewsets.SectorViewSet) diff --git a/creme/creme_api/tests/functional/test_documentation.py b/creme/creme_api/tests/functional/test_documentation.py index 80813feed4..130156a002 100644 --- a/creme/creme_api/tests/functional/test_documentation.py +++ b/creme/creme_api/tests/functional/test_documentation.py @@ -1,11 +1,8 @@ import yaml from django.urls import reverse_lazy +from parameterized import parameterized -import creme.creme_api -from creme.creme_api.views import ( - documentation_description, - documentation_title, -) +from creme.creme_api.api.routes import router from creme.creme_core.tests.base import CremeTestCase @@ -27,13 +24,26 @@ def test_permissions__superuser(self): def test_context(self): self.login() response = self.assertGET200(self.url) + self.assertTemplateUsed(response, 'creme_api/description.md') self.assertEqual(response['content-type'], 'application/vnd.oai.openapi') + + @parameterized.expand([ + (endpoint,) for endpoint in router.resources_list + ]) + def test_all_endpoints_have_documentation(self, endpoint): + self.login() + response = self.assertGET200(self.url) openapi_schema = yaml.safe_load(response.content) - self.assertEqual( - openapi_schema['info'], - {'title': documentation_title, - 'version': creme.creme_api.VERSION, - 'description': documentation_description}) + + errors = [] + for url, methods in openapi_schema['paths'].items(): + if endpoint not in url: + continue + for method, method_details in methods.items(): + if not method_details.get('description'): + errors.append((method, url)) + + self.assertFalse(errors, "Please document those endpoints.") class DocumentationViewTestCase(CremeTestCase): @@ -57,5 +67,5 @@ def test_context(self): self.assertTemplateUsed(response, 'creme_api/documentation.html') self.assertEqual(response.context["schema_url"], 'creme_api__openapi_schema') self.assertEqual( - response.context["creme_api__tokens_url"], 'http://testserver/creme_api/tokens') + response.context["creme_api__tokens_url"], 'http://testserver/creme_api/tokens/') self.assertEqual(response.context["token_type"], 'Token') diff --git a/creme/creme_api/urls.py b/creme/creme_api/urls.py index 44f17fb243..0e0e8881af 100644 --- a/creme/creme_api/urls.py +++ b/creme/creme_api/urls.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.urls import include, re_path from django.views.decorators.cache import cache_page @@ -11,12 +12,11 @@ SchemaView, ) +schema_view = SchemaView.as_view() +if not settings.DEBUG: + schema_view = cache_page(60 * 15)(schema_view) urlpatterns = [ - re_path( - r'^openapi[/]?$', - cache_page(60 * 15)(SchemaView.as_view()), - name='creme_api__openapi_schema', - ), + re_path(r'^openapi[/]?$', schema_view, name='creme_api__openapi_schema'), re_path(r'^documentation[/]?$', DocumentationView.as_view(), name='creme_api__documentation'), re_path(r'^configuration[/]?$', ConfigurationView.as_view(), name='creme_api__configuration'), re_path( diff --git a/creme/creme_api/views.py b/creme/creme_api/views.py index 136f0eb439..ccfa0c033e 100644 --- a/creme/creme_api/views.py +++ b/creme/creme_api/views.py @@ -54,7 +54,6 @@ def initial(self, request, *args, **kwargs): class _DocumentationBaseView(base.BricksView): title = _("Creme CRM API") - permissions = "creme_api" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -65,6 +64,7 @@ def get_context_data(self, **kwargs): class DocumentationView(_DocumentationBaseView): template_name = 'creme_api/documentation.html' extra_context = {'schema_url': 'creme_api__openapi_schema'} + permissions = "creme_api" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -76,6 +76,7 @@ def get_context_data(self, **kwargs): class ConfigurationView(_DocumentationBaseView): template_name = 'creme_api/configuration.html' + permissions = "creme_api.can_admin" def get_brick_ids(self): return [ApplicationsBrick.id_] @@ -92,6 +93,7 @@ class ApplicationCreation(generic.CremeModelCreationPopup): "Client Secret : {client_secret}\n\n" "This is the first and last time this secret displayed!" ) + permissions = "creme_api.can_admin" def get_success_message(self): return self.success_message.format( @@ -111,6 +113,7 @@ class ApplicationEdition(generic.CremeModelEditionPopup): model = Application form_class = ApplicationForm pk_url_kwarg = 'application_id' + permissions = "creme_api.can_admin" def get_form_kwargs(self): kwargs = super().get_form_kwargs() From 46ad7855a78f0d0c0fc0c688009848a86f808f84 Mon Sep 17 00:00:00 2001 From: Hugo Smett Date: Thu, 10 Feb 2022 16:46:15 +0100 Subject: [PATCH 05/12] tests --- creme/creme_api/api/contenttypes/viewsets.py | 5 ++++- creme/creme_api/tests/functional/test_documentation.py | 9 ++++++++- creme/creme_api/urls.py | 7 +------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/creme/creme_api/api/contenttypes/viewsets.py b/creme/creme_api/api/contenttypes/viewsets.py index 491d9b21b3..acc447a3e1 100644 --- a/creme/creme_api/api/contenttypes/viewsets.py +++ b/creme/creme_api/api/contenttypes/viewsets.py @@ -13,6 +13,9 @@ class ContentTypeViewSet(viewsets.ReadOnlyModelViewSet): list: List content types. """ - queryset = get_cremeentity_contenttype_queryset() + queryset = None serializer_class = ContentTypeSerializer schema = AutoSchema(tags=['Content Types']) + + def get_queryset(self): + return get_cremeentity_contenttype_queryset() diff --git a/creme/creme_api/tests/functional/test_documentation.py b/creme/creme_api/tests/functional/test_documentation.py index 130156a002..82d451f201 100644 --- a/creme/creme_api/tests/functional/test_documentation.py +++ b/creme/creme_api/tests/functional/test_documentation.py @@ -2,6 +2,7 @@ from django.urls import reverse_lazy from parameterized import parameterized +from creme import creme_api from creme.creme_api.api.routes import router from creme.creme_core.tests.base import CremeTestCase @@ -24,8 +25,14 @@ def test_permissions__superuser(self): def test_context(self): self.login() response = self.assertGET200(self.url) - self.assertTemplateUsed(response, 'creme_api/description.md') self.assertEqual(response['content-type'], 'application/vnd.oai.openapi') + self.assertTemplateUsed(response, 'creme_api/description.md') + + openapi_schema = yaml.safe_load(response.content) + info = openapi_schema['info'] + self.assertEqual(info['version'], creme_api.VERSION) + self.assertTrue(info['title']) + self.assertTrue(info['description']) @parameterized.expand([ (endpoint,) for endpoint in router.resources_list diff --git a/creme/creme_api/urls.py b/creme/creme_api/urls.py index 0e0e8881af..45b6a50b61 100644 --- a/creme/creme_api/urls.py +++ b/creme/creme_api/urls.py @@ -1,6 +1,4 @@ -from django.conf import settings from django.urls import include, re_path -from django.views.decorators.cache import cache_page from creme.creme_api.api.routes import router from creme.creme_api.api.tokens.views import TokenView @@ -12,11 +10,8 @@ SchemaView, ) -schema_view = SchemaView.as_view() -if not settings.DEBUG: - schema_view = cache_page(60 * 15)(schema_view) urlpatterns = [ - re_path(r'^openapi[/]?$', schema_view, name='creme_api__openapi_schema'), + re_path(r'^openapi[/]?$', SchemaView.as_view(), name='creme_api__openapi_schema'), re_path(r'^documentation[/]?$', DocumentationView.as_view(), name='creme_api__documentation'), re_path(r'^configuration[/]?$', ConfigurationView.as_view(), name='creme_api__configuration'), re_path( From bd2613b82a6aa28c53b251ec4f6045d3e8cfd052 Mon Sep 17 00:00:00 2001 From: Hugo Smett Date: Fri, 11 Feb 2022 13:13:00 +0100 Subject: [PATCH 06/12] test persons --- .../creme_api/api/contenttypes/serializers.py | 21 +- creme/creme_api/api/contenttypes/utils.py | 8 +- creme/creme_api/api/persons/serializers.py | 2 +- creme/creme_api/api/routes.py | 3 +- .../api/tokens/{views.py => viewsets.py} | 25 +- .../templates/creme_api/description.md | 2 +- creme/creme_api/tests/__init__.py | 14 + creme/creme_api/tests/functional/__init__.py | 0 .../{functional => }/test_authentication.py | 1 + creme/creme_api/tests/test_civilities.py | 118 ++++++++ creme/creme_api/tests/test_contacts.py | 254 ++++++++++++++++++ creme/creme_api/tests/test_contenttypes.py | 32 +++ .../{functional => }/test_credentials.py | 23 +- .../{functional => }/test_documentation.py | 8 +- creme/creme_api/tests/test_positions.py | 108 ++++++++ .../tests/{functional => }/test_roles.py | 23 +- creme/creme_api/tests/test_sectors.py | 108 ++++++++ .../tests/{functional => }/test_teams.py | 19 +- .../tests/{functional => }/test_tokens.py | 2 +- .../tests/{functional => }/test_users.py | 23 +- creme/creme_api/tests/utils.py | 116 ++------ creme/creme_api/urls.py | 5 - creme/creme_api/views.py | 14 +- 23 files changed, 786 insertions(+), 143 deletions(-) rename creme/creme_api/api/tokens/{views.py => viewsets.py} (61%) delete mode 100644 creme/creme_api/tests/functional/__init__.py rename creme/creme_api/tests/{functional => }/test_authentication.py (98%) create mode 100644 creme/creme_api/tests/test_civilities.py create mode 100644 creme/creme_api/tests/test_contacts.py create mode 100644 creme/creme_api/tests/test_contenttypes.py rename creme/creme_api/tests/{functional => }/test_credentials.py (92%) rename creme/creme_api/tests/{functional => }/test_documentation.py (93%) create mode 100644 creme/creme_api/tests/test_positions.py rename creme/creme_api/tests/{functional => }/test_roles.py (93%) create mode 100644 creme/creme_api/tests/test_sectors.py rename creme/creme_api/tests/{functional => }/test_teams.py (94%) rename creme/creme_api/tests/{functional => }/test_tokens.py (97%) rename creme/creme_api/tests/{functional => }/test_users.py (96%) diff --git a/creme/creme_api/api/contenttypes/serializers.py b/creme/creme_api/api/contenttypes/serializers.py index 9ce1331e0e..db6da49a76 100644 --- a/creme/creme_api/api/contenttypes/serializers.py +++ b/creme/creme_api/api/contenttypes/serializers.py @@ -1,14 +1,13 @@ - -from django.contrib.contenttypes.models import ContentType +from django.apps import apps from rest_framework import serializers -class ContentTypeSerializer(serializers.ModelSerializer): - """Readonly""" - class Meta: - model = ContentType - fields = [ - "id", - "app_label", - "model", - ] +class ContentTypeSerializer(serializers.BaseSerializer): + def to_representation(self, contenttype): + model = contenttype.model_class() + app_config = apps.get_app_config(contenttype.app_label) + return { + "id": contenttype.id, + "name": model._meta.verbose_name_plural, + "application": app_config.verbose_name, + } diff --git a/creme/creme_api/api/contenttypes/utils.py b/creme/creme_api/api/contenttypes/utils.py index dcbea559de..806ea48bba 100644 --- a/creme/creme_api/api/contenttypes/utils.py +++ b/creme/creme_api/api/contenttypes/utils.py @@ -3,8 +3,12 @@ from creme.creme_core.registry import creme_registry -def get_cremeentity_contenttype_queryset(): +def get_cremeentity_contenttypes(): models = list(creme_registry.iter_entity_models()) - content_types = ContentType.objects.get_for_models(*models).values() + return ContentType.objects.get_for_models(*models).values() + + +def get_cremeentity_contenttype_queryset(): + content_types = get_cremeentity_contenttypes() ct_queryset = ContentType.objects.filter(pk__in=[ct.id for ct in content_types]) return ct_queryset diff --git a/creme/creme_api/api/persons/serializers.py b/creme/creme_api/api/persons/serializers.py index 6606fe75e7..62ad45be80 100644 --- a/creme/creme_api/api/persons/serializers.py +++ b/creme/creme_api/api/persons/serializers.py @@ -38,7 +38,7 @@ class Meta(CremeEntitySerializer.Meta): 'sector', 'is_user', 'birthday', - 'image', + # 'image', # Need documents ] diff --git a/creme/creme_api/api/routes.py b/creme/creme_api/api/routes.py index 8207c2fa1c..405bf609ff 100644 --- a/creme/creme_api/api/routes.py +++ b/creme/creme_api/api/routes.py @@ -3,6 +3,7 @@ import creme.creme_api.api.auth.viewsets import creme.creme_api.api.contenttypes.viewsets import creme.creme_api.api.persons.viewsets +import creme.creme_api.api.tokens.viewsets class CremeRouter(routers.DefaultRouter): @@ -24,7 +25,7 @@ def register_viewset(self, resource_name, viewset): router = CremeRouter() - +router.register_viewset("tokens", creme.creme_api.api.tokens.viewsets.TokenViewSet) router.register_viewset( "contenttypes", creme.creme_api.api.contenttypes.viewsets.ContentTypeViewSet) router.register_viewset("users", creme.creme_api.api.auth.viewsets.UserViewSet) diff --git a/creme/creme_api/api/tokens/views.py b/creme/creme_api/api/tokens/viewsets.py similarity index 61% rename from creme/creme_api/api/tokens/views.py rename to creme/creme_api/api/tokens/viewsets.py index b76bb4a997..2724399494 100644 --- a/creme/creme_api/api/tokens/views.py +++ b/creme/creme_api/api/tokens/viewsets.py @@ -1,6 +1,5 @@ -from rest_framework import parsers, renderers +from rest_framework import mixins, parsers, renderers, viewsets from rest_framework.response import Response -from rest_framework.views import APIView from creme.creme_api.api.schemas import CremeSchema from creme.creme_api.models import Token @@ -8,7 +7,13 @@ from .serializers import TokenSerializer -class TokenView(APIView): +class TokenViewSet(mixins.CreateModelMixin, + viewsets.GenericViewSet): + """ + create: + Create a token. + + """ throttle_classes = [] permission_classes = [] parser_classes = [ @@ -22,18 +27,10 @@ class TokenView(APIView): serializer_class = TokenSerializer schema = CremeSchema(tags=["Tokens"]) - def get_serializer_context(self): - return { - 'request': self.request, - 'format': self.format_kwarg, - 'view': self - } - - def get_serializer(self, *args, **kwargs): - kwargs['context'] = self.get_serializer_context() - return self.serializer_class(*args, **kwargs) + # TODO: revoke_token ? + # TODO: introspect ? - def post(self, request, *args, **kwargs): + def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) application = serializer.validated_data['application'] diff --git a/creme/creme_api/templates/creme_api/description.md b/creme/creme_api/templates/creme_api/description.md index f15ec31c70..38dcbd82bf 100644 --- a/creme/creme_api/templates/creme_api/description.md +++ b/creme/creme_api/templates/creme_api/description.md @@ -1,7 +1,7 @@ # Drive Creme CRM from the outside Some general description -[Base url]({{ creme_api__base_url }}) +[Base url]({{ creme_api_rool_url }}) ## Authentication diff --git a/creme/creme_api/tests/__init__.py b/creme/creme_api/tests/__init__.py index e69de29bb2..7e2c455b7b 100644 --- a/creme/creme_api/tests/__init__.py +++ b/creme/creme_api/tests/__init__.py @@ -0,0 +1,14 @@ +from .test_authentication import * # noqa +from .test_civilities import * # noqa +from .test_contacts import * # noqa +from .test_contenttypes import * # noqa +from .test_credentials import * # noqa +from .test_documentation import * # noqa +from .test_models import * # noqa +from .test_positions import * # noqa +from .test_roles import * # noqa +from .test_sectors import * # noqa +from .test_teams import * # noqa +from .test_tokens import * # noqa +from .test_users import * # noqa +from .utils import * # noqa diff --git a/creme/creme_api/tests/functional/__init__.py b/creme/creme_api/tests/functional/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/creme/creme_api/tests/functional/test_authentication.py b/creme/creme_api/tests/test_authentication.py similarity index 98% rename from creme/creme_api/tests/functional/test_authentication.py rename to creme/creme_api/tests/test_authentication.py index 72eac1570a..94a511bdcc 100644 --- a/creme/creme_api/tests/functional/test_authentication.py +++ b/creme/creme_api/tests/test_authentication.py @@ -89,3 +89,4 @@ def test_authenticate10(self): self.client.credentials(HTTP_AUTHORIZATION=f'Token {token.code}') response = self.request() self.assertEqual(status.HTTP_200_OK, response.status_code) + self.assertEqual({'ok': True}, response.data) diff --git a/creme/creme_api/tests/test_civilities.py b/creme/creme_api/tests/test_civilities.py new file mode 100644 index 0000000000..fed2b22e3a --- /dev/null +++ b/creme/creme_api/tests/test_civilities.py @@ -0,0 +1,118 @@ +from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.persons.models import Civility + + +@Factory.register +def civility(factory, **kwargs): + data = factory.civility_data(**kwargs) + return Civility.objects.create(**data) + + +@Factory.register +def civility_data(factory, **kwargs): + kwargs.setdefault('title', 'Captain') + kwargs.setdefault('shortcut', 'Cpt') + return kwargs + + +class CreateCivilityTestCase(CremeAPITestCase): + url_name = 'creme_api__civilities-list' + method = 'post' + + def test_validation__required(self): + response = self.make_request(data={}) + self.assertValidationErrors(response, { + 'title': ['required'], + 'shortcut': ['required'], + }) + + def test_create_civility(self): + data = self.factory.civility_data() + response = self.make_request(data=data) + civility = Civility.objects.get(id=response.data['id']) + self.assertResponseEqual(response, 201, { + 'id': civility.id, + 'title': 'Captain', + 'shortcut': 'Cpt', + }) + self.assertEqual(civility.title, "Captain") + self.assertEqual(civility.shortcut, "Cpt") + + +class RetrieveCivilityTestCase(CremeAPITestCase): + url_name = 'creme_api__civilities-detail' + method = 'get' + + def test_retrieve_civility(self): + civility = self.factory.civility() + response = self.make_request(to=civility.id) + self.assertResponseEqual(response, 200, { + 'id': civility.id, + 'title': 'Captain', + 'shortcut': 'Cpt', + }) + + +class UpdateCivilityTestCase(CremeAPITestCase): + url_name = 'creme_api__civilities-detail' + method = 'put' + + def test_update_civility(self): + civility = self.factory.civility() + response = self.make_request(to=civility.id, data={ + 'title': "CAPTAIN", + 'shortcut': "CAP", + }) + self.assertResponseEqual(response, 200, { + 'id': civility.id, + 'title': 'CAPTAIN', + 'shortcut': 'CAP', + }) + civility.refresh_from_db() + self.assertEqual(civility.title, "CAPTAIN") + self.assertEqual(civility.shortcut, "CAP") + + +class PartialUpdateCivilityTestCase(CremeAPITestCase): + url_name = 'creme_api__civilities-detail' + method = 'patch' + + def test_partial_update_civility(self): + civility = self.factory.civility() + response = self.make_request(to=civility.id, data={ + 'shortcut': "CAP", + }) + self.assertResponseEqual(response, 200, { + 'id': civility.id, + 'title': 'Captain', + 'shortcut': 'CAP', + }) + civility.refresh_from_db() + self.assertEqual(civility.title, "Captain") + self.assertEqual(civility.shortcut, "CAP") + + +class ListCivilityTestCase(CremeAPITestCase): + url_name = 'creme_api__civilities-list' + method = 'get' + + def test_list_civilities(self): + Civility.objects.all().delete() + civility1 = self.factory.civility(title="1", shortcut="1") + civility2 = self.factory.civility(title="2", shortcut="2") + response = self.make_request() + self.assertResponseEqual(response, 200, [ + {'id': civility1.id, 'title': '1', 'shortcut': '1'}, + {'id': civility2.id, 'title': '2', 'shortcut': '2'}, + ]) + + +class DeleteCivilityTestCase(CremeAPITestCase): + url_name = 'creme_api__civilities-detail' + method = 'delete' + + def test_delete_civility(self): + civility = self.factory.civility() + response = self.make_request(to=civility.id) + self.assertResponseEqual(response, 204) + self.assertFalse(Civility.objects.filter(id=civility.id).exists()) diff --git a/creme/creme_api/tests/test_contacts.py b/creme/creme_api/tests/test_contacts.py new file mode 100644 index 0000000000..712c0d8ab6 --- /dev/null +++ b/creme/creme_api/tests/test_contacts.py @@ -0,0 +1,254 @@ +from datetime import date + +from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.persons import get_contact_model + +Contact = get_contact_model() + + +@Factory.register +def contact(factory, **kwargs): + data = factory.contact_data(**kwargs) + return Contact.objects.create(**data) + + +@Factory.register +def contact_data(factory, **kwargs): + if 'user' not in kwargs: + kwargs['user'] = factory.user().id + + if 'civility' not in kwargs: + kwargs['civility'] = factory.civility().id + + if 'position' not in kwargs: + kwargs['position'] = factory.position().id + + if 'sector' not in kwargs: + kwargs['sector'] = factory.sector().id + + data = { + # 'user': None, + 'description': "Description", + # 'billing_address': None, + # 'shipping_address': None, + # 'civility': None, + 'last_name': "Dupont", + 'first_name': "Jean", + 'skype': "jean.dupont", + 'phone': "+330100000000", + 'mobile': "+330600000000", + 'fax': "+330100000001", + 'email': "jean.dupont@provider.com", + 'url_site': "https://www.jean-dupont.provider.com", + # 'position': None, + 'full_position': "Full position", + # 'sector': None, + # 'is_user': None, + 'birthday': "2000-01-01", + } + data.update(**kwargs) + return data + + +class CreateContactTestCase(CremeAPITestCase): + url_name = 'creme_api__contacts-list' + method = 'post' + + def test_validation__required(self): + response = self.make_request(data={}) + self.assertValidationErrors(response, { + 'last_name': ['required'], + 'user': ['required'], + }) + + def test_create_contact(self): + user = self.factory.user() + civility = self.factory.civility() + position = self.factory.position() + sector = self.factory.sector() + data = self.factory.contact_data( + user=user.id, + civility=civility.id, + position=position.id, + sector=sector.id, + ) + response = self.make_request(data=data) + self.assertEqual(response.status_code, 201, response.data) + contact = Contact.objects.get(id=response.data['id']) + self.assertResponseEqual(response, 201, { + **data, + 'id': contact.id, + 'uuid': str(contact.uuid), + 'is_deleted': False, + 'created': self.to_iso8601(contact.created), + 'modified': self.to_iso8601(contact.modified), + 'is_user': None, + 'billing_address': None, + 'shipping_address': None, + }) + self.assertEqual(contact.birthday, date(2000, 1, 1)) + self.assertEqual(contact.description, data['description']) + self.assertEqual(contact.last_name, data['last_name']) + self.assertEqual(contact.first_name, data['first_name']) + self.assertEqual(contact.skype, data['skype']) + self.assertEqual(contact.phone, data['phone']) + self.assertEqual(contact.mobile, data['mobile']) + self.assertEqual(contact.fax, data['fax']) + self.assertEqual(contact.email, data['email']) + self.assertEqual(contact.url_site, data['url_site']) + self.assertEqual(contact.full_position, data['full_position']) + + +# class RetrieveContactTestCase(CremeAPITestCase): +# url_name = 'creme_api__contacts-detail' +# method = 'get' +# +# def test_get_contact(self): +# contact = self.factory.contact() +# response = self.make_request(to=contact.id) +# self.assertResponseEqual(response, 200, { +# 'id': contact.id, +# 'contactname': contact.contactname, +# 'last_name': contact.last_name, +# 'first_name': contact.first_name, +# 'email': contact.email, +# 'date_joined': self.to_iso8601(contact.date_joined), +# 'last_login': None, +# 'is_active': True, +# 'time_zone': 'Europe/Paris', +# 'theme': 'icecream' +# }) +# +# +# class UpdateContactTestCase(CremeAPITestCase): +# url_name = 'creme_api__contacts-detail' +# method = 'put' +# +# def test_validation__required(self): +# contact = self.factory.contact() +# response = self.make_request(to=contact.id, data={}) +# self.assertValidationErrors(response, { +# 'last_name': ['required'], +# 'user': ['required'], +# }) +# +# def test_update_contact(self): +# contact = self.factory.contact() +# +# data = self.factory.contact_data(last_name="Smith", contactname="Nick") +# response = self.make_request(to=contact.id, data=data) +# self.assertResponseEqual(response, 200, { +# 'id': contact.id, +# 'last_name': 'Smith', +# 'first_name': 'John', +# }) +# contact.refresh_from_db() +# self.assertEqual(contact.contactname, "Nick") +# self.assertEqual(contact.last_name, "Smith") +# +# +# class PartialUpdateContactTestCase(CremeAPITestCase): +# url_name = 'creme_api__contacts-detail' +# method = 'patch' +# +# def test_partial_update_contact(self): +# contact = self.factory.contact() +# data = {'theme': "chantilly"} +# response = self.make_request(to=contact.id, data=data) +# self.assertResponseEqual(response, 200, { +# 'id': contact.id, +# 'contactname': 'john.doe', +# 'last_name': 'Doe', +# 'first_name': 'John', +# 'email': 'john.doe@provider.com', +# 'date_joined': self.to_iso8601(contact.date_joined), +# 'last_login': None, +# 'is_active': True, +# 'is_supercontact': True, +# 'role': None, +# 'time_zone': 'Europe/Paris', +# 'theme': 'chantilly' +# }) +# contact.refresh_from_db() +# self.assertEqual(contact.theme, "chantilly") +# +# +# class ListContactTestCase(CremeAPITestCase): +# url_name = 'creme_api__contacts-list' +# method = 'get' +# +# def test_list_contacts(self): +# fulbert = Contact.objects.get() +# contact = self.factory.contact(contactname="contact", theme='chantilly') +# self.assertEqual(Contact.objects.count(), 2) +# +# response = self.make_request() +# self.assertResponseEqual(response, 200, [ +# { +# 'id': fulbert.id, +# 'contactname': 'root', +# 'last_name': 'Creme', +# 'first_name': 'Fulbert', +# 'email': fulbert.email, +# 'date_joined': self.to_iso8601(fulbert.date_joined), +# 'last_login': None, +# 'is_active': True, +# 'is_supercontact': True, +# 'role': None, +# 'time_zone': 'Europe/Paris', +# 'theme': 'icecream' +# }, +# { +# 'id': contact.id, +# 'contactname': 'contact', +# 'last_name': 'Doe', +# 'first_name': 'John', +# 'email': 'john.doe@provider.com', +# 'date_joined': self.to_iso8601(contact.date_joined), +# 'last_login': None, +# 'is_active': True, +# 'is_supercontact': True, +# 'role': None, +# 'time_zone': 'Europe/Paris', +# 'theme': 'chantilly' +# }, +# ]) +# +# +# class DeleteContactTestCase(CremeAPITestCase): +# url_name = 'creme_api__contacts-delete' +# method = 'post' +# +# def test_delete(self): +# url = reverse('creme_api__contacts-detail', args=[1]) +# response = self.client.delete(url, format='json') +# self.assertResponseEqual(response, 405) +# +# def test_validation__required(self): +# contact = self.factory.contact() +# response = self.make_request(to=contact.id, data={}) +# self.assertValidationErrors(response, { +# 'transfer_to': ['required'] +# }) +# +# def test_delete_contact(self): +# team = self.factory.team() +# contact1 = self.factory.contact(contactname='contact1') +# contact2 = self.factory.contact(contactname='contact2') +# contact = self.factory.contact(contact=contact2) +# +# data = {'transfer_to': contact1.id} +# response = self.make_request(to=contact2.id, data=data) +# self.assertResponseEqual(response, 204) +# +# self.assertFalse(Contact.objects.filter(contactname='contact2').exists()) +# contact.refresh_from_db() +# self.assertEqual(contact.contact, contact1) +# +# data = {'transfer_to': team.id} +# response = self.make_request(to=contact1.id, data=data) +# self.assertResponseEqual(response, 204) +# +# self.assertFalse(Contact.objects.filter(contactname='contact1').exists()) +# contact.refresh_from_db() +# self.assertEqual(contact.contact, team) diff --git a/creme/creme_api/tests/test_contenttypes.py b/creme/creme_api/tests/test_contenttypes.py new file mode 100644 index 0000000000..f45db534a6 --- /dev/null +++ b/creme/creme_api/tests/test_contenttypes.py @@ -0,0 +1,32 @@ +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import gettext as _ + +from creme.creme_api.api.contenttypes.utils import get_cremeentity_contenttypes +from creme.creme_api.tests.utils import CremeAPITestCase +from creme.persons import get_contact_model + +Contact = get_contact_model() + + +class RetrieveContentTypeTestCase(CremeAPITestCase): + url_name = 'creme_api__contenttypes-detail' + method = 'get' + + def test_retrieve_contenttype(self): + contact_ct = ContentType.objects.get_for_model(Contact) + + response = self.make_request(to=contact_ct.id) + self.assertResponseEqual(response, 200, { + 'id': contact_ct.id, + 'application': _('Accounts and Contacts'), + 'name': _("Contacts"), + }) + + +class ListContentTypeTestCase(CremeAPITestCase): + url_name = 'creme_api__contenttypes-list' + method = 'get' + + def test_list_contenttypes(self): + responses, data = self.consume_list() + self.assertEqual(len(data), len(get_cremeentity_contenttypes())) diff --git a/creme/creme_api/tests/functional/test_credentials.py b/creme/creme_api/tests/test_credentials.py similarity index 92% rename from creme/creme_api/tests/functional/test_credentials.py rename to creme/creme_api/tests/test_credentials.py index caf007edd0..5f9320df3d 100644 --- a/creme/creme_api/tests/functional/test_credentials.py +++ b/creme/creme_api/tests/test_credentials.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType -from creme.creme_api.tests.utils import CremeAPITestCase +from creme.creme_api.tests.utils import CremeAPITestCase, Factory from creme.creme_core.models import SetCredentials from creme.persons import get_contact_model, get_organisation_model @@ -10,6 +10,27 @@ Organisation = get_organisation_model() +@Factory.register +def credential(factory, **kwargs): + contact_ct = ContentType.objects.get_for_model(Contact) + perms = {'can_view', 'can_change', 'can_delete', 'can_link', 'can_unlink'} + data = { + 'set_type': SetCredentials.ESET_OWN, + 'ctype': contact_ct, + 'forbidden': False, + 'efilter': None, + **{p: True for p in perms} + } + data.update(**kwargs) + if 'role' not in data: + data['role'] = factory.role() + value = {k: data.pop(k) for k in perms} + creds = SetCredentials(**data) + creds.set_value(**value) + creds.save() + return creds + + class CreateSetCredentialTestCase(CremeAPITestCase): url_name = 'creme_api__credentials-list' method = 'post' diff --git a/creme/creme_api/tests/functional/test_documentation.py b/creme/creme_api/tests/test_documentation.py similarity index 93% rename from creme/creme_api/tests/functional/test_documentation.py rename to creme/creme_api/tests/test_documentation.py index 82d451f201..35f504492c 100644 --- a/creme/creme_api/tests/functional/test_documentation.py +++ b/creme/creme_api/tests/test_documentation.py @@ -46,9 +46,11 @@ def test_all_endpoints_have_documentation(self, endpoint): for url, methods in openapi_schema['paths'].items(): if endpoint not in url: continue - for method, method_details in methods.items(): - if not method_details.get('description'): - errors.append((method, url)) + errors.extend([ + (method, url) + for method, method_details in methods.items() + if not method_details.get('description') + ]) self.assertFalse(errors, "Please document those endpoints.") diff --git a/creme/creme_api/tests/test_positions.py b/creme/creme_api/tests/test_positions.py new file mode 100644 index 0000000000..ecd1830200 --- /dev/null +++ b/creme/creme_api/tests/test_positions.py @@ -0,0 +1,108 @@ +from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.persons.models import Position + + +@Factory.register +def position(factory, **kwargs): + data = factory.position_data(**kwargs) + return Position.objects.create(**data) + + +@Factory.register +def position_data(factory, **kwargs): + kwargs.setdefault('title', 'Captain') + return kwargs + + +class CreatePositionTestCase(CremeAPITestCase): + url_name = 'creme_api__positions-list' + method = 'post' + + def test_validation__required(self): + response = self.make_request(data={}) + self.assertValidationErrors(response, { + 'title': ['required'], + }) + + def test_create_position(self): + data = self.factory.position_data() + response = self.make_request(data=data) + position = Position.objects.get(id=response.data['id']) + self.assertResponseEqual(response, 201, { + 'id': position.id, + 'title': 'Captain', + }) + self.assertEqual(position.title, "Captain") + + +class RetrievePositionTestCase(CremeAPITestCase): + url_name = 'creme_api__positions-detail' + method = 'get' + + def test_retrieve_position(self): + position = self.factory.position() + response = self.make_request(to=position.id) + self.assertResponseEqual(response, 200, { + 'id': position.id, + 'title': 'Captain', + }) + + +class UpdatePositionTestCase(CremeAPITestCase): + url_name = 'creme_api__positions-detail' + method = 'put' + + def test_update_position(self): + position = self.factory.position() + response = self.make_request(to=position.id, data={ + 'title': "CAPTAIN", + }) + self.assertResponseEqual(response, 200, { + 'id': position.id, + 'title': 'CAPTAIN', + }) + position.refresh_from_db() + self.assertEqual(position.title, "CAPTAIN") + + +class PartialUpdatePositionTestCase(CremeAPITestCase): + url_name = 'creme_api__positions-detail' + method = 'patch' + + def test_partial_update_position(self): + position = self.factory.position() + response = self.make_request(to=position.id, data={ + 'title': 'CAPTAIN', + }) + self.assertResponseEqual(response, 200, { + 'id': position.id, + 'title': 'CAPTAIN', + }) + position.refresh_from_db() + self.assertEqual(position.title, "CAPTAIN") + + +class ListPositionTestCase(CremeAPITestCase): + url_name = 'creme_api__positions-list' + method = 'get' + + def test_list_positions(self): + Position.objects.all().delete() + position1 = self.factory.position(title="1") + position2 = self.factory.position(title="2") + response = self.make_request() + self.assertResponseEqual(response, 200, [ + {'id': position1.id, 'title': '1'}, + {'id': position2.id, 'title': '2'}, + ]) + + +class DeletePositionTestCase(CremeAPITestCase): + url_name = 'creme_api__positions-detail' + method = 'delete' + + def test_delete_position(self): + position = self.factory.position() + response = self.make_request(to=position.id) + self.assertResponseEqual(response, 204) + self.assertFalse(Position.objects.filter(id=position.id).exists()) diff --git a/creme/creme_api/tests/functional/test_roles.py b/creme/creme_api/tests/test_roles.py similarity index 93% rename from creme/creme_api/tests/functional/test_roles.py rename to creme/creme_api/tests/test_roles.py index 59464cbe00..741c266b6f 100644 --- a/creme/creme_api/tests/functional/test_roles.py +++ b/creme/creme_api/tests/test_roles.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType -from creme.creme_api.tests.utils import CremeAPITestCase +from creme.creme_api.tests.utils import CremeAPITestCase, Factory from creme.creme_core.models import UserRole from creme.persons import get_contact_model, get_organisation_model @@ -10,6 +10,27 @@ Organisation = get_organisation_model() +@Factory.register +def role(factory, **kwargs): + contact_ct = ContentType.objects.get_for_model(Contact) + orga_ct = ContentType.objects.get_for_model(Organisation) + data = { + 'name': "Basic", + 'allowed_apps': ['creme_core', 'creme_api', 'persons'], + 'admin_4_apps': ['creme_core', 'creme_api'], + 'creatable_ctypes': [contact_ct.id, orga_ct.id], + 'exportable_ctypes': [contact_ct.id], + } + data.update(**kwargs) + role = UserRole(name=data['name']) + role.allowed_apps = data['allowed_apps'] + role.admin_4_apps = data['admin_4_apps'] + role.save() + role.creatable_ctypes.set(data['creatable_ctypes']) + role.exportable_ctypes.set(data['exportable_ctypes']) + return role + + class CreateRoleTestCase(CremeAPITestCase): url_name = 'creme_api__roles-list' method = 'post' diff --git a/creme/creme_api/tests/test_sectors.py b/creme/creme_api/tests/test_sectors.py new file mode 100644 index 0000000000..9cd585595b --- /dev/null +++ b/creme/creme_api/tests/test_sectors.py @@ -0,0 +1,108 @@ +from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.persons.models import Sector + + +@Factory.register +def sector(factory, **kwargs): + data = factory.sector_data(**kwargs) + return Sector.objects.create(**data) + + +@Factory.register +def sector_data(factory, **kwargs): + kwargs.setdefault('title', 'Industry') + return kwargs + + +class CreateSectorTestCase(CremeAPITestCase): + url_name = 'creme_api__sectors-list' + method = 'post' + + def test_validation__required(self): + response = self.make_request(data={}) + self.assertValidationErrors(response, { + 'title': ['required'], + }) + + def test_create_sector(self): + data = self.factory.sector_data() + response = self.make_request(data=data) + sector = Sector.objects.get(id=response.data['id']) + self.assertResponseEqual(response, 201, { + 'id': sector.id, + 'title': 'Industry', + }) + self.assertEqual(sector.title, "Industry") + + +class RetrieveSectorTestCase(CremeAPITestCase): + url_name = 'creme_api__sectors-detail' + method = 'get' + + def test_retrieve_sector(self): + sector = self.factory.sector() + response = self.make_request(to=sector.id) + self.assertResponseEqual(response, 200, { + 'id': sector.id, + 'title': 'Industry', + }) + + +class UpdateSectorTestCase(CremeAPITestCase): + url_name = 'creme_api__sectors-detail' + method = 'put' + + def test_update_sector(self): + sector = self.factory.sector() + response = self.make_request(to=sector.id, data={ + 'title': "Agro", + }) + self.assertResponseEqual(response, 200, { + 'id': sector.id, + 'title': 'Agro', + }) + sector.refresh_from_db() + self.assertEqual(sector.title, "Agro") + + +class PartialUpdateSectorTestCase(CremeAPITestCase): + url_name = 'creme_api__sectors-detail' + method = 'patch' + + def test_partial_update_sector(self): + sector = self.factory.sector() + response = self.make_request(to=sector.id, data={ + 'title': 'Agro', + }) + self.assertResponseEqual(response, 200, { + 'id': sector.id, + 'title': 'Agro', + }) + sector.refresh_from_db() + self.assertEqual(sector.title, "Agro") + + +class ListSectorTestCase(CremeAPITestCase): + url_name = 'creme_api__sectors-list' + method = 'get' + + def test_list_sectors(self): + Sector.objects.all().delete() + sector1 = self.factory.sector(title="1") + sector2 = self.factory.sector(title="2") + response = self.make_request() + self.assertResponseEqual(response, 200, [ + {'id': sector1.id, 'title': '1'}, + {'id': sector2.id, 'title': '2'}, + ]) + + +class DeleteSectorTestCase(CremeAPITestCase): + url_name = 'creme_api__sectors-detail' + method = 'delete' + + def test_delete_sector(self): + sector = self.factory.sector() + response = self.make_request(to=sector.id) + self.assertResponseEqual(response, 204) + self.assertFalse(Sector.objects.filter(id=sector.id).exists()) diff --git a/creme/creme_api/tests/functional/test_teams.py b/creme/creme_api/tests/test_teams.py similarity index 94% rename from creme/creme_api/tests/functional/test_teams.py rename to creme/creme_api/tests/test_teams.py index 53c4a35242..2d407c38cd 100644 --- a/creme/creme_api/tests/functional/test_teams.py +++ b/creme/creme_api/tests/test_teams.py @@ -1,13 +1,30 @@ from django.contrib.auth import get_user_model from django.urls import reverse -from creme.creme_api.tests.utils import CremeAPITestCase +from creme.creme_api.tests.utils import CremeAPITestCase, Factory from creme.persons import get_contact_model CremeUser = get_user_model() Contact = get_contact_model() +@Factory.register +def team(factory, **kwargs): + data = { + 'username': 'Team #1', + } + data.update(**kwargs) + if 'name' in data: + data['username'] = data.pop('name') + data['is_team'] = True + teammates = data.pop('teammates', []) + + team = CremeUser.objects.create(**data) + team.teammates = teammates + + return team + + class CreateTeamTestCase(CremeAPITestCase): url_name = 'creme_api__teams-list' method = 'post' diff --git a/creme/creme_api/tests/functional/test_tokens.py b/creme/creme_api/tests/test_tokens.py similarity index 97% rename from creme/creme_api/tests/functional/test_tokens.py rename to creme/creme_api/tests/test_tokens.py index 6cde61d240..067632bbb7 100644 --- a/creme/creme_api/tests/functional/test_tokens.py +++ b/creme/creme_api/tests/test_tokens.py @@ -6,7 +6,7 @@ class TokensTestCase(CremeAPITestCase): auto_login = False - url_name = 'creme_api__tokens' + url_name = 'creme_api__tokens-list' method = 'post' def test_create_token__missing(self): diff --git a/creme/creme_api/tests/functional/test_users.py b/creme/creme_api/tests/test_users.py similarity index 96% rename from creme/creme_api/tests/functional/test_users.py rename to creme/creme_api/tests/test_users.py index 9797bca7dd..daf51189b0 100644 --- a/creme/creme_api/tests/functional/test_users.py +++ b/creme/creme_api/tests/test_users.py @@ -1,11 +1,32 @@ from django.contrib.auth import get_user_model from django.urls import reverse -from creme.creme_api.tests.utils import CremeAPITestCase +from creme.creme_api.tests.utils import CremeAPITestCase, Factory CremeUser = get_user_model() +@Factory.register +def user(factory, **kwargs): + data = factory.user_data(**kwargs) + return CremeUser.objects.create(**data) + + +@Factory.register +def user_data(factory, **kwargs): + data = { + 'username': 'john.doe', + 'first_name': 'John', + 'last_name': 'Doe', + 'email': 'john.doe@provider.com', + 'is_active': True, + 'is_superuser': True, + 'role': None, + } + data.update(**kwargs) + return data + + class CreateUserTestCase(CremeAPITestCase): url_name = 'creme_api__users-list' method = 'post' diff --git a/creme/creme_api/tests/utils.py b/creme/creme_api/tests/utils.py index 2e209fd04f..bece709ac0 100644 --- a/creme/creme_api/tests/utils.py +++ b/creme/creme_api/tests/utils.py @@ -1,101 +1,21 @@ -from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from django.urls import reverse from rest_framework.fields import DateTimeField from rest_framework.test import APITestCase from creme.creme_api.api.authentication import TokenAuthentication from creme.creme_api.models import Application, Token -from creme.creme_core.models import SetCredentials, UserRole -from creme.persons import get_contact_model, get_organisation_model - -Contact = get_contact_model() -CremeUser = get_user_model() -Organisation = get_organisation_model() class Factory: - def user(self, **kwargs): - data = { - 'username': 'john.doe', - 'first_name': 'John', - 'last_name': 'Doe', - 'email': 'john.doe@provider.com', - 'is_active': True, - 'is_superuser': True, - 'role': None, - } - data.update(**kwargs) - return CremeUser.objects.create(**data) - - def user_data(self, **kwargs): - data = { - 'username': "john.doe", - 'first_name': "John", - 'last_name': "Doe", - 'email': "john.doe@provider.com", - 'is_active': True, - "is_superuser": True, - 'role': None, - } - data.update(**kwargs) - return data + @classmethod + def register(cls, func): + if hasattr(cls, func.__name__): + raise AttributeError(func.__name__) + setattr(cls, func.__name__, classmethod(func)) - def team(self, **kwargs): - data = { - 'username': 'Team #1', - } - data.update(**kwargs) - if 'name' in data: - data['username'] = data.pop('name') - data['is_team'] = True - teammates = data.pop('teammates', []) - - team = CremeUser.objects.create(**data) - team.teammates = teammates - - return team - - def contact(self, **kwargs): - return Contact.objects.create(**kwargs) - - def role(self, **kwargs): - contact_ct = ContentType.objects.get_for_model(Contact) - orga_ct = ContentType.objects.get_for_model(Organisation) - data = { - 'name': "Basic", - 'allowed_apps': ['creme_core', 'creme_api', 'persons'], - 'admin_4_apps': ['creme_core', 'creme_api'], - 'creatable_ctypes': [contact_ct.id, orga_ct.id], - 'exportable_ctypes': [contact_ct.id], - } - data.update(**kwargs) - role = UserRole(name=data['name']) - role.allowed_apps = data['allowed_apps'] - role.admin_4_apps = data['admin_4_apps'] - role.save() - role.creatable_ctypes.set(data['creatable_ctypes']) - role.exportable_ctypes.set(data['exportable_ctypes']) - return role - - def credential(self, **kwargs): - contact_ct = ContentType.objects.get_for_model(Contact) - perms = {'can_view', 'can_change', 'can_delete', 'can_link', 'can_unlink'} - data = { - 'set_type': SetCredentials.ESET_OWN, - 'ctype': contact_ct, - 'forbidden': False, - 'efilter': None, - **{p: True for p in perms} - } - data.update(**kwargs) - if 'role' not in data: - data['role'] = self.role() - value = {k: data.pop(k) for k in perms} - creds = SetCredentials(**data) - creds.set_value(**value) - creds.save() - return creds + +def to_iso8601(value): + return DateTimeField().to_representation(value) class CremeAPITestCase(APITestCase): @@ -103,6 +23,7 @@ class CremeAPITestCase(APITestCase): url_name = None method = None maxDiff = None + to_iso8601 = staticmethod(to_iso8601) @classmethod def setUpClass(cls): @@ -149,10 +70,6 @@ def assertResponseEqual(self, response, status_code, data=None): else: self.assertEqual(response.data, data) - @staticmethod - def to_iso8601(value): - return DateTimeField().to_representation(value) - def make_request(self, *, to=None, data=None): assert self.url_name is not None assert self.method is not None @@ -160,3 +77,18 @@ def make_request(self, *, to=None, data=None): url = reverse(self.url_name, args=args) method = getattr(self.client, self.method) return method(url, data=data, format='json') + + def consume_list(self, data=None): + assert self.url_name is not None and self.url_name.endswith("-list") + assert self.method == 'get' + method = getattr(self.client, self.method) + + responses = [] + results = [] + url = reverse(self.url_name) + while url: + response = method(url, data=data, format='json') + responses.append(response) + results.extend(response.data['results']) + url = response.data['next'] + return responses, results diff --git a/creme/creme_api/urls.py b/creme/creme_api/urls.py index 45b6a50b61..96917177a8 100644 --- a/creme/creme_api/urls.py +++ b/creme/creme_api/urls.py @@ -1,7 +1,6 @@ from django.urls import include, re_path from creme.creme_api.api.routes import router -from creme.creme_api.api.tokens.views import TokenView from creme.creme_api.views import ( ApplicationCreation, ApplicationEdition, @@ -29,8 +28,4 @@ ), ]), ), -] + [ - re_path(r"^tokens/$", TokenView.as_view(), name="creme_api__tokens"), - # re_path(r"^revoke_token/$", oauth_views.RevokeTokenView.as_view(), name="revoke-token"), - # re_path(r"^introspect/$", oauth_views.IntrospectTokenView.as_view(), name="introspect"), ] + router.urls diff --git a/creme/creme_api/views.py b/creme/creme_api/views.py index ccfa0c033e..38ed7e8da2 100644 --- a/creme/creme_api/views.py +++ b/creme/creme_api/views.py @@ -27,12 +27,7 @@ class SchemaView(DRFSchemaView): public = True generator_class = openapi.SchemaGenerator - def get_description(self, request=None): - creme_api_app_config = apps.get_app_config('creme_api') - context = { - 'creme_api__base_url': self.request.build_absolute_uri( - creme_api_app_config.url_root) - } + def get_description(self, context=None, request=None): description = render_to_string( self.description_template, context=context, request=request) # Force django safestring into builtin string @@ -43,8 +38,11 @@ def get_title(self): def initial(self, request, *args, **kwargs): super().initial(request, *args, **kwargs) + creme_api_app_config = apps.get_app_config('creme_api') + creme_api_rool_url = self.request.build_absolute_uri(creme_api_app_config.url_root) + context = {'creme_api_rool_url': creme_api_rool_url} title = self.get_title() - description = self.get_description(request=request) + description = self.get_description(context=context, request=request) self.schema_generator = self.generator_class( title=title, description=description, @@ -69,7 +67,7 @@ class DocumentationView(_DocumentationBaseView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['creme_api__tokens_url'] = self.request.build_absolute_uri( - reverse("creme_api__tokens")) + reverse("creme_api__tokens-list")) context['token_type'] = TokenAuthentication.keyword return context From 6616dab507f7f4b726bb6154eb33559ff345c047 Mon Sep 17 00:00:00 2001 From: Hugo Smett Date: Mon, 14 Feb 2022 15:43:27 +0100 Subject: [PATCH 07/12] contacts --- creme/creme_api/api/core/exceptions.py | 9 + creme/creme_api/api/core/viewsets.py | 28 +- creme/creme_api/api/persons/serializers.py | 189 +++- creme/creme_api/api/persons/viewsets.py | 9 +- creme/creme_api/tests/__init__.py | 1 + creme/creme_api/tests/test_addresses.py | 27 + creme/creme_api/tests/test_civilities.py | 25 +- creme/creme_api/tests/test_contacts.py | 1037 ++++++++++++++++---- creme/creme_api/tests/test_contenttypes.py | 4 +- creme/creme_api/tests/test_credentials.py | 27 +- creme/creme_api/tests/test_positions.py | 25 +- creme/creme_api/tests/test_roles.py | 44 +- creme/creme_api/tests/test_sectors.py | 25 +- creme/creme_api/tests/test_teams.py | 59 +- creme/creme_api/tests/test_tokens.py | 10 +- creme/creme_api/tests/test_users.py | 93 +- creme/creme_api/tests/utils.py | 56 +- 17 files changed, 1247 insertions(+), 421 deletions(-) create mode 100644 creme/creme_api/api/core/exceptions.py create mode 100644 creme/creme_api/tests/test_addresses.py diff --git a/creme/creme_api/api/core/exceptions.py b/creme/creme_api/api/core/exceptions.py new file mode 100644 index 0000000000..4a618e3795 --- /dev/null +++ b/creme/creme_api/api/core/exceptions.py @@ -0,0 +1,9 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import status +from rest_framework.exceptions import APIException + + +class UnprocessableEntity(APIException): + status_code = status.HTTP_422_UNPROCESSABLE_ENTITY + default_detail = _('Unprocessable entity.') + default_code = 'unprocessable_entity' diff --git a/creme/creme_api/api/core/viewsets.py b/creme/creme_api/api/core/viewsets.py index 7a8d9c54e1..99abaca377 100644 --- a/creme/creme_api/api/core/viewsets.py +++ b/creme/creme_api/api/core/viewsets.py @@ -1,9 +1,9 @@ -from rest_framework import viewsets +from django.db.models import ProtectedError +from rest_framework import status, viewsets from rest_framework.decorators import action -from rest_framework.exceptions import ValidationError from rest_framework.response import Response -from creme.creme_core.core.exceptions import SpecificProtectedError +from creme.creme_api.api.core.exceptions import UnprocessableEntity from .serializers import SimpleCremeEntitySerializer @@ -14,8 +14,8 @@ class CremeModelViewSet(viewsets.ModelViewSet): def perform_destroy(self, instance): try: instance.delete() - except SpecificProtectedError as exc: - raise ValidationError(str(exc)) + except ProtectedError as exc: + raise UnprocessableEntity(str(exc), code="protected") def get_queryset(self): queryset = super().get_queryset() @@ -32,8 +32,8 @@ def trash(self, request, *args, **kwargs): try: instance.trash() - except SpecificProtectedError as exc: - raise ValidationError(str(exc)) + except ProtectedError as exc: + raise UnprocessableEntity(str(exc), code="protected") serializer = self.get_serializer(instance) return Response(serializer.data) @@ -42,10 +42,16 @@ def trash(self, request, *args, **kwargs): def restore(self, request, *args, **kwargs): instance = self.get_object() - try: - instance.restore() - except SpecificProtectedError as exc: - raise ValidationError(str(exc)) + instance.restore() serializer = self.get_serializer(instance) return Response(serializer.data) + + @action(methods=['post'], detail=True) + def clone(self, request, *args, **kwargs): + instance = self.get_object() + + new = instance.clone() + + serializer = self.get_serializer(new) + return Response(serializer.data, status=status.HTTP_201_CREATED) diff --git a/creme/creme_api/api/persons/serializers.py b/creme/creme_api/api/persons/serializers.py index 62ad45be80..4b8948de1a 100644 --- a/creme/creme_api/api/persons/serializers.py +++ b/creme/creme_api/api/persons/serializers.py @@ -1,3 +1,5 @@ +from django.db import transaction +from django.utils.translation import gettext as _ from rest_framework import serializers from creme.creme_api.api.core.serializers import ( @@ -17,8 +19,76 @@ StaffSize, ) +Address = get_address_model() + + +class AddressSerializer(serializers.ModelSerializer): + owner = CremeEntityRelatedField() + + class Meta: + model = Address + fields = [ + 'id', + 'name', + 'address', + 'po_box', + 'zipcode', + 'city', + 'department', + 'state', + 'country', + 'owner', + ] + + +class InnerAddressSerializer(serializers.ModelSerializer): + class Meta: + model = Address + fields = [ + 'address', + 'po_box', + 'zipcode', + 'city', + 'department', + 'state', + 'country', + ] + + +class CivilitySerializer(serializers.ModelSerializer): + class Meta: + model = Civility + fields = ['id', 'title', 'shortcut'] + + +class PositionSerializer(serializers.ModelSerializer): + class Meta: + model = Position + fields = ['id', 'title'] + + +class StaffSizeSerializer(serializers.ModelSerializer): + class Meta: + model = StaffSize + fields = ['id', 'size'] + + +class LegalFormSerializer(serializers.ModelSerializer): + class Meta: + model = LegalForm + fields = ['id', 'title'] + + +class SectorSerializer(serializers.ModelSerializer): + class Meta: + model = Sector + fields = ['id', 'title'] + class ContactSerializer(CremeEntitySerializer): + billing_address = InnerAddressSerializer(required=False) + shipping_address = InnerAddressSerializer(required=False) + class Meta(CremeEntitySerializer.Meta): model = get_contact_model() fields = CremeEntitySerializer.Meta.fields + [ @@ -41,6 +111,76 @@ class Meta(CremeEntitySerializer.Meta): # 'image', # Need documents ] + def to_representation(self, instance): + data = super().to_representation(instance) + if 'billing_address' in data and data['billing_address'] is None: + data.pop('billing_address') + if 'shipping_address' in data and data['shipping_address'] is None: + data.pop('shipping_address') + return data + + @transaction.atomic + def create(self, validated_data): + billing_address_data = validated_data.pop('billing_address', None) + shipping_address_data = validated_data.pop('shipping_address', None) + instance = super().create(validated_data) + save = False + if billing_address_data is not None: + save = True + instance.billing_address = self.fields['billing_address'].create({ + **billing_address_data, + 'owner': instance, + 'name': _('Billing address'), + }) + if shipping_address_data is not None: + save = True + instance.shipping_address = self.fields['shipping_address'].create({ + **shipping_address_data, + 'owner': instance, + 'name': _('Shipping address'), + }) + if save: + instance.save() + return instance + + @transaction.atomic + def update(self, instance, validated_data): + billing_address_data = validated_data.pop('billing_address', None) + shipping_address_data = validated_data.pop('shipping_address', None) + instance = super().update(instance, validated_data) + save = False + if billing_address_data is not None: + if instance.billing_address_id is not None: + self.fields['billing_address'].update( + instance.billing_address, { + **billing_address_data, + 'name': _('Billing address'), + }) + else: + save = True + instance.billing_address = self.fields['billing_address'].create({ + **billing_address_data, + 'owner': instance, + 'name': _('Billing address'), + }) + if shipping_address_data is not None: + if instance.shipping_address_id is not None: + self.fields['shipping_address'].update( + instance.shipping_address, { + **shipping_address_data, + 'name': _('Shipping address'), + }) + else: + save = True + instance.shipping_address = self.fields['shipping_address'].create({ + **shipping_address_data, + 'owner': instance, + 'name': _('Shipping address'), + }) + if save: + instance.save() + return instance + class OrganisationSerializer(CremeEntitySerializer): class Meta: @@ -68,52 +208,3 @@ class Meta: 'creation_date', 'image', ] - - -class AddressSerializer(serializers.ModelSerializer): - owner = CremeEntityRelatedField() - - class Meta: - model = get_address_model() - fields = [ - 'id', - 'name', - 'address', - 'po_box', - 'zipcode', - 'city', - 'department', - 'state', - 'country', - 'owner', - ] - - -class CivilitySerializer(serializers.ModelSerializer): - class Meta: - model = Civility - fields = ['id', 'title', 'shortcut'] - - -class PositionSerializer(serializers.ModelSerializer): - class Meta: - model = Position - fields = ['id', 'title'] - - -class StaffSizeSerializer(serializers.ModelSerializer): - class Meta: - model = StaffSize - fields = ['id', 'size'] - - -class LegalFormSerializer(serializers.ModelSerializer): - class Meta: - model = LegalForm - fields = ['id', 'title'] - - -class SectorSerializer(serializers.ModelSerializer): - class Meta: - model = Sector - fields = ['id', 'title'] diff --git a/creme/creme_api/api/persons/viewsets.py b/creme/creme_api/api/persons/viewsets.py index d2224aaed0..c947c922c1 100644 --- a/creme/creme_api/api/persons/viewsets.py +++ b/creme/creme_api/api/persons/viewsets.py @@ -50,8 +50,12 @@ class ContactViewSet(CremeEntityViewSet): restore: Restore a contact from the trash. + clone: + Clone a contact. + """ - queryset = persons.get_contact_model().objects.all() + queryset = persons.get_contact_model().objects.select_related( + 'billing_address', 'shipping_address') serializer_class = ContactSerializer schema = CremeSchema(tags=["Contacts"]) @@ -82,6 +86,9 @@ class OrganisationViewSet(CremeEntityViewSet): restore: Restore an organisation from the trash. + clone: + Clone an organisation. + """ queryset = persons.get_organisation_model().objects.all() serializer_class = OrganisationSerializer diff --git a/creme/creme_api/tests/__init__.py b/creme/creme_api/tests/__init__.py index 7e2c455b7b..b553b2d1d0 100644 --- a/creme/creme_api/tests/__init__.py +++ b/creme/creme_api/tests/__init__.py @@ -1,3 +1,4 @@ +from .test_addresses import * # noqa from .test_authentication import * # noqa from .test_civilities import * # noqa from .test_contacts import * # noqa diff --git a/creme/creme_api/tests/test_addresses.py b/creme/creme_api/tests/test_addresses.py new file mode 100644 index 0000000000..7d42f2e5c3 --- /dev/null +++ b/creme/creme_api/tests/test_addresses.py @@ -0,0 +1,27 @@ +from creme.creme_api.tests.utils import Factory +from creme.persons import get_address_model + +Address = get_address_model() + + +@Factory.register +def address(factory, **kwargs): + data = factory.address_data(**kwargs) + return Address.objects.create(**data) + + +@Factory.register +def address_data(factory, **kwargs): + data = { + # 'name': "Address name", + 'address': "1 Main Street", + 'po_box': "PO123", + 'zipcode': "ZIP123", + 'city': "City", + 'department': "Dept", + 'state': "State", + 'country': "Country", + # 'owner': "", + } + data.update(**kwargs) + return data diff --git a/creme/creme_api/tests/test_civilities.py b/creme/creme_api/tests/test_civilities.py index fed2b22e3a..781ec68e76 100644 --- a/creme/creme_api/tests/test_civilities.py +++ b/creme/creme_api/tests/test_civilities.py @@ -20,7 +20,7 @@ class CreateCivilityTestCase(CremeAPITestCase): method = 'post' def test_validation__required(self): - response = self.make_request(data={}) + response = self.make_request(data={}, status_code=400) self.assertValidationErrors(response, { 'title': ['required'], 'shortcut': ['required'], @@ -28,9 +28,9 @@ def test_validation__required(self): def test_create_civility(self): data = self.factory.civility_data() - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=201) civility = Civility.objects.get(id=response.data['id']) - self.assertResponseEqual(response, 201, { + self.assertPayloadEqual(response, { 'id': civility.id, 'title': 'Captain', 'shortcut': 'Cpt', @@ -45,8 +45,8 @@ class RetrieveCivilityTestCase(CremeAPITestCase): def test_retrieve_civility(self): civility = self.factory.civility() - response = self.make_request(to=civility.id) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=civility.id, status_code=200) + self.assertPayloadEqual(response, { 'id': civility.id, 'title': 'Captain', 'shortcut': 'Cpt', @@ -62,8 +62,8 @@ def test_update_civility(self): response = self.make_request(to=civility.id, data={ 'title': "CAPTAIN", 'shortcut': "CAP", - }) - self.assertResponseEqual(response, 200, { + }, status_code=200) + self.assertPayloadEqual(response, { 'id': civility.id, 'title': 'CAPTAIN', 'shortcut': 'CAP', @@ -81,8 +81,8 @@ def test_partial_update_civility(self): civility = self.factory.civility() response = self.make_request(to=civility.id, data={ 'shortcut': "CAP", - }) - self.assertResponseEqual(response, 200, { + }, status_code=200) + self.assertPayloadEqual(response, { 'id': civility.id, 'title': 'Captain', 'shortcut': 'CAP', @@ -100,8 +100,8 @@ def test_list_civilities(self): Civility.objects.all().delete() civility1 = self.factory.civility(title="1", shortcut="1") civility2 = self.factory.civility(title="2", shortcut="2") - response = self.make_request() - self.assertResponseEqual(response, 200, [ + response = self.make_request(status_code=200) + self.assertPayloadEqual(response, [ {'id': civility1.id, 'title': '1', 'shortcut': '1'}, {'id': civility2.id, 'title': '2', 'shortcut': '2'}, ]) @@ -113,6 +113,5 @@ class DeleteCivilityTestCase(CremeAPITestCase): def test_delete_civility(self): civility = self.factory.civility() - response = self.make_request(to=civility.id) - self.assertResponseEqual(response, 204) + self.make_request(to=civility.id, status_code=204) self.assertFalse(Civility.objects.filter(id=civility.id).exists()) diff --git a/creme/creme_api/tests/test_contacts.py b/creme/creme_api/tests/test_contacts.py index 712c0d8ab6..e396ee0cef 100644 --- a/creme/creme_api/tests/test_contacts.py +++ b/creme/creme_api/tests/test_contacts.py @@ -1,5 +1,7 @@ from datetime import date +from django.utils.translation import gettext as _ + from creme.creme_api.tests.utils import CremeAPITestCase, Factory from creme.persons import get_contact_model @@ -8,8 +10,35 @@ @Factory.register def contact(factory, **kwargs): + if 'user' not in kwargs: + kwargs['user'] = factory.user() + data = factory.contact_data(**kwargs) - return Contact.objects.create(**data) + + if 'civility' not in data: + data['civility'] = factory.civility() + + if 'position' not in data: + data['position'] = factory.position() + + if 'sector' not in data: + data['sector'] = factory.sector() + + if 'billing_address' not in data: + data['billing_address'] = factory.address_data() + + if 'shipping_address' not in data: + data['shipping_address'] = factory.address_data() + + billing_address_data = data.pop('billing_address') + shipping_address_data = data.pop('shipping_address') + contact = Contact.objects.create(**data) + if billing_address_data: + contact.billing_address = factory.address(owner=contact, **billing_address_data) + if shipping_address_data: + contact.shipping_address = factory.address(owner=contact, **shipping_address_data) + contact.save() + return contact @Factory.register @@ -17,21 +46,8 @@ def contact_data(factory, **kwargs): if 'user' not in kwargs: kwargs['user'] = factory.user().id - if 'civility' not in kwargs: - kwargs['civility'] = factory.civility().id - - if 'position' not in kwargs: - kwargs['position'] = factory.position().id - - if 'sector' not in kwargs: - kwargs['sector'] = factory.sector().id - data = { - # 'user': None, 'description': "Description", - # 'billing_address': None, - # 'shipping_address': None, - # 'civility': None, 'last_name': "Dupont", 'first_name': "Jean", 'skype': "jean.dupont", @@ -40,10 +56,7 @@ def contact_data(factory, **kwargs): 'fax': "+330100000001", 'email': "jean.dupont@provider.com", 'url_site': "https://www.jean-dupont.provider.com", - # 'position': None, 'full_position': "Full position", - # 'sector': None, - # 'is_user': None, 'birthday': "2000-01-01", } data.update(**kwargs) @@ -55,7 +68,7 @@ class CreateContactTestCase(CremeAPITestCase): method = 'post' def test_validation__required(self): - response = self.make_request(data={}) + response = self.make_request(data={}, status_code=400) self.assertValidationErrors(response, { 'last_name': ['required'], 'user': ['required'], @@ -72,183 +85,829 @@ def test_create_contact(self): position=position.id, sector=sector.id, ) - response = self.make_request(data=data) - self.assertEqual(response.status_code, 201, response.data) + response = self.make_request(data=data, status_code=201) contact = Contact.objects.get(id=response.data['id']) - self.assertResponseEqual(response, 201, { - **data, + self.assertPayloadEqual(response, { 'id': contact.id, + 'birthday': '2000-01-01', + 'civility': civility.id, 'uuid': str(contact.uuid), + 'created': self.to_iso8601(contact.created), + 'modified': self.to_iso8601(contact.modified), + 'description': 'Description', + 'email': 'jean.dupont@provider.com', + 'fax': '+330100000001', + 'first_name': 'Jean', + 'full_position': 'Full position', 'is_deleted': False, + 'is_user': None, + 'last_name': 'Dupont', + 'mobile': '+330600000000', + 'phone': '+330100000000', + 'position': position.id, + 'sector': sector.id, + 'skype': 'jean.dupont', + 'url_site': 'https://www.jean-dupont.provider.com', + 'user': user.id, + }) + self.assertEqual(contact.birthday, date(2000, 1, 1)) + self.assertEqual(contact.user, user) + self.assertEqual(contact.description, 'Description') + self.assertEqual(contact.civility, civility) + self.assertEqual(contact.position, position) + self.assertEqual(contact.sector, sector) + self.assertEqual(contact.last_name, 'Dupont') + self.assertEqual(contact.first_name, 'Jean') + self.assertEqual(contact.skype, 'jean.dupont') + self.assertEqual(contact.phone, '+330100000000') + self.assertEqual(contact.mobile, '+330600000000') + self.assertEqual(contact.fax, '+330100000001') + self.assertEqual(contact.email, 'jean.dupont@provider.com') + self.assertEqual(contact.url_site, 'https://www.jean-dupont.provider.com') + self.assertEqual(contact.full_position, 'Full position') + self.assertFalse(contact.is_deleted) + self.assertIsNone(contact.is_user) + self.assertIsNone(contact.billing_address_id) + self.assertIsNone(contact.shipping_address_id) + + def test_create_contact__with_addresses(self): + billing_address_data = self.factory.address_data() + shipping_address_data = self.factory.address_data() + data = self.factory.contact_data( + billing_address={**billing_address_data, 'name': "NOT USED"}, + shipping_address={**shipping_address_data, 'name': "NOT USED"}, + ) + response = self.make_request(data=data, status_code=201) + contact = Contact.objects.get(id=response.data['id']) + self.assertPayloadEqual(response, { + 'id': contact.id, + 'birthday': '2000-01-01', + 'civility': None, + 'uuid': str(contact.uuid), 'created': self.to_iso8601(contact.created), 'modified': self.to_iso8601(contact.modified), + 'description': 'Description', + 'email': 'jean.dupont@provider.com', + 'fax': '+330100000001', + 'first_name': 'Jean', + 'full_position': 'Full position', + 'is_deleted': False, 'is_user': None, - 'billing_address': None, - 'shipping_address': None, + 'last_name': 'Dupont', + 'mobile': '+330600000000', + 'phone': '+330100000000', + 'position': None, + 'sector': None, + 'skype': 'jean.dupont', + 'url_site': 'https://www.jean-dupont.provider.com', + 'user': data['user'], + 'billing_address': { + 'address': '1 Main Street', + 'city': 'City', + 'country': 'Country', + 'department': 'Dept', + 'po_box': 'PO123', + 'state': 'State', + 'zipcode': 'ZIP123', + }, + 'shipping_address': { + 'address': '1 Main Street', + 'city': 'City', + 'country': 'Country', + 'department': 'Dept', + 'po_box': 'PO123', + 'state': 'State', + 'zipcode': 'ZIP123', + }, }) + self.assertEqual(contact.birthday, date(2000, 1, 1)) - self.assertEqual(contact.description, data['description']) - self.assertEqual(contact.last_name, data['last_name']) - self.assertEqual(contact.first_name, data['first_name']) - self.assertEqual(contact.skype, data['skype']) - self.assertEqual(contact.phone, data['phone']) - self.assertEqual(contact.mobile, data['mobile']) - self.assertEqual(contact.fax, data['fax']) - self.assertEqual(contact.email, data['email']) - self.assertEqual(contact.url_site, data['url_site']) - self.assertEqual(contact.full_position, data['full_position']) - - -# class RetrieveContactTestCase(CremeAPITestCase): -# url_name = 'creme_api__contacts-detail' -# method = 'get' -# -# def test_get_contact(self): -# contact = self.factory.contact() -# response = self.make_request(to=contact.id) -# self.assertResponseEqual(response, 200, { -# 'id': contact.id, -# 'contactname': contact.contactname, -# 'last_name': contact.last_name, -# 'first_name': contact.first_name, -# 'email': contact.email, -# 'date_joined': self.to_iso8601(contact.date_joined), -# 'last_login': None, -# 'is_active': True, -# 'time_zone': 'Europe/Paris', -# 'theme': 'icecream' -# }) -# -# -# class UpdateContactTestCase(CremeAPITestCase): -# url_name = 'creme_api__contacts-detail' -# method = 'put' -# -# def test_validation__required(self): -# contact = self.factory.contact() -# response = self.make_request(to=contact.id, data={}) -# self.assertValidationErrors(response, { -# 'last_name': ['required'], -# 'user': ['required'], -# }) -# -# def test_update_contact(self): -# contact = self.factory.contact() -# -# data = self.factory.contact_data(last_name="Smith", contactname="Nick") -# response = self.make_request(to=contact.id, data=data) -# self.assertResponseEqual(response, 200, { -# 'id': contact.id, -# 'last_name': 'Smith', -# 'first_name': 'John', -# }) -# contact.refresh_from_db() -# self.assertEqual(contact.contactname, "Nick") -# self.assertEqual(contact.last_name, "Smith") -# -# -# class PartialUpdateContactTestCase(CremeAPITestCase): -# url_name = 'creme_api__contacts-detail' -# method = 'patch' -# -# def test_partial_update_contact(self): -# contact = self.factory.contact() -# data = {'theme': "chantilly"} -# response = self.make_request(to=contact.id, data=data) -# self.assertResponseEqual(response, 200, { -# 'id': contact.id, -# 'contactname': 'john.doe', -# 'last_name': 'Doe', -# 'first_name': 'John', -# 'email': 'john.doe@provider.com', -# 'date_joined': self.to_iso8601(contact.date_joined), -# 'last_login': None, -# 'is_active': True, -# 'is_supercontact': True, -# 'role': None, -# 'time_zone': 'Europe/Paris', -# 'theme': 'chantilly' -# }) -# contact.refresh_from_db() -# self.assertEqual(contact.theme, "chantilly") -# -# -# class ListContactTestCase(CremeAPITestCase): -# url_name = 'creme_api__contacts-list' -# method = 'get' -# -# def test_list_contacts(self): -# fulbert = Contact.objects.get() -# contact = self.factory.contact(contactname="contact", theme='chantilly') -# self.assertEqual(Contact.objects.count(), 2) -# -# response = self.make_request() -# self.assertResponseEqual(response, 200, [ -# { -# 'id': fulbert.id, -# 'contactname': 'root', -# 'last_name': 'Creme', -# 'first_name': 'Fulbert', -# 'email': fulbert.email, -# 'date_joined': self.to_iso8601(fulbert.date_joined), -# 'last_login': None, -# 'is_active': True, -# 'is_supercontact': True, -# 'role': None, -# 'time_zone': 'Europe/Paris', -# 'theme': 'icecream' -# }, -# { -# 'id': contact.id, -# 'contactname': 'contact', -# 'last_name': 'Doe', -# 'first_name': 'John', -# 'email': 'john.doe@provider.com', -# 'date_joined': self.to_iso8601(contact.date_joined), -# 'last_login': None, -# 'is_active': True, -# 'is_supercontact': True, -# 'role': None, -# 'time_zone': 'Europe/Paris', -# 'theme': 'chantilly' -# }, -# ]) -# -# -# class DeleteContactTestCase(CremeAPITestCase): -# url_name = 'creme_api__contacts-delete' -# method = 'post' -# -# def test_delete(self): -# url = reverse('creme_api__contacts-detail', args=[1]) -# response = self.client.delete(url, format='json') -# self.assertResponseEqual(response, 405) -# -# def test_validation__required(self): -# contact = self.factory.contact() -# response = self.make_request(to=contact.id, data={}) -# self.assertValidationErrors(response, { -# 'transfer_to': ['required'] -# }) -# -# def test_delete_contact(self): -# team = self.factory.team() -# contact1 = self.factory.contact(contactname='contact1') -# contact2 = self.factory.contact(contactname='contact2') -# contact = self.factory.contact(contact=contact2) -# -# data = {'transfer_to': contact1.id} -# response = self.make_request(to=contact2.id, data=data) -# self.assertResponseEqual(response, 204) -# -# self.assertFalse(Contact.objects.filter(contactname='contact2').exists()) -# contact.refresh_from_db() -# self.assertEqual(contact.contact, contact1) -# -# data = {'transfer_to': team.id} -# response = self.make_request(to=contact1.id, data=data) -# self.assertResponseEqual(response, 204) -# -# self.assertFalse(Contact.objects.filter(contactname='contact1').exists()) -# contact.refresh_from_db() -# self.assertEqual(contact.contact, team) + self.assertEqual(contact.user_id, data['user']) + self.assertEqual(contact.description, 'Description') + self.assertEqual(contact.last_name, 'Dupont') + self.assertEqual(contact.first_name, 'Jean') + self.assertEqual(contact.skype, 'jean.dupont') + self.assertEqual(contact.phone, '+330100000000') + self.assertEqual(contact.mobile, '+330600000000') + self.assertEqual(contact.fax, '+330100000001') + self.assertEqual(contact.email, 'jean.dupont@provider.com') + self.assertEqual(contact.url_site, 'https://www.jean-dupont.provider.com') + self.assertEqual(contact.full_position, 'Full position') + self.assertFalse(contact.is_deleted) + self.assertIsNone(contact.is_user) + + billing_address = contact.billing_address + self.assertEqual(billing_address.name, _('Billing address')) + self.assertEqual(billing_address.address, billing_address_data['address']) + self.assertEqual(billing_address.po_box, billing_address_data['po_box']) + self.assertEqual(billing_address.zipcode, billing_address_data['zipcode']) + self.assertEqual(billing_address.city, billing_address_data['city']) + self.assertEqual(billing_address.department, billing_address_data['department']) + self.assertEqual(billing_address.state, billing_address_data['state']) + self.assertEqual(billing_address.country, billing_address_data['country']) + self.assertEqual(billing_address.owner, contact) + + shipping_address = contact.shipping_address + self.assertEqual(shipping_address.name, _('Shipping address')) + self.assertEqual(shipping_address.address, shipping_address_data['address']) + self.assertEqual(shipping_address.po_box, shipping_address_data['po_box']) + self.assertEqual(shipping_address.zipcode, shipping_address_data['zipcode']) + self.assertEqual(shipping_address.city, shipping_address_data['city']) + self.assertEqual(shipping_address.department, shipping_address_data['department']) + self.assertEqual(shipping_address.state, shipping_address_data['state']) + self.assertEqual(shipping_address.country, shipping_address_data['country']) + self.assertEqual(shipping_address.owner, contact) + + +class RetrieveContactTestCase(CremeAPITestCase): + url_name = 'creme_api__contacts-detail' + method = 'get' + + def test_get_contact(self): + contact = self.factory.contact(billing_address=None) + response = self.make_request(to=contact.id, status_code=200) + self.assertPayloadEqual(response, { + 'id': contact.id, + 'birthday': '2000-01-01', + 'civility': contact.civility_id, + 'uuid': str(contact.uuid), + 'created': self.to_iso8601(contact.created), + 'modified': self.to_iso8601(contact.modified), + 'description': 'Description', + 'email': 'jean.dupont@provider.com', + 'fax': '+330100000001', + 'first_name': 'Jean', + 'full_position': 'Full position', + 'is_deleted': False, + 'is_user': None, + 'last_name': 'Dupont', + 'mobile': '+330600000000', + 'phone': '+330100000000', + 'position': contact.position_id, + 'sector': contact.sector_id, + 'skype': 'jean.dupont', + 'url_site': 'https://www.jean-dupont.provider.com', + 'user': contact.user_id, + 'shipping_address': { + 'address': '1 Main Street', + 'city': 'City', + 'country': 'Country', + 'department': 'Dept', + 'po_box': 'PO123', + 'state': 'State', + 'zipcode': 'ZIP123', + }, + }) + + +class UpdateContactTestCase(CremeAPITestCase): + url_name = 'creme_api__contacts-detail' + method = 'put' + + def test_validation__required(self): + contact = self.factory.contact() + response = self.make_request(to=contact.id, data={}, status_code=400) + self.assertValidationErrors(response, { + 'last_name': ['required'], + 'user': ['required'], + }) + + def test_update_contact(self): + contact = self.factory.contact(billing_address=None, civility=None) + civility = self.factory.civility() + sector = self.factory.sector() + + data = self.factory.contact_data( + user=contact.user_id, + civility=civility.id, + position=None, + sector=sector.id, + last_name="Smith", + first_name="Nick", + ) + response = self.make_request(to=contact.id, data=data, status_code=200) + contact.refresh_from_db() + self.assertPayloadEqual(response, { + 'id': contact.id, + 'birthday': '2000-01-01', + 'civility': civility.id, + 'uuid': str(contact.uuid), + 'created': self.to_iso8601(contact.created), + 'modified': self.to_iso8601(contact.modified), + 'description': 'Description', + 'email': 'jean.dupont@provider.com', + 'fax': '+330100000001', + 'first_name': 'Nick', + 'full_position': 'Full position', + 'is_deleted': False, + 'is_user': None, + 'last_name': 'Smith', + 'mobile': '+330600000000', + 'phone': '+330100000000', + 'position': None, + 'sector': sector.id, + 'skype': 'jean.dupont', + 'url_site': 'https://www.jean-dupont.provider.com', + 'user': contact.user_id, + 'shipping_address': { + 'address': '1 Main Street', + 'city': 'City', + 'country': 'Country', + 'department': 'Dept', + 'po_box': 'PO123', + 'state': 'State', + 'zipcode': 'ZIP123', + }, + }) + self.assertEqual(contact.birthday, date(2000, 1, 1)) + self.assertEqual(contact.user_id, data['user']) + self.assertEqual(contact.description, 'Description') + self.assertEqual(contact.civility, civility) + self.assertIsNone(contact.position) + self.assertEqual(contact.sector, sector) + self.assertEqual(contact.first_name, "Nick") + self.assertEqual(contact.last_name, "Smith") + self.assertEqual(contact.skype, 'jean.dupont') + self.assertEqual(contact.phone, '+330100000000') + self.assertEqual(contact.mobile, '+330600000000') + self.assertEqual(contact.fax, '+330100000001') + self.assertEqual(contact.email, 'jean.dupont@provider.com') + self.assertEqual(contact.url_site, 'https://www.jean-dupont.provider.com') + self.assertEqual(contact.full_position, 'Full position') + self.assertFalse(contact.is_deleted) + self.assertIsNone(contact.is_user) + + def test_update_contact__create_addresses(self): + contact = self.factory.contact(billing_address=None, shipping_address=None) + + billing_address_data = self.factory.address_data(address='billing street') + shipping_address_data = self.factory.address_data(address='shipping street') + data = self.factory.contact_data( + user=contact.user_id, + billing_address=billing_address_data, + shipping_address=shipping_address_data, + ) + response = self.make_request(to=contact.id, data=data, status_code=200) + contact.refresh_from_db() + self.assertPayloadEqual(response, { + 'id': contact.id, + 'birthday': '2000-01-01', + 'civility': contact.civility_id, + 'uuid': str(contact.uuid), + 'created': self.to_iso8601(contact.created), + 'modified': self.to_iso8601(contact.modified), + 'description': 'Description', + 'email': 'jean.dupont@provider.com', + 'fax': '+330100000001', + 'first_name': 'Jean', + 'full_position': 'Full position', + 'is_deleted': False, + 'is_user': None, + 'last_name': 'Dupont', + 'mobile': '+330600000000', + 'phone': '+330100000000', + 'position': contact.position_id, + 'sector': contact.sector_id, + 'skype': 'jean.dupont', + 'url_site': 'https://www.jean-dupont.provider.com', + 'user': contact.user_id, + 'billing_address': { + 'address': 'billing street', + 'city': 'City', + 'country': 'Country', + 'department': 'Dept', + 'po_box': 'PO123', + 'state': 'State', + 'zipcode': 'ZIP123', + }, + 'shipping_address': { + 'address': 'shipping street', + 'city': 'City', + 'country': 'Country', + 'department': 'Dept', + 'po_box': 'PO123', + 'state': 'State', + 'zipcode': 'ZIP123', + }, + }) + self.assertEqual(contact.birthday, date(2000, 1, 1)) + self.assertEqual(contact.user_id, data['user']) + self.assertEqual(contact.description, 'Description') + self.assertEqual(contact.first_name, "Jean") + self.assertEqual(contact.last_name, "Dupont") + self.assertEqual(contact.skype, 'jean.dupont') + self.assertEqual(contact.phone, '+330100000000') + self.assertEqual(contact.mobile, '+330600000000') + self.assertEqual(contact.fax, '+330100000001') + self.assertEqual(contact.email, 'jean.dupont@provider.com') + self.assertEqual(contact.url_site, 'https://www.jean-dupont.provider.com') + self.assertEqual(contact.full_position, 'Full position') + self.assertFalse(contact.is_deleted) + self.assertIsNone(contact.is_user) + + billing_address = contact.billing_address + self.assertEqual(billing_address.name, _('Billing address')) + self.assertEqual(billing_address.address, billing_address_data['address']) + self.assertEqual(billing_address.po_box, billing_address_data['po_box']) + self.assertEqual(billing_address.zipcode, billing_address_data['zipcode']) + self.assertEqual(billing_address.city, billing_address_data['city']) + self.assertEqual(billing_address.department, billing_address_data['department']) + self.assertEqual(billing_address.state, billing_address_data['state']) + self.assertEqual(billing_address.country, billing_address_data['country']) + self.assertEqual(billing_address.owner, contact) + + shipping_address = contact.shipping_address + self.assertEqual(shipping_address.name, _('Shipping address')) + self.assertEqual(shipping_address.address, shipping_address_data['address']) + self.assertEqual(shipping_address.po_box, shipping_address_data['po_box']) + self.assertEqual(shipping_address.zipcode, shipping_address_data['zipcode']) + self.assertEqual(shipping_address.city, shipping_address_data['city']) + self.assertEqual(shipping_address.department, shipping_address_data['department']) + self.assertEqual(shipping_address.state, shipping_address_data['state']) + self.assertEqual(shipping_address.country, shipping_address_data['country']) + self.assertEqual(shipping_address.owner, contact) + + def test_update_contact__update_addresses(self): + contact = self.factory.contact() + billing_address = contact.billing_address + shipping_address = contact.shipping_address + + billing_address_data = self.factory.address_data(address='billing street') + shipping_address_data = self.factory.address_data(address='shipping street') + data = self.factory.contact_data( + user=contact.user_id, + billing_address=billing_address_data, + shipping_address=shipping_address_data, + ) + response = self.make_request(to=contact.id, data=data, status_code=200) + contact.refresh_from_db() + self.assertPayloadEqual(response, { + 'id': contact.id, + 'birthday': '2000-01-01', + 'civility': contact.civility_id, + 'uuid': str(contact.uuid), + 'created': self.to_iso8601(contact.created), + 'modified': self.to_iso8601(contact.modified), + 'description': 'Description', + 'email': 'jean.dupont@provider.com', + 'fax': '+330100000001', + 'first_name': 'Jean', + 'full_position': 'Full position', + 'is_deleted': False, + 'is_user': None, + 'last_name': 'Dupont', + 'mobile': '+330600000000', + 'phone': '+330100000000', + 'position': contact.position_id, + 'sector': contact.sector_id, + 'skype': 'jean.dupont', + 'url_site': 'https://www.jean-dupont.provider.com', + 'user': contact.user_id, + 'billing_address': { + 'address': 'billing street', + 'city': 'City', + 'country': 'Country', + 'department': 'Dept', + 'po_box': 'PO123', + 'state': 'State', + 'zipcode': 'ZIP123', + }, + 'shipping_address': { + 'address': 'shipping street', + 'city': 'City', + 'country': 'Country', + 'department': 'Dept', + 'po_box': 'PO123', + 'state': 'State', + 'zipcode': 'ZIP123', + }, + }) + + self.assertEqual(contact.birthday, date(2000, 1, 1)) + self.assertEqual(contact.user_id, contact.user_id) + self.assertEqual(contact.description, 'Description') + self.assertEqual(contact.first_name, "Jean") + self.assertEqual(contact.last_name, "Dupont") + self.assertEqual(contact.skype, 'jean.dupont') + self.assertEqual(contact.phone, '+330100000000') + self.assertEqual(contact.mobile, '+330600000000') + self.assertEqual(contact.fax, '+330100000001') + self.assertEqual(contact.email, 'jean.dupont@provider.com') + self.assertEqual(contact.url_site, 'https://www.jean-dupont.provider.com') + self.assertEqual(contact.full_position, 'Full position') + self.assertFalse(contact.is_deleted) + self.assertIsNone(contact.is_user) + + billing_address.refresh_from_db() + self.assertEqual(billing_address.name, _('Billing address')) + self.assertEqual(billing_address.address, billing_address_data['address']) + self.assertEqual(billing_address.po_box, billing_address_data['po_box']) + self.assertEqual(billing_address.zipcode, billing_address_data['zipcode']) + self.assertEqual(billing_address.city, billing_address_data['city']) + self.assertEqual(billing_address.department, billing_address_data['department']) + self.assertEqual(billing_address.state, billing_address_data['state']) + self.assertEqual(billing_address.country, billing_address_data['country']) + self.assertEqual(billing_address.owner, contact) + + shipping_address.refresh_from_db() + self.assertEqual(shipping_address.name, _('Shipping address')) + self.assertEqual(shipping_address.address, shipping_address_data['address']) + self.assertEqual(shipping_address.po_box, shipping_address_data['po_box']) + self.assertEqual(shipping_address.zipcode, shipping_address_data['zipcode']) + self.assertEqual(shipping_address.city, shipping_address_data['city']) + self.assertEqual(shipping_address.department, shipping_address_data['department']) + self.assertEqual(shipping_address.state, shipping_address_data['state']) + self.assertEqual(shipping_address.country, shipping_address_data['country']) + self.assertEqual(shipping_address.owner, contact) + + +class PartialUpdateContactTestCase(CremeAPITestCase): + url_name = 'creme_api__contacts-detail' + method = 'patch' + + def test_partial_update_contact(self): + contact = self.factory.contact(billing_address=None) + data = { + 'first_name': "Nick", + 'last_name': "Smith", + } + response = self.make_request(to=contact.id, data=data, status_code=200) + contact.refresh_from_db() + self.assertPayloadEqual(response, { + 'id': contact.id, + 'birthday': '2000-01-01', + 'civility': contact.civility_id, + 'uuid': str(contact.uuid), + 'created': self.to_iso8601(contact.created), + 'modified': self.to_iso8601(contact.modified), + 'description': 'Description', + 'email': 'jean.dupont@provider.com', + 'fax': '+330100000001', + 'first_name': 'Nick', + 'full_position': 'Full position', + 'is_deleted': False, + 'is_user': None, + 'last_name': 'Smith', + 'mobile': '+330600000000', + 'phone': '+330100000000', + 'position': contact.position_id, + 'sector': contact.sector_id, + 'skype': 'jean.dupont', + 'url_site': 'https://www.jean-dupont.provider.com', + 'user': contact.user_id, + 'shipping_address': { + 'address': '1 Main Street', + 'city': 'City', + 'country': 'Country', + 'department': 'Dept', + 'po_box': 'PO123', + 'state': 'State', + 'zipcode': 'ZIP123', + }, + }) + self.assertEqual(contact.first_name, "Nick") + self.assertEqual(contact.last_name, "Smith") + + def test_partial_update_contact__create_addresses(self): + contact = self.factory.contact(billing_address=None, shipping_address=None) + + data = { + 'billing_address': { + 'address': 'billing street', + }, + 'shipping_address': { + 'address': 'shipping street', + }, + } + response = self.make_request(to=contact.id, data=data, status_code=200) + contact.refresh_from_db() + self.assertPayloadEqual(response, { + 'id': contact.id, + 'birthday': '2000-01-01', + 'civility': contact.civility_id, + 'uuid': str(contact.uuid), + 'created': self.to_iso8601(contact.created), + 'modified': self.to_iso8601(contact.modified), + 'description': 'Description', + 'email': 'jean.dupont@provider.com', + 'fax': '+330100000001', + 'first_name': 'Jean', + 'full_position': 'Full position', + 'is_deleted': False, + 'is_user': None, + 'last_name': 'Dupont', + 'mobile': '+330600000000', + 'phone': '+330100000000', + 'position': contact.position_id, + 'sector': contact.sector_id, + 'skype': 'jean.dupont', + 'url_site': 'https://www.jean-dupont.provider.com', + 'user': contact.user_id, + 'billing_address': { + 'address': 'billing street', + 'city': '', + 'country': '', + 'department': '', + 'po_box': '', + 'state': '', + 'zipcode': '', + }, + 'shipping_address': { + 'address': 'shipping street', + 'city': '', + 'country': '', + 'department': '', + 'po_box': '', + 'state': '', + 'zipcode': '', + }, + }) + + billing_address = contact.billing_address + self.assertEqual(billing_address.name, _('Billing address')) + self.assertEqual(billing_address.address, "billing street") + self.assertEqual(billing_address.po_box, "") + self.assertEqual(billing_address.zipcode, "") + self.assertEqual(billing_address.city, "") + self.assertEqual(billing_address.department, "") + self.assertEqual(billing_address.state, "") + self.assertEqual(billing_address.country, "") + self.assertEqual(billing_address.owner, contact) + + shipping_address = contact.shipping_address + self.assertEqual(shipping_address.name, _('Shipping address')) + self.assertEqual(shipping_address.address, "shipping street") + self.assertEqual(shipping_address.po_box, "") + self.assertEqual(shipping_address.zipcode, "") + self.assertEqual(shipping_address.city, "") + self.assertEqual(shipping_address.department, "") + self.assertEqual(shipping_address.state, "") + self.assertEqual(shipping_address.country, "") + self.assertEqual(shipping_address.owner, contact) + + def test_partial_update_contact__update_addresses(self): + contact = self.factory.contact() + billing_address = contact.billing_address + shipping_address = contact.shipping_address + + data = { + 'billing_address': { + 'address': 'billing street', + }, + 'shipping_address': { + 'address': 'shipping street', + }, + } + response = self.make_request(to=contact.id, data=data, status_code=200) + contact.refresh_from_db() + self.assertPayloadEqual(response, { + 'id': contact.id, + 'birthday': '2000-01-01', + 'civility': contact.civility_id, + 'uuid': str(contact.uuid), + 'created': self.to_iso8601(contact.created), + 'modified': self.to_iso8601(contact.modified), + 'description': 'Description', + 'email': 'jean.dupont@provider.com', + 'fax': '+330100000001', + 'first_name': 'Jean', + 'full_position': 'Full position', + 'is_deleted': False, + 'is_user': None, + 'last_name': 'Dupont', + 'mobile': '+330600000000', + 'phone': '+330100000000', + 'position': contact.position_id, + 'sector': contact.sector_id, + 'skype': 'jean.dupont', + 'url_site': 'https://www.jean-dupont.provider.com', + 'user': contact.user_id, + 'billing_address': { + 'address': 'billing street', + 'city': 'City', + 'country': 'Country', + 'department': 'Dept', + 'po_box': 'PO123', + 'state': 'State', + 'zipcode': 'ZIP123', + }, + 'shipping_address': { + 'address': 'shipping street', + 'city': 'City', + 'country': 'Country', + 'department': 'Dept', + 'po_box': 'PO123', + 'state': 'State', + 'zipcode': 'ZIP123', + }, + }) + billing_address.refresh_from_db() + self.assertEqual(billing_address.name, _('Billing address')) + self.assertEqual(billing_address.address, "billing street") + self.assertEqual(billing_address.po_box, "PO123") + self.assertEqual(billing_address.zipcode, "ZIP123") + self.assertEqual(billing_address.city, "City") + self.assertEqual(billing_address.department, "Dept") + self.assertEqual(billing_address.state, "State") + self.assertEqual(billing_address.country, "Country") + self.assertEqual(billing_address.owner, contact) + shipping_address.refresh_from_db() + self.assertEqual(shipping_address.name, _('Shipping address')) + self.assertEqual(shipping_address.address, "shipping street") + self.assertEqual(shipping_address.po_box, "PO123") + self.assertEqual(shipping_address.zipcode, "ZIP123") + self.assertEqual(shipping_address.city, "City") + self.assertEqual(shipping_address.department, "Dept") + self.assertEqual(shipping_address.state, "State") + self.assertEqual(shipping_address.country, "Country") + self.assertEqual(shipping_address.owner, contact) + + +class ListContactTestCase(CremeAPITestCase): + url_name = 'creme_api__contacts-list' + method = 'get' + + def test_list_contacts(self): + fulbert = Contact.objects.get() + contact = self.factory.contact(user=fulbert.user) + self.assertEqual(Contact.objects.count(), 2, Contact.objects.all()) + + response = self.make_request(status_code=200) + self.assertPayloadEqual(response, [ + { + 'id': fulbert.id, + 'birthday': None, + 'civility': None, + 'uuid': str(fulbert.uuid), + 'created': self.to_iso8601(fulbert.created), + 'modified': self.to_iso8601(fulbert.modified), + 'description': '', + 'email': fulbert.email, + 'fax': '', + 'first_name': 'Fulbert', + 'full_position': '', + 'is_deleted': False, + 'is_user': fulbert.is_user_id, + 'last_name': 'Creme', + 'mobile': '', + 'phone': '', + 'position': None, + 'sector': None, + 'skype': '', + 'url_site': '', + 'user': fulbert.user_id, + }, + { + 'id': contact.id, + 'birthday': '2000-01-01', + 'civility': contact.civility_id, + 'uuid': str(contact.uuid), + 'created': self.to_iso8601(contact.created), + 'modified': self.to_iso8601(contact.modified), + 'description': 'Description', + 'email': 'jean.dupont@provider.com', + 'fax': '+330100000001', + 'first_name': 'Jean', + 'full_position': 'Full position', + 'is_deleted': False, + 'is_user': None, + 'last_name': 'Dupont', + 'mobile': '+330600000000', + 'phone': '+330100000000', + 'position': contact.position_id, + 'sector': contact.sector_id, + 'skype': 'jean.dupont', + 'url_site': 'https://www.jean-dupont.provider.com', + 'user': fulbert.user_id, + 'billing_address': { + 'address': '1 Main Street', + 'city': 'City', + 'country': 'Country', + 'department': 'Dept', + 'po_box': 'PO123', + 'state': 'State', + 'zipcode': 'ZIP123', + }, + 'shipping_address': { + 'address': '1 Main Street', + 'city': 'City', + 'country': 'Country', + 'department': 'Dept', + 'po_box': 'PO123', + 'state': 'State', + 'zipcode': 'ZIP123', + }, + } + ]) + + +class TrashContactTestCase(CremeAPITestCase): + url_name = 'creme_api__contacts-trash' + method = 'post' + + def test_trash_contact__protected(self): + fulbert = Contact.objects.get() + response = self.make_request(to=fulbert.id, status_code=422) + self.assertEqual(response.data['detail'].code, 'protected') + + def test_trash_contact(self): + contact = self.factory.contact() + response = self.make_request(to=contact.id, status_code=200) + + contact.refresh_from_db() + self.assertPayloadEqual(response, { + 'id': contact.id, + 'uuid': str(contact.uuid), + 'created': self.to_iso8601(contact.created), + 'modified': self.to_iso8601(contact.modified), + 'is_deleted': True, + }) + self.make_request(to=contact.id, status_code=200) + + +class RestoreContactTestCase(CremeAPITestCase): + url_name = 'creme_api__contacts-restore' + method = 'post' + + def test_restore_contact(self): + contact = self.factory.contact(is_deleted=True) + contact.refresh_from_db() + self.assertTrue(contact.is_deleted) + + response = self.make_request(to=contact.id, status_code=200) + + contact.refresh_from_db() + self.assertPayloadEqual(response, { + 'id': contact.id, + 'uuid': str(contact.uuid), + 'created': self.to_iso8601(contact.created), + 'modified': self.to_iso8601(contact.modified), + 'is_deleted': False, + }) + self.make_request(to=contact.id, status_code=200) + + +class DeleteContactTestCase(CremeAPITestCase): + url_name = 'creme_api__contacts-detail' + method = 'delete' + + def test_delete_contact__protected(self): + fulbert = Contact.objects.get() + response = self.make_request(to=fulbert.id, status_code=422) + self.assertEqual(response.data['detail'].code, 'protected') + + def test_delete_contact(self): + contact = self.factory.contact() + self.make_request(to=contact.id, status_code=204) + self.assertFalse(Contact.objects.filter(id=contact.id).exists()) + + +class CloneContactTestCase(CremeAPITestCase): + url_name = 'creme_api__contacts-clone' + method = 'post' + + def test_clone_contact(self): + fulbert = Contact.objects.get() + + response = self.make_request(to=fulbert.id, status_code=201) + contact = Contact.objects.get(id=response.data['id']) + self.assertNotEqual(fulbert.id, contact.id) + self.assertNotEqual(fulbert.uuid, contact.uuid) + self.assertPayloadEqual(response, { + 'id': contact.id, + 'birthday': None, + 'civility': None, + 'uuid': str(contact.uuid), + 'created': self.to_iso8601(contact.created), + 'modified': self.to_iso8601(contact.modified), + 'description': '', + 'email': fulbert.email, + 'fax': '', + 'first_name': 'Fulbert', + 'full_position': '', + 'is_deleted': False, + 'is_user': None, + 'last_name': 'Creme', + 'mobile': '', + 'phone': '', + 'position': None, + 'sector': None, + 'skype': '', + 'url_site': '', + 'user': fulbert.user_id, + }) + + +class ListContactAddressesTestCase(CremeAPITestCase): + url_name = 'creme_api__contacts-addresses' + method = 'get' + + def test_delete_contact__protected(self): + fulbert = Contact.objects.get() + response = self.make_request(to=fulbert.id, status_code=422) + self.assertEqual(response.data['detail'].code, 'protected') + + def test_delete_contact(self): + contact = self.factory.contact() + self.make_request(to=contact.id, status_code=204) + self.assertFalse(Contact.objects.filter(id=contact.id).exists()) diff --git a/creme/creme_api/tests/test_contenttypes.py b/creme/creme_api/tests/test_contenttypes.py index f45db534a6..9f0108fad8 100644 --- a/creme/creme_api/tests/test_contenttypes.py +++ b/creme/creme_api/tests/test_contenttypes.py @@ -15,8 +15,8 @@ class RetrieveContentTypeTestCase(CremeAPITestCase): def test_retrieve_contenttype(self): contact_ct = ContentType.objects.get_for_model(Contact) - response = self.make_request(to=contact_ct.id) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=contact_ct.id, status_code=200) + self.assertPayloadEqual(response, { 'id': contact_ct.id, 'application': _('Accounts and Contacts'), 'name': _("Contacts"), diff --git a/creme/creme_api/tests/test_credentials.py b/creme/creme_api/tests/test_credentials.py index 5f9320df3d..472ccc04e4 100644 --- a/creme/creme_api/tests/test_credentials.py +++ b/creme/creme_api/tests/test_credentials.py @@ -36,7 +36,7 @@ class CreateSetCredentialTestCase(CremeAPITestCase): method = 'post' def test_validation__required(self): - response = self.make_request(data={}) + response = self.make_request(data={}, status_code=400) self.assertValidationErrors(response, { 'role': ['required'], 'set_type': ['required'], @@ -63,9 +63,9 @@ def test_create_setcredentials(self): "can_unlink": True, "forbidden": False, } - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=201) creds = SetCredentials.objects.get(id=response.data['id']) - self.assertResponseEqual(response, 201, { + self.assertPayloadEqual(response, { 'id': creds.id, "role": role.id, "set_type": SetCredentials.ESET_ALL, @@ -92,8 +92,8 @@ class RetrieveSetCredentialTestCase(CremeAPITestCase): def test_retrieve_setcredentials(self): contact_ct = ContentType.objects.get_for_model(Contact) creds = self.factory.credential() - response = self.make_request(to=creds.id) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=creds.id, status_code=200) + self.assertPayloadEqual(response, { 'id': creds.id, "role": creds.role_id, "set_type": SetCredentials.ESET_OWN, @@ -114,7 +114,7 @@ class UpdateSetCredentialTestCase(CremeAPITestCase): def test_validation__required(self): creds = self.factory.credential() - response = self.make_request(to=creds.id, data={}) + response = self.make_request(to=creds.id, data={}, status_code=400) self.assertValidationErrors(response, { 'set_type': ['required'], 'ctype': ['required'], @@ -141,8 +141,8 @@ def test_update_creds(self): "can_unlink": True, "forbidden": False, } - response = self.make_request(to=creds.id, data=data) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=creds.id, data=data, status_code=200) + self.assertPayloadEqual(response, { 'id': creds.id, "role": creds.role_id, "set_type": SetCredentials.ESET_ALL, @@ -178,8 +178,8 @@ def test_partial_update_creds(self): "can_delete": False, "forbidden": False, } - response = self.make_request(to=creds.id, data=data) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=creds.id, data=data, status_code=200) + self.assertPayloadEqual(response, { 'id': creds.id, "role": creds.role_id, "set_type": SetCredentials.ESET_ALL, @@ -211,8 +211,8 @@ def test_list_setcredentials(self): creds1 = self.factory.credential(role=role, ctype=contact_ct) creds2 = self.factory.credential(role=role, ctype=orga_ct, can_delete=False) - response = self.make_request() - self.assertResponseEqual(response, 200, [ + response = self.make_request(status_code=200) + self.assertPayloadEqual(response, [ { 'id': creds1.id, "role": role.id, @@ -248,6 +248,5 @@ class DeleteSetCredentialTestCase(CremeAPITestCase): def test_delete(self): creds = self.factory.credential() - response = self.make_request(to=creds.id) - self.assertResponseEqual(response, 204) + self.make_request(to=creds.id, status_code=204) self.assertFalse(SetCredentials.objects.filter(id=creds.id).exists()) diff --git a/creme/creme_api/tests/test_positions.py b/creme/creme_api/tests/test_positions.py index ecd1830200..c1ab1d0c8f 100644 --- a/creme/creme_api/tests/test_positions.py +++ b/creme/creme_api/tests/test_positions.py @@ -19,16 +19,16 @@ class CreatePositionTestCase(CremeAPITestCase): method = 'post' def test_validation__required(self): - response = self.make_request(data={}) + response = self.make_request(data={}, status_code=400) self.assertValidationErrors(response, { 'title': ['required'], }) def test_create_position(self): data = self.factory.position_data() - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=201) position = Position.objects.get(id=response.data['id']) - self.assertResponseEqual(response, 201, { + self.assertPayloadEqual(response, { 'id': position.id, 'title': 'Captain', }) @@ -41,8 +41,8 @@ class RetrievePositionTestCase(CremeAPITestCase): def test_retrieve_position(self): position = self.factory.position() - response = self.make_request(to=position.id) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=position.id, status_code=200) + self.assertPayloadEqual(response, { 'id': position.id, 'title': 'Captain', }) @@ -56,8 +56,8 @@ def test_update_position(self): position = self.factory.position() response = self.make_request(to=position.id, data={ 'title': "CAPTAIN", - }) - self.assertResponseEqual(response, 200, { + }, status_code=200) + self.assertPayloadEqual(response, { 'id': position.id, 'title': 'CAPTAIN', }) @@ -73,8 +73,8 @@ def test_partial_update_position(self): position = self.factory.position() response = self.make_request(to=position.id, data={ 'title': 'CAPTAIN', - }) - self.assertResponseEqual(response, 200, { + }, status_code=200) + self.assertPayloadEqual(response, { 'id': position.id, 'title': 'CAPTAIN', }) @@ -90,8 +90,8 @@ def test_list_positions(self): Position.objects.all().delete() position1 = self.factory.position(title="1") position2 = self.factory.position(title="2") - response = self.make_request() - self.assertResponseEqual(response, 200, [ + response = self.make_request(status_code=200) + self.assertPayloadEqual(response, [ {'id': position1.id, 'title': '1'}, {'id': position2.id, 'title': '2'}, ]) @@ -103,6 +103,5 @@ class DeletePositionTestCase(CremeAPITestCase): def test_delete_position(self): position = self.factory.position() - response = self.make_request(to=position.id) - self.assertResponseEqual(response, 204) + self.make_request(to=position.id, status_code=204) self.assertFalse(Position.objects.filter(id=position.id).exists()) diff --git a/creme/creme_api/tests/test_roles.py b/creme/creme_api/tests/test_roles.py index 741c266b6f..8d223de689 100644 --- a/creme/creme_api/tests/test_roles.py +++ b/creme/creme_api/tests/test_roles.py @@ -36,7 +36,7 @@ class CreateRoleTestCase(CremeAPITestCase): method = 'post' def test_validation__required(self): - response = self.make_request(data={}) + response = self.make_request(data={}, status_code=400) self.assertValidationErrors(response, { 'name': ['required'], 'allowed_apps': ['required'], @@ -54,7 +54,7 @@ def test_validation__name_unique(self): 'creatable_ctypes': [], 'exportable_ctypes': [], } - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=400) self.assertValidationError(response, 'name', ['unique']) def test_validation(self): @@ -67,7 +67,7 @@ def test_validation(self): 'creatable_ctypes': [contact_ct.id, orga_ct.id], 'exportable_ctypes': [contact_ct.id, orga_ct.id], } - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=400) self.assertValidationErrors(response, { 'admin_4_apps': ["admin_4_not_allowed_app", "admin_4_not_allowed_app"], 'creatable_ctypes': ["not_allowed_ctype", "not_allowed_ctype"], @@ -84,9 +84,9 @@ def test_create_role(self): 'creatable_ctypes': [contact_ct.id, orga_ct.id], 'exportable_ctypes': [], } - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=201) role = UserRole.objects.get(id=response.data['id']) - self.assertResponseEqual(response, 201, { + self.assertPayloadEqual(response, { 'id': role.id, 'name': "CEO", 'allowed_apps': {'creme_core', 'creme_api', 'persons'}, @@ -111,8 +111,8 @@ def test_retrieve_role(self): orga_ct = ContentType.objects.get_for_model(Organisation) role = self.factory.role() - response = self.make_request(to=role.id) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=role.id, status_code=200) + self.assertPayloadEqual(response, { 'id': role.id, 'name': "Basic", 'allowed_apps': {'creme_core', 'creme_api', 'persons'}, @@ -129,7 +129,7 @@ class UpdateRoleTestCase(CremeAPITestCase): def test_validation__required(self): role = self.factory.role() - response = self.make_request(to=role.id, data={}) + response = self.make_request(to=role.id, data={}, status_code=400) self.assertValidationErrors(response, { 'name': ['required'], 'allowed_apps': ['required'], @@ -149,7 +149,7 @@ def test_validation(self): 'creatable_ctypes': [contact_ct.id], 'exportable_ctypes': [orga_ct.id], } - response = self.make_request(to=role.id, data=data) + response = self.make_request(to=role.id, data=data, status_code=400) self.assertValidationErrors(response, { 'admin_4_apps': ["admin_4_not_allowed_app"], 'creatable_ctypes': ["not_allowed_ctype"], @@ -167,8 +167,8 @@ def test_update_role(self): 'creatable_ctypes': [contact_ct.id], 'exportable_ctypes': [orga_ct.id], } - response = self.make_request(to=role.id, data=data) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=role.id, data=data, status_code=200) + self.assertPayloadEqual(response, { 'id': role.id, 'name': "CEO", 'allowed_apps': {'creme_core', 'persons'}, @@ -195,7 +195,7 @@ def test_validation__name_unique(self): data = { 'name': "UniqueRoleName", } - response = self.make_request(to=role.id, data=data) + response = self.make_request(to=role.id, data=data, status_code=400) self.assertValidationError(response, 'name', ['unique']) def test_validation(self): @@ -203,7 +203,7 @@ def test_validation(self): data = { 'allowed_apps': ['creme_core'], } - response = self.make_request(to=role.id, data=data) + response = self.make_request(to=role.id, data=data, status_code=400) self.assertValidationErrors(response, { 'admin_4_apps': ["admin_4_not_allowed_app"], 'creatable_ctypes': ["not_allowed_ctype", "not_allowed_ctype"], @@ -218,8 +218,8 @@ def test_partial_update_role(self): data = { 'name': "CEO", } - response = self.make_request(to=role.id, data=data) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=role.id, data=data, status_code=200) + self.assertPayloadEqual(response, { 'id': role.id, 'name': "CEO", 'allowed_apps': {'creme_core', 'persons', 'creme_api'}, @@ -239,8 +239,8 @@ def test_partial_update_role(self): 'allowed_apps': ['creme_core', 'persons', 'creme_api'], 'exportable_ctypes': [contact_ct.id, orga_ct.id], } - response = self.make_request(to=role.id, data=data) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=role.id, data=data, status_code=200) + self.assertPayloadEqual(response, { 'id': role.id, 'name': "CEO", 'allowed_apps': {'creme_core', 'persons', 'creme_api'}, @@ -267,8 +267,8 @@ def test_list_roles(self): role1 = self.factory.role(name='Role #1') role2 = self.factory.role(name='Role #2') - response = self.make_request() - self.assertResponseEqual(response, 200, [ + response = self.make_request(status_code=200) + self.assertPayloadEqual(response, [ { 'id': role1.id, 'name': "Role #1", @@ -297,11 +297,9 @@ class DeleteRoleTestCase(CremeAPITestCase): def test_delete_role__protected(self): role = self.factory.role() self.factory.user(role=role) - response = self.make_request(to=role.id) - self.assertResponseEqual(response, 403) + self.make_request(to=role.id, status_code=403) def test_delete_role(self): role = self.factory.role() - response = self.make_request(to=role.id) - self.assertResponseEqual(response, 204) + self.make_request(to=role.id, status_code=204) self.assertFalse(UserRole.objects.filter(id=role.id).exists()) diff --git a/creme/creme_api/tests/test_sectors.py b/creme/creme_api/tests/test_sectors.py index 9cd585595b..814950e442 100644 --- a/creme/creme_api/tests/test_sectors.py +++ b/creme/creme_api/tests/test_sectors.py @@ -19,16 +19,16 @@ class CreateSectorTestCase(CremeAPITestCase): method = 'post' def test_validation__required(self): - response = self.make_request(data={}) + response = self.make_request(data={}, status_code=400) self.assertValidationErrors(response, { 'title': ['required'], }) def test_create_sector(self): data = self.factory.sector_data() - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=201) sector = Sector.objects.get(id=response.data['id']) - self.assertResponseEqual(response, 201, { + self.assertPayloadEqual(response, { 'id': sector.id, 'title': 'Industry', }) @@ -41,8 +41,8 @@ class RetrieveSectorTestCase(CremeAPITestCase): def test_retrieve_sector(self): sector = self.factory.sector() - response = self.make_request(to=sector.id) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=sector.id, status_code=200) + self.assertPayloadEqual(response, { 'id': sector.id, 'title': 'Industry', }) @@ -56,8 +56,8 @@ def test_update_sector(self): sector = self.factory.sector() response = self.make_request(to=sector.id, data={ 'title': "Agro", - }) - self.assertResponseEqual(response, 200, { + }, status_code=200) + self.assertPayloadEqual(response, { 'id': sector.id, 'title': 'Agro', }) @@ -73,8 +73,8 @@ def test_partial_update_sector(self): sector = self.factory.sector() response = self.make_request(to=sector.id, data={ 'title': 'Agro', - }) - self.assertResponseEqual(response, 200, { + }, status_code=200) + self.assertPayloadEqual(response, { 'id': sector.id, 'title': 'Agro', }) @@ -90,8 +90,8 @@ def test_list_sectors(self): Sector.objects.all().delete() sector1 = self.factory.sector(title="1") sector2 = self.factory.sector(title="2") - response = self.make_request() - self.assertResponseEqual(response, 200, [ + response = self.make_request(status_code=200) + self.assertPayloadEqual(response, [ {'id': sector1.id, 'title': '1'}, {'id': sector2.id, 'title': '2'}, ]) @@ -103,6 +103,5 @@ class DeleteSectorTestCase(CremeAPITestCase): def test_delete_sector(self): sector = self.factory.sector() - response = self.make_request(to=sector.id) - self.assertResponseEqual(response, 204) + self.make_request(to=sector.id, status_code=204) self.assertFalse(Sector.objects.filter(id=sector.id).exists()) diff --git a/creme/creme_api/tests/test_teams.py b/creme/creme_api/tests/test_teams.py index 2d407c38cd..9e935415a8 100644 --- a/creme/creme_api/tests/test_teams.py +++ b/creme/creme_api/tests/test_teams.py @@ -1,5 +1,4 @@ from django.contrib.auth import get_user_model -from django.urls import reverse from creme.creme_api.tests.utils import CremeAPITestCase, Factory from creme.persons import get_contact_model @@ -30,7 +29,7 @@ class CreateTeamTestCase(CremeAPITestCase): method = 'post' def test_validation__required(self): - response = self.make_request(data={}) + response = self.make_request(data={}, status_code=400) self.assertValidationErrors(response, { 'name': ['required'], 'teammates': ['required'], @@ -38,17 +37,17 @@ def test_validation__required(self): def test_validation__name_max_length(self): data = {'name': "a" * (CremeUser._meta.get_field('username').max_length + 1)} - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=400) self.assertValidationError(response, 'name', ['max_length']) def test_validation__name_invalid_chars(self): data = {'name': "*********"} - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=400) self.assertValidationError(response, 'name', ['invalid']) def test_validation__teammates(self): data = {'name': "TEAM", 'teammates': [9999]} - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=400) self.assertValidationError(response, 'teammates', ['does_not_exist']) def test_create_team(self): @@ -56,10 +55,10 @@ def test_create_team(self): user2 = self.factory.user(username="user2") data = {'name': "creme-team", 'teammates': [user1.id, user2.id]} - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=201) team = CremeUser.objects.get(id=response.data['id']) - self.assertResponseEqual(response, 201, { + self.assertPayloadEqual(response, { 'id': team.id, 'teammates': [user1.id, user2.id], 'name': "creme-team", @@ -78,8 +77,8 @@ def test_get_team(self): user = self.factory.user() team = self.factory.team(teammates=[user]) - response = self.make_request(to=team.id) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=team.id, status_code=200) + self.assertPayloadEqual(response, { 'id': team.id, 'teammates': [user.id], 'name': 'Team #1', @@ -92,7 +91,7 @@ class UpdateTeamTestCase(CremeAPITestCase): def test_validation__required(self): team = self.factory.team() - response = self.make_request(to=team.id, data={}) + response = self.make_request(to=team.id, data={}, status_code=400) self.assertValidationErrors(response, { 'name': ['required'], 'teammates': ['required'], @@ -104,9 +103,9 @@ def test_update_team(self): user2 = self.factory.user(username="user2") data = {'name': "Sales", 'teammates': [user2.id]} - response = self.make_request(to=team.id, data=data) + response = self.make_request(to=team.id, data=data, status_code=200) - self.assertResponseEqual(response, 200, { + self.assertPayloadEqual(response, { 'id': team.id, 'teammates': [user2.id], 'name': 'Sales', @@ -127,8 +126,8 @@ def test_partial_update_team__name(self): team = self.factory.team(teammates=[user]) data = {'name': "Sales"} - response = self.make_request(to=team.id, data=data) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=team.id, data=data, status_code=200) + self.assertPayloadEqual(response, { 'id': team.id, 'teammates': [user.id], 'name': 'Sales', @@ -146,8 +145,8 @@ def test_partial_update_team__teammates(self): # change user2 = self.factory.user(username='user2') data = {'teammates': [user2.id]} - response = self.make_request(to=team.id, data=data) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=team.id, data=data, status_code=200) + self.assertPayloadEqual(response, { 'id': team.id, 'teammates': [user2.id], 'name': 'Team #1', @@ -160,8 +159,8 @@ def test_partial_update_team__teammates(self): # empty data = {'teammates': []} - response = self.make_request(to=team.id, data=data) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=team.id, data=data, status_code=200) + self.assertPayloadEqual(response, { 'id': team.id, 'teammates': [], 'name': 'Team #1', @@ -185,8 +184,8 @@ def test_list_teams(self): teams = CremeUser.objects.filter(is_team=True) self.assertEqual(teams.count(), 2, teams) - response = self.make_request() - self.assertResponseEqual(response, 200, [ + response = self.make_request(status_code=200) + self.assertPayloadEqual(response, [ { 'id': team1.id, 'teammates': [user1.id], @@ -201,13 +200,17 @@ def test_list_teams(self): class DeleteTeamTestCase(CremeAPITestCase): - url_name = 'creme_api__teams-delete' - method = 'post' + url_name = 'creme_api__teams-detail' + method = 'delete' def test_delete(self): - url = reverse('creme_api__teams-detail', args=[1]) - response = self.client.delete(url, format='json') - self.assertResponseEqual(response, 405) + team = self.factory.team() + self.make_request(to=team.id, data={}, status_code=405) + + +class PostDeleteTeamTestCase(CremeAPITestCase): + url_name = 'creme_api__teams-delete' + method = 'post' def test_delete_team(self): user = self.factory.user() @@ -216,16 +219,14 @@ def test_delete_team(self): contact = self.factory.contact(user=team2) data = {'transfer_to': team1.id} - response = self.make_request(to=team2.id, data=data) - self.assertResponseEqual(response, 204) + self.make_request(to=team2.id, data=data, status_code=204) self.assertFalse(CremeUser.objects.filter(username='team2').exists()) contact.refresh_from_db() self.assertEqual(contact.user, team1) data = {'transfer_to': user.id} - response = self.make_request(to=team1.id, data=data) - self.assertResponseEqual(response, 204) + self.make_request(to=team1.id, data=data, status_code=204) self.assertFalse(CremeUser.objects.filter(username='team1').exists()) contact.refresh_from_db() diff --git a/creme/creme_api/tests/test_tokens.py b/creme/creme_api/tests/test_tokens.py index 067632bbb7..1a2c10331b 100644 --- a/creme/creme_api/tests/test_tokens.py +++ b/creme/creme_api/tests/test_tokens.py @@ -10,7 +10,7 @@ class TokensTestCase(CremeAPITestCase): method = 'post' def test_create_token__missing(self): - response = self.make_request(data={}) + response = self.make_request(data={}, status_code=400) self.assertValidationErrors(response, { 'client_id': ['required'], 'client_secret': ['required'], @@ -21,7 +21,7 @@ def test_create_token__empty(self): "client_id": "", # trim "client_secret": "", } - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=400) self.assertValidationErrors(response, { 'client_id': ['invalid'], # Must be a valid UUID. 'client_secret': ['blank'], @@ -32,7 +32,7 @@ def test_create_token__no_application(self): "client_id": uuid4().hex, "client_secret": "Secret", } - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=400) self.assertValidationErrors(response, { '': ['authentication_failure'], }) @@ -42,6 +42,6 @@ def test_create_token(self): "client_id": self.application.client_id, "client_secret": self.application._client_secret, } - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=200) token = Token.objects.get(application=self.application) - self.assertResponseEqual(response, 200, {"token": token.code}) + self.assertPayloadEqual(response, {"token": token.code}) diff --git a/creme/creme_api/tests/test_users.py b/creme/creme_api/tests/test_users.py index daf51189b0..0347499fd5 100644 --- a/creme/creme_api/tests/test_users.py +++ b/creme/creme_api/tests/test_users.py @@ -1,5 +1,4 @@ from django.contrib.auth import get_user_model -from django.urls import reverse from creme.creme_api.tests.utils import CremeAPITestCase, Factory @@ -32,7 +31,7 @@ class CreateUserTestCase(CremeAPITestCase): method = 'post' def test_validation__required(self): - response = self.make_request(data={}) + response = self.make_request(data={}, status_code=400) self.assertValidationErrors(response, { 'username': ['required'], 'first_name': ['required'], @@ -42,30 +41,30 @@ def test_validation__required(self): def test_validation__username_max_length(self): data = {'username': "a" * (CremeUser._meta.get_field('username').max_length + 1)} - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=400) self.assertValidationError(response, 'username', ['max_length']) def test_validation__username_invalid_chars(self): data = {'username': "*********"} - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=400) self.assertValidationError(response, 'username', ['invalid']) def test_validation__is_superuser_xor_role(self): role = self.factory.role() data = self.factory.user_data(is_superuser=False, role=None) - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=400) self.assertValidationError(response, '', ['is_superuser_xor_role']) data = self.factory.user_data(is_superuser=True, role=role.id) - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=400) self.assertValidationError(response, '', ['is_superuser_xor_role']) def test_create_superuser(self): data = self.factory.user_data(is_superuser=True, role=None) - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=201) user = CremeUser.objects.get(id=response.data['id']) - self.assertResponseEqual(response, 201, { + self.assertPayloadEqual(response, { 'id': user.id, 'username': 'john.doe', 'last_name': 'Doe', @@ -85,9 +84,9 @@ def test_create_superuser(self): def test_create_user(self): role = self.factory.role() data = self.factory.user_data(is_superuser=False, role=role.id) - response = self.make_request(data=data) + response = self.make_request(data=data, status_code=201) user = CremeUser.objects.get(id=response.data['id']) - self.assertResponseEqual(response, 201, { + self.assertPayloadEqual(response, { 'id': user.id, 'username': 'john.doe', 'last_name': 'Doe', @@ -110,8 +109,8 @@ class RetrieveUserTestCase(CremeAPITestCase): def test_get_user(self): user = self.factory.user() - response = self.make_request(to=user.id) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=user.id, status_code=200) + self.assertPayloadEqual(response, { 'id': user.id, 'username': user.username, 'last_name': user.last_name, @@ -133,7 +132,7 @@ class UpdateUserTestCase(CremeAPITestCase): def test_validation__required(self): user = self.factory.user() - response = self.make_request(to=user.id, data={}) + response = self.make_request(to=user.id, data={}, status_code=400) self.assertValidationErrors(response, { 'username': ['required'], 'first_name': ['required'], @@ -146,19 +145,19 @@ def test_validation__is_superuser_xor_role(self): user = self.factory.user(is_superuser=True, role=None) data = self.factory.user_data(is_superuser=False, role=None) - response = self.make_request(to=user.id, data=data) + response = self.make_request(to=user.id, data=data, status_code=400) self.assertValidationError(response, '', ['is_superuser_xor_role']) data = self.factory.user_data(is_superuser=True, role=role.id) - response = self.make_request(to=user.id, data=data) + response = self.make_request(to=user.id, data=data, status_code=400) self.assertValidationError(response, '', ['is_superuser_xor_role']) def test_update_user(self): user = self.factory.user() data = self.factory.user_data(last_name="Smith", username="Nick") - response = self.make_request(to=user.id, data=data) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=user.id, data=data, status_code=200) + self.assertPayloadEqual(response, { 'id': user.id, 'username': 'Nick', 'last_name': 'Smith', @@ -186,11 +185,11 @@ def test_validation__is_superuser_xor_role__superuser(self): user = self.factory.user(username='user1', is_superuser=True, role=None) data = {'role': role.id} - response = self.make_request(to=user.id, data=data) + response = self.make_request(to=user.id, data=data, status_code=400) self.assertValidationError(response, '', ['is_superuser_xor_role']) data = {'is_superuser': False} - response = self.make_request(to=user.id, data=data) + response = self.make_request(to=user.id, data=data, status_code=400) self.assertValidationError(response, '', ['is_superuser_xor_role']) def test_validation__is_superuser_xor_role__role(self): @@ -198,18 +197,18 @@ def test_validation__is_superuser_xor_role__role(self): user = self.factory.user(username='user2', is_superuser=False, role=role) data = {'role': None} - response = self.make_request(to=user.id, data=data) + response = self.make_request(to=user.id, data=data, status_code=400) self.assertValidationError(response, '', ['is_superuser_xor_role']) data = {'is_superuser': True} - response = self.make_request(to=user.id, data=data) + response = self.make_request(to=user.id, data=data, status_code=400) self.assertValidationError(response, '', ['is_superuser_xor_role']) def test_partial_update_user(self): user = self.factory.user() data = {'theme': "chantilly"} - response = self.make_request(to=user.id, data=data) - self.assertResponseEqual(response, 200, { + response = self.make_request(to=user.id, data=data, status_code=200) + self.assertPayloadEqual(response, { 'id': user.id, 'username': 'john.doe', 'last_name': 'Doe', @@ -236,8 +235,8 @@ def test_list_users(self): user = self.factory.user(username="user", theme='chantilly') self.assertEqual(CremeUser.objects.count(), 2) - response = self.make_request() - self.assertResponseEqual(response, 200, [ + response = self.make_request(status_code=200) + self.assertPayloadEqual(response, [ { 'id': fulbert.id, 'username': 'root', @@ -275,7 +274,7 @@ class SetPasswordUserTestCase(CremeAPITestCase): def test_password_validation__required(self): user = self.factory.user() - response = self.make_request(to=user.id, data={}) + response = self.make_request(to=user.id, data={}, status_code=400) self.assertValidationErrors(response, { 'password': ['required'] }) @@ -284,15 +283,15 @@ def test_password_validation__blank(self): user = self.factory.user() data = {'password': ''} - response = self.make_request(to=user.id, data=data) + response = self.make_request(to=user.id, data=data, status_code=400) self.assertValidationError(response, 'password', ['blank']) def test_password_validation__no_trim(self): user = self.factory.user() data = {'password': " StrongPassword "} - response = self.make_request(to=user.id, data=data) - self.assertResponseEqual(response, 200, {}) + response = self.make_request(to=user.id, data=data, status_code=200) + self.assertPayloadEqual(response, {}) user.refresh_from_db() self.assertTrue(user.check_password(" StrongPassword ")) @@ -306,44 +305,48 @@ def test_password_validation__similarity(self): ) data = {'password': user.username} - response = self.make_request(to=user.id, data=data) + response = self.make_request(to=user.id, data=data, status_code=400) self.assertValidationError(response, 'password', ['password_too_similar']) data = {'password': user.first_name} - response = self.make_request(to=user.id, data=data) + response = self.make_request(to=user.id, data=data, status_code=400) self.assertValidationError(response, 'password', ['password_too_similar']) data = {'password': user.last_name} - response = self.make_request(to=user.id, data=data) + response = self.make_request(to=user.id, data=data, status_code=400) self.assertValidationError(response, 'password', ['password_too_similar']) data = {'password': user.email.split("@")[0]} - response = self.make_request(to=user.id, data=data) + response = self.make_request(to=user.id, data=data, status_code=400) self.assertValidationError(response, 'password', ['password_too_similar']) def test_set_password_user(self): user = self.factory.user() data = {'password': "StrongPassword"} - response = self.make_request(to=user.id, data=data) - self.assertResponseEqual(response, 200, {}) + response = self.make_request(to=user.id, data=data, status_code=200) + self.assertPayloadEqual(response, {}) user.refresh_from_db() self.assertTrue(user.check_password("StrongPassword")) -class DeleteUserTestCase(CremeAPITestCase): - url_name = 'creme_api__users-delete' - method = 'post' +class DeletePositionTestCase(CremeAPITestCase): + url_name = 'creme_api__users-detail' + method = 'delete' def test_delete(self): - url = reverse('creme_api__users-detail', args=[1]) - response = self.client.delete(url, format='json') - self.assertResponseEqual(response, 405) + user = self.factory.user() + self.make_request(to=user.id, data={}, status_code=405) + + +class PostDeleteUserTestCase(CremeAPITestCase): + url_name = 'creme_api__users-delete' + method = 'post' def test_validation__required(self): user = self.factory.user() - response = self.make_request(to=user.id, data={}) + response = self.make_request(to=user.id, data={}, status_code=400) self.assertValidationErrors(response, { 'transfer_to': ['required'] }) @@ -355,16 +358,14 @@ def test_delete_user(self): contact = self.factory.contact(user=user2) data = {'transfer_to': user1.id} - response = self.make_request(to=user2.id, data=data) - self.assertResponseEqual(response, 204) + self.make_request(to=user2.id, data=data, status_code=204) self.assertFalse(CremeUser.objects.filter(username='user2').exists()) contact.refresh_from_db() self.assertEqual(contact.user, user1) data = {'transfer_to': team.id} - response = self.make_request(to=user1.id, data=data) - self.assertResponseEqual(response, 204) + self.make_request(to=user1.id, data=data, status_code=204) self.assertFalse(CremeUser.objects.filter(username='user1').exists()) contact.refresh_from_db() diff --git a/creme/creme_api/tests/utils.py b/creme/creme_api/tests/utils.py index bece709ac0..3b224f62e2 100644 --- a/creme/creme_api/tests/utils.py +++ b/creme/creme_api/tests/utils.py @@ -1,5 +1,10 @@ +import difflib +import pprint +from collections import OrderedDict + from django.urls import reverse from rest_framework.fields import DateTimeField +from rest_framework.response import Response from rest_framework.test import APITestCase from creme.creme_api.api.authentication import TokenAuthentication @@ -12,12 +17,23 @@ def register(cls, func): if hasattr(cls, func.__name__): raise AttributeError(func.__name__) setattr(cls, func.__name__, classmethod(func)) + return func def to_iso8601(value): return DateTimeField().to_representation(value) +class PrettyPrinter(pprint.PrettyPrinter): + _dispatch = pprint.PrettyPrinter._dispatch + _dispatch[OrderedDict.__repr__] = pprint.PrettyPrinter._pprint_dict + + +def pformat(obj): + return PrettyPrinter(indent=2, width=80, depth=None, + compact=False, sort_dicts=True).pformat(obj) + + class CremeAPITestCase(APITestCase): auto_login = True url_name = None @@ -56,27 +72,41 @@ def assertValidationErrors(self, response, errors): } self.assertEqual(current_errors, errors, response.data) - def assertResponseEqual(self, response, status_code, data=None): - self.assertEqual(response.status_code, status_code, response.data) - if data is None: - return + def _assertPayloadEqual(self, first, second): + if first != second: + first = self._prepare_payload(first) + diff = '\n'.join(difflib.unified_diff( + pformat(first).splitlines(), + pformat(second).splitlines())) + self.fail(f"Payload error:\n{diff}") + + def _prepare_payload(self, data): + if isinstance(data, list): + return [self._prepare_payload(obj) for obj in data] if isinstance(data, dict): - self.assertDictEqual(dict(response.data), data) - elif isinstance(data, list): - self.assertEqual(len(response.data['results']), len(data)) - for i, (obj1, obj2) in enumerate(zip(response.data['results'], data)): - self.assertDictEqual( - dict(obj1), obj2, msg=f"Elements response.data['results'][{i}] differ.") + return {key: self._prepare_payload(value) for key, value in data.items()} + return data + + def assertPayloadEqual(self, response, expected): + self.assertIsInstance(response, Response, 'First argument is not a Response') + data = response.data + if isinstance(expected, dict): + self._assertPayloadEqual(data, expected) + elif isinstance(expected, list): + self.assertEqual(len(data['results']), len(expected)) + self._assertPayloadEqual(data['results'], expected) else: - self.assertEqual(response.data, data) + self.assertEqual(response, expected) - def make_request(self, *, to=None, data=None): + def make_request(self, *, to=None, data=None, status_code=None): assert self.url_name is not None assert self.method is not None args = [to] if to is not None else None url = reverse(self.url_name, args=args) method = getattr(self.client, self.method) - return method(url, data=data, format='json') + response = method(url, data=data, format='json') + self.assertEqual(response.status_code, status_code, response.data) + return response def consume_list(self, data=None): assert self.url_name is not None and self.url_name.endswith("-list") From ef184cfd001c091677fde4ba4f232b687b58f666 Mon Sep 17 00:00:00 2001 From: Hugo Smett Date: Tue, 10 May 2022 16:18:29 +0200 Subject: [PATCH 08/12] Fix Applications update & delete views --- .../templates/creme_api/bricks/applications.html | 7 +++---- creme/creme_api/urls.py | 6 ++++++ creme/creme_api/views.py | 9 +++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/creme/creme_api/templates/creme_api/bricks/applications.html b/creme/creme_api/templates/creme_api/bricks/applications.html index 01d06b4a55..81d4fe5161 100644 --- a/creme/creme_api/templates/creme_api/bricks/applications.html +++ b/creme/creme_api/templates/creme_api/bricks/applications.html @@ -32,21 +32,20 @@ {% brick_table_column title=_('Action') status='action' %} {% endblock %} -{% block brick_table_rows %}{% url 'creme_api__delete_application' as delete_url %} +{% block brick_table_rows %} {% for application in page.object_list %} {% print_field object=application field='name' %} {% print_field object=application field='client_id' %} {% print_field object=application field='enabled' %} {% print_field object=application field='token_duration' %} {% trans 'seconds' %} -{# {% print_field object=application field='_client_secret' %}#} {{application.created|date:"DATE_FORMAT"}} {{application.modified|date:"DATE_FORMAT"}} {% url 'creme_api__edit_application' application.id as edit_url %} {% brick_table_action id='edit' url=edit_url label=_('Edit this application') %} - {% url 'creme_api__delete_application' application.id as delete_url %} - {% brick_table_action id='edit' url=delete_url label=_('Delete this application') icon='delete' %} + + {% brick_table_action id='delete' url='creme_api__delete_application'|url:application.id __id=application.id label=_('Delete this application') icon='delete' %} {% endfor %} diff --git a/creme/creme_api/urls.py b/creme/creme_api/urls.py index 96917177a8..e0c0c99aee 100644 --- a/creme/creme_api/urls.py +++ b/creme/creme_api/urls.py @@ -4,6 +4,7 @@ from creme.creme_api.views import ( ApplicationCreation, ApplicationEdition, + ApplicationDeletion, ConfigurationView, DocumentationView, SchemaView, @@ -26,6 +27,11 @@ ApplicationEdition.as_view(), name='creme_api__edit_application', ), + re_path( + r'^delete/(?P\d+)[/]?$', + ApplicationDeletion.as_view(), + name='creme_api__delete_application', + ), ]), ), ] + router.urls diff --git a/creme/creme_api/views.py b/creme/creme_api/views.py index 38ed7e8da2..eb859eb015 100644 --- a/creme/creme_api/views.py +++ b/creme/creme_api/views.py @@ -113,7 +113,8 @@ class ApplicationEdition(generic.CremeModelEditionPopup): pk_url_kwarg = 'application_id' permissions = "creme_api.can_admin" - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['request'] = self.request - return kwargs + +class ApplicationDeletion(generic.CremeModelDeletion): + model = Application + pk_url_kwarg = 'application_id' + permissions = "creme_api.can_admin" From 5812d58d16cc6ee945ba7ae4a49a27794a7bc9d9 Mon Sep 17 00:00:00 2001 From: Hugo Smett Date: Tue, 10 May 2022 16:19:04 +0200 Subject: [PATCH 09/12] Remove addresses global views for now --- creme/creme_api/api/persons/viewsets.py | 26 ------------------------- creme/creme_api/api/routes.py | 1 - 2 files changed, 27 deletions(-) diff --git a/creme/creme_api/api/persons/viewsets.py b/creme/creme_api/api/persons/viewsets.py index c947c922c1..998c4ccc2e 100644 --- a/creme/creme_api/api/persons/viewsets.py +++ b/creme/creme_api/api/persons/viewsets.py @@ -95,32 +95,6 @@ class OrganisationViewSet(CremeEntityViewSet): schema = CremeSchema(tags=["Organisations"]) -class AddressViewSet(CremeModelViewSet): - """ - create: - Create an address. - - retrieve: - Retrieve an address. - - update: - Update an address. - - partial_update: - Partially update an address. - - list: - List addresses. - - delete: - Delete an address. - - """ - queryset = persons.get_address_model().objects.all() - serializer_class = AddressSerializer - schema = CremeSchema(tags=["Addresses"]) - - class CivilityViewSet(CremeModelViewSet): """ create: diff --git a/creme/creme_api/api/routes.py b/creme/creme_api/api/routes.py index 405bf609ff..c458da375c 100644 --- a/creme/creme_api/api/routes.py +++ b/creme/creme_api/api/routes.py @@ -34,7 +34,6 @@ def register_viewset(self, resource_name, viewset): router.register_viewset("credentials", creme.creme_api.api.auth.viewsets.SetCredentialsViewSet) router.register_viewset("contacts", creme.creme_api.api.persons.viewsets.ContactViewSet) router.register_viewset("organisations", creme.creme_api.api.persons.viewsets.OrganisationViewSet) -router.register_viewset("addresses", creme.creme_api.api.persons.viewsets.AddressViewSet) router.register_viewset("civilities", creme.creme_api.api.persons.viewsets.CivilityViewSet) router.register_viewset("positions", creme.creme_api.api.persons.viewsets.PositionViewSet) router.register_viewset("staff_sizes", creme.creme_api.api.persons.viewsets.StaffSizeViewSet) From 4784b1d912d2003822f18f35d432cf0a0a32a03c Mon Sep 17 00:00:00 2001 From: Hugo Smett Date: Tue, 10 May 2022 16:19:36 +0200 Subject: [PATCH 10/12] Test legal forms and staff sizes --- creme/creme_api/tests/__init__.py | 26 ++++++ creme/creme_api/tests/test_contacts.py | 15 --- creme/creme_api/tests/test_legal_forms.py | 107 ++++++++++++++++++++++ creme/creme_api/tests/test_staff_sizes.py | 107 ++++++++++++++++++++++ creme/creme_api/tests/test_users.py | 2 +- 5 files changed, 241 insertions(+), 16 deletions(-) create mode 100644 creme/creme_api/tests/test_legal_forms.py create mode 100644 creme/creme_api/tests/test_staff_sizes.py diff --git a/creme/creme_api/tests/__init__.py b/creme/creme_api/tests/__init__.py index b553b2d1d0..12deb92798 100644 --- a/creme/creme_api/tests/__init__.py +++ b/creme/creme_api/tests/__init__.py @@ -1,3 +1,5 @@ +from django.test import TestCase + from .test_addresses import * # noqa from .test_authentication import * # noqa from .test_civilities import * # noqa @@ -5,11 +7,35 @@ from .test_contenttypes import * # noqa from .test_credentials import * # noqa from .test_documentation import * # noqa +from .test_legal_forms import * # noqa from .test_models import * # noqa from .test_positions import * # noqa from .test_roles import * # noqa from .test_sectors import * # noqa +from .test_staff_sizes import * # noqa from .test_teams import * # noqa from .test_tokens import * # noqa from .test_users import * # noqa from .utils import * # noqa + +from creme.creme_api.api.routes import router + + +class CollectionTestCase(TestCase): + def test_all_routes_covered(self): + expected_tests = { + (pattern.name, action) + for pattern in router.get_urls() + for action in pattern.callback.actions + } + found_tests = { + (obj.url_name, obj.method) + for obj in globals().values() + if isinstance(obj, type) and issubclass(obj, CremeAPITestCase) + } + missing = expected_tests - found_tests + if missing: + msg = ( + f"Missing tests for routes:\n" + + "\n".join(f"{action.upper()}\t{url}" for url, action in sorted(missing))) + self.fail(msg) diff --git a/creme/creme_api/tests/test_contacts.py b/creme/creme_api/tests/test_contacts.py index e396ee0cef..e5bf7b22f8 100644 --- a/creme/creme_api/tests/test_contacts.py +++ b/creme/creme_api/tests/test_contacts.py @@ -896,18 +896,3 @@ def test_clone_contact(self): 'url_site': '', 'user': fulbert.user_id, }) - - -class ListContactAddressesTestCase(CremeAPITestCase): - url_name = 'creme_api__contacts-addresses' - method = 'get' - - def test_delete_contact__protected(self): - fulbert = Contact.objects.get() - response = self.make_request(to=fulbert.id, status_code=422) - self.assertEqual(response.data['detail'].code, 'protected') - - def test_delete_contact(self): - contact = self.factory.contact() - self.make_request(to=contact.id, status_code=204) - self.assertFalse(Contact.objects.filter(id=contact.id).exists()) diff --git a/creme/creme_api/tests/test_legal_forms.py b/creme/creme_api/tests/test_legal_forms.py new file mode 100644 index 0000000000..f448442300 --- /dev/null +++ b/creme/creme_api/tests/test_legal_forms.py @@ -0,0 +1,107 @@ +from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.persons.models import LegalForm + + +@Factory.register +def legal_form(factory, **kwargs): + data = factory.legal_form_data(**kwargs) + return LegalForm.objects.create(**data) + + +@Factory.register +def legal_form_data(factory, **kwargs): + kwargs.setdefault('title', 'Trust') + return kwargs + + +class CreateLegalFormTestCase(CremeAPITestCase): + url_name = 'creme_api__legal_forms-list' + method = 'post' + + def test_validation__required(self): + response = self.make_request(data={}, status_code=400) + self.assertValidationErrors(response, { + 'title': ['required'], + }) + + def test_create_legal_form(self): + data = self.factory.legal_form_data() + response = self.make_request(data=data, status_code=201) + legal_form = LegalForm.objects.get(id=response.data['id']) + self.assertPayloadEqual(response, { + 'id': legal_form.id, + 'title': 'Trust', + }) + self.assertEqual(legal_form.title, "Trust") + + +class RetrieveLegalFormTestCase(CremeAPITestCase): + url_name = 'creme_api__legal_forms-detail' + method = 'get' + + def test_retrieve_legal_form(self): + legal_form = self.factory.legal_form() + response = self.make_request(to=legal_form.id, status_code=200) + self.assertPayloadEqual(response, { + 'id': legal_form.id, + 'title': 'Trust', + }) + + +class UpdateLegalFormTestCase(CremeAPITestCase): + url_name = 'creme_api__legal_forms-detail' + method = 'put' + + def test_update_legal_form(self): + legal_form = self.factory.legal_form() + response = self.make_request(to=legal_form.id, data={ + 'title': "Corporation", + }, status_code=200) + self.assertPayloadEqual(response, { + 'id': legal_form.id, + 'title': 'Corporation', + }) + legal_form.refresh_from_db() + self.assertEqual(legal_form.title, "Corporation") + + +class PartialUpdateLegalFormTestCase(CremeAPITestCase): + url_name = 'creme_api__legal_forms-detail' + method = 'patch' + + def test_partial_update_legal_form(self): + legal_form = self.factory.legal_form() + response = self.make_request(to=legal_form.id, data={ + 'title': "Corporation", + }, status_code=200) + self.assertPayloadEqual(response, { + 'id': legal_form.id, + 'title': 'Corporation', + }) + legal_form.refresh_from_db() + self.assertEqual(legal_form.title, "Corporation") + + +class ListLegalFormTestCase(CremeAPITestCase): + url_name = 'creme_api__legal_forms-list' + method = 'get' + + def test_list_legal_forms(self): + LegalForm.objects.all().delete() + legal_form1 = self.factory.legal_form(title="1") + legal_form2 = self.factory.legal_form(title="2") + response = self.make_request(status_code=200) + self.assertPayloadEqual(response, [ + {'id': legal_form1.id, 'title': '1'}, + {'id': legal_form2.id, 'title': '2'}, + ]) + + +class DeleteLegalFormTestCase(CremeAPITestCase): + url_name = 'creme_api__legal_forms-detail' + method = 'delete' + + def test_delete_legal_form(self): + legal_form = self.factory.legal_form() + self.make_request(to=legal_form.id, status_code=204) + self.assertFalse(LegalForm.objects.filter(id=legal_form.id).exists()) diff --git a/creme/creme_api/tests/test_staff_sizes.py b/creme/creme_api/tests/test_staff_sizes.py new file mode 100644 index 0000000000..0a6cfeb6cf --- /dev/null +++ b/creme/creme_api/tests/test_staff_sizes.py @@ -0,0 +1,107 @@ +from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.persons.models import StaffSize + + +@Factory.register +def staff_size(factory, **kwargs): + data = factory.staff_size_data(**kwargs) + return StaffSize.objects.create(**data) + + +@Factory.register +def staff_size_data(factory, **kwargs): + kwargs.setdefault('size', '1 - 10') + return kwargs + + +class CreateStaffSizeTestCase(CremeAPITestCase): + url_name = 'creme_api__staff_sizes-list' + method = 'post' + + def test_validation__required(self): + response = self.make_request(data={}, status_code=400) + self.assertValidationErrors(response, { + 'size': ['required'], + }) + + def test_create_staff_size(self): + data = self.factory.staff_size_data() + response = self.make_request(data=data, status_code=201) + staff_size = StaffSize.objects.get(id=response.data['id']) + self.assertPayloadEqual(response, { + 'id': staff_size.id, + 'size': '1 - 10', + }) + self.assertEqual(staff_size.size, '1 - 10') + + +class RetrieveStaffSizeTestCase(CremeAPITestCase): + url_name = 'creme_api__staff_sizes-detail' + method = 'get' + + def test_retrieve_staff_size(self): + staff_size = self.factory.staff_size() + response = self.make_request(to=staff_size.id, status_code=200) + self.assertPayloadEqual(response, { + 'id': staff_size.id, + 'size': '1 - 10', + }) + + +class UpdateStaffSizeTestCase(CremeAPITestCase): + url_name = 'creme_api__staff_sizes-detail' + method = 'put' + + def test_update_staff_size(self): + staff_size = self.factory.staff_size() + response = self.make_request(to=staff_size.id, data={ + 'size': '1 - 100', + }, status_code=200) + self.assertPayloadEqual(response, { + 'id': staff_size.id, + 'size': '1 - 100', + }) + staff_size.refresh_from_db() + self.assertEqual(staff_size.size, '1 - 100') + + +class PartialUpdateStaffSizeTestCase(CremeAPITestCase): + url_name = 'creme_api__staff_sizes-detail' + method = 'patch' + + def test_partial_update_staff_size(self): + staff_size = self.factory.staff_size() + response = self.make_request(to=staff_size.id, data={ + 'size': '1 - 1000', + }, status_code=200) + self.assertPayloadEqual(response, { + 'id': staff_size.id, + 'size': '1 - 1000', + }) + staff_size.refresh_from_db() + self.assertEqual(staff_size.size, '1 - 1000') + + +class ListStaffSizeTestCase(CremeAPITestCase): + url_name = 'creme_api__staff_sizes-list' + method = 'get' + + def test_list_staff_sizes(self): + StaffSize.objects.all().delete() + staff_size1 = self.factory.staff_size(size="1 - 10") + staff_size2 = self.factory.staff_size(size="10 - 20") + response = self.make_request(status_code=200) + self.assertPayloadEqual(response, [ + {'id': staff_size1.id, 'size': "1 - 10"}, + {'id': staff_size2.id, 'size': "10 - 20"}, + ]) + + +class DeleteStaffSizeTestCase(CremeAPITestCase): + url_name = 'creme_api__staff_sizes-detail' + method = 'delete' + + def test_delete_staff_size(self): + staff_size = self.factory.staff_size() + self.make_request(to=staff_size.id, status_code=204) + self.assertFalse(StaffSize.objects.filter(id=staff_size.id).exists()) diff --git a/creme/creme_api/tests/test_users.py b/creme/creme_api/tests/test_users.py index 0347499fd5..a53b6a94b6 100644 --- a/creme/creme_api/tests/test_users.py +++ b/creme/creme_api/tests/test_users.py @@ -331,7 +331,7 @@ def test_set_password_user(self): self.assertTrue(user.check_password("StrongPassword")) -class DeletePositionTestCase(CremeAPITestCase): +class DeleteUserTestCase(CremeAPITestCase): url_name = 'creme_api__users-detail' method = 'delete' From 35868178324808f682619bbb885df6e914891615 Mon Sep 17 00:00:00 2001 From: Hugo Smett Date: Thu, 12 May 2022 15:15:10 +0200 Subject: [PATCH 11/12] test factories; test organisations --- creme/creme_api/api/auth/serializers.py | 176 +-- creme/creme_api/api/auth/viewsets.py | 68 +- creme/creme_api/api/authentication.py | 28 +- creme/creme_api/api/contenttypes/viewsets.py | 3 +- creme/creme_api/api/core/exceptions.py | 4 +- creme/creme_api/api/core/serializers.py | 24 +- creme/creme_api/api/core/viewsets.py | 9 +- creme/creme_api/api/pagination.py | 2 +- creme/creme_api/api/persons/serializers.py | 244 ++-- creme/creme_api/api/persons/viewsets.py | 13 +- creme/creme_api/api/routes.py | 31 +- creme/creme_api/api/schemas.py | 3 +- creme/creme_api/api/tokens/serializers.py | 25 +- creme/creme_api/api/tokens/viewsets.py | 8 +- creme/creme_api/apps.py | 5 +- creme/creme_api/bricks.py | 18 +- creme/creme_api/migrations/0001_initial.py | 123 +- creme/creme_api/models.py | 43 +- creme/creme_api/tests/__init__.py | 19 +- creme/creme_api/tests/factories/__init__.py | 26 + creme/creme_api/tests/factories/auth.py | 109 ++ creme/creme_api/tests/factories/persons.py | 147 ++ .../tests/factories/test_factories.py | 37 + creme/creme_api/tests/test_addresses.py | 44 +- creme/creme_api/tests/test_authentication.py | 44 +- creme/creme_api/tests/test_civilities.py | 151 +- creme/creme_api/tests/test_contacts.py | 1247 +++++++---------- creme/creme_api/tests/test_contenttypes.py | 21 +- creme/creme_api/tests/test_credentials.py | 293 ++-- creme/creme_api/tests/test_documentation.py | 58 +- creme/creme_api/tests/test_legal_forms.py | 138 +- creme/creme_api/tests/test_models.py | 18 +- creme/creme_api/tests/test_organisations.py | 767 ++++++++++ creme/creme_api/tests/test_positions.py | 138 +- creme/creme_api/tests/test_roles.py | 380 ++--- creme/creme_api/tests/test_sectors.py | 138 +- creme/creme_api/tests/test_staff_sizes.py | 144 +- creme/creme_api/tests/test_teams.py | 252 ++-- creme/creme_api/tests/test_tokens.py | 35 +- creme/creme_api/tests/test_users.py | 451 +++--- creme/creme_api/tests/utils.py | 44 +- creme/creme_api/urls.py | 54 +- creme/creme_api/views.py | 32 +- setup.cfg | 1 + 44 files changed, 3356 insertions(+), 2259 deletions(-) create mode 100644 creme/creme_api/tests/factories/__init__.py create mode 100644 creme/creme_api/tests/factories/auth.py create mode 100644 creme/creme_api/tests/factories/persons.py create mode 100644 creme/creme_api/tests/factories/test_factories.py create mode 100644 creme/creme_api/tests/test_organisations.py diff --git a/creme/creme_api/api/auth/serializers.py b/creme/creme_api/api/auth/serializers.py index 603a7142b2..22f3c3e9d3 100644 --- a/creme/creme_api/api/auth/serializers.py +++ b/creme/creme_api/api/auth/serializers.py @@ -18,52 +18,52 @@ class PasswordSerializer(serializers.Serializer): password = serializers.CharField( - label=_('Password'), trim_whitespace=False, write_only=True, required=True) + label=_("Password"), trim_whitespace=False, write_only=True, required=True + ) def validate_password(self, password): password_validation.validate_password(password, self.instance) return password def save(self): - self.instance.set_password(self.validated_data['password']) + self.instance.set_password(self.validated_data["password"]) self.instance.save() return self.instance class UserSerializer(serializers.ModelSerializer): default_error_messages = { - 'is_superuser_xor_role': _("A user must either have a role, or be a superuser.") + "is_superuser_xor_role": _("A user must either have a role, or be a superuser.") } class Meta: model = CremeUser fields = [ - 'id', - 'username', - 'last_name', - 'first_name', - 'email', - - 'date_joined', - 'last_login', - 'is_active', + "id", + "username", + "last_name", + "first_name", + "email", + "date_joined", + "last_login", + "is_active", # 'is_staff', - 'is_superuser', - 'role', + "is_superuser", + "role", # 'is_team', # 'teammates_set', - 'time_zone', - 'theme', + "time_zone", + "theme", # 'settings', ] read_only_fields = [ - 'date_joined', - 'last_login', + "date_joined", + "last_login", ] extra_kwargs = { - 'first_name': {'required': True}, - 'last_name': {'required': True}, - 'email': {'required': True}, + "first_name": {"required": True}, + "last_name": {"required": True}, + "email": {"required": True}, } def validate(self, attrs): @@ -74,8 +74,8 @@ def validate(self, attrs): is_superuser = self.instance.is_superuser role_id = self.instance.role_id - has_is_superuser = bool(attrs.get('is_superuser', is_superuser)) - has_role = bool(attrs.get('role', role_id)) + has_is_superuser = bool(attrs.get("is_superuser", is_superuser)) + has_role = bool(attrs.get("role", role_id)) if not (has_is_superuser ^ has_role): self.fail("is_superuser_xor_role") return attrs @@ -85,21 +85,19 @@ class TeamSerializer(serializers.ModelSerializer): teammates = serializers.PrimaryKeyRelatedField( queryset=CremeUser.objects.filter(is_team=False, is_staff=False), many=True, - label=_('Teammates'), + label=_("Teammates"), required=True, - source='teammates_set', + source="teammates_set", ) class Meta: model = CremeUser fields = [ - 'id', - 'username', - 'teammates', + "id", + "username", + "teammates", ] - extra_kwargs = { - 'username': {"label": _("Team name")} - } + extra_kwargs = {"username": {"label": _("Team name")}} def __init__(self, *args, **kwargs): super(TeamSerializer, self).__init__(*args, **kwargs) @@ -108,7 +106,7 @@ def __init__(self, *args, **kwargs): self.fields["name"] = username_field def save(self, **kwargs): - kwargs['is_team'] = True + kwargs["is_team"] = True team = super().save(**kwargs) return team @@ -118,24 +116,24 @@ class DeleteUserSerializer(serializers.ModelSerializer): Serializer which assigns the fields with type CremeUserForeignKey referencing a given user A to another user B, then deletes A. """ + transfer_to = serializers.PrimaryKeyRelatedField( - queryset=CremeUser.objects.none(), - required=True + queryset=CremeUser.objects.none(), required=True ) class Meta: model = CremeUser - fields = ['transfer_to'] + fields = ["transfer_to"] def __init__(self, instance=None, **kwargs): super().__init__(instance=instance, **kwargs) users = CremeUser.objects.exclude(is_staff=True) if instance is not None: users = users.exclude(pk=instance.pk) - self.fields['transfer_to'].queryset = users + self.fields["transfer_to"].queryset = users def save(self, **kwargs): - CremeUserForeignKey._TRANSFER_TO_USER = self.validated_data['transfer_to'] + CremeUserForeignKey._TRANSFER_TO_USER = self.validated_data["transfer_to"] try: self.instance.delete() @@ -145,38 +143,41 @@ def save(self, **kwargs): class UserRoleSerializer(serializers.ModelSerializer): allowed_apps = serializers.MultipleChoiceField( - label=_('Allowed applications'), + label=_("Allowed applications"), choices=(), ) admin_4_apps = serializers.MultipleChoiceField( - label=_('Administrated applications'), + label=_("Administrated applications"), choices=(), help_text=_( - 'These applications can be configured. ' - 'For example, the concerned users can create new choices ' - 'available in forms (eg: position for contacts).' + "These applications can be configured. " + "For example, the concerned users can create new choices " + "available in forms (eg: position for contacts)." ), ) creatable_ctypes = serializers.PrimaryKeyRelatedField( - label=_('Creatable resources'), + label=_("Creatable resources"), many=True, queryset=ContentType.objects.none(), ) exportable_ctypes = serializers.PrimaryKeyRelatedField( - label=_('Exportable resources'), + label=_("Exportable resources"), many=True, queryset=ContentType.objects.none(), help_text=_( - 'This types of entities can be downloaded as CSV/XLS ' - 'files (in the corresponding list-views).' + "This types of entities can be downloaded as CSV/XLS " + "files (in the corresponding list-views)." ), ) default_error_messages = { - 'admin_4_not_allowed_app': _('App "{app}" is not an allowed app for this role.'), - 'not_allowed_ctype': _( + "admin_4_not_allowed_app": _( + 'App "{app}" is not an allowed app for this role.' + ), + "not_allowed_ctype": _( 'Content type "{ct}" ({id}) is part of the app "{app}" ' - 'which is not an allowed app for this role.'), + "which is not an allowed app for this role." + ), } class Meta: @@ -188,7 +189,7 @@ class Meta: "admin_4_apps", "creatable_ctypes", "exportable_ctypes", - "credentials" + "credentials", ] read_only_fields = [ "credentials", @@ -199,23 +200,27 @@ def __init__(self, *args, **kwargs): apps = list(creme_app_configs()) CRED_REGULAR = CremeAppConfig.CRED_REGULAR - allowed_apps_f = self.fields['allowed_apps'] + allowed_apps_f = self.fields["allowed_apps"] allowed_apps_f.choices = ( - (app.label, str(app.verbose_name)) for app in apps if app.credentials & CRED_REGULAR + (app.label, str(app.verbose_name)) + for app in apps + if app.credentials & CRED_REGULAR ) CRED_ADMIN = CremeAppConfig.CRED_ADMIN - admin_4_apps_f = self.fields['admin_4_apps'] + admin_4_apps_f = self.fields["admin_4_apps"] admin_4_apps_f.choices = ( - (app.label, str(app.verbose_name)) for app in apps if app.credentials & CRED_ADMIN + (app.label, str(app.verbose_name)) + for app in apps + if app.credentials & CRED_ADMIN ) ct_queryset = get_cremeentity_contenttype_queryset() - creatable_ctypes_f = self.fields['creatable_ctypes'] + creatable_ctypes_f = self.fields["creatable_ctypes"] creatable_ctypes_f.child_relation.queryset = ct_queryset.all() - exportable_ctypes_f = self.fields['exportable_ctypes'] + exportable_ctypes_f = self.fields["exportable_ctypes"] exportable_ctypes_f.child_relation.queryset = ct_queryset.all() def build_error_detail(self, error_code, **kwargs): @@ -243,7 +248,7 @@ def validate(self, attrs): allowed_ctypes = set(filtered_entity_ctypes(allowed_apps)) - for admin_not_allowed_app in (admin_4_apps - allowed_apps): + for admin_not_allowed_app in admin_4_apps - allowed_apps: errors["admin_4_apps"].append( self.build_error_detail( "admin_4_not_allowed_app", @@ -251,7 +256,7 @@ def validate(self, attrs): ) ) - for create_not_allowed_ctype in (creatable_ctypes - allowed_ctypes): + for create_not_allowed_ctype in creatable_ctypes - allowed_ctypes: errors["creatable_ctypes"].append( self.build_error_detail( "not_allowed_ctype", @@ -261,13 +266,14 @@ def validate(self, attrs): ) ) - for export_not_allowed_ctype in (exportable_ctypes - allowed_ctypes): + for export_not_allowed_ctype in exportable_ctypes - allowed_ctypes: errors["exportable_ctypes"].append( self.build_error_detail( "not_allowed_ctype", id=export_not_allowed_ctype.id, ct=export_not_allowed_ctype.model, - app=export_not_allowed_ctype.app_label) + app=export_not_allowed_ctype.app_label, + ) ) if errors: @@ -278,20 +284,20 @@ def validate(self, attrs): class SetCredentialsSerializer(serializers.ModelSerializer): can_view = serializers.BooleanField( - label=_('View'), + label=_("View"), required=True, ) can_change = serializers.BooleanField( - label=_('Change'), + label=_("Change"), required=True, ) can_delete = serializers.BooleanField( - label=_('Delete'), + label=_("Delete"), required=True, ) can_link = serializers.BooleanField( - label=_('Link'), + label=_("Link"), required=True, help_text=_( "You must have the permission to link on 2 entities " @@ -302,11 +308,11 @@ class SetCredentialsSerializer(serializers.ModelSerializer): ), ) can_unlink = serializers.BooleanField( - label=_('Unlink'), + label=_("Unlink"), required=True, help_text=_( - 'You must have the permission to unlink on ' - '2 entities to delete a relationship between them.' + "You must have the permission to unlink on " + "2 entities to delete a relationship between them." ), ) @@ -326,21 +332,21 @@ class Meta: "efilter", ] read_only_fields = [ - 'efilter', + "efilter", ] extra_kwargs = { - 'set_type': {'required': True}, - 'ctype': {'required': True}, - 'forbidden': {'required': True}, + "set_type": {"required": True}, + "ctype": {"required": True}, + "forbidden": {"required": True}, } def update(self, instance, validated_data): instance.set_value( - can_view=validated_data.pop('can_view', instance.can_view), - can_change=validated_data.pop('can_change', instance.can_change), - can_delete=validated_data.pop('can_delete', instance.can_delete), - can_link=validated_data.pop('can_link', instance.can_link), - can_unlink=validated_data.pop('can_unlink', instance.can_unlink), + can_view=validated_data.pop("can_view", instance.can_view), + can_change=validated_data.pop("can_change", instance.can_change), + can_delete=validated_data.pop("can_delete", instance.can_delete), + can_link=validated_data.pop("can_link", instance.can_link), + can_unlink=validated_data.pop("can_unlink", instance.can_unlink), ) return super().update(instance, validated_data) @@ -353,18 +359,18 @@ class SetCredentialsCreateSerializer(SetCredentialsSerializer): def create(self, validated_data): instance = SetCredentials( - role=validated_data['role'], - set_type=validated_data['set_type'], - ctype=validated_data['ctype'], - forbidden=validated_data['forbidden'], + role=validated_data["role"], + set_type=validated_data["set_type"], + ctype=validated_data["ctype"], + forbidden=validated_data["forbidden"], # efilter=validated_data['efilter'], ) instance.set_value( - can_view=validated_data['can_view'], - can_change=validated_data['can_change'], - can_delete=validated_data['can_delete'], - can_link=validated_data['can_link'], - can_unlink=validated_data['can_unlink'], + can_view=validated_data["can_view"], + can_change=validated_data["can_change"], + can_delete=validated_data["can_delete"], + can_link=validated_data["can_link"], + can_unlink=validated_data["can_unlink"], ) instance.save() return instance diff --git a/creme/creme_api/api/auth/viewsets.py b/creme/creme_api/api/auth/viewsets.py index ecfceacf9e..f619b9d905 100644 --- a/creme/creme_api/api/auth/viewsets.py +++ b/creme/creme_api/api/auth/viewsets.py @@ -21,12 +21,14 @@ CremeUser = get_user_model() -class UserViewSet(mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - # mixins.DestroyModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet): +class UserViewSet( + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + # mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): """ create: Create a user. @@ -50,11 +52,12 @@ class UserViewSet(mixins.CreateModelMixin, Delete a user. """ + queryset = CremeUser.objects.filter(is_team=False, is_staff=False) serializer_class = UserSerializer schema = CremeSchema(tags=["Users"], operation_id_base="Users") - @action(methods=['post'], detail=True, serializer_class=PasswordSerializer) + @action(methods=["post"], detail=True, serializer_class=PasswordSerializer) def set_password(self, request, pk): instance = self.get_object() serializer = self.get_serializer(instance, data=request.data) @@ -65,7 +68,7 @@ def set_password(self, request, pk): return Response(serializer.data) @action( - methods=['post'], + methods=["post"], detail=True, serializer_class=DeleteUserSerializer, url_path="delete", @@ -80,12 +83,14 @@ def delete_user(self, request, pk): return Response(status=status.HTTP_204_NO_CONTENT) -class TeamViewSet(mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - # mixins.DestroyModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet): +class TeamViewSet( + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + # mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): """ create: Create a team. @@ -106,12 +111,13 @@ class TeamViewSet(mixins.CreateModelMixin, Delete a team. """ + queryset = CremeUser.objects.filter(is_team=True, is_staff=False) serializer_class = TeamSerializer schema = CremeSchema(tags=["Teams"], operation_id_base="Teams") @action( - methods=['post'], + methods=["post"], detail=True, serializer_class=DeleteUserSerializer, url_path="delete", @@ -126,12 +132,14 @@ def delete_team(self, request, pk): return Response(status=status.HTTP_204_NO_CONTENT) -class UserRoleViewSet(mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet): +class UserRoleViewSet( + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): """ create: @@ -153,6 +161,7 @@ class UserRoleViewSet(mixins.CreateModelMixin, List roles. """ + queryset = UserRole.objects.all() serializer_class = UserRoleSerializer schema = CremeSchema(tags=["Roles"]) @@ -164,12 +173,14 @@ def perform_destroy(self, instance): raise PermissionDenied(e.args[0]) from e -class SetCredentialsViewSet(mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet): +class SetCredentialsViewSet( + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): """ create: @@ -191,11 +202,12 @@ class SetCredentialsViewSet(mixins.CreateModelMixin, List credential sets. """ + queryset = SetCredentials.objects.all() serializer_class = SetCredentialsSerializer schema = CremeSchema(tags=["Credential Sets"]) def get_serializer_class(self): - if self.action == 'create': + if self.action == "create": return SetCredentialsCreateSerializer return super().get_serializer_class() diff --git a/creme/creme_api/api/authentication.py b/creme/creme_api/api/authentication.py index c832b0678a..e0e8db2b71 100644 --- a/creme/creme_api/api/authentication.py +++ b/creme/creme_api/api/authentication.py @@ -13,13 +13,15 @@ class TokenAuthentication(BaseAuthentication): Token based authentication """ - keyword = 'Token' + keyword = "Token" errors = { - 'empty': _('Invalid token header. No credentials provided.'), - 'too_long': _('Invalid token header. Token string should not contain spaces.'), - 'encoding': _('Invalid token header. Token string should not contain invalid characters.'), - 'invalid': _('Incorrect authentication credentials.'), - 'expired': _('Incorrect authentication credentials. Token has expired.'), + "empty": _("Invalid token header. No credentials provided."), + "too_long": _("Invalid token header. Token string should not contain spaces."), + "encoding": _( + "Invalid token header. Token string should not contain invalid characters." + ), + "invalid": _("Incorrect authentication credentials."), + "expired": _("Incorrect authentication credentials. Token has expired."), } def authentication_failure(self, code): @@ -35,25 +37,25 @@ def authenticate(self, request): return None if len(auth) == 1: - raise self.authentication_failure('empty') + raise self.authentication_failure("empty") elif len(auth) > 2: - raise self.authentication_failure('too_long') + raise self.authentication_failure("too_long") try: token_code = auth[1].decode() except UnicodeError: - raise self.authentication_failure('encoding') + raise self.authentication_failure("encoding") try: - token = Token.objects.select_related('application').get(code=token_code) + token = Token.objects.select_related("application").get(code=token_code) except Token.DoesNotExist: - raise self.authentication_failure('invalid') + raise self.authentication_failure("invalid") if not token.application.can_authenticate(request=request): - raise self.authentication_failure('invalid') + raise self.authentication_failure("invalid") if token.is_expired(): - raise self.authentication_failure('expired') + raise self.authentication_failure("expired") request.token = token request.application = token.application diff --git a/creme/creme_api/api/contenttypes/viewsets.py b/creme/creme_api/api/contenttypes/viewsets.py index acc447a3e1..10643e1f16 100644 --- a/creme/creme_api/api/contenttypes/viewsets.py +++ b/creme/creme_api/api/contenttypes/viewsets.py @@ -13,9 +13,10 @@ class ContentTypeViewSet(viewsets.ReadOnlyModelViewSet): list: List content types. """ + queryset = None serializer_class = ContentTypeSerializer - schema = AutoSchema(tags=['Content Types']) + schema = AutoSchema(tags=["Content Types"]) def get_queryset(self): return get_cremeentity_contenttype_queryset() diff --git a/creme/creme_api/api/core/exceptions.py b/creme/creme_api/api/core/exceptions.py index 4a618e3795..4411524528 100644 --- a/creme/creme_api/api/core/exceptions.py +++ b/creme/creme_api/api/core/exceptions.py @@ -5,5 +5,5 @@ class UnprocessableEntity(APIException): status_code = status.HTTP_422_UNPROCESSABLE_ENTITY - default_detail = _('Unprocessable entity.') - default_code = 'unprocessable_entity' + default_detail = _("Unprocessable entity.") + default_code = "unprocessable_entity" diff --git a/creme/creme_api/api/core/serializers.py b/creme/creme_api/api/core/serializers.py index c9cfce3a9c..b8c4269148 100644 --- a/creme/creme_api/api/core/serializers.py +++ b/creme/creme_api/api/core/serializers.py @@ -9,9 +9,9 @@ class CremeEntityRelatedField(serializers.RelatedField): queryset = CremeEntity.objects.all() default_error_messages = { - 'required': _('This field is required.'), - 'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'), - 'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'), + "required": _("This field is required."), + "does_not_exist": _('Invalid pk "{pk_value}" - object does not exist.'), + "incorrect_type": _("Incorrect type. Expected pk value, received {data_type}."), } def use_pk_only_optimization(self): @@ -22,9 +22,9 @@ def to_internal_value(self, data): creme_entity = self.get_queryset().get(pk=data) return creme_entity.get_real_entity() except ObjectDoesNotExist: - self.fail('does_not_exist', pk_value=data) + self.fail("does_not_exist", pk_value=data) except (TypeError, ValueError): - self.fail('incorrect_type', data_type=type(data).__name__) + self.fail("incorrect_type", data_type=type(data).__name__) def to_representation(self, value): return value.pk @@ -34,11 +34,11 @@ class SimpleCremeEntitySerializer(serializers.ModelSerializer): class Meta: model = CremeEntity fields = [ - 'id', - 'uuid', - 'created', - 'modified', - 'is_deleted', + "id", + "uuid", + "created", + "modified", + "is_deleted", ] @@ -48,6 +48,6 @@ class CremeEntitySerializer(SimpleCremeEntitySerializer): class Meta(SimpleCremeEntitySerializer.Meta): model = CremeEntity fields = SimpleCremeEntitySerializer.Meta.fields + [ - 'user', - 'description', + "user", + "description", ] diff --git a/creme/creme_api/api/core/viewsets.py b/creme/creme_api/api/core/viewsets.py index 99abaca377..737c777e88 100644 --- a/creme/creme_api/api/core/viewsets.py +++ b/creme/creme_api/api/core/viewsets.py @@ -9,7 +9,7 @@ class CremeModelViewSet(viewsets.ModelViewSet): - LOCK_METHODS = {'POST', 'PUT' 'PATCH'} + LOCK_METHODS = {"POST", "PUT" "PATCH"} def perform_destroy(self, instance): try: @@ -25,8 +25,7 @@ def get_queryset(self): class CremeEntityViewSet(CremeModelViewSet): - - @action(methods=['post'], detail=True, serializer_class=SimpleCremeEntitySerializer) + @action(methods=["post"], detail=True, serializer_class=SimpleCremeEntitySerializer) def trash(self, request, *args, **kwargs): instance = self.get_object() @@ -38,7 +37,7 @@ def trash(self, request, *args, **kwargs): serializer = self.get_serializer(instance) return Response(serializer.data) - @action(methods=['post'], detail=True, serializer_class=SimpleCremeEntitySerializer) + @action(methods=["post"], detail=True, serializer_class=SimpleCremeEntitySerializer) def restore(self, request, *args, **kwargs): instance = self.get_object() @@ -47,7 +46,7 @@ def restore(self, request, *args, **kwargs): serializer = self.get_serializer(instance) return Response(serializer.data) - @action(methods=['post'], detail=True) + @action(methods=["post"], detail=True) def clone(self, request, *args, **kwargs): instance = self.get_object() diff --git a/creme/creme_api/api/pagination.py b/creme/creme_api/api/pagination.py index 6f6fddcfe5..6c6abe55c3 100644 --- a/creme/creme_api/api/pagination.py +++ b/creme/creme_api/api/pagination.py @@ -2,4 +2,4 @@ class CremeCursorPagination(CursorPagination): - ordering = 'id' + ordering = "id" diff --git a/creme/creme_api/api/persons/serializers.py b/creme/creme_api/api/persons/serializers.py index 4b8948de1a..004c4b7554 100644 --- a/creme/creme_api/api/persons/serializers.py +++ b/creme/creme_api/api/persons/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers from creme.creme_api.api.core.serializers import ( - CremeEntityRelatedField, + # CremeEntityRelatedField, CremeEntitySerializer, ) from creme.persons import ( @@ -22,189 +22,197 @@ Address = get_address_model() -class AddressSerializer(serializers.ModelSerializer): - owner = CremeEntityRelatedField() - - class Meta: - model = Address - fields = [ - 'id', - 'name', - 'address', - 'po_box', - 'zipcode', - 'city', - 'department', - 'state', - 'country', - 'owner', - ] +# class AddressSerializer(serializers.ModelSerializer): +# owner = CremeEntityRelatedField() +# +# class Meta: +# model = Address +# fields = [ +# 'id', +# 'name', +# 'address', +# 'po_box', +# 'zipcode', +# 'city', +# 'department', +# 'state', +# 'country', +# 'owner', +# ] class InnerAddressSerializer(serializers.ModelSerializer): class Meta: model = Address fields = [ - 'address', - 'po_box', - 'zipcode', - 'city', - 'department', - 'state', - 'country', + "address", + "po_box", + "zipcode", + "city", + "department", + "state", + "country", ] class CivilitySerializer(serializers.ModelSerializer): class Meta: model = Civility - fields = ['id', 'title', 'shortcut'] + fields = ["id", "title", "shortcut"] class PositionSerializer(serializers.ModelSerializer): class Meta: model = Position - fields = ['id', 'title'] + fields = ["id", "title"] class StaffSizeSerializer(serializers.ModelSerializer): class Meta: model = StaffSize - fields = ['id', 'size'] + fields = ["id", "size"] class LegalFormSerializer(serializers.ModelSerializer): class Meta: model = LegalForm - fields = ['id', 'title'] + fields = ["id", "title"] class SectorSerializer(serializers.ModelSerializer): class Meta: model = Sector - fields = ['id', 'title'] + fields = ["id", "title"] -class ContactSerializer(CremeEntitySerializer): +class PersonWithAddressesMixin(serializers.Serializer): billing_address = InnerAddressSerializer(required=False) shipping_address = InnerAddressSerializer(required=False) - class Meta(CremeEntitySerializer.Meta): - model = get_contact_model() - fields = CremeEntitySerializer.Meta.fields + [ - 'billing_address', - 'shipping_address', - 'civility', - 'last_name', - 'first_name', - 'skype', - 'phone', - 'mobile', - 'fax', - 'email', - 'url_site', - 'position', - 'full_position', - 'sector', - 'is_user', - 'birthday', - # 'image', # Need documents - ] - def to_representation(self, instance): data = super().to_representation(instance) - if 'billing_address' in data and data['billing_address'] is None: - data.pop('billing_address') - if 'shipping_address' in data and data['shipping_address'] is None: - data.pop('shipping_address') + if "billing_address" in data and data["billing_address"] is None: + data.pop("billing_address") + if "shipping_address" in data and data["shipping_address"] is None: + data.pop("shipping_address") return data @transaction.atomic def create(self, validated_data): - billing_address_data = validated_data.pop('billing_address', None) - shipping_address_data = validated_data.pop('shipping_address', None) + billing_address_data = validated_data.pop("billing_address", None) + shipping_address_data = validated_data.pop("shipping_address", None) instance = super().create(validated_data) save = False if billing_address_data is not None: save = True - instance.billing_address = self.fields['billing_address'].create({ - **billing_address_data, - 'owner': instance, - 'name': _('Billing address'), - }) + instance.billing_address = self.fields["billing_address"].create( + { + **billing_address_data, + "owner": instance, + "name": _("Billing address"), + } + ) if shipping_address_data is not None: save = True - instance.shipping_address = self.fields['shipping_address'].create({ - **shipping_address_data, - 'owner': instance, - 'name': _('Shipping address'), - }) + instance.shipping_address = self.fields["shipping_address"].create( + { + **shipping_address_data, + "owner": instance, + "name": _("Shipping address"), + } + ) if save: instance.save() return instance @transaction.atomic def update(self, instance, validated_data): - billing_address_data = validated_data.pop('billing_address', None) - shipping_address_data = validated_data.pop('shipping_address', None) - instance = super().update(instance, validated_data) - save = False + billing_address_data = validated_data.pop("billing_address", None) + shipping_address_data = validated_data.pop("shipping_address", None) if billing_address_data is not None: if instance.billing_address_id is not None: - self.fields['billing_address'].update( - instance.billing_address, { + self.fields["billing_address"].update( + instance.billing_address, + { **billing_address_data, - 'name': _('Billing address'), - }) + "name": _("Billing address"), + }, + ) else: - save = True - instance.billing_address = self.fields['billing_address'].create({ - **billing_address_data, - 'owner': instance, - 'name': _('Billing address'), - }) + instance.billing_address = self.fields["billing_address"].create( + { + **billing_address_data, + "owner": instance, + "name": _("Billing address"), + } + ) if shipping_address_data is not None: if instance.shipping_address_id is not None: - self.fields['shipping_address'].update( - instance.shipping_address, { + self.fields["shipping_address"].update( + instance.shipping_address, + { **shipping_address_data, - 'name': _('Shipping address'), - }) + "name": _("Shipping address"), + }, + ) else: - save = True - instance.shipping_address = self.fields['shipping_address'].create({ - **shipping_address_data, - 'owner': instance, - 'name': _('Shipping address'), - }) - if save: - instance.save() - return instance + instance.shipping_address = self.fields["shipping_address"].create( + { + **shipping_address_data, + "owner": instance, + "name": _("Shipping address"), + } + ) + return super().update(instance, validated_data) + + +class ContactSerializer(PersonWithAddressesMixin, CremeEntitySerializer): + class Meta(CremeEntitySerializer.Meta): + model = get_contact_model() + fields = CremeEntitySerializer.Meta.fields + [ + "billing_address", + "shipping_address", + "civility", + "last_name", + "first_name", + "skype", + "phone", + "mobile", + "fax", + "email", + "url_site", + "position", + "full_position", + "sector", + "is_user", + "birthday", + # 'image', # Need documents + ] -class OrganisationSerializer(CremeEntitySerializer): +class OrganisationSerializer(PersonWithAddressesMixin, CremeEntitySerializer): class Meta: model = get_organisation_model() fields = CremeEntitySerializer.Meta.fields + [ - 'billing_address', - 'shipping_address', - 'name', - 'is_managed', - 'phone', - 'fax', - 'email', - 'url_site', - 'sector', - 'legal_form', - 'staff_size', - 'capital', - 'annual_revenue', - 'siren', - 'naf', - 'siret', - 'rcs', - 'tvaintra', - 'subject_to_vat', - 'creation_date', - 'image', + "billing_address", + "shipping_address", + "name", + "is_managed", + "phone", + "fax", + "email", + "url_site", + "sector", + "legal_form", + "staff_size", + "capital", + "annual_revenue", + "siren", + "naf", + "siret", + "rcs", + "tvaintra", + "subject_to_vat", + "creation_date", + # 'image', ] diff --git a/creme/creme_api/api/persons/viewsets.py b/creme/creme_api/api/persons/viewsets.py index 998c4ccc2e..2a71bccf4f 100644 --- a/creme/creme_api/api/persons/viewsets.py +++ b/creme/creme_api/api/persons/viewsets.py @@ -12,8 +12,7 @@ StaffSize, ) -from .serializers import ( - AddressSerializer, +from .serializers import ( # AddressSerializer, CivilitySerializer, ContactSerializer, LegalFormSerializer, @@ -54,8 +53,10 @@ class ContactViewSet(CremeEntityViewSet): Clone a contact. """ + queryset = persons.get_contact_model().objects.select_related( - 'billing_address', 'shipping_address') + "billing_address", "shipping_address" + ) serializer_class = ContactSerializer schema = CremeSchema(tags=["Contacts"]) @@ -90,6 +91,7 @@ class OrganisationViewSet(CremeEntityViewSet): Clone an organisation. """ + queryset = persons.get_organisation_model().objects.all() serializer_class = OrganisationSerializer schema = CremeSchema(tags=["Organisations"]) @@ -116,6 +118,7 @@ class CivilityViewSet(CremeModelViewSet): Delete a civility """ + queryset = Civility.objects.all() serializer_class = CivilitySerializer schema = CremeSchema(tags=["Civilities"]) @@ -142,6 +145,7 @@ class PositionViewSet(CremeModelViewSet): Delete a position """ + queryset = Position.objects.all() serializer_class = PositionSerializer schema = CremeSchema(tags=["Positions"]) @@ -168,6 +172,7 @@ class StaffSizeViewSet(CremeModelViewSet): Delete a staff size """ + queryset = StaffSize.objects.all() serializer_class = StaffSizeSerializer schema = CremeSchema(tags=["Staff sizes"]) @@ -194,6 +199,7 @@ class LegalFormViewSet(CremeModelViewSet): Delete a legal form """ + queryset = LegalForm.objects.all() serializer_class = LegalFormSerializer schema = CremeSchema(tags=["Legal forms"]) @@ -220,6 +226,7 @@ class SectorViewSet(CremeModelViewSet): Delete a sector """ + queryset = Sector.objects.all() serializer_class = SectorSerializer schema = CremeSchema(tags=["Sectors"]) diff --git a/creme/creme_api/api/routes.py b/creme/creme_api/api/routes.py index c458da375c..1c7d508818 100644 --- a/creme/creme_api/api/routes.py +++ b/creme/creme_api/api/routes.py @@ -18,24 +18,35 @@ def register_viewset(self, resource_name, viewset): self.resources_list.add(resource_name) return self.register( - resource_name, - viewset, - basename=f'creme_api__{resource_name}' + resource_name, viewset, basename=f"creme_api__{resource_name}" ) router = CremeRouter() router.register_viewset("tokens", creme.creme_api.api.tokens.viewsets.TokenViewSet) router.register_viewset( - "contenttypes", creme.creme_api.api.contenttypes.viewsets.ContentTypeViewSet) + "contenttypes", creme.creme_api.api.contenttypes.viewsets.ContentTypeViewSet +) router.register_viewset("users", creme.creme_api.api.auth.viewsets.UserViewSet) router.register_viewset("teams", creme.creme_api.api.auth.viewsets.TeamViewSet) router.register_viewset("roles", creme.creme_api.api.auth.viewsets.UserRoleViewSet) -router.register_viewset("credentials", creme.creme_api.api.auth.viewsets.SetCredentialsViewSet) +router.register_viewset( + "credentials", creme.creme_api.api.auth.viewsets.SetCredentialsViewSet +) router.register_viewset("contacts", creme.creme_api.api.persons.viewsets.ContactViewSet) -router.register_viewset("organisations", creme.creme_api.api.persons.viewsets.OrganisationViewSet) -router.register_viewset("civilities", creme.creme_api.api.persons.viewsets.CivilityViewSet) -router.register_viewset("positions", creme.creme_api.api.persons.viewsets.PositionViewSet) -router.register_viewset("staff_sizes", creme.creme_api.api.persons.viewsets.StaffSizeViewSet) -router.register_viewset("legal_forms", creme.creme_api.api.persons.viewsets.LegalFormViewSet) +router.register_viewset( + "organisations", creme.creme_api.api.persons.viewsets.OrganisationViewSet +) +router.register_viewset( + "civilities", creme.creme_api.api.persons.viewsets.CivilityViewSet +) +router.register_viewset( + "positions", creme.creme_api.api.persons.viewsets.PositionViewSet +) +router.register_viewset( + "staff_sizes", creme.creme_api.api.persons.viewsets.StaffSizeViewSet +) +router.register_viewset( + "legal_forms", creme.creme_api.api.persons.viewsets.LegalFormViewSet +) router.register_viewset("sectors", creme.creme_api.api.persons.viewsets.SectorViewSet) diff --git a/creme/creme_api/api/schemas.py b/creme/creme_api/api/schemas.py index 81d985d725..0849233d6f 100644 --- a/creme/creme_api/api/schemas.py +++ b/creme/creme_api/api/schemas.py @@ -2,9 +2,8 @@ class CremeSchema(AutoSchema): - def get_operation_id(self, path, method): - method_name = getattr(self.view, 'action', method.lower()) + method_name = getattr(self.view, "action", method.lower()) action = self._to_camel_case(method_name) name = self.get_operation_id_base(path, method, action) return action + name diff --git a/creme/creme_api/api/tokens/serializers.py b/creme/creme_api/api/tokens/serializers.py index 3bc117cdf1..38da01c356 100644 --- a/creme/creme_api/api/tokens/serializers.py +++ b/creme/creme_api/api/tokens/serializers.py @@ -6,34 +6,27 @@ class TokenSerializer(serializers.Serializer): default_error_messages = { - 'authentication_failure': _('Unable to log in with provided credentials.') + "authentication_failure": _("Unable to log in with provided credentials.") } - client_id = serializers.UUIDField( - label=_("Client ID"), - write_only=True - ) + client_id = serializers.UUIDField(label=_("Client ID"), write_only=True) client_secret = serializers.CharField( - label=_("Client secret"), - style={'input_type': 'password'}, - write_only=True - ) - token = serializers.CharField( - label=_("Token"), - read_only=True + label=_("Client secret"), style={"input_type": "password"}, write_only=True ) + token = serializers.CharField(label=_("Token"), read_only=True) def validate(self, attrs): client_id = attrs["client_id"] client_secret = attrs["client_secret"] application = Application.authenticate( - client_id, client_secret, - request=self.context['request'], + client_id, + client_secret, + request=self.context["request"], ) if not application: - self.fail('authentication_failure') + self.fail("authentication_failure") - attrs['application'] = application + attrs["application"] = application return attrs diff --git a/creme/creme_api/api/tokens/viewsets.py b/creme/creme_api/api/tokens/viewsets.py index 2724399494..ace5b8ab6e 100644 --- a/creme/creme_api/api/tokens/viewsets.py +++ b/creme/creme_api/api/tokens/viewsets.py @@ -7,13 +7,13 @@ from .serializers import TokenSerializer -class TokenViewSet(mixins.CreateModelMixin, - viewsets.GenericViewSet): +class TokenViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): """ create: Create a token. """ + throttle_classes = [] permission_classes = [] parser_classes = [ @@ -33,6 +33,6 @@ class TokenViewSet(mixins.CreateModelMixin, def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - application = serializer.validated_data['application'] + application = serializer.validated_data["application"] token = Token.objects.create(application=application) - return Response({'token': token.code}) + return Response({"token": token.code}) diff --git a/creme/creme_api/apps.py b/creme/creme_api/apps.py index e657af884a..1a0674f04b 100644 --- a/creme/creme_api/apps.py +++ b/creme/creme_api/apps.py @@ -5,10 +5,11 @@ class CremeApiConfig(CremeAppConfig): default = True - name = 'creme.creme_api' - verbose_name = _('Creme Api') + name = "creme.creme_api" + verbose_name = _("Creme Api") dependencies = ["creme.creme_core"] def register_bricks(self, brick_registry): from .bricks import ApplicationsBrick + brick_registry.register(ApplicationsBrick) diff --git a/creme/creme_api/bricks.py b/creme/creme_api/bricks.py index 1c08eed2d3..9ac85694df 100644 --- a/creme/creme_api/bricks.py +++ b/creme/creme_api/bricks.py @@ -6,22 +6,22 @@ class ApplicationsBrick(QuerysetBrick): - id_ = QuerysetBrick.generate_id('creme_api', 'applications') - verbose_name = _('Applications') + id_ = QuerysetBrick.generate_id("creme_api", "applications") + verbose_name = _("Applications") description = _( - 'Displays the list of the applications that are allowed ' - 'to access Creme CRM web services.\n' + "Displays the list of the applications that are allowed " + "to access Creme CRM web services.\n" ) - dependencies = (Application, ) - template_name = 'creme_api/bricks/applications.html' - order_by = 'name' + dependencies = (Application,) + template_name = "creme_api/bricks/applications.html" + order_by = "name" def detailview_display(self, context): - messages = list(get_messages(context['request'])) + messages = list(get_messages(context["request"])) secret_application_message = messages[0] if messages else None btc = self.get_template_context( context, Application.objects.all(), - secret_application_message=secret_application_message + secret_application_message=secret_application_message, ) return self._render(btc) diff --git a/creme/creme_api/migrations/0001_initial.py b/creme/creme_api/migrations/0001_initial.py index afa2199657..59529158cd 100644 --- a/creme/creme_api/migrations/0001_initial.py +++ b/creme/creme_api/migrations/0001_initial.py @@ -14,37 +14,118 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Application', + name="Application", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(db_index=True, max_length=255, unique=True, verbose_name='Name')), - ('client_id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True, verbose_name='Client ID')), - ('client_secret', models.CharField(blank=True, max_length=255, verbose_name='Client secret')), - ('enabled', models.BooleanField(default=True, verbose_name='Enabled')), - ('token_duration', models.IntegerField(default=3600, help_text='Number of seconds during which tokens will be valid. It will only affect newly created tokens.', verbose_name='Tokens duration')), - ('created', creme.creme_core.models.fields.CreationDateTimeField(blank=True, default=django.utils.timezone.now, editable=False, verbose_name='Creation date')), - ('modified', creme.creme_core.models.fields.ModificationDateTimeField(blank=True, default=django.utils.timezone.now, editable=False, verbose_name='Last modification')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + db_index=True, max_length=255, unique=True, verbose_name="Name" + ), + ), + ( + "client_id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + unique=True, + verbose_name="Client ID", + ), + ), + ( + "client_secret", + models.CharField( + blank=True, max_length=255, verbose_name="Client secret" + ), + ), + ("enabled", models.BooleanField(default=True, verbose_name="Enabled")), + ( + "token_duration", + models.IntegerField( + default=3600, + help_text="Number of seconds during which tokens will be valid. It will only affect newly created tokens.", + verbose_name="Tokens duration", + ), + ), + ( + "created", + creme.creme_core.models.fields.CreationDateTimeField( + blank=True, + default=django.utils.timezone.now, + editable=False, + verbose_name="Creation date", + ), + ), + ( + "modified", + creme.creme_core.models.fields.ModificationDateTimeField( + blank=True, + default=django.utils.timezone.now, + editable=False, + verbose_name="Last modification", + ), + ), ], options={ - 'verbose_name': 'Application', - 'verbose_name_plural': 'Applications', - 'ordering': ['name'], + "verbose_name": "Application", + "verbose_name_plural": "Applications", + "ordering": ["name"], }, ), migrations.CreateModel( - name='Token', + name="Token", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(db_index=True, default=creme.creme_api.models.default_token_code, max_length=255, unique=True)), - ('expires', models.DateTimeField()), - ('created', creme.creme_core.models.fields.CreationDateTimeField(blank=True, default=django.utils.timezone.now, editable=False)), - ('modified', creme.creme_core.models.fields.ModificationDateTimeField(blank=True, default=django.utils.timezone.now, editable=False)), - ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='creme_api.application')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "code", + models.CharField( + db_index=True, + default=creme.creme_api.models.default_token_code, + max_length=255, + unique=True, + ), + ), + ("expires", models.DateTimeField()), + ( + "created", + creme.creme_core.models.fields.CreationDateTimeField( + blank=True, default=django.utils.timezone.now, editable=False + ), + ), + ( + "modified", + creme.creme_core.models.fields.ModificationDateTimeField( + blank=True, default=django.utils.timezone.now, editable=False + ), + ), + ( + "application", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="creme_api.application", + ), + ), ], ), ] diff --git a/creme/creme_api/models.py b/creme/creme_api/models.py index c65128c6a6..c7d6196bcf 100644 --- a/creme/creme_api/models.py +++ b/creme/creme_api/models.py @@ -16,7 +16,7 @@ def generate_secret(length, chars=(string.ascii_letters + string.digits)): - return ''.join(secrets.choice(chars) for i in range(length)) + return "".join(secrets.choice(chars) for i in range(length)) def default_application_client_secret(): @@ -28,7 +28,10 @@ class Application(CremeModel): # Allow to restrict this application to a subset of resources ? name = models.CharField( - verbose_name=_("Name"), max_length=255, unique=True, db_index=True, + verbose_name=_("Name"), + max_length=255, + unique=True, + db_index=True, ) client_id = models.UUIDField( @@ -40,31 +43,35 @@ class Application(CremeModel): editable=False, ) client_secret = models.CharField( - verbose_name=_("Client secret"), max_length=255, blank=True, + verbose_name=_("Client secret"), + max_length=255, + blank=True, ) _client_secret = None enabled = models.BooleanField( - verbose_name=_("Enabled"), default=True, + verbose_name=_("Enabled"), + default=True, ) token_duration = models.IntegerField( - verbose_name=_('Tokens duration'), default=3600, - help_text=_("Number of seconds during which tokens will be valid. " - "It will only affect newly created tokens."), + verbose_name=_("Tokens duration"), + default=3600, + help_text=_( + "Number of seconds during which tokens will be valid. " + "It will only affect newly created tokens." + ), ) - created = CreationDateTimeField( - verbose_name=_('Creation date'), editable=False - ) + created = CreationDateTimeField(verbose_name=_("Creation date"), editable=False) modified = ModificationDateTimeField( - verbose_name=_('Last modification'), editable=False + verbose_name=_("Last modification"), editable=False ) class Meta: verbose_name = _("Application") verbose_name_plural = _("Applications") - app_label = 'creme_api' - ordering = ['name'] + app_label = "creme_api" + ordering = ["name"] def __str__(self): return self.name @@ -77,6 +84,7 @@ def check_client_secret(self, raw_client_secret): def setter(rcs): self.set_client_secret(rcs) self.save(update_fields=["client_secret"]) + return check_password(raw_client_secret, self.client_secret, setter) def save(self, **kwargs): @@ -94,8 +102,9 @@ def authenticate(client_id, client_secret, request=None): except (Application.DoesNotExist, ValidationError): Application().set_client_secret(client_secret) else: - if application.check_client_secret(client_secret) \ - and application.can_authenticate(request=request): + if application.check_client_secret( + client_secret + ) and application.can_authenticate(request=request): return application @@ -109,7 +118,9 @@ class Token(models.Model): application = models.ForeignKey(Application, on_delete=models.CASCADE) - code = models.CharField(max_length=255, unique=True, db_index=True, default=default_token_code) + code = models.CharField( + max_length=255, unique=True, db_index=True, default=default_token_code + ) expires = models.DateTimeField() created = CreationDateTimeField(editable=False) diff --git a/creme/creme_api/tests/__init__.py b/creme/creme_api/tests/__init__.py index 12deb92798..981544b681 100644 --- a/creme/creme_api/tests/__init__.py +++ b/creme/creme_api/tests/__init__.py @@ -1,4 +1,6 @@ -from django.test import TestCase +from django.test import TestCase as DjangoTestCase + +from creme.creme_api.api.routes import router as creme_api_router from .test_addresses import * # noqa from .test_authentication import * # noqa @@ -9,6 +11,7 @@ from .test_documentation import * # noqa from .test_legal_forms import * # noqa from .test_models import * # noqa +from .test_organisations import * # noqa from .test_positions import * # noqa from .test_roles import * # noqa from .test_sectors import * # noqa @@ -16,16 +19,14 @@ from .test_teams import * # noqa from .test_tokens import * # noqa from .test_users import * # noqa -from .utils import * # noqa - -from creme.creme_api.api.routes import router +from .utils import CremeAPITestCase -class CollectionTestCase(TestCase): +class CollectionTestCase(DjangoTestCase): def test_all_routes_covered(self): expected_tests = { (pattern.name, action) - for pattern in router.get_urls() + for pattern in creme_api_router.get_urls() for action in pattern.callback.actions } found_tests = { @@ -35,7 +36,7 @@ def test_all_routes_covered(self): } missing = expected_tests - found_tests if missing: - msg = ( - f"Missing tests for routes:\n" - + "\n".join(f"{action.upper()}\t{url}" for url, action in sorted(missing))) + msg = "Missing tests for routes:\n" + "\n".join( + f"{action.upper()}\t{url}" for url, action in sorted(missing) + ) self.fail(msg) diff --git a/creme/creme_api/tests/factories/__init__.py b/creme/creme_api/tests/factories/__init__.py new file mode 100644 index 0000000000..5bee3f0ca5 --- /dev/null +++ b/creme/creme_api/tests/factories/__init__.py @@ -0,0 +1,26 @@ +from .auth import CredentialFactory, RoleFactory, TeamFactory, UserFactory +from .persons import ( + AddressFactory, + CivilityFactory, + ContactFactory, + LegalFormFactory, + OrganisationFactory, + PositionFactory, + SectorFactory, + StaffSizeFactory, +) + +__all__ = [ + "CredentialFactory", + "RoleFactory", + "TeamFactory", + "UserFactory", + "AddressFactory", + "CivilityFactory", + "ContactFactory", + "LegalFormFactory", + "OrganisationFactory", + "PositionFactory", + "SectorFactory", + "StaffSizeFactory", +] diff --git a/creme/creme_api/tests/factories/auth.py b/creme/creme_api/tests/factories/auth.py new file mode 100644 index 0000000000..08f25f2cd6 --- /dev/null +++ b/creme/creme_api/tests/factories/auth.py @@ -0,0 +1,109 @@ +import factory +from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType +from factory.django import DjangoModelFactory + +from creme import persons +from creme.creme_core.models import SetCredentials, UserRole + + +class RoleFactory(DjangoModelFactory): + class Meta: + model = UserRole + + name = "Basic" + allowed_apps = ["creme_core", "creme_api", "persons"] + admin_4_apps = ["creme_core", "creme_api"] + + @factory.post_generation + def creatable_ctypes(self, create, extracted, **kwargs): + if not create: + return + if extracted is None: + contact_ct = ContentType.objects.get_for_model(persons.get_contact_model()) + orga_ct = ContentType.objects.get_for_model( + persons.get_organisation_model() + ) + extracted = [contact_ct.id, orga_ct.id] + self.creatable_ctypes.set(extracted) + + @factory.post_generation + def exportable_ctypes(self, create, extracted, **kwargs): + if not create: + return + if extracted is None: + contact_ct = ContentType.objects.get_for_model(persons.get_contact_model()) + extracted = [contact_ct.id] + self.exportable_ctypes.set(extracted) + + +def build_username(user): + return "%s.%s" % (user.first_name.lower(), user.last_name.lower()) + + +def build_email(user): + return "%s@provider.com" % build_username(user) + + +class UserFactory(DjangoModelFactory): + class Meta: + model = get_user_model() + django_get_or_create = ["username"] + + first_name = "John" + last_name = "Doe" + username = factory.lazy_attribute(build_username) + email = factory.lazy_attribute(build_email) + is_active = True + is_superuser = True + role = factory.Maybe( + "is_superuser", + yes_declaration=None, + no_declaration=factory.SubFactory(RoleFactory), + ) + + +class TeamFactory(DjangoModelFactory): + is_team = True + username = factory.lazy_attribute(lambda t: t.name) + + class Meta: + model = get_user_model() + + class Params: + name = "Team #1" + + @factory.post_generation + def teammates(self, create, extracted, **kwargs): + if not create: + return + if extracted: + self.teammates = extracted + + +class CredentialFactory(DjangoModelFactory): + role = factory.SubFactory(RoleFactory) + set_type = SetCredentials.ESET_OWN + forbidden = False + efilter = None + ctype = factory.Iterator(ContentType.objects.all()) + + can_view = True + can_change = True + can_delete = True + can_link = True + can_unlink = True + + _permissions = {"can_view", "can_change", "can_delete", "can_link", "can_unlink"} + + class Meta: + model = SetCredentials + + @classmethod + def _create(cls, model_class, *args, **kwargs): + """Create an instance of the model, and save it to the database.""" + permission = {p: kwargs.pop(p) for p in cls._permissions} + instance = model_class(**kwargs) + instance.set_value(**permission) + instance.save() + return instance diff --git a/creme/creme_api/tests/factories/persons.py b/creme/creme_api/tests/factories/persons.py new file mode 100644 index 0000000000..9a95d1fab0 --- /dev/null +++ b/creme/creme_api/tests/factories/persons.py @@ -0,0 +1,147 @@ +import factory +from django.utils.translation import gettext as _ +from factory.django import DjangoModelFactory + +from creme import persons +from creme.persons.models import ( + Address, + Civility, + LegalForm, + Position, + Sector, + StaffSize, +) + +from .auth import UserFactory, build_email, build_username + + +class CivilityFactory(DjangoModelFactory): + title = "Captain" + shortcut = "Cpt" + + class Meta: + model = Civility + + +class PositionFactory(DjangoModelFactory): + title = "Captain" + + class Meta: + model = Position + + +class SectorFactory(DjangoModelFactory): + title = "Industry" + + class Meta: + model = Sector + + +class AddressFactory(DjangoModelFactory): + name = "Address Name" + address = "1 Main Street" + po_box = "PO123" + zipcode = "ZIP123" + city = "City" + department = "Dept" + state = "State" + country = "Country" + + owner = None # Generic Relation; must be provided + # content_type = factory.LazyAttribute( + # lambda o: get_content_type(o.owner) + # ) + # object_id = factory.SelfAttribute('owner.id') + + class Meta: + model = Address + + +class PersonWithAddressesMixin(DjangoModelFactory): + @factory.post_generation + def billing_address(self, create, extracted, **kwargs): + if not create: + return + if extracted is True or kwargs: + kwargs["owner"] = self + self.billing_address = AddressFactory( + **{"name": _("Billing address"), **kwargs} + ) + + @factory.post_generation + def shipping_address(self, create, extracted, **kwargs): + if not create: + return + if extracted is True or kwargs: + kwargs["owner"] = self + self.shipping_address = AddressFactory( + **{"name": _("Shipping address"), **kwargs} + ) + + +def build_url_site(user): + return "https://www.%s.provider.com" % build_username(user) + + +class ContactFactory(PersonWithAddressesMixin, DjangoModelFactory): + user = factory.SubFactory(UserFactory) + description = "Description" + + first_name = "Jean" + last_name = "Dupont" + skype = "jean.dupont" + phone = "+330100000000" + mobile = "+330600000000" + fax = "+330100000001" + email = factory.lazy_attribute(build_email) + url_site = factory.lazy_attribute(build_url_site) + full_position = "Full position" + birthday = "2000-01-01" + + civility = factory.SubFactory(CivilityFactory) + position = factory.SubFactory(PositionFactory) + sector = factory.SubFactory(SectorFactory) + + class Meta: + model = persons.get_contact_model() + + +class LegalFormFactory(DjangoModelFactory): + title = "Trust" + + class Meta: + model = LegalForm + + +class StaffSizeFactory(DjangoModelFactory): + size = "1 - 10" + + class Meta: + model = StaffSize + + +class OrganisationFactory(PersonWithAddressesMixin, DjangoModelFactory): + user = factory.SubFactory(UserFactory) + description = "Description" + + name = "Creme Organisation" + phone = "+330100000000" + fax = "+330100000001" + email = "creme.organisation@provider.com" + url_site = "https://www.creme.organisation.provider.com" + capital = 50000 + annual_revenue = "1500000" + siren = "001001001001" + naf = "002002002002" + siret = "003003003003" + rcs = "004004004004" + tvaintra = "005005005005" + subject_to_vat = True + creation_date = "2005-05-24" + + sector = factory.SubFactory(SectorFactory) + legal_form = factory.SubFactory(LegalFormFactory) + staff_size = factory.SubFactory(StaffSizeFactory) + + class Meta: + model = persons.get_organisation_model() diff --git a/creme/creme_api/tests/factories/test_factories.py b/creme/creme_api/tests/factories/test_factories.py new file mode 100644 index 0000000000..84f23d9c67 --- /dev/null +++ b/creme/creme_api/tests/factories/test_factories.py @@ -0,0 +1,37 @@ +from django.test import TestCase + +from .persons import ContactFactory + + +class ContactFactoryTestCase(TestCase): + def test_contact_factory(self): + contact = ContactFactory() + + self.assertIsNotNone(contact.user) + self.assertIsNotNone(contact.description) + self.assertIsNotNone(contact.first_name) + self.assertIsNotNone(contact.last_name) + self.assertIsNotNone(contact.skype) + self.assertIsNotNone(contact.phone) + self.assertIsNotNone(contact.mobile) + self.assertIsNotNone(contact.fax) + self.assertIsNotNone(contact.email) + self.assertIsNotNone(contact.url_site) + self.assertIsNotNone(contact.full_position) + self.assertIsNotNone(contact.birthday) + self.assertIsNotNone(contact.civility) + self.assertIsNotNone(contact.position) + self.assertIsNotNone(contact.sector) + + self.assertIsNone(contact.billing_address) + self.assertIsNone(contact.shipping_address) + + def test_contact_factory__with_addresses(self): + contact = ContactFactory(billing_address=True, shipping_address=True) + contact.refresh_from_db() + + self.assertIsNotNone(contact.billing_address) + self.assertEqual(contact.billing_address.owner, contact) + + self.assertIsNotNone(contact.shipping_address) + self.assertEqual(contact.shipping_address.owner, contact) diff --git a/creme/creme_api/tests/test_addresses.py b/creme/creme_api/tests/test_addresses.py index 7d42f2e5c3..e920e26843 100644 --- a/creme/creme_api/tests/test_addresses.py +++ b/creme/creme_api/tests/test_addresses.py @@ -1,27 +1,17 @@ -from creme.creme_api.tests.utils import Factory -from creme.persons import get_address_model - -Address = get_address_model() - - -@Factory.register -def address(factory, **kwargs): - data = factory.address_data(**kwargs) - return Address.objects.create(**data) - - -@Factory.register -def address_data(factory, **kwargs): - data = { - # 'name': "Address name", - 'address': "1 Main Street", - 'po_box': "PO123", - 'zipcode': "ZIP123", - 'city': "City", - 'department': "Dept", - 'state': "State", - 'country': "Country", - # 'owner': "", - } - data.update(**kwargs) - return data +# from creme.persons import get_address_model +# from .factories import AddressFactory +# Address = get_address_model() +# +# # +# # @Tests.register +# # def factory_address_equal(tests, instance, *, owner, name, **kwargs): +# +# self.assertEqual(address.name, name) +# self.assertEqual(address.address, "1 Main Street") +# self.assertEqual(address.po_box, "PO123") +# self.assertEqual(address.zipcode, "ZIP123") +# self.assertEqual(address.city, "City") +# self.assertEqual(address.department, "Dept") +# self.assertEqual(address.state, "State") +# self.assertEqual(address.country, "Country") +# self.assertEqual(address.owner, owner) diff --git a/creme/creme_api/tests/test_authentication.py b/creme/creme_api/tests/test_authentication.py index 94a511bdcc..4d3c10f876 100644 --- a/creme/creme_api/tests/test_authentication.py +++ b/creme/creme_api/tests/test_authentication.py @@ -14,14 +14,16 @@ class TestAuthenticationView(APIView): authentication_classes = [TokenAuthentication] def get(self, request): - return Response(data={'ok': True}) + return Response(data={"ok": True}) class TokenAuthenticationAPITestCase(APITestCase, URLPatternsTestCase): urlpatterns = [ - path("test-authentication/", - TestAuthenticationView.as_view(), - name="api_tests__test-authentication") + path( + "test-authentication/", + TestAuthenticationView.as_view(), + name="api_tests__test-authentication", + ) ] url = reverse_lazy("api_tests__test-authentication") @@ -31,7 +33,7 @@ def request(self): def assert401(self, error_code=None): response = self.request() self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - error_detail = response.data['detail'] + error_detail = response.data["detail"] if error_code: code = error_code message = TokenAuthentication.errors[error_code] @@ -46,47 +48,47 @@ def test_authenticate01(self): self.assert401() def test_authenticate02(self): - self.client.credentials(HTTP_AUTHORIZATION='') + self.client.credentials(HTTP_AUTHORIZATION="") self.assert401() def test_authenticate03(self): - self.client.credentials(HTTP_AUTHORIZATION='Bearer') + self.client.credentials(HTTP_AUTHORIZATION="Bearer") self.assert401() def test_authenticate04(self): - self.client.credentials(HTTP_AUTHORIZATION='Token') - self.assert401(error_code='empty') + self.client.credentials(HTTP_AUTHORIZATION="Token") + self.assert401(error_code="empty") def test_authenticate05(self): - self.client.credentials(HTTP_AUTHORIZATION='Token T1 T2') - self.assert401(error_code='too_long') + self.client.credentials(HTTP_AUTHORIZATION="Token T1 T2") + self.assert401(error_code="too_long") def test_authenticate06(self): - self.client.credentials(HTTP_AUTHORIZATION=b'Token \xa1') - self.assert401(error_code='encoding') + self.client.credentials(HTTP_AUTHORIZATION=b"Token \xa1") + self.assert401(error_code="encoding") def test_authenticate07(self): application = Application.objects.create(name="APITestCase") Token.objects.create(application=application) - self.client.credentials(HTTP_AUTHORIZATION=b'Token TEST') - self.assert401(error_code='invalid') + self.client.credentials(HTTP_AUTHORIZATION=b"Token TEST") + self.assert401(error_code="invalid") def test_authenticate08(self): application = Application.objects.create(name="APITestCase", enabled=False) token = Token.objects.create(application=application) - self.client.credentials(HTTP_AUTHORIZATION=f'Token {token.code}') - self.assert401(error_code='invalid') + self.client.credentials(HTTP_AUTHORIZATION=f"Token {token.code}") + self.assert401(error_code="invalid") def test_authenticate09(self): application = Application.objects.create(name="APITestCase") token = Token.objects.create(application=application, expires=timezone.now()) - self.client.credentials(HTTP_AUTHORIZATION=f'Token {token.code}') - self.assert401(error_code='expired') + self.client.credentials(HTTP_AUTHORIZATION=f"Token {token.code}") + self.assert401(error_code="expired") def test_authenticate10(self): application = Application.objects.create(name="APITestCase") token = Token.objects.create(application=application) - self.client.credentials(HTTP_AUTHORIZATION=f'Token {token.code}') + self.client.credentials(HTTP_AUTHORIZATION=f"Token {token.code}") response = self.request() self.assertEqual(status.HTTP_200_OK, response.status_code) - self.assertEqual({'ok': True}, response.data) + self.assertEqual({"ok": True}, response.data) diff --git a/creme/creme_api/tests/test_civilities.py b/creme/creme_api/tests/test_civilities.py index 781ec68e76..a20f3161a6 100644 --- a/creme/creme_api/tests/test_civilities.py +++ b/creme/creme_api/tests/test_civilities.py @@ -1,117 +1,132 @@ -from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.creme_api.tests.utils import CremeAPITestCase from creme.persons.models import Civility - -@Factory.register -def civility(factory, **kwargs): - data = factory.civility_data(**kwargs) - return Civility.objects.create(**data) - - -@Factory.register -def civility_data(factory, **kwargs): - kwargs.setdefault('title', 'Captain') - kwargs.setdefault('shortcut', 'Cpt') - return kwargs +from .factories import CivilityFactory class CreateCivilityTestCase(CremeAPITestCase): - url_name = 'creme_api__civilities-list' - method = 'post' + url_name = "creme_api__civilities-list" + method = "post" def test_validation__required(self): response = self.make_request(data={}, status_code=400) - self.assertValidationErrors(response, { - 'title': ['required'], - 'shortcut': ['required'], - }) + self.assertValidationErrors( + response, + { + "title": ["required"], + "shortcut": ["required"], + }, + ) def test_create_civility(self): - data = self.factory.civility_data() + data = {"title": "Captain", "shortcut": "Cpt"} response = self.make_request(data=data, status_code=201) - civility = Civility.objects.get(id=response.data['id']) - self.assertPayloadEqual(response, { - 'id': civility.id, - 'title': 'Captain', - 'shortcut': 'Cpt', - }) + civility = Civility.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + "id": civility.id, + "title": "Captain", + "shortcut": "Cpt", + }, + ) self.assertEqual(civility.title, "Captain") self.assertEqual(civility.shortcut, "Cpt") class RetrieveCivilityTestCase(CremeAPITestCase): - url_name = 'creme_api__civilities-detail' - method = 'get' + url_name = "creme_api__civilities-detail" + method = "get" def test_retrieve_civility(self): - civility = self.factory.civility() + civility = CivilityFactory() response = self.make_request(to=civility.id, status_code=200) - self.assertPayloadEqual(response, { - 'id': civility.id, - 'title': 'Captain', - 'shortcut': 'Cpt', - }) + self.assertPayloadEqual( + response, + { + "id": civility.id, + "title": "Captain", + "shortcut": "Cpt", + }, + ) class UpdateCivilityTestCase(CremeAPITestCase): - url_name = 'creme_api__civilities-detail' - method = 'put' + url_name = "creme_api__civilities-detail" + method = "put" def test_update_civility(self): - civility = self.factory.civility() - response = self.make_request(to=civility.id, data={ - 'title': "CAPTAIN", - 'shortcut': "CAP", - }, status_code=200) - self.assertPayloadEqual(response, { - 'id': civility.id, - 'title': 'CAPTAIN', - 'shortcut': 'CAP', - }) + civility = CivilityFactory() + response = self.make_request( + to=civility.id, + data={ + "title": "CAPTAIN", + "shortcut": "CAP", + }, + status_code=200, + ) + self.assertPayloadEqual( + response, + { + "id": civility.id, + "title": "CAPTAIN", + "shortcut": "CAP", + }, + ) civility.refresh_from_db() self.assertEqual(civility.title, "CAPTAIN") self.assertEqual(civility.shortcut, "CAP") class PartialUpdateCivilityTestCase(CremeAPITestCase): - url_name = 'creme_api__civilities-detail' - method = 'patch' + url_name = "creme_api__civilities-detail" + method = "patch" def test_partial_update_civility(self): - civility = self.factory.civility() - response = self.make_request(to=civility.id, data={ - 'shortcut': "CAP", - }, status_code=200) - self.assertPayloadEqual(response, { - 'id': civility.id, - 'title': 'Captain', - 'shortcut': 'CAP', - }) + civility = CivilityFactory() + response = self.make_request( + to=civility.id, + data={ + "shortcut": "CAP", + }, + status_code=200, + ) + self.assertPayloadEqual( + response, + { + "id": civility.id, + "title": "Captain", + "shortcut": "CAP", + }, + ) civility.refresh_from_db() self.assertEqual(civility.title, "Captain") self.assertEqual(civility.shortcut, "CAP") class ListCivilityTestCase(CremeAPITestCase): - url_name = 'creme_api__civilities-list' - method = 'get' + url_name = "creme_api__civilities-list" + method = "get" def test_list_civilities(self): Civility.objects.all().delete() - civility1 = self.factory.civility(title="1", shortcut="1") - civility2 = self.factory.civility(title="2", shortcut="2") + civility1 = CivilityFactory(title="1", shortcut="1") + civility2 = CivilityFactory(title="2", shortcut="2") response = self.make_request(status_code=200) - self.assertPayloadEqual(response, [ - {'id': civility1.id, 'title': '1', 'shortcut': '1'}, - {'id': civility2.id, 'title': '2', 'shortcut': '2'}, - ]) + self.assertPayloadEqual( + response, + [ + {"id": civility1.id, "title": "1", "shortcut": "1"}, + {"id": civility2.id, "title": "2", "shortcut": "2"}, + ], + ) class DeleteCivilityTestCase(CremeAPITestCase): - url_name = 'creme_api__civilities-detail' - method = 'delete' + url_name = "creme_api__civilities-detail" + method = "delete" def test_delete_civility(self): - civility = self.factory.civility() + civility = CivilityFactory() self.make_request(to=civility.id, status_code=204) self.assertFalse(Civility.objects.filter(id=civility.id).exists()) diff --git a/creme/creme_api/tests/test_contacts.py b/creme/creme_api/tests/test_contacts.py index e5bf7b22f8..fa6cb0bac5 100644 --- a/creme/creme_api/tests/test_contacts.py +++ b/creme/creme_api/tests/test_contacts.py @@ -2,897 +2,638 @@ from django.utils.translation import gettext as _ -from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.creme_api.tests.utils import CremeAPITestCase from creme.persons import get_contact_model -Contact = get_contact_model() - - -@Factory.register -def contact(factory, **kwargs): - if 'user' not in kwargs: - kwargs['user'] = factory.user() - - data = factory.contact_data(**kwargs) - - if 'civility' not in data: - data['civility'] = factory.civility() - - if 'position' not in data: - data['position'] = factory.position() - - if 'sector' not in data: - data['sector'] = factory.sector() - - if 'billing_address' not in data: - data['billing_address'] = factory.address_data() +from .factories import ( + CivilityFactory, + ContactFactory, + PositionFactory, + SectorFactory, + UserFactory, +) - if 'shipping_address' not in data: - data['shipping_address'] = factory.address_data() - - billing_address_data = data.pop('billing_address') - shipping_address_data = data.pop('shipping_address') - contact = Contact.objects.create(**data) - if billing_address_data: - contact.billing_address = factory.address(owner=contact, **billing_address_data) - if shipping_address_data: - contact.shipping_address = factory.address(owner=contact, **shipping_address_data) - contact.save() - return contact - - -@Factory.register -def contact_data(factory, **kwargs): - if 'user' not in kwargs: - kwargs['user'] = factory.user().id +Contact = get_contact_model() - data = { - 'description': "Description", - 'last_name': "Dupont", - 'first_name': "Jean", - 'skype': "jean.dupont", - 'phone': "+330100000000", - 'mobile': "+330600000000", - 'fax': "+330100000001", - 'email': "jean.dupont@provider.com", - 'url_site': "https://www.jean-dupont.provider.com", - 'full_position': "Full position", - 'birthday': "2000-01-01", - } - data.update(**kwargs) - return data +default_contact_data = { + "description": "Description", + "first_name": "Jean", + "last_name": "Dupont", + "skype": "jean.dupont", + "phone": "+330100000000", + "mobile": "+330600000000", + "fax": "+330100000001", + "email": "jean.dupont@provider.com", + "url_site": "https://www.jean.dupont.provider.com", + "full_position": "Full position", + "birthday": "2000-01-01", + "civility": None, + "position": None, + "sector": None, +} + +default_address_data = { + "address": "1 Main Street", + "po_box": "PO123", + "zipcode": "ZIP123", + "city": "City", + "department": "Dept", + "state": "State", + "country": "Country", +} class CreateContactTestCase(CremeAPITestCase): - url_name = 'creme_api__contacts-list' - method = 'post' + url_name = "creme_api__contacts-list" + method = "post" def test_validation__required(self): response = self.make_request(data={}, status_code=400) - self.assertValidationErrors(response, { - 'last_name': ['required'], - 'user': ['required'], - }) + self.assertValidationErrors( + response, + { + "last_name": ["required"], + "user": ["required"], + }, + ) def test_create_contact(self): - user = self.factory.user() - civility = self.factory.civility() - position = self.factory.position() - sector = self.factory.sector() - data = self.factory.contact_data( - user=user.id, - civility=civility.id, - position=position.id, - sector=sector.id, - ) + user = UserFactory() + civility = CivilityFactory() + position = PositionFactory() + sector = SectorFactory() + data = { + **default_contact_data, + "user": user.id, + "civility": civility.id, + "position": position.id, + "sector": sector.id, + } response = self.make_request(data=data, status_code=201) - contact = Contact.objects.get(id=response.data['id']) - self.assertPayloadEqual(response, { - 'id': contact.id, - 'birthday': '2000-01-01', - 'civility': civility.id, - 'uuid': str(contact.uuid), - 'created': self.to_iso8601(contact.created), - 'modified': self.to_iso8601(contact.modified), - 'description': 'Description', - 'email': 'jean.dupont@provider.com', - 'fax': '+330100000001', - 'first_name': 'Jean', - 'full_position': 'Full position', - 'is_deleted': False, - 'is_user': None, - 'last_name': 'Dupont', - 'mobile': '+330600000000', - 'phone': '+330100000000', - 'position': position.id, - 'sector': sector.id, - 'skype': 'jean.dupont', - 'url_site': 'https://www.jean-dupont.provider.com', - 'user': user.id, - }) + contact = Contact.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + **data, + "id": contact.id, + "uuid": str(contact.uuid), + "created": self.to_iso8601(contact.created), + "modified": self.to_iso8601(contact.modified), + "is_deleted": False, + "is_user": None, + }, + ) self.assertEqual(contact.birthday, date(2000, 1, 1)) + self.assertEqual(contact.description, "Description") + self.assertEqual(contact.last_name, "Dupont") + self.assertEqual(contact.first_name, "Jean") + self.assertEqual(contact.skype, "jean.dupont") + self.assertEqual(contact.phone, "+330100000000") + self.assertEqual(contact.mobile, "+330600000000") + self.assertEqual(contact.fax, "+330100000001") + self.assertEqual(contact.email, "jean.dupont@provider.com") + self.assertEqual(contact.url_site, "https://www.jean.dupont.provider.com") + self.assertEqual(contact.full_position, "Full position") + self.assertFalse(contact.is_deleted) + self.assertIsNone(contact.is_user) self.assertEqual(contact.user, user) - self.assertEqual(contact.description, 'Description') self.assertEqual(contact.civility, civility) self.assertEqual(contact.position, position) self.assertEqual(contact.sector, sector) - self.assertEqual(contact.last_name, 'Dupont') - self.assertEqual(contact.first_name, 'Jean') - self.assertEqual(contact.skype, 'jean.dupont') - self.assertEqual(contact.phone, '+330100000000') - self.assertEqual(contact.mobile, '+330600000000') - self.assertEqual(contact.fax, '+330100000001') - self.assertEqual(contact.email, 'jean.dupont@provider.com') - self.assertEqual(contact.url_site, 'https://www.jean-dupont.provider.com') - self.assertEqual(contact.full_position, 'Full position') - self.assertFalse(contact.is_deleted) - self.assertIsNone(contact.is_user) self.assertIsNone(contact.billing_address_id) self.assertIsNone(contact.shipping_address_id) def test_create_contact__with_addresses(self): - billing_address_data = self.factory.address_data() - shipping_address_data = self.factory.address_data() - data = self.factory.contact_data( - billing_address={**billing_address_data, 'name': "NOT USED"}, - shipping_address={**shipping_address_data, 'name': "NOT USED"}, - ) + user = UserFactory() + data = { + **default_contact_data, + "user": user.id, + "billing_address": {**default_address_data, "name": "NOT DISPLAYED"}, + "shipping_address": {**default_address_data, "name": "NOT DISPLAYED"}, + } response = self.make_request(data=data, status_code=201) - contact = Contact.objects.get(id=response.data['id']) - self.assertPayloadEqual(response, { - 'id': contact.id, - 'birthday': '2000-01-01', - 'civility': None, - 'uuid': str(contact.uuid), - 'created': self.to_iso8601(contact.created), - 'modified': self.to_iso8601(contact.modified), - 'description': 'Description', - 'email': 'jean.dupont@provider.com', - 'fax': '+330100000001', - 'first_name': 'Jean', - 'full_position': 'Full position', - 'is_deleted': False, - 'is_user': None, - 'last_name': 'Dupont', - 'mobile': '+330600000000', - 'phone': '+330100000000', - 'position': None, - 'sector': None, - 'skype': 'jean.dupont', - 'url_site': 'https://www.jean-dupont.provider.com', - 'user': data['user'], - 'billing_address': { - 'address': '1 Main Street', - 'city': 'City', - 'country': 'Country', - 'department': 'Dept', - 'po_box': 'PO123', - 'state': 'State', - 'zipcode': 'ZIP123', - }, - 'shipping_address': { - 'address': '1 Main Street', - 'city': 'City', - 'country': 'Country', - 'department': 'Dept', - 'po_box': 'PO123', - 'state': 'State', - 'zipcode': 'ZIP123', + contact = Contact.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + **data, + "id": contact.id, + "uuid": str(contact.uuid), + "created": self.to_iso8601(contact.created), + "modified": self.to_iso8601(contact.modified), + "billing_address": default_address_data, + "shipping_address": default_address_data, + "is_deleted": False, + "is_user": None, }, - }) + ) self.assertEqual(contact.birthday, date(2000, 1, 1)) - self.assertEqual(contact.user_id, data['user']) - self.assertEqual(contact.description, 'Description') - self.assertEqual(contact.last_name, 'Dupont') - self.assertEqual(contact.first_name, 'Jean') - self.assertEqual(contact.skype, 'jean.dupont') - self.assertEqual(contact.phone, '+330100000000') - self.assertEqual(contact.mobile, '+330600000000') - self.assertEqual(contact.fax, '+330100000001') - self.assertEqual(contact.email, 'jean.dupont@provider.com') - self.assertEqual(contact.url_site, 'https://www.jean-dupont.provider.com') - self.assertEqual(contact.full_position, 'Full position') + self.assertEqual(contact.description, "Description") + self.assertEqual(contact.last_name, "Dupont") + self.assertEqual(contact.first_name, "Jean") + self.assertEqual(contact.skype, "jean.dupont") + self.assertEqual(contact.phone, "+330100000000") + self.assertEqual(contact.mobile, "+330600000000") + self.assertEqual(contact.fax, "+330100000001") + self.assertEqual(contact.email, "jean.dupont@provider.com") + self.assertEqual(contact.url_site, "https://www.jean.dupont.provider.com") + self.assertEqual(contact.full_position, "Full position") self.assertFalse(contact.is_deleted) self.assertIsNone(contact.is_user) - - billing_address = contact.billing_address - self.assertEqual(billing_address.name, _('Billing address')) - self.assertEqual(billing_address.address, billing_address_data['address']) - self.assertEqual(billing_address.po_box, billing_address_data['po_box']) - self.assertEqual(billing_address.zipcode, billing_address_data['zipcode']) - self.assertEqual(billing_address.city, billing_address_data['city']) - self.assertEqual(billing_address.department, billing_address_data['department']) - self.assertEqual(billing_address.state, billing_address_data['state']) - self.assertEqual(billing_address.country, billing_address_data['country']) - self.assertEqual(billing_address.owner, contact) - - shipping_address = contact.shipping_address - self.assertEqual(shipping_address.name, _('Shipping address')) - self.assertEqual(shipping_address.address, shipping_address_data['address']) - self.assertEqual(shipping_address.po_box, shipping_address_data['po_box']) - self.assertEqual(shipping_address.zipcode, shipping_address_data['zipcode']) - self.assertEqual(shipping_address.city, shipping_address_data['city']) - self.assertEqual(shipping_address.department, shipping_address_data['department']) - self.assertEqual(shipping_address.state, shipping_address_data['state']) - self.assertEqual(shipping_address.country, shipping_address_data['country']) - self.assertEqual(shipping_address.owner, contact) + self.assertEqual(contact.user, user) + self.assertIsNone(contact.civility) + self.assertIsNone(contact.position) + self.assertIsNone(contact.sector) + + self.assertEqual(contact.billing_address.name, _("Billing address")) + self.assertEqual(contact.billing_address.address, "1 Main Street") + self.assertEqual(contact.billing_address.po_box, "PO123") + self.assertEqual(contact.billing_address.zipcode, "ZIP123") + self.assertEqual(contact.billing_address.city, "City") + self.assertEqual(contact.billing_address.department, "Dept") + self.assertEqual(contact.billing_address.state, "State") + self.assertEqual(contact.billing_address.country, "Country") + self.assertEqual(contact.billing_address.owner, contact) + + self.assertEqual(contact.shipping_address.name, _("Shipping address")) + self.assertEqual(contact.shipping_address.address, "1 Main Street") + self.assertEqual(contact.shipping_address.po_box, "PO123") + self.assertEqual(contact.shipping_address.zipcode, "ZIP123") + self.assertEqual(contact.shipping_address.city, "City") + self.assertEqual(contact.shipping_address.department, "Dept") + self.assertEqual(contact.shipping_address.state, "State") + self.assertEqual(contact.shipping_address.country, "Country") + self.assertEqual(contact.shipping_address.owner, contact) class RetrieveContactTestCase(CremeAPITestCase): - url_name = 'creme_api__contacts-detail' - method = 'get' + url_name = "creme_api__contacts-detail" + method = "get" def test_get_contact(self): - contact = self.factory.contact(billing_address=None) + contact = ContactFactory(shipping_address=True) response = self.make_request(to=contact.id, status_code=200) - self.assertPayloadEqual(response, { - 'id': contact.id, - 'birthday': '2000-01-01', - 'civility': contact.civility_id, - 'uuid': str(contact.uuid), - 'created': self.to_iso8601(contact.created), - 'modified': self.to_iso8601(contact.modified), - 'description': 'Description', - 'email': 'jean.dupont@provider.com', - 'fax': '+330100000001', - 'first_name': 'Jean', - 'full_position': 'Full position', - 'is_deleted': False, - 'is_user': None, - 'last_name': 'Dupont', - 'mobile': '+330600000000', - 'phone': '+330100000000', - 'position': contact.position_id, - 'sector': contact.sector_id, - 'skype': 'jean.dupont', - 'url_site': 'https://www.jean-dupont.provider.com', - 'user': contact.user_id, - 'shipping_address': { - 'address': '1 Main Street', - 'city': 'City', - 'country': 'Country', - 'department': 'Dept', - 'po_box': 'PO123', - 'state': 'State', - 'zipcode': 'ZIP123', + self.assertPayloadEqual( + response, + { + **default_contact_data, + "id": contact.id, + "civility": contact.civility_id, + "uuid": str(contact.uuid), + "created": self.to_iso8601(contact.created), + "modified": self.to_iso8601(contact.modified), + "is_deleted": False, + "is_user": None, + "position": contact.position_id, + "sector": contact.sector_id, + "user": contact.user_id, + "shipping_address": default_address_data, }, - }) + ) class UpdateContactTestCase(CremeAPITestCase): - url_name = 'creme_api__contacts-detail' - method = 'put' + url_name = "creme_api__contacts-detail" + method = "put" def test_validation__required(self): - contact = self.factory.contact() + contact = ContactFactory() response = self.make_request(to=contact.id, data={}, status_code=400) - self.assertValidationErrors(response, { - 'last_name': ['required'], - 'user': ['required'], - }) + self.assertValidationErrors( + response, + { + "last_name": ["required"], + "user": ["required"], + }, + ) def test_update_contact(self): - contact = self.factory.contact(billing_address=None, civility=None) - civility = self.factory.civility() - sector = self.factory.sector() - - data = self.factory.contact_data( - user=contact.user_id, - civility=civility.id, - position=None, - sector=sector.id, - last_name="Smith", - first_name="Nick", - ) + contact = ContactFactory(shipping_address=True, civility=None) + civility = CivilityFactory() + sector = SectorFactory() + + data = { + **default_contact_data, + "user": contact.user_id, + "civility": civility.id, + "position": None, + "sector": sector.id, + "last_name": "Smith", + "first_name": "Nick", + } response = self.make_request(to=contact.id, data=data, status_code=200) contact.refresh_from_db() - self.assertPayloadEqual(response, { - 'id': contact.id, - 'birthday': '2000-01-01', - 'civility': civility.id, - 'uuid': str(contact.uuid), - 'created': self.to_iso8601(contact.created), - 'modified': self.to_iso8601(contact.modified), - 'description': 'Description', - 'email': 'jean.dupont@provider.com', - 'fax': '+330100000001', - 'first_name': 'Nick', - 'full_position': 'Full position', - 'is_deleted': False, - 'is_user': None, - 'last_name': 'Smith', - 'mobile': '+330600000000', - 'phone': '+330100000000', - 'position': None, - 'sector': sector.id, - 'skype': 'jean.dupont', - 'url_site': 'https://www.jean-dupont.provider.com', - 'user': contact.user_id, - 'shipping_address': { - 'address': '1 Main Street', - 'city': 'City', - 'country': 'Country', - 'department': 'Dept', - 'po_box': 'PO123', - 'state': 'State', - 'zipcode': 'ZIP123', + self.assertPayloadEqual( + response, + { + **data, + "id": contact.id, + "uuid": str(contact.uuid), + "created": self.to_iso8601(contact.created), + "modified": self.to_iso8601(contact.modified), + "is_deleted": False, + "is_user": None, + "shipping_address": default_address_data, }, - }) - self.assertEqual(contact.birthday, date(2000, 1, 1)) - self.assertEqual(contact.user_id, data['user']) - self.assertEqual(contact.description, 'Description') - self.assertEqual(contact.civility, civility) - self.assertIsNone(contact.position) - self.assertEqual(contact.sector, sector) - self.assertEqual(contact.first_name, "Nick") - self.assertEqual(contact.last_name, "Smith") - self.assertEqual(contact.skype, 'jean.dupont') - self.assertEqual(contact.phone, '+330100000000') - self.assertEqual(contact.mobile, '+330600000000') - self.assertEqual(contact.fax, '+330100000001') - self.assertEqual(contact.email, 'jean.dupont@provider.com') - self.assertEqual(contact.url_site, 'https://www.jean-dupont.provider.com') - self.assertEqual(contact.full_position, 'Full position') - self.assertFalse(contact.is_deleted) - self.assertIsNone(contact.is_user) + ) def test_update_contact__create_addresses(self): - contact = self.factory.contact(billing_address=None, shipping_address=None) - - billing_address_data = self.factory.address_data(address='billing street') - shipping_address_data = self.factory.address_data(address='shipping street') - data = self.factory.contact_data( - user=contact.user_id, - billing_address=billing_address_data, - shipping_address=shipping_address_data, - ) + contact = ContactFactory() + + data = { + **default_contact_data, + "user": contact.user_id, + "billing_address": {**default_address_data, "address": "billing street"}, + "shipping_address": {**default_address_data, "address": "shipping street"}, + } response = self.make_request(to=contact.id, data=data, status_code=200) contact.refresh_from_db() - self.assertPayloadEqual(response, { - 'id': contact.id, - 'birthday': '2000-01-01', - 'civility': contact.civility_id, - 'uuid': str(contact.uuid), - 'created': self.to_iso8601(contact.created), - 'modified': self.to_iso8601(contact.modified), - 'description': 'Description', - 'email': 'jean.dupont@provider.com', - 'fax': '+330100000001', - 'first_name': 'Jean', - 'full_position': 'Full position', - 'is_deleted': False, - 'is_user': None, - 'last_name': 'Dupont', - 'mobile': '+330600000000', - 'phone': '+330100000000', - 'position': contact.position_id, - 'sector': contact.sector_id, - 'skype': 'jean.dupont', - 'url_site': 'https://www.jean-dupont.provider.com', - 'user': contact.user_id, - 'billing_address': { - 'address': 'billing street', - 'city': 'City', - 'country': 'Country', - 'department': 'Dept', - 'po_box': 'PO123', - 'state': 'State', - 'zipcode': 'ZIP123', - }, - 'shipping_address': { - 'address': 'shipping street', - 'city': 'City', - 'country': 'Country', - 'department': 'Dept', - 'po_box': 'PO123', - 'state': 'State', - 'zipcode': 'ZIP123', + self.assertPayloadEqual( + response, + { + **data, + "id": contact.id, + "uuid": str(contact.uuid), + "created": self.to_iso8601(contact.created), + "modified": self.to_iso8601(contact.modified), + "is_deleted": False, + "is_user": None, }, - }) - self.assertEqual(contact.birthday, date(2000, 1, 1)) - self.assertEqual(contact.user_id, data['user']) - self.assertEqual(contact.description, 'Description') - self.assertEqual(contact.first_name, "Jean") - self.assertEqual(contact.last_name, "Dupont") - self.assertEqual(contact.skype, 'jean.dupont') - self.assertEqual(contact.phone, '+330100000000') - self.assertEqual(contact.mobile, '+330600000000') - self.assertEqual(contact.fax, '+330100000001') - self.assertEqual(contact.email, 'jean.dupont@provider.com') - self.assertEqual(contact.url_site, 'https://www.jean-dupont.provider.com') - self.assertEqual(contact.full_position, 'Full position') - self.assertFalse(contact.is_deleted) - self.assertIsNone(contact.is_user) - - billing_address = contact.billing_address - self.assertEqual(billing_address.name, _('Billing address')) - self.assertEqual(billing_address.address, billing_address_data['address']) - self.assertEqual(billing_address.po_box, billing_address_data['po_box']) - self.assertEqual(billing_address.zipcode, billing_address_data['zipcode']) - self.assertEqual(billing_address.city, billing_address_data['city']) - self.assertEqual(billing_address.department, billing_address_data['department']) - self.assertEqual(billing_address.state, billing_address_data['state']) - self.assertEqual(billing_address.country, billing_address_data['country']) - self.assertEqual(billing_address.owner, contact) - - shipping_address = contact.shipping_address - self.assertEqual(shipping_address.name, _('Shipping address')) - self.assertEqual(shipping_address.address, shipping_address_data['address']) - self.assertEqual(shipping_address.po_box, shipping_address_data['po_box']) - self.assertEqual(shipping_address.zipcode, shipping_address_data['zipcode']) - self.assertEqual(shipping_address.city, shipping_address_data['city']) - self.assertEqual(shipping_address.department, shipping_address_data['department']) - self.assertEqual(shipping_address.state, shipping_address_data['state']) - self.assertEqual(shipping_address.country, shipping_address_data['country']) - self.assertEqual(shipping_address.owner, contact) + ) + self.assertEqual(contact.billing_address.name, _("Billing address")) + self.assertEqual(contact.billing_address.address, "billing street") + self.assertEqual(contact.billing_address.po_box, "PO123") + self.assertEqual(contact.billing_address.zipcode, "ZIP123") + self.assertEqual(contact.billing_address.city, "City") + self.assertEqual(contact.billing_address.department, "Dept") + self.assertEqual(contact.billing_address.state, "State") + self.assertEqual(contact.billing_address.country, "Country") + self.assertEqual(contact.billing_address.owner, contact) + + self.assertEqual(contact.shipping_address.name, _("Shipping address")) + self.assertEqual(contact.shipping_address.address, "shipping street") + self.assertEqual(contact.shipping_address.po_box, "PO123") + self.assertEqual(contact.shipping_address.zipcode, "ZIP123") + self.assertEqual(contact.shipping_address.city, "City") + self.assertEqual(contact.shipping_address.department, "Dept") + self.assertEqual(contact.shipping_address.state, "State") + self.assertEqual(contact.shipping_address.country, "Country") + self.assertEqual(contact.shipping_address.owner, contact) def test_update_contact__update_addresses(self): - contact = self.factory.contact() - billing_address = contact.billing_address - shipping_address = contact.shipping_address - - billing_address_data = self.factory.address_data(address='billing street') - shipping_address_data = self.factory.address_data(address='shipping street') - data = self.factory.contact_data( - user=contact.user_id, - billing_address=billing_address_data, - shipping_address=shipping_address_data, - ) + contact = ContactFactory(billing_address=True, shipping_address=True) + + data = { + **default_contact_data, + "user": contact.user_id, + "billing_address": {**default_address_data, "address": "billing street"}, + "shipping_address": {**default_address_data, "address": "shipping street"}, + } response = self.make_request(to=contact.id, data=data, status_code=200) contact.refresh_from_db() - self.assertPayloadEqual(response, { - 'id': contact.id, - 'birthday': '2000-01-01', - 'civility': contact.civility_id, - 'uuid': str(contact.uuid), - 'created': self.to_iso8601(contact.created), - 'modified': self.to_iso8601(contact.modified), - 'description': 'Description', - 'email': 'jean.dupont@provider.com', - 'fax': '+330100000001', - 'first_name': 'Jean', - 'full_position': 'Full position', - 'is_deleted': False, - 'is_user': None, - 'last_name': 'Dupont', - 'mobile': '+330600000000', - 'phone': '+330100000000', - 'position': contact.position_id, - 'sector': contact.sector_id, - 'skype': 'jean.dupont', - 'url_site': 'https://www.jean-dupont.provider.com', - 'user': contact.user_id, - 'billing_address': { - 'address': 'billing street', - 'city': 'City', - 'country': 'Country', - 'department': 'Dept', - 'po_box': 'PO123', - 'state': 'State', - 'zipcode': 'ZIP123', - }, - 'shipping_address': { - 'address': 'shipping street', - 'city': 'City', - 'country': 'Country', - 'department': 'Dept', - 'po_box': 'PO123', - 'state': 'State', - 'zipcode': 'ZIP123', + self.assertPayloadEqual( + response, + { + **data, + "id": contact.id, + "uuid": str(contact.uuid), + "created": self.to_iso8601(contact.created), + "modified": self.to_iso8601(contact.modified), + "is_deleted": False, + "is_user": None, }, - }) - - self.assertEqual(contact.birthday, date(2000, 1, 1)) - self.assertEqual(contact.user_id, contact.user_id) - self.assertEqual(contact.description, 'Description') - self.assertEqual(contact.first_name, "Jean") - self.assertEqual(contact.last_name, "Dupont") - self.assertEqual(contact.skype, 'jean.dupont') - self.assertEqual(contact.phone, '+330100000000') - self.assertEqual(contact.mobile, '+330600000000') - self.assertEqual(contact.fax, '+330100000001') - self.assertEqual(contact.email, 'jean.dupont@provider.com') - self.assertEqual(contact.url_site, 'https://www.jean-dupont.provider.com') - self.assertEqual(contact.full_position, 'Full position') - self.assertFalse(contact.is_deleted) - self.assertIsNone(contact.is_user) - - billing_address.refresh_from_db() - self.assertEqual(billing_address.name, _('Billing address')) - self.assertEqual(billing_address.address, billing_address_data['address']) - self.assertEqual(billing_address.po_box, billing_address_data['po_box']) - self.assertEqual(billing_address.zipcode, billing_address_data['zipcode']) - self.assertEqual(billing_address.city, billing_address_data['city']) - self.assertEqual(billing_address.department, billing_address_data['department']) - self.assertEqual(billing_address.state, billing_address_data['state']) - self.assertEqual(billing_address.country, billing_address_data['country']) - self.assertEqual(billing_address.owner, contact) - - shipping_address.refresh_from_db() - self.assertEqual(shipping_address.name, _('Shipping address')) - self.assertEqual(shipping_address.address, shipping_address_data['address']) - self.assertEqual(shipping_address.po_box, shipping_address_data['po_box']) - self.assertEqual(shipping_address.zipcode, shipping_address_data['zipcode']) - self.assertEqual(shipping_address.city, shipping_address_data['city']) - self.assertEqual(shipping_address.department, shipping_address_data['department']) - self.assertEqual(shipping_address.state, shipping_address_data['state']) - self.assertEqual(shipping_address.country, shipping_address_data['country']) - self.assertEqual(shipping_address.owner, contact) + ) + self.assertEqual(contact.billing_address.name, _("Billing address")) + self.assertEqual(contact.billing_address.address, "billing street") + self.assertEqual(contact.billing_address.po_box, "PO123") + self.assertEqual(contact.billing_address.zipcode, "ZIP123") + self.assertEqual(contact.billing_address.city, "City") + self.assertEqual(contact.billing_address.department, "Dept") + self.assertEqual(contact.billing_address.state, "State") + self.assertEqual(contact.billing_address.country, "Country") + self.assertEqual(contact.billing_address.owner, contact) + + self.assertEqual(contact.shipping_address.name, _("Shipping address")) + self.assertEqual(contact.shipping_address.address, "shipping street") + self.assertEqual(contact.shipping_address.po_box, "PO123") + self.assertEqual(contact.shipping_address.zipcode, "ZIP123") + self.assertEqual(contact.shipping_address.city, "City") + self.assertEqual(contact.shipping_address.department, "Dept") + self.assertEqual(contact.shipping_address.state, "State") + self.assertEqual(contact.shipping_address.country, "Country") + self.assertEqual(contact.shipping_address.owner, contact) class PartialUpdateContactTestCase(CremeAPITestCase): - url_name = 'creme_api__contacts-detail' - method = 'patch' + url_name = "creme_api__contacts-detail" + method = "patch" def test_partial_update_contact(self): - contact = self.factory.contact(billing_address=None) - data = { - 'first_name': "Nick", - 'last_name': "Smith", - } + contact = ContactFactory( + shipping_address=True, sector=None, civility=None, position=None + ) + sector = SectorFactory() + data = {"first_name": "Nick", "last_name": "Smith", "sector": sector.id} response = self.make_request(to=contact.id, data=data, status_code=200) contact.refresh_from_db() - self.assertPayloadEqual(response, { - 'id': contact.id, - 'birthday': '2000-01-01', - 'civility': contact.civility_id, - 'uuid': str(contact.uuid), - 'created': self.to_iso8601(contact.created), - 'modified': self.to_iso8601(contact.modified), - 'description': 'Description', - 'email': 'jean.dupont@provider.com', - 'fax': '+330100000001', - 'first_name': 'Nick', - 'full_position': 'Full position', - 'is_deleted': False, - 'is_user': None, - 'last_name': 'Smith', - 'mobile': '+330600000000', - 'phone': '+330100000000', - 'position': contact.position_id, - 'sector': contact.sector_id, - 'skype': 'jean.dupont', - 'url_site': 'https://www.jean-dupont.provider.com', - 'user': contact.user_id, - 'shipping_address': { - 'address': '1 Main Street', - 'city': 'City', - 'country': 'Country', - 'department': 'Dept', - 'po_box': 'PO123', - 'state': 'State', - 'zipcode': 'ZIP123', + self.assertPayloadEqual( + response, + { + **default_contact_data, + "id": contact.id, + "user": contact.user_id, + "uuid": str(contact.uuid), + "created": self.to_iso8601(contact.created), + "modified": self.to_iso8601(contact.modified), + "is_deleted": False, + "is_user": None, + "shipping_address": default_address_data, + **data, }, - }) + ) self.assertEqual(contact.first_name, "Nick") self.assertEqual(contact.last_name, "Smith") + self.assertEqual(contact.sector, sector) def test_partial_update_contact__create_addresses(self): - contact = self.factory.contact(billing_address=None, shipping_address=None) + contact = ContactFactory(sector=None, civility=None, position=None) data = { - 'billing_address': { - 'address': 'billing street', - }, - 'shipping_address': { - 'address': 'shipping street', - }, + "billing_address": {"address": "billing street"}, + "shipping_address": {"address": "shipping street"}, } response = self.make_request(to=contact.id, data=data, status_code=200) contact.refresh_from_db() - self.assertPayloadEqual(response, { - 'id': contact.id, - 'birthday': '2000-01-01', - 'civility': contact.civility_id, - 'uuid': str(contact.uuid), - 'created': self.to_iso8601(contact.created), - 'modified': self.to_iso8601(contact.modified), - 'description': 'Description', - 'email': 'jean.dupont@provider.com', - 'fax': '+330100000001', - 'first_name': 'Jean', - 'full_position': 'Full position', - 'is_deleted': False, - 'is_user': None, - 'last_name': 'Dupont', - 'mobile': '+330600000000', - 'phone': '+330100000000', - 'position': contact.position_id, - 'sector': contact.sector_id, - 'skype': 'jean.dupont', - 'url_site': 'https://www.jean-dupont.provider.com', - 'user': contact.user_id, - 'billing_address': { - 'address': 'billing street', - 'city': '', - 'country': '', - 'department': '', - 'po_box': '', - 'state': '', - 'zipcode': '', - }, - 'shipping_address': { - 'address': 'shipping street', - 'city': '', - 'country': '', - 'department': '', - 'po_box': '', - 'state': '', - 'zipcode': '', + self.assertPayloadEqual( + response, + { + **default_contact_data, + "id": contact.id, + "uuid": str(contact.uuid), + "created": self.to_iso8601(contact.created), + "modified": self.to_iso8601(contact.modified), + "is_deleted": False, + "is_user": None, + "user": contact.user_id, + "billing_address": { + "address": "billing street", + "city": "", + "country": "", + "department": "", + "po_box": "", + "state": "", + "zipcode": "", + }, + "shipping_address": { + "address": "shipping street", + "city": "", + "country": "", + "department": "", + "po_box": "", + "state": "", + "zipcode": "", + }, }, - }) - - billing_address = contact.billing_address - self.assertEqual(billing_address.name, _('Billing address')) - self.assertEqual(billing_address.address, "billing street") - self.assertEqual(billing_address.po_box, "") - self.assertEqual(billing_address.zipcode, "") - self.assertEqual(billing_address.city, "") - self.assertEqual(billing_address.department, "") - self.assertEqual(billing_address.state, "") - self.assertEqual(billing_address.country, "") - self.assertEqual(billing_address.owner, contact) - - shipping_address = contact.shipping_address - self.assertEqual(shipping_address.name, _('Shipping address')) - self.assertEqual(shipping_address.address, "shipping street") - self.assertEqual(shipping_address.po_box, "") - self.assertEqual(shipping_address.zipcode, "") - self.assertEqual(shipping_address.city, "") - self.assertEqual(shipping_address.department, "") - self.assertEqual(shipping_address.state, "") - self.assertEqual(shipping_address.country, "") - self.assertEqual(shipping_address.owner, contact) + ) + + self.assertEqual(contact.billing_address.name, _("Billing address")) + self.assertEqual(contact.billing_address.address, "billing street") + self.assertEqual(contact.billing_address.po_box, "") + self.assertEqual(contact.billing_address.zipcode, "") + self.assertEqual(contact.billing_address.city, "") + self.assertEqual(contact.billing_address.department, "") + self.assertEqual(contact.billing_address.state, "") + self.assertEqual(contact.billing_address.country, "") + self.assertEqual(contact.billing_address.owner, contact) + + self.assertEqual(contact.shipping_address.name, _("Shipping address")) + self.assertEqual(contact.shipping_address.address, "shipping street") + self.assertEqual(contact.shipping_address.po_box, "") + self.assertEqual(contact.shipping_address.zipcode, "") + self.assertEqual(contact.shipping_address.city, "") + self.assertEqual(contact.shipping_address.department, "") + self.assertEqual(contact.shipping_address.state, "") + self.assertEqual(contact.shipping_address.country, "") + self.assertEqual(contact.shipping_address.owner, contact) def test_partial_update_contact__update_addresses(self): - contact = self.factory.contact() - billing_address = contact.billing_address - shipping_address = contact.shipping_address + contact = ContactFactory(billing_address=True, shipping_address=True) data = { - 'billing_address': { - 'address': 'billing street', + "billing_address": { + "address": "billing street", }, - 'shipping_address': { - 'address': 'shipping street', + "shipping_address": { + "address": "shipping street", }, } response = self.make_request(to=contact.id, data=data, status_code=200) contact.refresh_from_db() - self.assertPayloadEqual(response, { - 'id': contact.id, - 'birthday': '2000-01-01', - 'civility': contact.civility_id, - 'uuid': str(contact.uuid), - 'created': self.to_iso8601(contact.created), - 'modified': self.to_iso8601(contact.modified), - 'description': 'Description', - 'email': 'jean.dupont@provider.com', - 'fax': '+330100000001', - 'first_name': 'Jean', - 'full_position': 'Full position', - 'is_deleted': False, - 'is_user': None, - 'last_name': 'Dupont', - 'mobile': '+330600000000', - 'phone': '+330100000000', - 'position': contact.position_id, - 'sector': contact.sector_id, - 'skype': 'jean.dupont', - 'url_site': 'https://www.jean-dupont.provider.com', - 'user': contact.user_id, - 'billing_address': { - 'address': 'billing street', - 'city': 'City', - 'country': 'Country', - 'department': 'Dept', - 'po_box': 'PO123', - 'state': 'State', - 'zipcode': 'ZIP123', - }, - 'shipping_address': { - 'address': 'shipping street', - 'city': 'City', - 'country': 'Country', - 'department': 'Dept', - 'po_box': 'PO123', - 'state': 'State', - 'zipcode': 'ZIP123', + self.assertPayloadEqual( + response, + { + **default_contact_data, + "id": contact.id, + "birthday": "2000-01-01", + "civility": contact.civility_id, + "uuid": str(contact.uuid), + "created": self.to_iso8601(contact.created), + "modified": self.to_iso8601(contact.modified), + "is_deleted": False, + "is_user": None, + "position": contact.position_id, + "sector": contact.sector_id, + "user": contact.user_id, + "billing_address": { + **default_address_data, + "address": "billing street", + }, + "shipping_address": { + **default_address_data, + "address": "shipping street", + }, }, - }) - billing_address.refresh_from_db() - self.assertEqual(billing_address.name, _('Billing address')) - self.assertEqual(billing_address.address, "billing street") - self.assertEqual(billing_address.po_box, "PO123") - self.assertEqual(billing_address.zipcode, "ZIP123") - self.assertEqual(billing_address.city, "City") - self.assertEqual(billing_address.department, "Dept") - self.assertEqual(billing_address.state, "State") - self.assertEqual(billing_address.country, "Country") - self.assertEqual(billing_address.owner, contact) - shipping_address.refresh_from_db() - self.assertEqual(shipping_address.name, _('Shipping address')) - self.assertEqual(shipping_address.address, "shipping street") - self.assertEqual(shipping_address.po_box, "PO123") - self.assertEqual(shipping_address.zipcode, "ZIP123") - self.assertEqual(shipping_address.city, "City") - self.assertEqual(shipping_address.department, "Dept") - self.assertEqual(shipping_address.state, "State") - self.assertEqual(shipping_address.country, "Country") - self.assertEqual(shipping_address.owner, contact) + ) + self.assertEqual(contact.billing_address.name, _("Billing address")) + self.assertEqual(contact.billing_address.address, "billing street") + self.assertEqual(contact.billing_address.po_box, "PO123") + self.assertEqual(contact.billing_address.zipcode, "ZIP123") + self.assertEqual(contact.billing_address.city, "City") + self.assertEqual(contact.billing_address.department, "Dept") + self.assertEqual(contact.billing_address.state, "State") + self.assertEqual(contact.billing_address.country, "Country") + self.assertEqual(contact.billing_address.owner, contact) + + self.assertEqual(contact.shipping_address.name, _("Shipping address")) + self.assertEqual(contact.shipping_address.address, "shipping street") + self.assertEqual(contact.shipping_address.po_box, "PO123") + self.assertEqual(contact.shipping_address.zipcode, "ZIP123") + self.assertEqual(contact.shipping_address.city, "City") + self.assertEqual(contact.shipping_address.department, "Dept") + self.assertEqual(contact.shipping_address.state, "State") + self.assertEqual(contact.shipping_address.country, "Country") + self.assertEqual(contact.shipping_address.owner, contact) class ListContactTestCase(CremeAPITestCase): - url_name = 'creme_api__contacts-list' - method = 'get' + url_name = "creme_api__contacts-list" + method = "get" def test_list_contacts(self): fulbert = Contact.objects.get() - contact = self.factory.contact(user=fulbert.user) + contact = ContactFactory( + user=fulbert.user, billing_address=True, shipping_address=True + ) self.assertEqual(Contact.objects.count(), 2, Contact.objects.all()) response = self.make_request(status_code=200) - self.assertPayloadEqual(response, [ - { - 'id': fulbert.id, - 'birthday': None, - 'civility': None, - 'uuid': str(fulbert.uuid), - 'created': self.to_iso8601(fulbert.created), - 'modified': self.to_iso8601(fulbert.modified), - 'description': '', - 'email': fulbert.email, - 'fax': '', - 'first_name': 'Fulbert', - 'full_position': '', - 'is_deleted': False, - 'is_user': fulbert.is_user_id, - 'last_name': 'Creme', - 'mobile': '', - 'phone': '', - 'position': None, - 'sector': None, - 'skype': '', - 'url_site': '', - 'user': fulbert.user_id, - }, - { - 'id': contact.id, - 'birthday': '2000-01-01', - 'civility': contact.civility_id, - 'uuid': str(contact.uuid), - 'created': self.to_iso8601(contact.created), - 'modified': self.to_iso8601(contact.modified), - 'description': 'Description', - 'email': 'jean.dupont@provider.com', - 'fax': '+330100000001', - 'first_name': 'Jean', - 'full_position': 'Full position', - 'is_deleted': False, - 'is_user': None, - 'last_name': 'Dupont', - 'mobile': '+330600000000', - 'phone': '+330100000000', - 'position': contact.position_id, - 'sector': contact.sector_id, - 'skype': 'jean.dupont', - 'url_site': 'https://www.jean-dupont.provider.com', - 'user': fulbert.user_id, - 'billing_address': { - 'address': '1 Main Street', - 'city': 'City', - 'country': 'Country', - 'department': 'Dept', - 'po_box': 'PO123', - 'state': 'State', - 'zipcode': 'ZIP123', + self.assertPayloadEqual( + response, + [ + { + "id": fulbert.id, + "birthday": None, + "civility": None, + "uuid": str(fulbert.uuid), + "created": self.to_iso8601(fulbert.created), + "modified": self.to_iso8601(fulbert.modified), + "description": "", + "email": fulbert.email, + "fax": "", + "first_name": "Fulbert", + "full_position": "", + "is_deleted": False, + "is_user": fulbert.is_user_id, + "last_name": "Creme", + "mobile": "", + "phone": "", + "position": None, + "sector": None, + "skype": "", + "url_site": "", + "user": fulbert.user_id, }, - 'shipping_address': { - 'address': '1 Main Street', - 'city': 'City', - 'country': 'Country', - 'department': 'Dept', - 'po_box': 'PO123', - 'state': 'State', - 'zipcode': 'ZIP123', + { + **default_contact_data, + "id": contact.id, + "civility": contact.civility_id, + "uuid": str(contact.uuid), + "created": self.to_iso8601(contact.created), + "modified": self.to_iso8601(contact.modified), + "is_deleted": False, + "is_user": None, + "position": contact.position_id, + "sector": contact.sector_id, + "user": fulbert.user_id, + "billing_address": default_address_data, + "shipping_address": default_address_data, }, - } - ]) + ], + ) class TrashContactTestCase(CremeAPITestCase): - url_name = 'creme_api__contacts-trash' - method = 'post' + url_name = "creme_api__contacts-trash" + method = "post" def test_trash_contact__protected(self): fulbert = Contact.objects.get() response = self.make_request(to=fulbert.id, status_code=422) - self.assertEqual(response.data['detail'].code, 'protected') + self.assertEqual(response.data["detail"].code, "protected") def test_trash_contact(self): - contact = self.factory.contact() + contact = ContactFactory() response = self.make_request(to=contact.id, status_code=200) contact.refresh_from_db() - self.assertPayloadEqual(response, { - 'id': contact.id, - 'uuid': str(contact.uuid), - 'created': self.to_iso8601(contact.created), - 'modified': self.to_iso8601(contact.modified), - 'is_deleted': True, - }) + self.assertPayloadEqual( + response, + { + "id": contact.id, + "uuid": str(contact.uuid), + "created": self.to_iso8601(contact.created), + "modified": self.to_iso8601(contact.modified), + "is_deleted": True, + }, + ) self.make_request(to=contact.id, status_code=200) class RestoreContactTestCase(CremeAPITestCase): - url_name = 'creme_api__contacts-restore' - method = 'post' + url_name = "creme_api__contacts-restore" + method = "post" def test_restore_contact(self): - contact = self.factory.contact(is_deleted=True) + contact = ContactFactory(is_deleted=True) contact.refresh_from_db() self.assertTrue(contact.is_deleted) response = self.make_request(to=contact.id, status_code=200) contact.refresh_from_db() - self.assertPayloadEqual(response, { - 'id': contact.id, - 'uuid': str(contact.uuid), - 'created': self.to_iso8601(contact.created), - 'modified': self.to_iso8601(contact.modified), - 'is_deleted': False, - }) + self.assertPayloadEqual( + response, + { + "id": contact.id, + "uuid": str(contact.uuid), + "created": self.to_iso8601(contact.created), + "modified": self.to_iso8601(contact.modified), + "is_deleted": False, + }, + ) self.make_request(to=contact.id, status_code=200) class DeleteContactTestCase(CremeAPITestCase): - url_name = 'creme_api__contacts-detail' - method = 'delete' + url_name = "creme_api__contacts-detail" + method = "delete" def test_delete_contact__protected(self): fulbert = Contact.objects.get() response = self.make_request(to=fulbert.id, status_code=422) - self.assertEqual(response.data['detail'].code, 'protected') + self.assertEqual(response.data["detail"].code, "protected") def test_delete_contact(self): - contact = self.factory.contact() + contact = ContactFactory() self.make_request(to=contact.id, status_code=204) self.assertFalse(Contact.objects.filter(id=contact.id).exists()) class CloneContactTestCase(CremeAPITestCase): - url_name = 'creme_api__contacts-clone' - method = 'post' + url_name = "creme_api__contacts-clone" + method = "post" def test_clone_contact(self): fulbert = Contact.objects.get() response = self.make_request(to=fulbert.id, status_code=201) - contact = Contact.objects.get(id=response.data['id']) + contact = Contact.objects.get(id=response.data["id"]) self.assertNotEqual(fulbert.id, contact.id) self.assertNotEqual(fulbert.uuid, contact.uuid) - self.assertPayloadEqual(response, { - 'id': contact.id, - 'birthday': None, - 'civility': None, - 'uuid': str(contact.uuid), - 'created': self.to_iso8601(contact.created), - 'modified': self.to_iso8601(contact.modified), - 'description': '', - 'email': fulbert.email, - 'fax': '', - 'first_name': 'Fulbert', - 'full_position': '', - 'is_deleted': False, - 'is_user': None, - 'last_name': 'Creme', - 'mobile': '', - 'phone': '', - 'position': None, - 'sector': None, - 'skype': '', - 'url_site': '', - 'user': fulbert.user_id, - }) + self.assertPayloadEqual( + response, + { + "id": contact.id, + "birthday": None, + "civility": None, + "uuid": str(contact.uuid), + "created": self.to_iso8601(contact.created), + "modified": self.to_iso8601(contact.modified), + "description": "", + "email": fulbert.email, + "fax": "", + "first_name": "Fulbert", + "full_position": "", + "is_deleted": False, + "is_user": None, + "last_name": "Creme", + "mobile": "", + "phone": "", + "position": None, + "sector": None, + "skype": "", + "url_site": "", + "user": fulbert.user_id, + }, + ) diff --git a/creme/creme_api/tests/test_contenttypes.py b/creme/creme_api/tests/test_contenttypes.py index 9f0108fad8..a803f2f3ca 100644 --- a/creme/creme_api/tests/test_contenttypes.py +++ b/creme/creme_api/tests/test_contenttypes.py @@ -9,23 +9,26 @@ class RetrieveContentTypeTestCase(CremeAPITestCase): - url_name = 'creme_api__contenttypes-detail' - method = 'get' + url_name = "creme_api__contenttypes-detail" + method = "get" def test_retrieve_contenttype(self): contact_ct = ContentType.objects.get_for_model(Contact) response = self.make_request(to=contact_ct.id, status_code=200) - self.assertPayloadEqual(response, { - 'id': contact_ct.id, - 'application': _('Accounts and Contacts'), - 'name': _("Contacts"), - }) + self.assertPayloadEqual( + response, + { + "id": contact_ct.id, + "application": _("Accounts and Contacts"), + "name": _("Contacts"), + }, + ) class ListContentTypeTestCase(CremeAPITestCase): - url_name = 'creme_api__contenttypes-list' - method = 'get' + url_name = "creme_api__contenttypes-list" + method = "get" def test_list_contenttypes(self): responses, data = self.consume_list() diff --git a/creme/creme_api/tests/test_credentials.py b/creme/creme_api/tests/test_credentials.py index 472ccc04e4..9116599cbf 100644 --- a/creme/creme_api/tests/test_credentials.py +++ b/creme/creme_api/tests/test_credentials.py @@ -1,57 +1,41 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType -from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.creme_api.tests.utils import CremeAPITestCase from creme.creme_core.models import SetCredentials from creme.persons import get_contact_model, get_organisation_model +from .factories import CredentialFactory, RoleFactory + CremeUser = get_user_model() Contact = get_contact_model() Organisation = get_organisation_model() -@Factory.register -def credential(factory, **kwargs): - contact_ct = ContentType.objects.get_for_model(Contact) - perms = {'can_view', 'can_change', 'can_delete', 'can_link', 'can_unlink'} - data = { - 'set_type': SetCredentials.ESET_OWN, - 'ctype': contact_ct, - 'forbidden': False, - 'efilter': None, - **{p: True for p in perms} - } - data.update(**kwargs) - if 'role' not in data: - data['role'] = factory.role() - value = {k: data.pop(k) for k in perms} - creds = SetCredentials(**data) - creds.set_value(**value) - creds.save() - return creds - - class CreateSetCredentialTestCase(CremeAPITestCase): - url_name = 'creme_api__credentials-list' - method = 'post' + url_name = "creme_api__credentials-list" + method = "post" def test_validation__required(self): response = self.make_request(data={}, status_code=400) - self.assertValidationErrors(response, { - 'role': ['required'], - 'set_type': ['required'], - 'ctype': ['required'], - 'can_view': ['required'], - 'can_change': ['required'], - 'can_delete': ['required'], - 'can_link': ['required'], - 'can_unlink': ['required'], - 'forbidden': ['required'], - }) + self.assertValidationErrors( + response, + { + "role": ["required"], + "set_type": ["required"], + "ctype": ["required"], + "can_view": ["required"], + "can_change": ["required"], + "can_delete": ["required"], + "can_link": ["required"], + "can_unlink": ["required"], + "forbidden": ["required"], + }, + ) def test_create_setcredentials(self): contact_ct = ContentType.objects.get_for_model(Contact) - role = self.factory.role() + role = RoleFactory() data = { "role": role.id, "set_type": SetCredentials.ESET_ALL, @@ -64,20 +48,23 @@ def test_create_setcredentials(self): "forbidden": False, } response = self.make_request(data=data, status_code=201) - creds = SetCredentials.objects.get(id=response.data['id']) - self.assertPayloadEqual(response, { - 'id': creds.id, - "role": role.id, - "set_type": SetCredentials.ESET_ALL, - "ctype": contact_ct.id, - "can_view": True, - "can_change": True, - "can_delete": False, - "can_link": True, - "can_unlink": True, - "forbidden": False, - "efilter": None, - }) + creds = SetCredentials.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + "id": creds.id, + "role": role.id, + "set_type": SetCredentials.ESET_ALL, + "ctype": contact_ct.id, + "can_view": True, + "can_change": True, + "can_delete": False, + "can_link": True, + "can_unlink": True, + "forbidden": False, + "efilter": None, + }, + ) self.assertEqual(creds.role, role) self.assertEqual(creds.set_type, SetCredentials.ESET_ALL) self.assertEqual(creds.ctype, contact_ct) @@ -86,49 +73,54 @@ def test_create_setcredentials(self): class RetrieveSetCredentialTestCase(CremeAPITestCase): - url_name = 'creme_api__credentials-detail' - method = 'get' + url_name = "creme_api__credentials-detail" + method = "get" def test_retrieve_setcredentials(self): - contact_ct = ContentType.objects.get_for_model(Contact) - creds = self.factory.credential() + creds = CredentialFactory() response = self.make_request(to=creds.id, status_code=200) - self.assertPayloadEqual(response, { - 'id': creds.id, - "role": creds.role_id, - "set_type": SetCredentials.ESET_OWN, - "ctype": contact_ct.id, - "can_view": True, - "can_change": True, - "can_delete": True, - "can_link": True, - "can_unlink": True, - "forbidden": False, - "efilter": None, - }) + self.assertPayloadEqual( + response, + { + "id": creds.id, + "role": creds.role_id, + "set_type": SetCredentials.ESET_OWN, + "ctype": creds.ctype.id, + "can_view": True, + "can_change": True, + "can_delete": True, + "can_link": True, + "can_unlink": True, + "forbidden": False, + "efilter": None, + }, + ) class UpdateSetCredentialTestCase(CremeAPITestCase): - url_name = 'creme_api__credentials-detail' - method = 'put' + url_name = "creme_api__credentials-detail" + method = "put" def test_validation__required(self): - creds = self.factory.credential() + creds = CredentialFactory() response = self.make_request(to=creds.id, data={}, status_code=400) - self.assertValidationErrors(response, { - 'set_type': ['required'], - 'ctype': ['required'], - 'can_view': ['required'], - 'can_change': ['required'], - 'can_delete': ['required'], - 'can_link': ['required'], - 'can_unlink': ['required'], - 'forbidden': ['required'], - }) + self.assertValidationErrors( + response, + { + "set_type": ["required"], + "ctype": ["required"], + "can_view": ["required"], + "can_change": ["required"], + "can_delete": ["required"], + "can_link": ["required"], + "can_unlink": ["required"], + "forbidden": ["required"], + }, + ) def test_update_creds(self): orga_ct = ContentType.objects.get_for_model(Organisation) - creds = self.factory.credential() + creds = CredentialFactory() role_id = creds.role_id data = { "role": 123456, @@ -142,19 +134,22 @@ def test_update_creds(self): "forbidden": False, } response = self.make_request(to=creds.id, data=data, status_code=200) - self.assertPayloadEqual(response, { - 'id': creds.id, - "role": creds.role_id, - "set_type": SetCredentials.ESET_ALL, - "ctype": orga_ct.id, - "can_view": True, - "can_change": True, - "can_delete": False, - "can_link": True, - "can_unlink": True, - "forbidden": False, - "efilter": None, - }) + self.assertPayloadEqual( + response, + { + "id": creds.id, + "role": creds.role_id, + "set_type": SetCredentials.ESET_ALL, + "ctype": orga_ct.id, + "can_view": True, + "can_change": True, + "can_delete": False, + "can_link": True, + "can_unlink": True, + "forbidden": False, + "efilter": None, + }, + ) creds.refresh_from_db() self.assertEqual(creds.role_id, role_id) self.assertEqual(creds.set_type, SetCredentials.ESET_ALL) @@ -164,12 +159,12 @@ def test_update_creds(self): class PartialUpdateSetCredentialTestCase(CremeAPITestCase): - url_name = 'creme_api__credentials-detail' - method = 'patch' + url_name = "creme_api__credentials-detail" + method = "patch" def test_partial_update_creds(self): orga_ct = ContentType.objects.get_for_model(Organisation) - creds = self.factory.credential() + creds = CredentialFactory() role_id = creds.role_id data = { "role": 123456, @@ -179,19 +174,22 @@ def test_partial_update_creds(self): "forbidden": False, } response = self.make_request(to=creds.id, data=data, status_code=200) - self.assertPayloadEqual(response, { - 'id': creds.id, - "role": creds.role_id, - "set_type": SetCredentials.ESET_ALL, - "ctype": orga_ct.id, - "can_view": True, - "can_change": True, - "can_delete": False, - "can_link": True, - "can_unlink": True, - "forbidden": False, - "efilter": None, - }) + self.assertPayloadEqual( + response, + { + "id": creds.id, + "role": creds.role_id, + "set_type": SetCredentials.ESET_ALL, + "ctype": orga_ct.id, + "can_view": True, + "can_change": True, + "can_delete": False, + "can_link": True, + "can_unlink": True, + "forbidden": False, + "efilter": None, + }, + ) creds.refresh_from_db() self.assertEqual(creds.role_id, role_id) self.assertEqual(creds.set_type, SetCredentials.ESET_ALL) @@ -201,52 +199,55 @@ def test_partial_update_creds(self): class ListSetCredentialTestCase(CremeAPITestCase): - url_name = 'creme_api__credentials-list' - method = 'get' + url_name = "creme_api__credentials-list" + method = "get" def test_list_setcredentials(self): contact_ct = ContentType.objects.get_for_model(Contact) orga_ct = ContentType.objects.get_for_model(Organisation) - role = self.factory.role() - creds1 = self.factory.credential(role=role, ctype=contact_ct) - creds2 = self.factory.credential(role=role, ctype=orga_ct, can_delete=False) + role = RoleFactory() + creds1 = CredentialFactory(role=role, ctype=contact_ct) + creds2 = CredentialFactory(role=role, ctype=orga_ct, can_delete=False) response = self.make_request(status_code=200) - self.assertPayloadEqual(response, [ - { - 'id': creds1.id, - "role": role.id, - "set_type": SetCredentials.ESET_OWN, - "ctype": contact_ct.id, - "can_view": True, - "can_change": True, - "can_delete": True, - "can_link": True, - "can_unlink": True, - "forbidden": False, - "efilter": None, - }, - { - 'id': creds2.id, - "role": role.id, - "set_type": SetCredentials.ESET_OWN, - "ctype": orga_ct.id, - "can_view": True, - "can_change": True, - "can_delete": False, - "can_link": True, - "can_unlink": True, - "forbidden": False, - "efilter": None, - } - ]) + self.assertPayloadEqual( + response, + [ + { + "id": creds1.id, + "role": role.id, + "set_type": SetCredentials.ESET_OWN, + "ctype": contact_ct.id, + "can_view": True, + "can_change": True, + "can_delete": True, + "can_link": True, + "can_unlink": True, + "forbidden": False, + "efilter": None, + }, + { + "id": creds2.id, + "role": role.id, + "set_type": SetCredentials.ESET_OWN, + "ctype": orga_ct.id, + "can_view": True, + "can_change": True, + "can_delete": False, + "can_link": True, + "can_unlink": True, + "forbidden": False, + "efilter": None, + }, + ], + ) class DeleteSetCredentialTestCase(CremeAPITestCase): - url_name = 'creme_api__credentials-detail' - method = 'delete' + url_name = "creme_api__credentials-detail" + method = "delete" def test_delete(self): - creds = self.factory.credential() + creds = CredentialFactory() self.make_request(to=creds.id, status_code=204) self.assertFalse(SetCredentials.objects.filter(id=creds.id).exists()) diff --git a/creme/creme_api/tests/test_documentation.py b/creme/creme_api/tests/test_documentation.py index 35f504492c..fbb6f3fb04 100644 --- a/creme/creme_api/tests/test_documentation.py +++ b/creme/creme_api/tests/test_documentation.py @@ -8,73 +8,75 @@ class SchemaViewTestCase(CremeTestCase): - url = reverse_lazy('creme_api__openapi_schema') + url = reverse_lazy("creme_api__openapi_schema") def test_permissions__permission_denied(self): - self.login(is_superuser=False, allowed_apps=('creme_core',)) + self.login(is_superuser=False, allowed_apps=("creme_core",)) self.assertGET403(self.url) def test_permissions__allowed_app(self): - self.login(is_superuser=False, allowed_apps=('creme_core', 'creme_api')) + self.login(is_superuser=False, allowed_apps=("creme_core", "creme_api")) self.assertGET200(self.url) def test_permissions__superuser(self): - self.login(is_superuser=True, allowed_apps=('creme_core',)) + self.login(is_superuser=True, allowed_apps=("creme_core",)) self.assertGET200(self.url) def test_context(self): self.login() response = self.assertGET200(self.url) - self.assertEqual(response['content-type'], 'application/vnd.oai.openapi') - self.assertTemplateUsed(response, 'creme_api/description.md') + self.assertEqual(response["content-type"], "application/vnd.oai.openapi") + self.assertTemplateUsed(response, "creme_api/description.md") openapi_schema = yaml.safe_load(response.content) - info = openapi_schema['info'] - self.assertEqual(info['version'], creme_api.VERSION) - self.assertTrue(info['title']) - self.assertTrue(info['description']) - - @parameterized.expand([ - (endpoint,) for endpoint in router.resources_list - ]) + info = openapi_schema["info"] + self.assertEqual(info["version"], creme_api.VERSION) + self.assertTrue(info["title"]) + self.assertTrue(info["description"]) + + @parameterized.expand([(endpoint,) for endpoint in router.resources_list]) def test_all_endpoints_have_documentation(self, endpoint): self.login() response = self.assertGET200(self.url) openapi_schema = yaml.safe_load(response.content) errors = [] - for url, methods in openapi_schema['paths'].items(): + for url, methods in openapi_schema["paths"].items(): if endpoint not in url: continue - errors.extend([ - (method, url) - for method, method_details in methods.items() - if not method_details.get('description') - ]) + errors.extend( + [ + (method, url) + for method, method_details in methods.items() + if not method_details.get("description") + ] + ) self.assertFalse(errors, "Please document those endpoints.") class DocumentationViewTestCase(CremeTestCase): - url = reverse_lazy('creme_api__documentation') + url = reverse_lazy("creme_api__documentation") def test_permissions__permission_denied(self): - self.login(is_superuser=False, allowed_apps=('creme_core',)) + self.login(is_superuser=False, allowed_apps=("creme_core",)) self.assertGET403(self.url) def test_permissions__allowed_app(self): - self.login(is_superuser=False, allowed_apps=('creme_core', 'creme_api')) + self.login(is_superuser=False, allowed_apps=("creme_core", "creme_api")) self.assertGET200(self.url) def test_permissions__superuser(self): - self.login(is_superuser=True, allowed_apps=('creme_core',)) + self.login(is_superuser=True, allowed_apps=("creme_core",)) self.assertGET200(self.url) def test_context(self): self.login() response = self.assertGET200(self.url) - self.assertTemplateUsed(response, 'creme_api/documentation.html') - self.assertEqual(response.context["schema_url"], 'creme_api__openapi_schema') + self.assertTemplateUsed(response, "creme_api/documentation.html") + self.assertEqual(response.context["schema_url"], "creme_api__openapi_schema") self.assertEqual( - response.context["creme_api__tokens_url"], 'http://testserver/creme_api/tokens/') - self.assertEqual(response.context["token_type"], 'Token') + response.context["creme_api__tokens_url"], + "http://testserver/creme_api/tokens/", + ) + self.assertEqual(response.context["token_type"], "Token") diff --git a/creme/creme_api/tests/test_legal_forms.py b/creme/creme_api/tests/test_legal_forms.py index f448442300..f11c24243b 100644 --- a/creme/creme_api/tests/test_legal_forms.py +++ b/creme/creme_api/tests/test_legal_forms.py @@ -1,107 +1,123 @@ -from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.creme_api.tests.utils import CremeAPITestCase from creme.persons.models import LegalForm - -@Factory.register -def legal_form(factory, **kwargs): - data = factory.legal_form_data(**kwargs) - return LegalForm.objects.create(**data) - - -@Factory.register -def legal_form_data(factory, **kwargs): - kwargs.setdefault('title', 'Trust') - return kwargs +from .factories import LegalFormFactory class CreateLegalFormTestCase(CremeAPITestCase): - url_name = 'creme_api__legal_forms-list' - method = 'post' + url_name = "creme_api__legal_forms-list" + method = "post" def test_validation__required(self): response = self.make_request(data={}, status_code=400) - self.assertValidationErrors(response, { - 'title': ['required'], - }) + self.assertValidationErrors( + response, + { + "title": ["required"], + }, + ) def test_create_legal_form(self): - data = self.factory.legal_form_data() + data = {"title": "Trust"} response = self.make_request(data=data, status_code=201) - legal_form = LegalForm.objects.get(id=response.data['id']) - self.assertPayloadEqual(response, { - 'id': legal_form.id, - 'title': 'Trust', - }) + legal_form = LegalForm.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + "id": legal_form.id, + "title": "Trust", + }, + ) self.assertEqual(legal_form.title, "Trust") class RetrieveLegalFormTestCase(CremeAPITestCase): - url_name = 'creme_api__legal_forms-detail' - method = 'get' + url_name = "creme_api__legal_forms-detail" + method = "get" def test_retrieve_legal_form(self): - legal_form = self.factory.legal_form() + legal_form = LegalFormFactory() response = self.make_request(to=legal_form.id, status_code=200) - self.assertPayloadEqual(response, { - 'id': legal_form.id, - 'title': 'Trust', - }) + self.assertPayloadEqual( + response, + { + "id": legal_form.id, + "title": "Trust", + }, + ) class UpdateLegalFormTestCase(CremeAPITestCase): - url_name = 'creme_api__legal_forms-detail' - method = 'put' + url_name = "creme_api__legal_forms-detail" + method = "put" def test_update_legal_form(self): - legal_form = self.factory.legal_form() - response = self.make_request(to=legal_form.id, data={ - 'title': "Corporation", - }, status_code=200) - self.assertPayloadEqual(response, { - 'id': legal_form.id, - 'title': 'Corporation', - }) + legal_form = LegalFormFactory() + response = self.make_request( + to=legal_form.id, + data={ + "title": "Corporation", + }, + status_code=200, + ) + self.assertPayloadEqual( + response, + { + "id": legal_form.id, + "title": "Corporation", + }, + ) legal_form.refresh_from_db() self.assertEqual(legal_form.title, "Corporation") class PartialUpdateLegalFormTestCase(CremeAPITestCase): - url_name = 'creme_api__legal_forms-detail' - method = 'patch' + url_name = "creme_api__legal_forms-detail" + method = "patch" def test_partial_update_legal_form(self): - legal_form = self.factory.legal_form() - response = self.make_request(to=legal_form.id, data={ - 'title': "Corporation", - }, status_code=200) - self.assertPayloadEqual(response, { - 'id': legal_form.id, - 'title': 'Corporation', - }) + legal_form = LegalFormFactory() + response = self.make_request( + to=legal_form.id, + data={ + "title": "Corporation", + }, + status_code=200, + ) + self.assertPayloadEqual( + response, + { + "id": legal_form.id, + "title": "Corporation", + }, + ) legal_form.refresh_from_db() self.assertEqual(legal_form.title, "Corporation") class ListLegalFormTestCase(CremeAPITestCase): - url_name = 'creme_api__legal_forms-list' - method = 'get' + url_name = "creme_api__legal_forms-list" + method = "get" def test_list_legal_forms(self): LegalForm.objects.all().delete() - legal_form1 = self.factory.legal_form(title="1") - legal_form2 = self.factory.legal_form(title="2") + legal_form1 = LegalFormFactory(title="1") + legal_form2 = LegalFormFactory(title="2") response = self.make_request(status_code=200) - self.assertPayloadEqual(response, [ - {'id': legal_form1.id, 'title': '1'}, - {'id': legal_form2.id, 'title': '2'}, - ]) + self.assertPayloadEqual( + response, + [ + {"id": legal_form1.id, "title": "1"}, + {"id": legal_form2.id, "title": "2"}, + ], + ) class DeleteLegalFormTestCase(CremeAPITestCase): - url_name = 'creme_api__legal_forms-detail' - method = 'delete' + url_name = "creme_api__legal_forms-detail" + method = "delete" def test_delete_legal_form(self): - legal_form = self.factory.legal_form() + legal_form = LegalFormFactory() self.make_request(to=legal_form.id, status_code=204) self.assertFalse(LegalForm.objects.filter(id=legal_form.id).exists()) diff --git a/creme/creme_api/tests/test_models.py b/creme/creme_api/tests/test_models.py index 11849f4d72..8848531747 100644 --- a/creme/creme_api/tests/test_models.py +++ b/creme/creme_api/tests/test_models.py @@ -33,16 +33,22 @@ def test_save01(self): application.save() self.assertIsNotNone(application._client_secret) self.assertIsNotNone(application.client_secret) - self.assertTrue(check_password(application._client_secret, application.client_secret)) + self.assertTrue( + check_password(application._client_secret, application.client_secret) + ) application.save() - self.assertTrue(check_password(application._client_secret, application.client_secret)) + self.assertTrue( + check_password(application._client_secret, application.client_secret) + ) def test_save02(self): application = Application.objects.create(name="TestCase") self.assertIsNotNone(application._client_secret) self.assertIsNotNone(application.client_secret) - self.assertTrue(check_password(application._client_secret, application.client_secret)) + self.assertTrue( + check_password(application._client_secret, application.client_secret) + ) def test_check_client_secret(self): application = Application(name="TestCase") @@ -71,7 +77,8 @@ def test_authenticate03(self): def test_authenticate04(self): application = Application.objects.create(name="TestCase") authenticated_application = Application.authenticate( - application.client_id, application._client_secret) + application.client_id, application._client_secret + ) self.assertEqual(authenticated_application.pk, application.pk) @@ -88,7 +95,8 @@ def test_save01(self): token.save() expected_expires = timezone.now() + timezone.timedelta(seconds=20) self.assertAlmostEqual( - token.expires, expected_expires, delta=timezone.timedelta(seconds=1)) + token.expires, expected_expires, delta=timezone.timedelta(seconds=1) + ) def test_save02(self): application = Application.objects.create(name="TestCase", token_duration=20) diff --git a/creme/creme_api/tests/test_organisations.py b/creme/creme_api/tests/test_organisations.py new file mode 100644 index 0000000000..74354402e0 --- /dev/null +++ b/creme/creme_api/tests/test_organisations.py @@ -0,0 +1,767 @@ +from datetime import date + +from django.utils.translation import gettext as _ + +from creme.creme_api.tests.utils import CremeAPITestCase +from creme.persons import get_organisation_model + +from .factories import ( + LegalFormFactory, + OrganisationFactory, + SectorFactory, + StaffSizeFactory, + UserFactory, +) + +Organisation = get_organisation_model() + +default_organisation_data = { + "description": "Description", + "name": "Creme Organisation", + "phone": "+330100000000", + "fax": "+330100000001", + "email": "creme.organisation@provider.com", + "url_site": "https://www.creme.organisation.provider.com", + "capital": 50000, + "annual_revenue": "1500000", + "siren": "001001001001", + "naf": "002002002002", + "siret": "003003003003", + "rcs": "004004004004", + "tvaintra": "005005005005", + "subject_to_vat": True, + "creation_date": "2005-05-24", + "sector": None, + "legal_form": None, + "staff_size": None, +} + +default_address_data = { + "address": "1 Main Street", + "po_box": "PO123", + "zipcode": "ZIP123", + "city": "City", + "department": "Dept", + "state": "State", + "country": "Country", +} + + +class CreateOrganisationTestCase(CremeAPITestCase): + url_name = "creme_api__organisations-list" + method = "post" + + def test_validation__required(self): + response = self.make_request(data={}, status_code=400) + self.assertValidationErrors( + response, + { + "name": ["required"], + "user": ["required"], + }, + ) + + def test_create_organisation(self): + user = UserFactory() + sector = SectorFactory() + legal_form = LegalFormFactory() + staff_size = StaffSizeFactory() + data = { + **default_organisation_data, + "user": user.id, + "sector": sector.id, + "legal_form": legal_form.id, + "staff_size": staff_size.id, + } + response = self.make_request(data=data, status_code=201) + organisation = Organisation.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + **data, + "id": organisation.id, + "uuid": str(organisation.uuid), + "created": self.to_iso8601(organisation.created), + "modified": self.to_iso8601(organisation.modified), + "is_deleted": False, + "is_managed": False, + }, + ) + self.assertFalse(organisation.is_deleted) + self.assertEqual(organisation.user, user) + self.assertEqual(organisation.description, "Description") + self.assertIsNone(organisation.billing_address) + self.assertIsNone(organisation.shipping_address) + self.assertEqual(organisation.name, "Creme Organisation") + self.assertFalse(organisation.is_managed) + self.assertEqual(organisation.phone, "+330100000000") + self.assertEqual(organisation.fax, "+330100000001") + self.assertEqual(organisation.email, "creme.organisation@provider.com") + self.assertEqual( + organisation.url_site, "https://www.creme.organisation.provider.com" + ) + self.assertEqual(organisation.sector, sector) + self.assertEqual(organisation.legal_form, legal_form) + self.assertEqual(organisation.staff_size, staff_size) + self.assertEqual(organisation.capital, 50000) + self.assertEqual(organisation.annual_revenue, "1500000") + self.assertEqual(organisation.siren, "001001001001") + self.assertEqual(organisation.naf, "002002002002") + self.assertEqual(organisation.siret, "003003003003") + self.assertEqual(organisation.rcs, "004004004004") + self.assertEqual(organisation.tvaintra, "005005005005") + self.assertTrue(organisation.subject_to_vat) + self.assertEqual(organisation.creation_date, date(2005, 5, 24)) + + def test_create_organisation__managed_readonly(self): + user = UserFactory() + data = { + **default_organisation_data, + "user": user.id, + "is_managed": True, + } + response = self.make_request(data=data, status_code=201) + organisation = Organisation.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + **data, + "id": organisation.id, + "uuid": str(organisation.uuid), + "created": self.to_iso8601(organisation.created), + "modified": self.to_iso8601(organisation.modified), + "is_managed": False, + "is_deleted": False, + }, + ) + self.assertFalse(organisation.is_managed) + + def test_create_organisation__with_addresses(self): + user = UserFactory() + data = { + **default_organisation_data, + "user": user.id, + "billing_address": {**default_address_data, "name": "NOT DISPLAYED"}, + "shipping_address": {**default_address_data, "name": "NOT DISPLAYED"}, + } + response = self.make_request(data=data, status_code=201) + organisation = Organisation.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + **data, + "id": organisation.id, + "uuid": str(organisation.uuid), + "created": self.to_iso8601(organisation.created), + "modified": self.to_iso8601(organisation.modified), + "is_deleted": False, + "is_managed": False, + "billing_address": default_address_data, + "shipping_address": default_address_data, + }, + ) + + billing_address = organisation.billing_address + self.assertEqual(billing_address.name, _("Billing address")) + self.assertEqual(billing_address.address, "1 Main Street") + self.assertEqual(billing_address.po_box, "PO123") + self.assertEqual(billing_address.zipcode, "ZIP123") + self.assertEqual(billing_address.city, "City") + self.assertEqual(billing_address.department, "Dept") + self.assertEqual(billing_address.state, "State") + self.assertEqual(billing_address.country, "Country") + self.assertEqual(billing_address.owner, organisation) + + shipping_address = organisation.shipping_address + self.assertEqual(shipping_address.name, _("Shipping address")) + self.assertEqual(shipping_address.address, "1 Main Street") + self.assertEqual(shipping_address.po_box, "PO123") + self.assertEqual(shipping_address.zipcode, "ZIP123") + self.assertEqual(shipping_address.city, "City") + self.assertEqual(shipping_address.department, "Dept") + self.assertEqual(shipping_address.state, "State") + self.assertEqual(shipping_address.country, "Country") + self.assertEqual(shipping_address.owner, organisation) + + +class RetrieveOrganisationTestCase(CremeAPITestCase): + url_name = "creme_api__organisations-detail" + method = "get" + + def test_get_organisation(self): + organisation = OrganisationFactory(shipping_address=True) + response = self.make_request(to=organisation.id, status_code=200) + self.assertPayloadEqual( + response, + { + "id": organisation.id, + "uuid": str(organisation.uuid), + "created": self.to_iso8601(organisation.created), + "modified": self.to_iso8601(organisation.modified), + "description": "Description", + "name": "Creme Organisation", + "phone": "+330100000000", + "fax": "+330100000001", + "email": "creme.organisation@provider.com", + "url_site": "https://www.creme.organisation.provider.com", + "capital": 50000, + "annual_revenue": "1500000", + "siren": "001001001001", + "naf": "002002002002", + "siret": "003003003003", + "rcs": "004004004004", + "tvaintra": "005005005005", + "subject_to_vat": True, + "creation_date": "2005-05-24", + "is_deleted": False, + "is_managed": False, + "sector": organisation.sector_id, + "legal_form": organisation.legal_form_id, + "staff_size": organisation.staff_size_id, + "user": organisation.user_id, + "shipping_address": default_address_data, + }, + ) + + +class UpdateOrganisationTestCase(CremeAPITestCase): + url_name = "creme_api__organisations-detail" + method = "put" + + def test_validation__required(self): + organisation = OrganisationFactory() + response = self.make_request(to=organisation.id, data={}, status_code=400) + self.assertValidationErrors( + response, + { + "name": ["required"], + "user": ["required"], + }, + ) + + def test_update_organisation(self): + organisation = OrganisationFactory(shipping_address=True, sector=None) + user = UserFactory() + legal_form = LegalFormFactory() + sector = SectorFactory() + + data = { + **default_organisation_data, + "user": user.id, + "legal_form": legal_form.id, + "staff_size": None, + "sector": sector.id, + "name": "A Creme Company", + } + response = self.make_request(to=organisation.id, data=data, status_code=200) + organisation.refresh_from_db() + self.assertPayloadEqual( + response, + { + **data, + "id": organisation.id, + "uuid": str(organisation.uuid), + "created": self.to_iso8601(organisation.created), + "modified": self.to_iso8601(organisation.modified), + "is_deleted": False, + "is_managed": False, + "shipping_address": default_address_data, + }, + ) + self.assertFalse(organisation.is_deleted) + self.assertEqual(organisation.user, organisation.user) + self.assertEqual(organisation.description, "Description") + self.assertIsNone(organisation.billing_address) + self.assertEqual(organisation.name, "A Creme Company") + self.assertFalse(organisation.is_managed) + self.assertEqual(organisation.phone, "+330100000000") + self.assertEqual(organisation.fax, "+330100000001") + self.assertEqual(organisation.email, "creme.organisation@provider.com") + self.assertEqual( + organisation.url_site, "https://www.creme.organisation.provider.com" + ) + self.assertEqual(organisation.sector, sector) + self.assertEqual(organisation.legal_form, legal_form) + self.assertIsNone(organisation.staff_size) + self.assertEqual(organisation.capital, 50000) + self.assertEqual(organisation.annual_revenue, "1500000") + self.assertEqual(organisation.siren, "001001001001") + self.assertEqual(organisation.naf, "002002002002") + self.assertEqual(organisation.siret, "003003003003") + self.assertEqual(organisation.rcs, "004004004004") + self.assertEqual(organisation.tvaintra, "005005005005") + self.assertTrue(organisation.subject_to_vat) + self.assertEqual(organisation.creation_date, date(2005, 5, 24)) + + shipping_address = organisation.shipping_address + self.assertEqual(shipping_address.name, _("Shipping address")) + self.assertEqual(shipping_address.address, "1 Main Street") + self.assertEqual(shipping_address.po_box, "PO123") + self.assertEqual(shipping_address.zipcode, "ZIP123") + self.assertEqual(shipping_address.city, "City") + self.assertEqual(shipping_address.department, "Dept") + self.assertEqual(shipping_address.state, "State") + self.assertEqual(shipping_address.country, "Country") + self.assertEqual(shipping_address.owner, organisation) + + def test_update_organisation__create_addresses(self): + organisation = OrganisationFactory() + + data = { + **default_organisation_data, + "user": organisation.user_id, + "billing_address": {**default_address_data, "address": "billing street"}, + "shipping_address": {**default_address_data, "address": "shipping street"}, + } + response = self.make_request(to=organisation.id, data=data, status_code=200) + organisation.refresh_from_db() + self.assertPayloadEqual( + response, + { + **data, + "id": organisation.id, + "uuid": str(organisation.uuid), + "created": self.to_iso8601(organisation.created), + "modified": self.to_iso8601(organisation.modified), + "is_deleted": False, + "is_managed": False, + }, + ) + self.assertFalse(organisation.is_deleted) + self.assertEqual(organisation.user, organisation.user) + self.assertEqual(organisation.description, "Description") + self.assertEqual(organisation.name, "Creme Organisation") + self.assertFalse(organisation.is_managed) + self.assertEqual(organisation.phone, "+330100000000") + self.assertEqual(organisation.fax, "+330100000001") + self.assertEqual(organisation.email, "creme.organisation@provider.com") + self.assertEqual( + organisation.url_site, "https://www.creme.organisation.provider.com" + ) + self.assertEqual(organisation.capital, 50000) + self.assertEqual(organisation.annual_revenue, "1500000") + self.assertEqual(organisation.siren, "001001001001") + self.assertEqual(organisation.naf, "002002002002") + self.assertEqual(organisation.siret, "003003003003") + self.assertEqual(organisation.rcs, "004004004004") + self.assertEqual(organisation.tvaintra, "005005005005") + self.assertTrue(organisation.subject_to_vat) + self.assertEqual(organisation.creation_date, date(2005, 5, 24)) + + billing_address = organisation.billing_address + self.assertEqual(billing_address.name, _("Billing address")) + self.assertEqual(billing_address.address, "billing street") + self.assertEqual(billing_address.po_box, "PO123") + self.assertEqual(billing_address.zipcode, "ZIP123") + self.assertEqual(billing_address.city, "City") + self.assertEqual(billing_address.department, "Dept") + self.assertEqual(billing_address.state, "State") + self.assertEqual(billing_address.country, "Country") + self.assertEqual(billing_address.owner, organisation) + + shipping_address = organisation.shipping_address + self.assertEqual(shipping_address.name, _("Shipping address")) + self.assertEqual(shipping_address.address, "shipping street") + self.assertEqual(shipping_address.po_box, "PO123") + self.assertEqual(shipping_address.zipcode, "ZIP123") + self.assertEqual(shipping_address.city, "City") + self.assertEqual(shipping_address.department, "Dept") + self.assertEqual(shipping_address.state, "State") + self.assertEqual(shipping_address.country, "Country") + self.assertEqual(shipping_address.owner, organisation) + + def test_update_organisation__update_addresses(self): + organisation = OrganisationFactory(billing_address=True, shipping_address=True) + + data = { + **default_organisation_data, + "user": organisation.user_id, + "billing_address": {**default_address_data, "address": "billing street"}, + "shipping_address": {**default_address_data, "address": "shipping street"}, + } + response = self.make_request(to=organisation.id, data=data, status_code=200) + organisation.refresh_from_db() + self.assertPayloadEqual( + response, + { + **data, + "id": organisation.id, + "uuid": str(organisation.uuid), + "created": self.to_iso8601(organisation.created), + "modified": self.to_iso8601(organisation.modified), + "is_deleted": False, + "is_managed": False, + }, + ) + + self.assertFalse(organisation.is_deleted) + self.assertEqual(organisation.user, organisation.user) + self.assertEqual(organisation.description, "Description") + self.assertEqual(organisation.name, "Creme Organisation") + self.assertFalse(organisation.is_managed) + self.assertEqual(organisation.phone, "+330100000000") + self.assertEqual(organisation.fax, "+330100000001") + self.assertEqual(organisation.email, "creme.organisation@provider.com") + self.assertEqual( + organisation.url_site, "https://www.creme.organisation.provider.com" + ) + self.assertEqual(organisation.capital, 50000) + self.assertEqual(organisation.annual_revenue, "1500000") + self.assertEqual(organisation.siren, "001001001001") + self.assertEqual(organisation.naf, "002002002002") + self.assertEqual(organisation.siret, "003003003003") + self.assertEqual(organisation.rcs, "004004004004") + self.assertEqual(organisation.tvaintra, "005005005005") + self.assertTrue(organisation.subject_to_vat) + self.assertEqual(organisation.creation_date, date(2005, 5, 24)) + + billing_address = organisation.billing_address + self.assertEqual(billing_address.name, _("Billing address")) + self.assertEqual(billing_address.address, "billing street") + self.assertEqual(billing_address.po_box, "PO123") + self.assertEqual(billing_address.zipcode, "ZIP123") + self.assertEqual(billing_address.city, "City") + self.assertEqual(billing_address.department, "Dept") + self.assertEqual(billing_address.state, "State") + self.assertEqual(billing_address.country, "Country") + self.assertEqual(billing_address.owner, organisation) + + shipping_address = organisation.shipping_address + self.assertEqual(shipping_address.name, _("Shipping address")) + self.assertEqual(shipping_address.address, "shipping street") + self.assertEqual(shipping_address.po_box, "PO123") + self.assertEqual(shipping_address.zipcode, "ZIP123") + self.assertEqual(shipping_address.city, "City") + self.assertEqual(shipping_address.department, "Dept") + self.assertEqual(shipping_address.state, "State") + self.assertEqual(shipping_address.country, "Country") + self.assertEqual(shipping_address.owner, organisation) + + +class PartialUpdateOrganisationTestCase(CremeAPITestCase): + url_name = "creme_api__organisations-detail" + method = "patch" + + def test_partial_update_organisation(self): + organisation = OrganisationFactory( + shipping_address=True, legal_form=None, sector=None + ) + legal_form = LegalFormFactory() + sector = SectorFactory() + data = { + "legal_form": legal_form.id, + "staff_size": None, + "sector": sector.id, + "name": "A Creme Company", + } + response = self.make_request(to=organisation.id, data=data, status_code=200) + organisation.refresh_from_db() + self.assertPayloadEqual( + response, + { + **default_organisation_data, + "id": organisation.id, + "user": organisation.user_id, + "uuid": str(organisation.uuid), + "created": self.to_iso8601(organisation.created), + "modified": self.to_iso8601(organisation.modified), + "is_deleted": False, + "is_managed": False, + "shipping_address": default_address_data, + **data, + }, + ) + self.assertEqual(organisation.legal_form, legal_form) + self.assertIsNone(organisation.staff_size) + self.assertEqual(organisation.sector, sector) + self.assertEqual(organisation.name, "A Creme Company") + + def test_partial_update_organisation__create_addresses(self): + organisation = OrganisationFactory() + + data = { + "billing_address": { + "address": "billing street", + }, + "shipping_address": { + "address": "shipping street", + }, + } + response = self.make_request(to=organisation.id, data=data, status_code=200) + organisation.refresh_from_db() + self.assertPayloadEqual( + response, + { + **default_organisation_data, + "id": organisation.id, + "user": organisation.user_id, + "uuid": str(organisation.uuid), + "created": self.to_iso8601(organisation.created), + "modified": self.to_iso8601(organisation.modified), + "sector": organisation.sector_id, + "legal_form": organisation.legal_form_id, + "staff_size": organisation.staff_size_id, + "is_deleted": False, + "is_managed": False, + "billing_address": { + "address": "billing street", + "city": "", + "country": "", + "department": "", + "po_box": "", + "state": "", + "zipcode": "", + }, + "shipping_address": { + "address": "shipping street", + "city": "", + "country": "", + "department": "", + "po_box": "", + "state": "", + "zipcode": "", + }, + }, + ) + + billing_address = organisation.billing_address + self.assertEqual(billing_address.name, _("Billing address")) + self.assertEqual(billing_address.address, "billing street") + self.assertEqual(billing_address.po_box, "") + self.assertEqual(billing_address.zipcode, "") + self.assertEqual(billing_address.city, "") + self.assertEqual(billing_address.department, "") + self.assertEqual(billing_address.state, "") + self.assertEqual(billing_address.country, "") + self.assertEqual(billing_address.owner, organisation) + + shipping_address = organisation.shipping_address + self.assertEqual(shipping_address.name, _("Shipping address")) + self.assertEqual(shipping_address.address, "shipping street") + self.assertEqual(shipping_address.po_box, "") + self.assertEqual(shipping_address.zipcode, "") + self.assertEqual(shipping_address.city, "") + self.assertEqual(shipping_address.department, "") + self.assertEqual(shipping_address.state, "") + self.assertEqual(shipping_address.country, "") + self.assertEqual(shipping_address.owner, organisation) + + def test_partial_update_organisation__update_addresses(self): + organisation = OrganisationFactory(billing_address=True, shipping_address=True) + + data = { + "billing_address": { + "address": "billing street", + }, + "shipping_address": { + "address": "shipping street", + }, + } + response = self.make_request(to=organisation.id, data=data, status_code=200) + organisation.refresh_from_db() + self.assertPayloadEqual( + response, + { + **default_organisation_data, + "user": organisation.user_id, + "id": organisation.id, + "is_deleted": False, + "is_managed": False, + "uuid": str(organisation.uuid), + "created": self.to_iso8601(organisation.created), + "modified": self.to_iso8601(organisation.modified), + "sector": organisation.sector_id, + "legal_form": organisation.legal_form_id, + "staff_size": organisation.staff_size_id, + "billing_address": { + **default_address_data, + "address": "billing street", + }, + "shipping_address": { + **default_address_data, + "address": "shipping street", + }, + }, + ) + billing_address = organisation.billing_address + self.assertEqual(billing_address.name, _("Billing address")) + self.assertEqual(billing_address.address, "billing street") + self.assertEqual(billing_address.po_box, "PO123") + self.assertEqual(billing_address.zipcode, "ZIP123") + self.assertEqual(billing_address.city, "City") + self.assertEqual(billing_address.department, "Dept") + self.assertEqual(billing_address.state, "State") + self.assertEqual(billing_address.country, "Country") + self.assertEqual(billing_address.owner, organisation) + + shipping_address = organisation.shipping_address + self.assertEqual(shipping_address.name, _("Shipping address")) + self.assertEqual(shipping_address.address, "shipping street") + self.assertEqual(shipping_address.po_box, "PO123") + self.assertEqual(shipping_address.zipcode, "ZIP123") + self.assertEqual(shipping_address.city, "City") + self.assertEqual(shipping_address.department, "Dept") + self.assertEqual(shipping_address.state, "State") + self.assertEqual(shipping_address.country, "Country") + self.assertEqual(shipping_address.owner, organisation) + + +class ListOrganisationTestCase(CremeAPITestCase): + url_name = "creme_api__organisations-list" + method = "get" + + def test_list_organisations(self): + managed_organisation = Organisation.objects.get(is_managed=True) + organisation = OrganisationFactory.create( + billing_address=True, shipping_address=True + ) + self.assertEqual(Organisation.objects.count(), 2, Organisation.objects.all()) + + response = self.make_request(status_code=200) + self.assertPayloadEqual( + response, + [ + { + "id": managed_organisation.id, + "uuid": str(managed_organisation.uuid), + "created": self.to_iso8601(managed_organisation.created), + "modified": self.to_iso8601(managed_organisation.modified), + "user": managed_organisation.user_id, + "annual_revenue": "", + "capital": None, + "creation_date": None, + "description": "", + "email": "", + "fax": "", + "is_deleted": False, + "is_managed": True, + "legal_form": None, + "naf": "", + "name": _("ReplaceByYourSociety"), + "phone": "", + "rcs": "", + "sector": None, + "siren": "", + "siret": "", + "staff_size": None, + "subject_to_vat": True, + "tvaintra": "", + "url_site": "", + }, + { + **default_organisation_data, + "id": organisation.id, + "uuid": str(organisation.uuid), + "created": self.to_iso8601(organisation.created), + "modified": self.to_iso8601(organisation.modified), + "user": organisation.user_id, + "is_deleted": False, + "is_managed": False, + "billing_address": default_address_data, + "shipping_address": default_address_data, + "legal_form": organisation.legal_form_id, + "sector": organisation.sector_id, + "staff_size": organisation.staff_size_id, + }, + ], + ) + + +class TrashOrganisationTestCase(CremeAPITestCase): + url_name = "creme_api__organisations-trash" + method = "post" + + def test_trash_organisation__protected(self): + last_managed_organisation = Organisation.objects.get(is_managed=True) + response = self.make_request(to=last_managed_organisation.id, status_code=422) + self.assertEqual(response.data["detail"].code, "protected") + + def test_trash_organisation(self): + organisation = OrganisationFactory() + response = self.make_request(to=organisation.id, status_code=200) + + organisation.refresh_from_db() + self.assertPayloadEqual( + response, + { + "id": organisation.id, + "uuid": str(organisation.uuid), + "created": self.to_iso8601(organisation.created), + "modified": self.to_iso8601(organisation.modified), + "is_deleted": True, + }, + ) + self.make_request(to=organisation.id, status_code=200) + + +class RestoreOrganisationTestCase(CremeAPITestCase): + url_name = "creme_api__organisations-restore" + method = "post" + + def test_restore_organisation(self): + organisation = OrganisationFactory(is_deleted=True) + organisation.refresh_from_db() + self.assertTrue(organisation.is_deleted) + + response = self.make_request(to=organisation.id, status_code=200) + + organisation.refresh_from_db() + self.assertPayloadEqual( + response, + { + "id": organisation.id, + "uuid": str(organisation.uuid), + "created": self.to_iso8601(organisation.created), + "modified": self.to_iso8601(organisation.modified), + "is_deleted": False, + }, + ) + self.assertFalse(organisation.is_deleted) + self.make_request(to=organisation.id, status_code=200) + organisation.refresh_from_db() + self.assertFalse(organisation.is_deleted) + + +class DeleteOrganisationTestCase(CremeAPITestCase): + url_name = "creme_api__organisations-detail" + method = "delete" + + def test_delete_organisation__protected(self): + last_managed_organisation = Organisation.objects.get(is_managed=True) + response = self.make_request(to=last_managed_organisation.id, status_code=422) + self.assertEqual(response.data["detail"].code, "protected") + + def test_delete_organisation(self): + organisation = OrganisationFactory() + self.make_request(to=organisation.id, status_code=204) + self.assertFalse(Organisation.objects.filter(id=organisation.id).exists()) + + +class CloneOrganisationTestCase(CremeAPITestCase): + url_name = "creme_api__organisations-clone" + method = "post" + + def test_clone_organisation(self): + organisation = OrganisationFactory(shipping_address=True) + + response = self.make_request(to=organisation.id, status_code=201) + cloned_organisation = Organisation.objects.get(id=response.data["id"]) + self.assertNotEqual(cloned_organisation.id, organisation.id) + self.assertNotEqual(cloned_organisation.uuid, organisation.uuid) + self.assertPayloadEqual( + response, + { + **default_organisation_data, + "id": cloned_organisation.id, + "uuid": str(cloned_organisation.uuid), + "created": self.to_iso8601(cloned_organisation.created), + "modified": self.to_iso8601(cloned_organisation.modified), + "is_deleted": False, + "is_managed": False, + "user": organisation.user_id, + "sector": organisation.sector_id, + "legal_form": organisation.legal_form_id, + "staff_size": organisation.staff_size_id, + "shipping_address": default_address_data, + }, + ) diff --git a/creme/creme_api/tests/test_positions.py b/creme/creme_api/tests/test_positions.py index c1ab1d0c8f..899329dfb5 100644 --- a/creme/creme_api/tests/test_positions.py +++ b/creme/creme_api/tests/test_positions.py @@ -1,107 +1,123 @@ -from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.creme_api.tests.utils import CremeAPITestCase from creme.persons.models import Position - -@Factory.register -def position(factory, **kwargs): - data = factory.position_data(**kwargs) - return Position.objects.create(**data) - - -@Factory.register -def position_data(factory, **kwargs): - kwargs.setdefault('title', 'Captain') - return kwargs +from .factories import PositionFactory class CreatePositionTestCase(CremeAPITestCase): - url_name = 'creme_api__positions-list' - method = 'post' + url_name = "creme_api__positions-list" + method = "post" def test_validation__required(self): response = self.make_request(data={}, status_code=400) - self.assertValidationErrors(response, { - 'title': ['required'], - }) + self.assertValidationErrors( + response, + { + "title": ["required"], + }, + ) def test_create_position(self): - data = self.factory.position_data() + data = {"title": "Captain"} response = self.make_request(data=data, status_code=201) - position = Position.objects.get(id=response.data['id']) - self.assertPayloadEqual(response, { - 'id': position.id, - 'title': 'Captain', - }) + position = Position.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + "id": position.id, + "title": "Captain", + }, + ) self.assertEqual(position.title, "Captain") class RetrievePositionTestCase(CremeAPITestCase): - url_name = 'creme_api__positions-detail' - method = 'get' + url_name = "creme_api__positions-detail" + method = "get" def test_retrieve_position(self): - position = self.factory.position() + position = PositionFactory() response = self.make_request(to=position.id, status_code=200) - self.assertPayloadEqual(response, { - 'id': position.id, - 'title': 'Captain', - }) + self.assertPayloadEqual( + response, + { + "id": position.id, + "title": "Captain", + }, + ) class UpdatePositionTestCase(CremeAPITestCase): - url_name = 'creme_api__positions-detail' - method = 'put' + url_name = "creme_api__positions-detail" + method = "put" def test_update_position(self): - position = self.factory.position() - response = self.make_request(to=position.id, data={ - 'title': "CAPTAIN", - }, status_code=200) - self.assertPayloadEqual(response, { - 'id': position.id, - 'title': 'CAPTAIN', - }) + position = PositionFactory() + response = self.make_request( + to=position.id, + data={ + "title": "CAPTAIN", + }, + status_code=200, + ) + self.assertPayloadEqual( + response, + { + "id": position.id, + "title": "CAPTAIN", + }, + ) position.refresh_from_db() self.assertEqual(position.title, "CAPTAIN") class PartialUpdatePositionTestCase(CremeAPITestCase): - url_name = 'creme_api__positions-detail' - method = 'patch' + url_name = "creme_api__positions-detail" + method = "patch" def test_partial_update_position(self): - position = self.factory.position() - response = self.make_request(to=position.id, data={ - 'title': 'CAPTAIN', - }, status_code=200) - self.assertPayloadEqual(response, { - 'id': position.id, - 'title': 'CAPTAIN', - }) + position = PositionFactory() + response = self.make_request( + to=position.id, + data={ + "title": "CAPTAIN", + }, + status_code=200, + ) + self.assertPayloadEqual( + response, + { + "id": position.id, + "title": "CAPTAIN", + }, + ) position.refresh_from_db() self.assertEqual(position.title, "CAPTAIN") class ListPositionTestCase(CremeAPITestCase): - url_name = 'creme_api__positions-list' - method = 'get' + url_name = "creme_api__positions-list" + method = "get" def test_list_positions(self): Position.objects.all().delete() - position1 = self.factory.position(title="1") - position2 = self.factory.position(title="2") + position1 = PositionFactory(title="1") + position2 = PositionFactory(title="2") response = self.make_request(status_code=200) - self.assertPayloadEqual(response, [ - {'id': position1.id, 'title': '1'}, - {'id': position2.id, 'title': '2'}, - ]) + self.assertPayloadEqual( + response, + [ + {"id": position1.id, "title": "1"}, + {"id": position2.id, "title": "2"}, + ], + ) class DeletePositionTestCase(CremeAPITestCase): - url_name = 'creme_api__positions-detail' - method = 'delete' + url_name = "creme_api__positions-detail" + method = "delete" def test_delete_position(self): - position = self.factory.position() + position = PositionFactory() self.make_request(to=position.id, status_code=204) self.assertFalse(Position.objects.filter(id=position.id).exists()) diff --git a/creme/creme_api/tests/test_roles.py b/creme/creme_api/tests/test_roles.py index 8d223de689..f1b9408c5c 100644 --- a/creme/creme_api/tests/test_roles.py +++ b/creme/creme_api/tests/test_roles.py @@ -1,305 +1,319 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType -from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.creme_api.tests.utils import CremeAPITestCase from creme.creme_core.models import UserRole from creme.persons import get_contact_model, get_organisation_model +from .factories import RoleFactory, UserFactory + CremeUser = get_user_model() Contact = get_contact_model() Organisation = get_organisation_model() -@Factory.register -def role(factory, **kwargs): - contact_ct = ContentType.objects.get_for_model(Contact) - orga_ct = ContentType.objects.get_for_model(Organisation) - data = { - 'name': "Basic", - 'allowed_apps': ['creme_core', 'creme_api', 'persons'], - 'admin_4_apps': ['creme_core', 'creme_api'], - 'creatable_ctypes': [contact_ct.id, orga_ct.id], - 'exportable_ctypes': [contact_ct.id], - } - data.update(**kwargs) - role = UserRole(name=data['name']) - role.allowed_apps = data['allowed_apps'] - role.admin_4_apps = data['admin_4_apps'] - role.save() - role.creatable_ctypes.set(data['creatable_ctypes']) - role.exportable_ctypes.set(data['exportable_ctypes']) - return role - - class CreateRoleTestCase(CremeAPITestCase): - url_name = 'creme_api__roles-list' - method = 'post' + url_name = "creme_api__roles-list" + method = "post" def test_validation__required(self): response = self.make_request(data={}, status_code=400) - self.assertValidationErrors(response, { - 'name': ['required'], - 'allowed_apps': ['required'], - 'admin_4_apps': ['required'], - 'creatable_ctypes': ['required'], - 'exportable_ctypes': ['required'], - }) + self.assertValidationErrors( + response, + { + "name": ["required"], + "allowed_apps": ["required"], + "admin_4_apps": ["required"], + "creatable_ctypes": ["required"], + "exportable_ctypes": ["required"], + }, + ) def test_validation__name_unique(self): - self.factory.role(name="UniqueRoleName") + RoleFactory(name="UniqueRoleName") data = { - 'name': "UniqueRoleName", - 'allowed_apps': [], - 'admin_4_apps': [], - 'creatable_ctypes': [], - 'exportable_ctypes': [], + "name": "UniqueRoleName", + "allowed_apps": [], + "admin_4_apps": [], + "creatable_ctypes": [], + "exportable_ctypes": [], } response = self.make_request(data=data, status_code=400) - self.assertValidationError(response, 'name', ['unique']) + self.assertValidationError(response, "name", ["unique"]) def test_validation(self): contact_ct = ContentType.objects.get_for_model(Contact) orga_ct = ContentType.objects.get_for_model(Organisation) data = { - 'name': "CEO", - 'allowed_apps': ['creme_core'], - 'admin_4_apps': ['creme_core', 'creme_api', 'persons'], - 'creatable_ctypes': [contact_ct.id, orga_ct.id], - 'exportable_ctypes': [contact_ct.id, orga_ct.id], + "name": "CEO", + "allowed_apps": ["creme_core"], + "admin_4_apps": ["creme_core", "creme_api", "persons"], + "creatable_ctypes": [contact_ct.id, orga_ct.id], + "exportable_ctypes": [contact_ct.id, orga_ct.id], } response = self.make_request(data=data, status_code=400) - self.assertValidationErrors(response, { - 'admin_4_apps': ["admin_4_not_allowed_app", "admin_4_not_allowed_app"], - 'creatable_ctypes': ["not_allowed_ctype", "not_allowed_ctype"], - 'exportable_ctypes': ["not_allowed_ctype", "not_allowed_ctype"], - }) + self.assertValidationErrors( + response, + { + "admin_4_apps": ["admin_4_not_allowed_app", "admin_4_not_allowed_app"], + "creatable_ctypes": ["not_allowed_ctype", "not_allowed_ctype"], + "exportable_ctypes": ["not_allowed_ctype", "not_allowed_ctype"], + }, + ) def test_create_role(self): contact_ct = ContentType.objects.get_for_model(Contact) orga_ct = ContentType.objects.get_for_model(Organisation) data = { - 'name': "CEO", - 'allowed_apps': ['creme_core', 'creme_api', 'persons'], - 'admin_4_apps': ['creme_core', 'creme_api'], - 'creatable_ctypes': [contact_ct.id, orga_ct.id], - 'exportable_ctypes': [], + "name": "CEO", + "allowed_apps": ["creme_core", "creme_api", "persons"], + "admin_4_apps": ["creme_core", "creme_api"], + "creatable_ctypes": [contact_ct.id, orga_ct.id], + "exportable_ctypes": [], } response = self.make_request(data=data, status_code=201) - role = UserRole.objects.get(id=response.data['id']) - self.assertPayloadEqual(response, { - 'id': role.id, - 'name': "CEO", - 'allowed_apps': {'creme_core', 'creme_api', 'persons'}, - 'admin_4_apps': {'creme_core', 'creme_api'}, - 'creatable_ctypes': [contact_ct.id, orga_ct.id], - 'exportable_ctypes': [], - 'credentials': [], - }) + role = UserRole.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + "id": role.id, + "name": "CEO", + "allowed_apps": {"creme_core", "creme_api", "persons"}, + "admin_4_apps": {"creme_core", "creme_api"}, + "creatable_ctypes": [contact_ct.id, orga_ct.id], + "exportable_ctypes": [], + "credentials": [], + }, + ) self.assertEqual(role.name, "CEO") - self.assertEqual(role.allowed_apps, {'creme_core', 'persons', 'creme_api'}) - self.assertEqual(role.admin_4_apps, {'creme_core', 'creme_api'}) + self.assertEqual(role.allowed_apps, {"creme_core", "persons", "creme_api"}) + self.assertEqual(role.admin_4_apps, {"creme_core", "creme_api"}) self.assertEqual(list(role.creatable_ctypes.all()), [contact_ct, orga_ct]) self.assertEqual(list(role.exportable_ctypes.all()), []) class RetrieveRoleTestCase(CremeAPITestCase): - url_name = 'creme_api__roles-detail' - method = 'get' + url_name = "creme_api__roles-detail" + method = "get" def test_retrieve_role(self): contact_ct = ContentType.objects.get_for_model(Contact) orga_ct = ContentType.objects.get_for_model(Organisation) - role = self.factory.role() + role = RoleFactory() response = self.make_request(to=role.id, status_code=200) - self.assertPayloadEqual(response, { - 'id': role.id, - 'name': "Basic", - 'allowed_apps': {'creme_core', 'creme_api', 'persons'}, - 'admin_4_apps': {'creme_core', 'creme_api'}, - 'creatable_ctypes': [contact_ct.id, orga_ct.id], - 'exportable_ctypes': [contact_ct.id], - 'credentials': [], - }) + self.assertPayloadEqual( + response, + { + "id": role.id, + "name": "Basic", + "allowed_apps": {"creme_core", "creme_api", "persons"}, + "admin_4_apps": {"creme_core", "creme_api"}, + "creatable_ctypes": [contact_ct.id, orga_ct.id], + "exportable_ctypes": [contact_ct.id], + "credentials": [], + }, + ) class UpdateRoleTestCase(CremeAPITestCase): - url_name = 'creme_api__roles-detail' - method = 'put' + url_name = "creme_api__roles-detail" + method = "put" def test_validation__required(self): - role = self.factory.role() + role = RoleFactory() response = self.make_request(to=role.id, data={}, status_code=400) - self.assertValidationErrors(response, { - 'name': ['required'], - 'allowed_apps': ['required'], - 'admin_4_apps': ['required'], - 'creatable_ctypes': ['required'], - 'exportable_ctypes': ['required'], - }) + self.assertValidationErrors( + response, + { + "name": ["required"], + "allowed_apps": ["required"], + "admin_4_apps": ["required"], + "creatable_ctypes": ["required"], + "exportable_ctypes": ["required"], + }, + ) def test_validation(self): contact_ct = ContentType.objects.get_for_model(Contact) orga_ct = ContentType.objects.get_for_model(Organisation) - role = self.factory.role() + role = RoleFactory() data = { - 'name': "CEO", - 'allowed_apps': ['creme_core'], - 'admin_4_apps': ['creme_core', 'persons'], - 'creatable_ctypes': [contact_ct.id], - 'exportable_ctypes': [orga_ct.id], + "name": "CEO", + "allowed_apps": ["creme_core"], + "admin_4_apps": ["creme_core", "persons"], + "creatable_ctypes": [contact_ct.id], + "exportable_ctypes": [orga_ct.id], } response = self.make_request(to=role.id, data=data, status_code=400) - self.assertValidationErrors(response, { - 'admin_4_apps': ["admin_4_not_allowed_app"], - 'creatable_ctypes': ["not_allowed_ctype"], - 'exportable_ctypes': ["not_allowed_ctype"], - }) + self.assertValidationErrors( + response, + { + "admin_4_apps": ["admin_4_not_allowed_app"], + "creatable_ctypes": ["not_allowed_ctype"], + "exportable_ctypes": ["not_allowed_ctype"], + }, + ) def test_update_role(self): contact_ct = ContentType.objects.get_for_model(Contact) orga_ct = ContentType.objects.get_for_model(Organisation) - role = self.factory.role() + role = RoleFactory() data = { - 'name': "CEO", - 'allowed_apps': ['creme_core', 'persons'], - 'admin_4_apps': ['creme_core', 'persons'], - 'creatable_ctypes': [contact_ct.id], - 'exportable_ctypes': [orga_ct.id], + "name": "CEO", + "allowed_apps": ["creme_core", "persons"], + "admin_4_apps": ["creme_core", "persons"], + "creatable_ctypes": [contact_ct.id], + "exportable_ctypes": [orga_ct.id], } response = self.make_request(to=role.id, data=data, status_code=200) - self.assertPayloadEqual(response, { - 'id': role.id, - 'name': "CEO", - 'allowed_apps': {'creme_core', 'persons'}, - 'admin_4_apps': {'creme_core', 'persons'}, - 'creatable_ctypes': [contact_ct.id], - 'exportable_ctypes': [orga_ct.id], - 'credentials': [], - }) + self.assertPayloadEqual( + response, + { + "id": role.id, + "name": "CEO", + "allowed_apps": {"creme_core", "persons"}, + "admin_4_apps": {"creme_core", "persons"}, + "creatable_ctypes": [contact_ct.id], + "exportable_ctypes": [orga_ct.id], + "credentials": [], + }, + ) role.refresh_from_db() self.assertEqual(role.name, "CEO") - self.assertEqual(role.allowed_apps, {'creme_core', 'persons'}) - self.assertEqual(role.admin_4_apps, {'creme_core', 'persons'}) + self.assertEqual(role.allowed_apps, {"creme_core", "persons"}) + self.assertEqual(role.admin_4_apps, {"creme_core", "persons"}) self.assertEqual(list(role.creatable_ctypes.all()), [contact_ct]) self.assertEqual(list(role.exportable_ctypes.all()), [orga_ct]) class PartialUpdateRoleTestCase(CremeAPITestCase): - url_name = 'creme_api__roles-detail' - method = 'patch' + url_name = "creme_api__roles-detail" + method = "patch" def test_validation__name_unique(self): - self.factory.role(name="UniqueRoleName") - role = self.factory.role(name="OtherName") + RoleFactory(name="UniqueRoleName") + role = RoleFactory(name="OtherName") data = { - 'name': "UniqueRoleName", + "name": "UniqueRoleName", } response = self.make_request(to=role.id, data=data, status_code=400) - self.assertValidationError(response, 'name', ['unique']) + self.assertValidationError(response, "name", ["unique"]) def test_validation(self): - role = self.factory.role() + role = RoleFactory() data = { - 'allowed_apps': ['creme_core'], + "allowed_apps": ["creme_core"], } response = self.make_request(to=role.id, data=data, status_code=400) - self.assertValidationErrors(response, { - 'admin_4_apps': ["admin_4_not_allowed_app"], - 'creatable_ctypes': ["not_allowed_ctype", "not_allowed_ctype"], - 'exportable_ctypes': ["not_allowed_ctype"], - }) + self.assertValidationErrors( + response, + { + "admin_4_apps": ["admin_4_not_allowed_app"], + "creatable_ctypes": ["not_allowed_ctype", "not_allowed_ctype"], + "exportable_ctypes": ["not_allowed_ctype"], + }, + ) def test_partial_update_role(self): contact_ct = ContentType.objects.get_for_model(Contact) orga_ct = ContentType.objects.get_for_model(Organisation) - role = self.factory.role() + role = RoleFactory() data = { - 'name': "CEO", + "name": "CEO", } response = self.make_request(to=role.id, data=data, status_code=200) - self.assertPayloadEqual(response, { - 'id': role.id, - 'name': "CEO", - 'allowed_apps': {'creme_core', 'persons', 'creme_api'}, - 'admin_4_apps': {'creme_core', 'creme_api'}, - 'creatable_ctypes': [contact_ct.id, orga_ct.id], - 'exportable_ctypes': [contact_ct.id], - 'credentials': [], - }) + self.assertPayloadEqual( + response, + { + "id": role.id, + "name": "CEO", + "allowed_apps": {"creme_core", "persons", "creme_api"}, + "admin_4_apps": {"creme_core", "creme_api"}, + "creatable_ctypes": [contact_ct.id, orga_ct.id], + "exportable_ctypes": [contact_ct.id], + "credentials": [], + }, + ) role.refresh_from_db() self.assertEqual(role.name, "CEO") - self.assertEqual(role.allowed_apps, {'creme_core', 'persons', 'creme_api'}) - self.assertEqual(role.admin_4_apps, {'creme_core', 'creme_api'}) + self.assertEqual(role.allowed_apps, {"creme_core", "persons", "creme_api"}) + self.assertEqual(role.admin_4_apps, {"creme_core", "creme_api"}) self.assertEqual(list(role.creatable_ctypes.all()), [contact_ct, orga_ct]) self.assertEqual(list(role.exportable_ctypes.all()), [contact_ct]) data = { - 'allowed_apps': ['creme_core', 'persons', 'creme_api'], - 'exportable_ctypes': [contact_ct.id, orga_ct.id], + "allowed_apps": ["creme_core", "persons", "creme_api"], + "exportable_ctypes": [contact_ct.id, orga_ct.id], } response = self.make_request(to=role.id, data=data, status_code=200) - self.assertPayloadEqual(response, { - 'id': role.id, - 'name': "CEO", - 'allowed_apps': {'creme_core', 'persons', 'creme_api'}, - 'admin_4_apps': {'creme_core', 'creme_api'}, - 'creatable_ctypes': [contact_ct.id, orga_ct.id], - 'exportable_ctypes': [contact_ct.id, orga_ct.id], - 'credentials': [], - }) + self.assertPayloadEqual( + response, + { + "id": role.id, + "name": "CEO", + "allowed_apps": {"creme_core", "persons", "creme_api"}, + "admin_4_apps": {"creme_core", "creme_api"}, + "creatable_ctypes": [contact_ct.id, orga_ct.id], + "exportable_ctypes": [contact_ct.id, orga_ct.id], + "credentials": [], + }, + ) role.refresh_from_db() self.assertEqual(role.name, "CEO") - self.assertEqual(role.allowed_apps, {'creme_core', 'persons', 'creme_api'}) - self.assertEqual(role.admin_4_apps, {'creme_core', 'creme_api'}) + self.assertEqual(role.allowed_apps, {"creme_core", "persons", "creme_api"}) + self.assertEqual(role.admin_4_apps, {"creme_core", "creme_api"}) self.assertEqual(list(role.creatable_ctypes.all()), [contact_ct, orga_ct]) self.assertEqual(list(role.exportable_ctypes.all()), [contact_ct, orga_ct]) class ListRoleTestCase(CremeAPITestCase): - url_name = 'creme_api__roles-list' - method = 'get' + url_name = "creme_api__roles-list" + method = "get" def test_list_roles(self): contact_ct = ContentType.objects.get_for_model(Contact) orga_ct = ContentType.objects.get_for_model(Organisation) - role1 = self.factory.role(name='Role #1') - role2 = self.factory.role(name='Role #2') + role1 = RoleFactory(name="Role #1") + role2 = RoleFactory(name="Role #2") response = self.make_request(status_code=200) - self.assertPayloadEqual(response, [ - { - 'id': role1.id, - 'name': "Role #1", - 'allowed_apps': {'creme_core', 'persons', 'creme_api'}, - 'admin_4_apps': {'creme_core', 'creme_api'}, - 'creatable_ctypes': [contact_ct.id, orga_ct.id], - 'exportable_ctypes': [contact_ct.id], - 'credentials': [], - }, - { - 'id': role2.id, - 'name': "Role #2", - 'allowed_apps': {'creme_core', 'persons', 'creme_api'}, - 'admin_4_apps': {'creme_core', 'creme_api'}, - 'creatable_ctypes': [contact_ct.id, orga_ct.id], - 'exportable_ctypes': [contact_ct.id], - 'credentials': [], - } - ]) + self.assertPayloadEqual( + response, + [ + { + "id": role1.id, + "name": "Role #1", + "allowed_apps": {"creme_core", "persons", "creme_api"}, + "admin_4_apps": {"creme_core", "creme_api"}, + "creatable_ctypes": [contact_ct.id, orga_ct.id], + "exportable_ctypes": [contact_ct.id], + "credentials": [], + }, + { + "id": role2.id, + "name": "Role #2", + "allowed_apps": {"creme_core", "persons", "creme_api"}, + "admin_4_apps": {"creme_core", "creme_api"}, + "creatable_ctypes": [contact_ct.id, orga_ct.id], + "exportable_ctypes": [contact_ct.id], + "credentials": [], + }, + ], + ) class DeleteRoleTestCase(CremeAPITestCase): - url_name = 'creme_api__roles-detail' - method = 'delete' + url_name = "creme_api__roles-detail" + method = "delete" def test_delete_role__protected(self): - role = self.factory.role() - self.factory.user(role=role) + role = RoleFactory() + UserFactory(role=role) self.make_request(to=role.id, status_code=403) def test_delete_role(self): - role = self.factory.role() + role = RoleFactory() self.make_request(to=role.id, status_code=204) self.assertFalse(UserRole.objects.filter(id=role.id).exists()) diff --git a/creme/creme_api/tests/test_sectors.py b/creme/creme_api/tests/test_sectors.py index 814950e442..15886fa960 100644 --- a/creme/creme_api/tests/test_sectors.py +++ b/creme/creme_api/tests/test_sectors.py @@ -1,107 +1,123 @@ -from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.creme_api.tests.utils import CremeAPITestCase from creme.persons.models import Sector - -@Factory.register -def sector(factory, **kwargs): - data = factory.sector_data(**kwargs) - return Sector.objects.create(**data) - - -@Factory.register -def sector_data(factory, **kwargs): - kwargs.setdefault('title', 'Industry') - return kwargs +from .factories import SectorFactory class CreateSectorTestCase(CremeAPITestCase): - url_name = 'creme_api__sectors-list' - method = 'post' + url_name = "creme_api__sectors-list" + method = "post" def test_validation__required(self): response = self.make_request(data={}, status_code=400) - self.assertValidationErrors(response, { - 'title': ['required'], - }) + self.assertValidationErrors( + response, + { + "title": ["required"], + }, + ) def test_create_sector(self): - data = self.factory.sector_data() + data = {"title": "Industry"} response = self.make_request(data=data, status_code=201) - sector = Sector.objects.get(id=response.data['id']) - self.assertPayloadEqual(response, { - 'id': sector.id, - 'title': 'Industry', - }) + sector = Sector.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + "id": sector.id, + "title": "Industry", + }, + ) self.assertEqual(sector.title, "Industry") class RetrieveSectorTestCase(CremeAPITestCase): - url_name = 'creme_api__sectors-detail' - method = 'get' + url_name = "creme_api__sectors-detail" + method = "get" def test_retrieve_sector(self): - sector = self.factory.sector() + sector = SectorFactory() response = self.make_request(to=sector.id, status_code=200) - self.assertPayloadEqual(response, { - 'id': sector.id, - 'title': 'Industry', - }) + self.assertPayloadEqual( + response, + { + "id": sector.id, + "title": "Industry", + }, + ) class UpdateSectorTestCase(CremeAPITestCase): - url_name = 'creme_api__sectors-detail' - method = 'put' + url_name = "creme_api__sectors-detail" + method = "put" def test_update_sector(self): - sector = self.factory.sector() - response = self.make_request(to=sector.id, data={ - 'title': "Agro", - }, status_code=200) - self.assertPayloadEqual(response, { - 'id': sector.id, - 'title': 'Agro', - }) + sector = SectorFactory() + response = self.make_request( + to=sector.id, + data={ + "title": "Agro", + }, + status_code=200, + ) + self.assertPayloadEqual( + response, + { + "id": sector.id, + "title": "Agro", + }, + ) sector.refresh_from_db() self.assertEqual(sector.title, "Agro") class PartialUpdateSectorTestCase(CremeAPITestCase): - url_name = 'creme_api__sectors-detail' - method = 'patch' + url_name = "creme_api__sectors-detail" + method = "patch" def test_partial_update_sector(self): - sector = self.factory.sector() - response = self.make_request(to=sector.id, data={ - 'title': 'Agro', - }, status_code=200) - self.assertPayloadEqual(response, { - 'id': sector.id, - 'title': 'Agro', - }) + sector = SectorFactory() + response = self.make_request( + to=sector.id, + data={ + "title": "Agro", + }, + status_code=200, + ) + self.assertPayloadEqual( + response, + { + "id": sector.id, + "title": "Agro", + }, + ) sector.refresh_from_db() self.assertEqual(sector.title, "Agro") class ListSectorTestCase(CremeAPITestCase): - url_name = 'creme_api__sectors-list' - method = 'get' + url_name = "creme_api__sectors-list" + method = "get" def test_list_sectors(self): Sector.objects.all().delete() - sector1 = self.factory.sector(title="1") - sector2 = self.factory.sector(title="2") + sector1 = SectorFactory(title="1") + sector2 = SectorFactory(title="2") response = self.make_request(status_code=200) - self.assertPayloadEqual(response, [ - {'id': sector1.id, 'title': '1'}, - {'id': sector2.id, 'title': '2'}, - ]) + self.assertPayloadEqual( + response, + [ + {"id": sector1.id, "title": "1"}, + {"id": sector2.id, "title": "2"}, + ], + ) class DeleteSectorTestCase(CremeAPITestCase): - url_name = 'creme_api__sectors-detail' - method = 'delete' + url_name = "creme_api__sectors-detail" + method = "delete" def test_delete_sector(self): - sector = self.factory.sector() + sector = SectorFactory() self.make_request(to=sector.id, status_code=204) self.assertFalse(Sector.objects.filter(id=sector.id).exists()) diff --git a/creme/creme_api/tests/test_staff_sizes.py b/creme/creme_api/tests/test_staff_sizes.py index 0a6cfeb6cf..b90c954e6c 100644 --- a/creme/creme_api/tests/test_staff_sizes.py +++ b/creme/creme_api/tests/test_staff_sizes.py @@ -1,107 +1,123 @@ -from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.creme_api.tests.utils import CremeAPITestCase from creme.persons.models import StaffSize - -@Factory.register -def staff_size(factory, **kwargs): - data = factory.staff_size_data(**kwargs) - return StaffSize.objects.create(**data) - - -@Factory.register -def staff_size_data(factory, **kwargs): - kwargs.setdefault('size', '1 - 10') - return kwargs +from .factories import StaffSizeFactory class CreateStaffSizeTestCase(CremeAPITestCase): - url_name = 'creme_api__staff_sizes-list' - method = 'post' + url_name = "creme_api__staff_sizes-list" + method = "post" def test_validation__required(self): response = self.make_request(data={}, status_code=400) - self.assertValidationErrors(response, { - 'size': ['required'], - }) + self.assertValidationErrors( + response, + { + "size": ["required"], + }, + ) def test_create_staff_size(self): - data = self.factory.staff_size_data() + data = {"size": "1 - 10"} response = self.make_request(data=data, status_code=201) - staff_size = StaffSize.objects.get(id=response.data['id']) - self.assertPayloadEqual(response, { - 'id': staff_size.id, - 'size': '1 - 10', - }) - self.assertEqual(staff_size.size, '1 - 10') + staff_size = StaffSize.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + "id": staff_size.id, + "size": "1 - 10", + }, + ) + self.assertEqual(staff_size.size, "1 - 10") class RetrieveStaffSizeTestCase(CremeAPITestCase): - url_name = 'creme_api__staff_sizes-detail' - method = 'get' + url_name = "creme_api__staff_sizes-detail" + method = "get" def test_retrieve_staff_size(self): - staff_size = self.factory.staff_size() + staff_size = StaffSizeFactory() response = self.make_request(to=staff_size.id, status_code=200) - self.assertPayloadEqual(response, { - 'id': staff_size.id, - 'size': '1 - 10', - }) + self.assertPayloadEqual( + response, + { + "id": staff_size.id, + "size": "1 - 10", + }, + ) class UpdateStaffSizeTestCase(CremeAPITestCase): - url_name = 'creme_api__staff_sizes-detail' - method = 'put' + url_name = "creme_api__staff_sizes-detail" + method = "put" def test_update_staff_size(self): - staff_size = self.factory.staff_size() - response = self.make_request(to=staff_size.id, data={ - 'size': '1 - 100', - }, status_code=200) - self.assertPayloadEqual(response, { - 'id': staff_size.id, - 'size': '1 - 100', - }) + staff_size = StaffSizeFactory() + response = self.make_request( + to=staff_size.id, + data={ + "size": "1 - 100", + }, + status_code=200, + ) + self.assertPayloadEqual( + response, + { + "id": staff_size.id, + "size": "1 - 100", + }, + ) staff_size.refresh_from_db() - self.assertEqual(staff_size.size, '1 - 100') + self.assertEqual(staff_size.size, "1 - 100") class PartialUpdateStaffSizeTestCase(CremeAPITestCase): - url_name = 'creme_api__staff_sizes-detail' - method = 'patch' + url_name = "creme_api__staff_sizes-detail" + method = "patch" def test_partial_update_staff_size(self): - staff_size = self.factory.staff_size() - response = self.make_request(to=staff_size.id, data={ - 'size': '1 - 1000', - }, status_code=200) - self.assertPayloadEqual(response, { - 'id': staff_size.id, - 'size': '1 - 1000', - }) + staff_size = StaffSizeFactory() + response = self.make_request( + to=staff_size.id, + data={ + "size": "1 - 1000", + }, + status_code=200, + ) + self.assertPayloadEqual( + response, + { + "id": staff_size.id, + "size": "1 - 1000", + }, + ) staff_size.refresh_from_db() - self.assertEqual(staff_size.size, '1 - 1000') + self.assertEqual(staff_size.size, "1 - 1000") class ListStaffSizeTestCase(CremeAPITestCase): - url_name = 'creme_api__staff_sizes-list' - method = 'get' + url_name = "creme_api__staff_sizes-list" + method = "get" def test_list_staff_sizes(self): StaffSize.objects.all().delete() - staff_size1 = self.factory.staff_size(size="1 - 10") - staff_size2 = self.factory.staff_size(size="10 - 20") + staff_size1 = StaffSizeFactory(size="1 - 10") + staff_size2 = StaffSizeFactory(size="10 - 20") response = self.make_request(status_code=200) - self.assertPayloadEqual(response, [ - {'id': staff_size1.id, 'size': "1 - 10"}, - {'id': staff_size2.id, 'size': "10 - 20"}, - ]) + self.assertPayloadEqual( + response, + [ + {"id": staff_size1.id, "size": "1 - 10"}, + {"id": staff_size2.id, "size": "10 - 20"}, + ], + ) class DeleteStaffSizeTestCase(CremeAPITestCase): - url_name = 'creme_api__staff_sizes-detail' - method = 'delete' + url_name = "creme_api__staff_sizes-detail" + method = "delete" def test_delete_staff_size(self): - staff_size = self.factory.staff_size() + staff_size = StaffSizeFactory() self.make_request(to=staff_size.id, status_code=204) self.assertFalse(StaffSize.objects.filter(id=staff_size.id).exists()) diff --git a/creme/creme_api/tests/test_teams.py b/creme/creme_api/tests/test_teams.py index 9e935415a8..200c7ec6d6 100644 --- a/creme/creme_api/tests/test_teams.py +++ b/creme/creme_api/tests/test_teams.py @@ -1,68 +1,59 @@ from django.contrib.auth import get_user_model -from creme.creme_api.tests.utils import CremeAPITestCase, Factory +from creme.creme_api.tests.utils import CremeAPITestCase from creme.persons import get_contact_model +from .factories import ContactFactory, TeamFactory, UserFactory + CremeUser = get_user_model() Contact = get_contact_model() -@Factory.register -def team(factory, **kwargs): - data = { - 'username': 'Team #1', - } - data.update(**kwargs) - if 'name' in data: - data['username'] = data.pop('name') - data['is_team'] = True - teammates = data.pop('teammates', []) - - team = CremeUser.objects.create(**data) - team.teammates = teammates - - return team - - class CreateTeamTestCase(CremeAPITestCase): - url_name = 'creme_api__teams-list' - method = 'post' + url_name = "creme_api__teams-list" + method = "post" def test_validation__required(self): response = self.make_request(data={}, status_code=400) - self.assertValidationErrors(response, { - 'name': ['required'], - 'teammates': ['required'], - }) + self.assertValidationErrors( + response, + { + "name": ["required"], + "teammates": ["required"], + }, + ) def test_validation__name_max_length(self): - data = {'name': "a" * (CremeUser._meta.get_field('username').max_length + 1)} + data = {"name": "a" * (CremeUser._meta.get_field("username").max_length + 1)} response = self.make_request(data=data, status_code=400) - self.assertValidationError(response, 'name', ['max_length']) + self.assertValidationError(response, "name", ["max_length"]) def test_validation__name_invalid_chars(self): - data = {'name': "*********"} + data = {"name": "*********"} response = self.make_request(data=data, status_code=400) - self.assertValidationError(response, 'name', ['invalid']) + self.assertValidationError(response, "name", ["invalid"]) def test_validation__teammates(self): - data = {'name': "TEAM", 'teammates': [9999]} + data = {"name": "TEAM", "teammates": [9999]} response = self.make_request(data=data, status_code=400) - self.assertValidationError(response, 'teammates', ['does_not_exist']) + self.assertValidationError(response, "teammates", ["does_not_exist"]) def test_create_team(self): - user1 = self.factory.user(username="user1") - user2 = self.factory.user(username="user2") + user1 = UserFactory(username="user1") + user2 = UserFactory(username="user2") - data = {'name': "creme-team", 'teammates': [user1.id, user2.id]} + data = {"name": "creme-team", "teammates": [user1.id, user2.id]} response = self.make_request(data=data, status_code=201) - team = CremeUser.objects.get(id=response.data['id']) + team = CremeUser.objects.get(id=response.data["id"]) - self.assertPayloadEqual(response, { - 'id': team.id, - 'teammates': [user1.id, user2.id], - 'name': "creme-team", - }) + self.assertPayloadEqual( + response, + { + "id": team.id, + "teammates": [user1.id, user2.id], + "name": "creme-team", + }, + ) self.assertTrue(team.is_team) self.assertEqual(team.username, "creme-team") @@ -70,46 +61,55 @@ def test_create_team(self): class RetrieveTeamTestCase(CremeAPITestCase): - url_name = 'creme_api__teams-detail' - method = 'get' + url_name = "creme_api__teams-detail" + method = "get" def test_get_team(self): - user = self.factory.user() - team = self.factory.team(teammates=[user]) + user = UserFactory() + team = TeamFactory(teammates=[user]) response = self.make_request(to=team.id, status_code=200) - self.assertPayloadEqual(response, { - 'id': team.id, - 'teammates': [user.id], - 'name': 'Team #1', - }) + self.assertPayloadEqual( + response, + { + "id": team.id, + "teammates": [user.id], + "name": "Team #1", + }, + ) class UpdateTeamTestCase(CremeAPITestCase): - url_name = 'creme_api__teams-detail' - method = 'put' + url_name = "creme_api__teams-detail" + method = "put" def test_validation__required(self): - team = self.factory.team() + team = TeamFactory() response = self.make_request(to=team.id, data={}, status_code=400) - self.assertValidationErrors(response, { - 'name': ['required'], - 'teammates': ['required'], - }) + self.assertValidationErrors( + response, + { + "name": ["required"], + "teammates": ["required"], + }, + ) def test_update_team(self): - user = self.factory.user() - team = self.factory.team(teammates=[user]) + user = UserFactory() + team = TeamFactory(teammates=[user]) - user2 = self.factory.user(username="user2") - data = {'name': "Sales", 'teammates': [user2.id]} + user2 = UserFactory(username="user2") + data = {"name": "Sales", "teammates": [user2.id]} response = self.make_request(to=team.id, data=data, status_code=200) - self.assertPayloadEqual(response, { - 'id': team.id, - 'teammates': [user2.id], - 'name': 'Sales', - }) + self.assertPayloadEqual( + response, + { + "id": team.id, + "teammates": [user2.id], + "name": "Sales", + }, + ) team.refresh_from_db() self.assertTrue(team.is_team) @@ -118,20 +118,23 @@ def test_update_team(self): class PartialUpdateTeamTestCase(CremeAPITestCase): - url_name = 'creme_api__teams-detail' - method = 'patch' + url_name = "creme_api__teams-detail" + method = "patch" def test_partial_update_team__name(self): - user = self.factory.user() - team = self.factory.team(teammates=[user]) + user = UserFactory() + team = TeamFactory(teammates=[user]) - data = {'name': "Sales"} + data = {"name": "Sales"} response = self.make_request(to=team.id, data=data, status_code=200) - self.assertPayloadEqual(response, { - 'id': team.id, - 'teammates': [user.id], - 'name': 'Sales', - }) + self.assertPayloadEqual( + response, + { + "id": team.id, + "teammates": [user.id], + "name": "Sales", + }, + ) team.refresh_from_db() self.assertTrue(team.is_team) @@ -139,18 +142,21 @@ def test_partial_update_team__name(self): self.assertEqual(team.teammates, {user.id: user}) def test_partial_update_team__teammates(self): - user = self.factory.user() - team = self.factory.team(teammates=[user]) + user = UserFactory() + team = TeamFactory(teammates=[user]) # change - user2 = self.factory.user(username='user2') - data = {'teammates': [user2.id]} + user2 = UserFactory(username="user2") + data = {"teammates": [user2.id]} response = self.make_request(to=team.id, data=data, status_code=200) - self.assertPayloadEqual(response, { - 'id': team.id, - 'teammates': [user2.id], - 'name': 'Team #1', - }) + self.assertPayloadEqual( + response, + { + "id": team.id, + "teammates": [user2.id], + "name": "Team #1", + }, + ) team.refresh_from_db() self.assertTrue(team.is_team) @@ -158,13 +164,16 @@ def test_partial_update_team__teammates(self): self.assertEqual(team.teammates, {user2.id: user2}) # empty - data = {'teammates': []} + data = {"teammates": []} response = self.make_request(to=team.id, data=data, status_code=200) - self.assertPayloadEqual(response, { - 'id': team.id, - 'teammates': [], - 'name': 'Team #1', - }) + self.assertPayloadEqual( + response, + { + "id": team.id, + "teammates": [], + "name": "Team #1", + }, + ) team.refresh_from_db() self.assertTrue(team.is_team) @@ -173,61 +182,64 @@ def test_partial_update_team__teammates(self): class ListTeamTestCase(CremeAPITestCase): - url_name = 'creme_api__teams-list' - method = 'get' + url_name = "creme_api__teams-list" + method = "get" def test_list_teams(self): - user1 = self.factory.user(username="user1") - team1 = self.factory.team(name='test1', teammates=[user1]) - user2 = self.factory.user(username="user2") - team2 = self.factory.team(name='test2', teammates=[user1, user2]) + user1 = UserFactory(username="user1") + team1 = TeamFactory(name="test1", teammates=[user1]) + user2 = UserFactory(username="user2") + team2 = TeamFactory(name="test2", teammates=[user1, user2]) teams = CremeUser.objects.filter(is_team=True) self.assertEqual(teams.count(), 2, teams) response = self.make_request(status_code=200) - self.assertPayloadEqual(response, [ - { - 'id': team1.id, - 'teammates': [user1.id], - 'name': 'test1', - }, - { - 'id': team2.id, - 'teammates': [user1.id, user2.id], - 'name': 'test2', - }, - ]) + self.assertPayloadEqual( + response, + [ + { + "id": team1.id, + "teammates": [user1.id], + "name": "test1", + }, + { + "id": team2.id, + "teammates": [user1.id, user2.id], + "name": "test2", + }, + ], + ) class DeleteTeamTestCase(CremeAPITestCase): - url_name = 'creme_api__teams-detail' - method = 'delete' + url_name = "creme_api__teams-detail" + method = "delete" def test_delete(self): - team = self.factory.team() + team = TeamFactory() self.make_request(to=team.id, data={}, status_code=405) class PostDeleteTeamTestCase(CremeAPITestCase): - url_name = 'creme_api__teams-delete' - method = 'post' + url_name = "creme_api__teams-delete" + method = "post" def test_delete_team(self): - user = self.factory.user() - team1 = self.factory.team(name='team1') - team2 = self.factory.team(name='team2') - contact = self.factory.contact(user=team2) + user = UserFactory() + team1 = TeamFactory(name="team1") + team2 = TeamFactory(name="team2") + contact = ContactFactory(user=team2) - data = {'transfer_to': team1.id} + data = {"transfer_to": team1.id} self.make_request(to=team2.id, data=data, status_code=204) - self.assertFalse(CremeUser.objects.filter(username='team2').exists()) + self.assertFalse(CremeUser.objects.filter(username="team2").exists()) contact.refresh_from_db() self.assertEqual(contact.user, team1) - data = {'transfer_to': user.id} + data = {"transfer_to": user.id} self.make_request(to=team1.id, data=data, status_code=204) - self.assertFalse(CremeUser.objects.filter(username='team1').exists()) + self.assertFalse(CremeUser.objects.filter(username="team1").exists()) contact.refresh_from_db() self.assertEqual(contact.user, user) diff --git a/creme/creme_api/tests/test_tokens.py b/creme/creme_api/tests/test_tokens.py index 1a2c10331b..dfca263adc 100644 --- a/creme/creme_api/tests/test_tokens.py +++ b/creme/creme_api/tests/test_tokens.py @@ -6,15 +6,18 @@ class TokensTestCase(CremeAPITestCase): auto_login = False - url_name = 'creme_api__tokens-list' - method = 'post' + url_name = "creme_api__tokens-list" + method = "post" def test_create_token__missing(self): response = self.make_request(data={}, status_code=400) - self.assertValidationErrors(response, { - 'client_id': ['required'], - 'client_secret': ['required'], - }) + self.assertValidationErrors( + response, + { + "client_id": ["required"], + "client_secret": ["required"], + }, + ) def test_create_token__empty(self): data = { @@ -22,10 +25,13 @@ def test_create_token__empty(self): "client_secret": "", } response = self.make_request(data=data, status_code=400) - self.assertValidationErrors(response, { - 'client_id': ['invalid'], # Must be a valid UUID. - 'client_secret': ['blank'], - }) + self.assertValidationErrors( + response, + { + "client_id": ["invalid"], # Must be a valid UUID. + "client_secret": ["blank"], + }, + ) def test_create_token__no_application(self): data = { @@ -33,9 +39,12 @@ def test_create_token__no_application(self): "client_secret": "Secret", } response = self.make_request(data=data, status_code=400) - self.assertValidationErrors(response, { - '': ['authentication_failure'], - }) + self.assertValidationErrors( + response, + { + "": ["authentication_failure"], + }, + ) def test_create_token(self): data = { diff --git a/creme/creme_api/tests/test_users.py b/creme/creme_api/tests/test_users.py index a53b6a94b6..d01e496136 100644 --- a/creme/creme_api/tests/test_users.py +++ b/creme/creme_api/tests/test_users.py @@ -1,295 +1,306 @@ from django.contrib.auth import get_user_model -from creme.creme_api.tests.utils import CremeAPITestCase, Factory - -CremeUser = get_user_model() +from creme.creme_api.tests.utils import CremeAPITestCase +from .factories import ContactFactory, RoleFactory, TeamFactory, UserFactory -@Factory.register -def user(factory, **kwargs): - data = factory.user_data(**kwargs) - return CremeUser.objects.create(**data) +CremeUser = get_user_model() -@Factory.register -def user_data(factory, **kwargs): - data = { - 'username': 'john.doe', - 'first_name': 'John', - 'last_name': 'Doe', - 'email': 'john.doe@provider.com', - 'is_active': True, - 'is_superuser': True, - 'role': None, - } - data.update(**kwargs) - return data +default_user_data = { + "first_name": "John", + "last_name": "Doe", + "username": "john.doe", + "email": "john.doe@provider.com", + "is_active": True, + "is_superuser": True, + "role": None, +} class CreateUserTestCase(CremeAPITestCase): - url_name = 'creme_api__users-list' - method = 'post' + url_name = "creme_api__users-list" + method = "post" def test_validation__required(self): response = self.make_request(data={}, status_code=400) - self.assertValidationErrors(response, { - 'username': ['required'], - 'first_name': ['required'], - 'last_name': ['required'], - 'email': ['required'], - }) + self.assertValidationErrors( + response, + { + "username": ["required"], + "first_name": ["required"], + "last_name": ["required"], + "email": ["required"], + }, + ) def test_validation__username_max_length(self): - data = {'username': "a" * (CremeUser._meta.get_field('username').max_length + 1)} + data = { + "username": "a" * (CremeUser._meta.get_field("username").max_length + 1) + } response = self.make_request(data=data, status_code=400) - self.assertValidationError(response, 'username', ['max_length']) + self.assertValidationError(response, "username", ["max_length"]) def test_validation__username_invalid_chars(self): - data = {'username': "*********"} + data = {"username": "*********"} response = self.make_request(data=data, status_code=400) - self.assertValidationError(response, 'username', ['invalid']) + self.assertValidationError(response, "username", ["invalid"]) def test_validation__is_superuser_xor_role(self): - role = self.factory.role() + role = RoleFactory() - data = self.factory.user_data(is_superuser=False, role=None) + data = {**default_user_data, "is_superuser": False, "role": None} response = self.make_request(data=data, status_code=400) - self.assertValidationError(response, '', ['is_superuser_xor_role']) + self.assertValidationError(response, "", ["is_superuser_xor_role"]) - data = self.factory.user_data(is_superuser=True, role=role.id) + data = {**default_user_data, "is_superuser": True, "role": role.id} response = self.make_request(data=data, status_code=400) - self.assertValidationError(response, '', ['is_superuser_xor_role']) + self.assertValidationError(response, "", ["is_superuser_xor_role"]) def test_create_superuser(self): - data = self.factory.user_data(is_superuser=True, role=None) + data = {**default_user_data, "is_superuser": True, "role": None} response = self.make_request(data=data, status_code=201) - user = CremeUser.objects.get(id=response.data['id']) - self.assertPayloadEqual(response, { - 'id': user.id, - 'username': 'john.doe', - 'last_name': 'Doe', - 'first_name': 'John', - 'email': 'john.doe@provider.com', - 'date_joined': self.to_iso8601(user.date_joined), - 'last_login': None, - 'is_active': True, - 'is_superuser': True, - 'role': None, - 'time_zone': 'Europe/Paris', - 'theme': 'icecream' - }) + user = CremeUser.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + "id": user.id, + "username": "john.doe", + "last_name": "Doe", + "first_name": "John", + "email": "john.doe@provider.com", + "date_joined": self.to_iso8601(user.date_joined), + "last_login": None, + "is_active": True, + "is_superuser": True, + "role": None, + "time_zone": "Europe/Paris", + "theme": "icecream", + }, + ) self.assertEqual(user.username, "john.doe") self.assertTrue(user.is_superuser) def test_create_user(self): - role = self.factory.role() - data = self.factory.user_data(is_superuser=False, role=role.id) + role = RoleFactory() + data = {**default_user_data, "is_superuser": False, "role": role.id} response = self.make_request(data=data, status_code=201) - user = CremeUser.objects.get(id=response.data['id']) - self.assertPayloadEqual(response, { - 'id': user.id, - 'username': 'john.doe', - 'last_name': 'Doe', - 'first_name': 'John', - 'email': 'john.doe@provider.com', - 'date_joined': self.to_iso8601(user.date_joined), - 'last_login': None, - 'is_active': True, - 'is_superuser': False, - 'role': role.id, - 'time_zone': 'Europe/Paris', - 'theme': 'icecream' - }) + user = CremeUser.objects.get(id=response.data["id"]) + self.assertPayloadEqual( + response, + { + "id": user.id, + "username": "john.doe", + "last_name": "Doe", + "first_name": "John", + "email": "john.doe@provider.com", + "date_joined": self.to_iso8601(user.date_joined), + "last_login": None, + "is_active": True, + "is_superuser": False, + "role": role.id, + "time_zone": "Europe/Paris", + "theme": "icecream", + }, + ) self.assertEqual(user.username, "john.doe") class RetrieveUserTestCase(CremeAPITestCase): - url_name = 'creme_api__users-detail' - method = 'get' + url_name = "creme_api__users-detail" + method = "get" def test_get_user(self): - user = self.factory.user() + user = UserFactory() response = self.make_request(to=user.id, status_code=200) - self.assertPayloadEqual(response, { - 'id': user.id, - 'username': user.username, - 'last_name': user.last_name, - 'first_name': user.first_name, - 'email': user.email, - 'date_joined': self.to_iso8601(user.date_joined), - 'last_login': None, - 'is_active': True, - 'is_superuser': True, - 'role': None, - 'time_zone': 'Europe/Paris', - 'theme': 'icecream' - }) + self.assertPayloadEqual( + response, + { + "id": user.id, + "username": user.username, + "last_name": user.last_name, + "first_name": user.first_name, + "email": user.email, + "date_joined": self.to_iso8601(user.date_joined), + "last_login": None, + "is_active": True, + "is_superuser": True, + "role": None, + "time_zone": "Europe/Paris", + "theme": "icecream", + }, + ) class UpdateUserTestCase(CremeAPITestCase): - url_name = 'creme_api__users-detail' - method = 'put' + url_name = "creme_api__users-detail" + method = "put" def test_validation__required(self): - user = self.factory.user() + user = UserFactory() response = self.make_request(to=user.id, data={}, status_code=400) - self.assertValidationErrors(response, { - 'username': ['required'], - 'first_name': ['required'], - 'last_name': ['required'], - 'email': ['required'], - }) + self.assertValidationErrors( + response, + { + "username": ["required"], + "first_name": ["required"], + "last_name": ["required"], + "email": ["required"], + }, + ) def test_validation__is_superuser_xor_role(self): - role = self.factory.role() - user = self.factory.user(is_superuser=True, role=None) + role = RoleFactory() + user = UserFactory(is_superuser=True, role=None) - data = self.factory.user_data(is_superuser=False, role=None) + data = {**default_user_data, "is_superuser": False, "role": None} response = self.make_request(to=user.id, data=data, status_code=400) - self.assertValidationError(response, '', ['is_superuser_xor_role']) + self.assertValidationError(response, "", ["is_superuser_xor_role"]) - data = self.factory.user_data(is_superuser=True, role=role.id) + data = {**default_user_data, "is_superuser": True, "role": role.id} response = self.make_request(to=user.id, data=data, status_code=400) - self.assertValidationError(response, '', ['is_superuser_xor_role']) + self.assertValidationError(response, "", ["is_superuser_xor_role"]) def test_update_user(self): - user = self.factory.user() + user = UserFactory() - data = self.factory.user_data(last_name="Smith", username="Nick") + data = {**default_user_data, "last_name": "Smith", "username": "Nick"} response = self.make_request(to=user.id, data=data, status_code=200) - self.assertPayloadEqual(response, { - 'id': user.id, - 'username': 'Nick', - 'last_name': 'Smith', - 'first_name': 'John', - 'email': 'john.doe@provider.com', - 'date_joined': self.to_iso8601(user.date_joined), - 'last_login': None, - 'is_active': True, - 'is_superuser': True, - 'role': None, - 'time_zone': 'Europe/Paris', - 'theme': 'icecream' - }) + self.assertPayloadEqual( + response, + { + **data, + "id": user.id, + "date_joined": self.to_iso8601(user.date_joined), + "last_login": None, + "role": None, + "time_zone": "Europe/Paris", + "theme": "icecream", + }, + ) user.refresh_from_db() self.assertEqual(user.username, "Nick") self.assertEqual(user.last_name, "Smith") class PartialUpdateUserTestCase(CremeAPITestCase): - url_name = 'creme_api__users-detail' - method = 'patch' + url_name = "creme_api__users-detail" + method = "patch" def test_validation__is_superuser_xor_role__superuser(self): - role = self.factory.role() - user = self.factory.user(username='user1', is_superuser=True, role=None) + role = RoleFactory() + user = UserFactory(username="user1", is_superuser=True, role=None) - data = {'role': role.id} + data = {"role": role.id} response = self.make_request(to=user.id, data=data, status_code=400) - self.assertValidationError(response, '', ['is_superuser_xor_role']) + self.assertValidationError(response, "", ["is_superuser_xor_role"]) - data = {'is_superuser': False} + data = {"is_superuser": False} response = self.make_request(to=user.id, data=data, status_code=400) - self.assertValidationError(response, '', ['is_superuser_xor_role']) + self.assertValidationError(response, "", ["is_superuser_xor_role"]) def test_validation__is_superuser_xor_role__role(self): - role = self.factory.role() - user = self.factory.user(username='user2', is_superuser=False, role=role) + role = RoleFactory() + user = UserFactory(username="user2", is_superuser=False, role=role) - data = {'role': None} + data = {"role": None} response = self.make_request(to=user.id, data=data, status_code=400) - self.assertValidationError(response, '', ['is_superuser_xor_role']) + self.assertValidationError(response, "", ["is_superuser_xor_role"]) - data = {'is_superuser': True} + data = {"is_superuser": True} response = self.make_request(to=user.id, data=data, status_code=400) - self.assertValidationError(response, '', ['is_superuser_xor_role']) + self.assertValidationError(response, "", ["is_superuser_xor_role"]) def test_partial_update_user(self): - user = self.factory.user() - data = {'theme': "chantilly"} + user = UserFactory() + data = {"theme": "chantilly"} response = self.make_request(to=user.id, data=data, status_code=200) - self.assertPayloadEqual(response, { - 'id': user.id, - 'username': 'john.doe', - 'last_name': 'Doe', - 'first_name': 'John', - 'email': 'john.doe@provider.com', - 'date_joined': self.to_iso8601(user.date_joined), - 'last_login': None, - 'is_active': True, - 'is_superuser': True, - 'role': None, - 'time_zone': 'Europe/Paris', - 'theme': 'chantilly' - }) + self.assertPayloadEqual( + response, + { + "id": user.id, + "username": "john.doe", + "last_name": "Doe", + "first_name": "John", + "email": "john.doe@provider.com", + "date_joined": self.to_iso8601(user.date_joined), + "last_login": None, + "is_active": True, + "is_superuser": True, + "role": None, + "time_zone": "Europe/Paris", + "theme": "chantilly", + }, + ) user.refresh_from_db() self.assertEqual(user.theme, "chantilly") class ListUserTestCase(CremeAPITestCase): - url_name = 'creme_api__users-list' - method = 'get' + url_name = "creme_api__users-list" + method = "get" def test_list_users(self): fulbert = CremeUser.objects.get() - user = self.factory.user(username="user", theme='chantilly') + user = UserFactory(username="user", theme="chantilly") self.assertEqual(CremeUser.objects.count(), 2) response = self.make_request(status_code=200) - self.assertPayloadEqual(response, [ - { - 'id': fulbert.id, - 'username': 'root', - 'last_name': 'Creme', - 'first_name': 'Fulbert', - 'email': fulbert.email, - 'date_joined': self.to_iso8601(fulbert.date_joined), - 'last_login': None, - 'is_active': True, - 'is_superuser': True, - 'role': None, - 'time_zone': 'Europe/Paris', - 'theme': 'icecream' - }, - { - 'id': user.id, - 'username': 'user', - 'last_name': 'Doe', - 'first_name': 'John', - 'email': 'john.doe@provider.com', - 'date_joined': self.to_iso8601(user.date_joined), - 'last_login': None, - 'is_active': True, - 'is_superuser': True, - 'role': None, - 'time_zone': 'Europe/Paris', - 'theme': 'chantilly' - }, - ]) + self.assertPayloadEqual( + response, + [ + { + "id": fulbert.id, + "username": "root", + "last_name": "Creme", + "first_name": "Fulbert", + "email": fulbert.email, + "date_joined": self.to_iso8601(fulbert.date_joined), + "last_login": None, + "is_active": True, + "is_superuser": True, + "role": None, + "time_zone": "Europe/Paris", + "theme": "icecream", + }, + { + "id": user.id, + "username": "user", + "last_name": "Doe", + "first_name": "John", + "email": "john.doe@provider.com", + "date_joined": self.to_iso8601(user.date_joined), + "last_login": None, + "is_active": True, + "is_superuser": True, + "role": None, + "time_zone": "Europe/Paris", + "theme": "chantilly", + }, + ], + ) class SetPasswordUserTestCase(CremeAPITestCase): - url_name = 'creme_api__users-set-password' - method = 'post' + url_name = "creme_api__users-set-password" + method = "post" def test_password_validation__required(self): - user = self.factory.user() + user = UserFactory() response = self.make_request(to=user.id, data={}, status_code=400) - self.assertValidationErrors(response, { - 'password': ['required'] - }) + self.assertValidationErrors(response, {"password": ["required"]}) def test_password_validation__blank(self): - user = self.factory.user() + user = UserFactory() - data = {'password': ''} + data = {"password": ""} response = self.make_request(to=user.id, data=data, status_code=400) - self.assertValidationError(response, 'password', ['blank']) + self.assertValidationError(response, "password", ["blank"]) def test_password_validation__no_trim(self): - user = self.factory.user() + user = UserFactory() - data = {'password': " StrongPassword "} + data = {"password": " StrongPassword "} response = self.make_request(to=user.id, data=data, status_code=200) self.assertPayloadEqual(response, {}) @@ -297,33 +308,33 @@ def test_password_validation__no_trim(self): self.assertTrue(user.check_password(" StrongPassword ")) def test_password_validation__similarity(self): - user = self.factory.user( + user = UserFactory( username="76aa224e-056a", first_name="4816-ac3e", last_name="ffe6e2c0748c", - email='df8e4b1a4f39@provider.com' + email="df8e4b1a4f39@provider.com", ) - data = {'password': user.username} + data = {"password": user.username} response = self.make_request(to=user.id, data=data, status_code=400) - self.assertValidationError(response, 'password', ['password_too_similar']) + self.assertValidationError(response, "password", ["password_too_similar"]) - data = {'password': user.first_name} + data = {"password": user.first_name} response = self.make_request(to=user.id, data=data, status_code=400) - self.assertValidationError(response, 'password', ['password_too_similar']) + self.assertValidationError(response, "password", ["password_too_similar"]) - data = {'password': user.last_name} + data = {"password": user.last_name} response = self.make_request(to=user.id, data=data, status_code=400) - self.assertValidationError(response, 'password', ['password_too_similar']) + self.assertValidationError(response, "password", ["password_too_similar"]) - data = {'password': user.email.split("@")[0]} + data = {"password": user.email.split("@")[0]} response = self.make_request(to=user.id, data=data, status_code=400) - self.assertValidationError(response, 'password', ['password_too_similar']) + self.assertValidationError(response, "password", ["password_too_similar"]) def test_set_password_user(self): - user = self.factory.user() + user = UserFactory() - data = {'password': "StrongPassword"} + data = {"password": "StrongPassword"} response = self.make_request(to=user.id, data=data, status_code=200) self.assertPayloadEqual(response, {}) @@ -332,41 +343,39 @@ def test_set_password_user(self): class DeleteUserTestCase(CremeAPITestCase): - url_name = 'creme_api__users-detail' - method = 'delete' + url_name = "creme_api__users-detail" + method = "delete" def test_delete(self): - user = self.factory.user() + user = UserFactory() self.make_request(to=user.id, data={}, status_code=405) class PostDeleteUserTestCase(CremeAPITestCase): - url_name = 'creme_api__users-delete' - method = 'post' + url_name = "creme_api__users-delete" + method = "post" def test_validation__required(self): - user = self.factory.user() + user = UserFactory() response = self.make_request(to=user.id, data={}, status_code=400) - self.assertValidationErrors(response, { - 'transfer_to': ['required'] - }) + self.assertValidationErrors(response, {"transfer_to": ["required"]}) def test_delete_user(self): - team = self.factory.team() - user1 = self.factory.user(username='user1') - user2 = self.factory.user(username='user2') - contact = self.factory.contact(user=user2) + team = TeamFactory() + user1 = UserFactory(username="user1") + user2 = UserFactory(username="user2") + contact = ContactFactory(user=user2) - data = {'transfer_to': user1.id} + data = {"transfer_to": user1.id} self.make_request(to=user2.id, data=data, status_code=204) - self.assertFalse(CremeUser.objects.filter(username='user2').exists()) + self.assertFalse(CremeUser.objects.filter(username="user2").exists()) contact.refresh_from_db() self.assertEqual(contact.user, user1) - data = {'transfer_to': team.id} + data = {"transfer_to": team.id} self.make_request(to=user1.id, data=data, status_code=204) - self.assertFalse(CremeUser.objects.filter(username='user1').exists()) + self.assertFalse(CremeUser.objects.filter(username="user1").exists()) contact.refresh_from_db() self.assertEqual(contact.user, team) diff --git a/creme/creme_api/tests/utils.py b/creme/creme_api/tests/utils.py index 3b224f62e2..e97f826c51 100644 --- a/creme/creme_api/tests/utils.py +++ b/creme/creme_api/tests/utils.py @@ -11,15 +11,6 @@ from creme.creme_api.models import Application, Token -class Factory: - @classmethod - def register(cls, func): - if hasattr(cls, func.__name__): - raise AttributeError(func.__name__) - setattr(cls, func.__name__, classmethod(func)) - return func - - def to_iso8601(value): return DateTimeField().to_representation(value) @@ -30,8 +21,9 @@ class PrettyPrinter(pprint.PrettyPrinter): def pformat(obj): - return PrettyPrinter(indent=2, width=80, depth=None, - compact=False, sort_dicts=True).pformat(obj) + return PrettyPrinter( + indent=2, width=80, depth=None, compact=False, sort_dicts=True + ).pformat(obj) class CremeAPITestCase(APITestCase): @@ -45,7 +37,6 @@ class CremeAPITestCase(APITestCase): def setUpClass(cls): super().setUpClass() cls.application = Application.objects.create(name="APITestCase") - cls.factory = Factory() def login(self, application): self.token = Token.objects.create(application=application) @@ -75,26 +66,29 @@ def assertValidationErrors(self, response, errors): def _assertPayloadEqual(self, first, second): if first != second: first = self._prepare_payload(first) - diff = '\n'.join(difflib.unified_diff( - pformat(first).splitlines(), - pformat(second).splitlines())) + second = self._prepare_payload(second) + diff = "\n".join( + difflib.unified_diff( + pformat(first).splitlines(), pformat(second).splitlines() + ) + ) self.fail(f"Payload error:\n{diff}") def _prepare_payload(self, data): if isinstance(data, list): return [self._prepare_payload(obj) for obj in data] if isinstance(data, dict): - return {key: self._prepare_payload(value) for key, value in data.items()} + return {key: self._prepare_payload(data[key]) for key in sorted(data)} return data def assertPayloadEqual(self, response, expected): - self.assertIsInstance(response, Response, 'First argument is not a Response') + self.assertIsInstance(response, Response, "First argument is not a Response") data = response.data if isinstance(expected, dict): - self._assertPayloadEqual(data, expected) + self._assertPayloadEqual(expected, data) elif isinstance(expected, list): - self.assertEqual(len(data['results']), len(expected)) - self._assertPayloadEqual(data['results'], expected) + self.assertEqual(len(expected), len(data["results"])) + self._assertPayloadEqual(expected, data["results"]) else: self.assertEqual(response, expected) @@ -104,21 +98,21 @@ def make_request(self, *, to=None, data=None, status_code=None): args = [to] if to is not None else None url = reverse(self.url_name, args=args) method = getattr(self.client, self.method) - response = method(url, data=data, format='json') + response = method(url, data=data, format="json") self.assertEqual(response.status_code, status_code, response.data) return response def consume_list(self, data=None): assert self.url_name is not None and self.url_name.endswith("-list") - assert self.method == 'get' + assert self.method == "get" method = getattr(self.client, self.method) responses = [] results = [] url = reverse(self.url_name) while url: - response = method(url, data=data, format='json') + response = method(url, data=data, format="json") responses.append(response) - results.extend(response.data['results']) - url = response.data['next'] + results.extend(response.data["results"]) + url = response.data["next"] return responses, results diff --git a/creme/creme_api/urls.py b/creme/creme_api/urls.py index e0c0c99aee..e2a2453abe 100644 --- a/creme/creme_api/urls.py +++ b/creme/creme_api/urls.py @@ -3,35 +3,45 @@ from creme.creme_api.api.routes import router from creme.creme_api.views import ( ApplicationCreation, - ApplicationEdition, ApplicationDeletion, + ApplicationEdition, ConfigurationView, DocumentationView, SchemaView, ) urlpatterns = [ - re_path(r'^openapi[/]?$', SchemaView.as_view(), name='creme_api__openapi_schema'), - re_path(r'^documentation[/]?$', DocumentationView.as_view(), name='creme_api__documentation'), - re_path(r'^configuration[/]?$', ConfigurationView.as_view(), name='creme_api__configuration'), + re_path(r"^openapi[/]?$", SchemaView.as_view(), name="creme_api__openapi_schema"), + re_path( + r"^documentation[/]?$", + DocumentationView.as_view(), + name="creme_api__documentation", + ), + re_path( + r"^configuration[/]?$", + ConfigurationView.as_view(), + name="creme_api__configuration", + ), re_path( - r'^configuration/applications/', - include([ - re_path( - r'^add[/]?$', - ApplicationCreation.as_view(), - name='creme_api__create_application', - ), - re_path( - r'^edit/(?P\d+)[/]?$', - ApplicationEdition.as_view(), - name='creme_api__edit_application', - ), - re_path( - r'^delete/(?P\d+)[/]?$', - ApplicationDeletion.as_view(), - name='creme_api__delete_application', - ), - ]), + r"^configuration/applications/", + include( + [ + re_path( + r"^add[/]?$", + ApplicationCreation.as_view(), + name="creme_api__create_application", + ), + re_path( + r"^edit/(?P\d+)[/]?$", + ApplicationEdition.as_view(), + name="creme_api__edit_application", + ), + re_path( + r"^delete/(?P\d+)[/]?$", + ApplicationDeletion.as_view(), + name="creme_api__delete_application", + ), + ] + ), ), ] + router.urls diff --git a/creme/creme_api/views.py b/creme/creme_api/views.py index eb859eb015..999da86343 100644 --- a/creme/creme_api/views.py +++ b/creme/creme_api/views.py @@ -29,7 +29,8 @@ class SchemaView(DRFSchemaView): def get_description(self, context=None, request=None): description = render_to_string( - self.description_template, context=context, request=request) + self.description_template, context=context, request=request + ) # Force django safestring into builtin string return description + "" @@ -38,9 +39,11 @@ def get_title(self): def initial(self, request, *args, **kwargs): super().initial(request, *args, **kwargs) - creme_api_app_config = apps.get_app_config('creme_api') - creme_api_rool_url = self.request.build_absolute_uri(creme_api_app_config.url_root) - context = {'creme_api_rool_url': creme_api_rool_url} + creme_api_app_config = apps.get_app_config("creme_api") + creme_api_rool_url = self.request.build_absolute_uri( + creme_api_app_config.url_root + ) + context = {"creme_api_rool_url": creme_api_rool_url} title = self.get_title() description = self.get_description(context=context, request=request) self.schema_generator = self.generator_class( @@ -55,25 +58,26 @@ class _DocumentationBaseView(base.BricksView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = self.title + context["page_title"] = self.title return context class DocumentationView(_DocumentationBaseView): - template_name = 'creme_api/documentation.html' - extra_context = {'schema_url': 'creme_api__openapi_schema'} + template_name = "creme_api/documentation.html" + extra_context = {"schema_url": "creme_api__openapi_schema"} permissions = "creme_api" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['creme_api__tokens_url'] = self.request.build_absolute_uri( - reverse("creme_api__tokens-list")) - context['token_type'] = TokenAuthentication.keyword + context["creme_api__tokens_url"] = self.request.build_absolute_uri( + reverse("creme_api__tokens-list") + ) + context["token_type"] = TokenAuthentication.keyword return context class ConfigurationView(_DocumentationBaseView): - template_name = 'creme_api/configuration.html' + template_name = "creme_api/configuration.html" permissions = "creme_api.can_admin" def get_brick_ids(self): @@ -83,7 +87,7 @@ def get_brick_ids(self): class ApplicationCreation(generic.CremeModelCreationPopup): model = Application form_class = ApplicationForm - title = _('New Application') + title = _("New Application") success_message = _( "The application «{application_name}» has been created. " "Identifiers have been generated, here they are: \n\n" @@ -110,11 +114,11 @@ def form_valid(self, form): class ApplicationEdition(generic.CremeModelEditionPopup): model = Application form_class = ApplicationForm - pk_url_kwarg = 'application_id' + pk_url_kwarg = "application_id" permissions = "creme_api.can_admin" class ApplicationDeletion(generic.CremeModelDeletion): model = Application - pk_url_kwarg = 'application_id' + pk_url_kwarg = "application_id" permissions = "creme_api.can_admin" diff --git a/setup.cfg b/setup.cfg index c518aec28a..c448495ba0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -62,6 +62,7 @@ dev= polib ~= 1.1.0 pyenchant ~= 3.2.2 beautifulsoup4 ~= 4.10.0 + factory_boy ~= 3.2.1 mysql= mysqlclient ~=2.1.0 pgsql= From d13c1e5654ed6202af3792f3edc4af211bf55a85 Mon Sep 17 00:00:00 2001 From: Hugo Smett Date: Fri, 13 May 2022 12:16:06 +0200 Subject: [PATCH 12/12] documentation; rename application id and secret --- creme/creme_api/README | 18 +------ creme/creme_api/api/authentication.py | 2 +- creme/creme_api/api/pagination.py | 2 + creme/creme_api/api/persons/serializers.py | 3 +- creme/creme_api/api/tokens/serializers.py | 17 +++--- creme/creme_api/api/tokens/viewsets.py | 9 +++- creme/creme_api/apps.py | 9 ++++ creme/creme_api/menu.py | 10 ++++ creme/creme_api/migrations/0001_initial.py | 8 +-- creme/creme_api/models.py | 43 ++++++++------- .../creme_api/bricks/applications.html | 6 +-- .../templates/creme_api/description.md | 54 +++++++++++++++++-- creme/creme_api/tests/test_documentation.py | 2 +- creme/creme_api/tests/test_models.py | 54 +++++++++++-------- creme/creme_api/tests/test_tokens.py | 20 +++---- creme/creme_api/views.py | 16 +++--- 16 files changed, 171 insertions(+), 102 deletions(-) create mode 100644 creme/creme_api/menu.py diff --git a/creme/creme_api/README b/creme/creme_api/README index 9ab7adbb49..c7a09f8239 100644 --- a/creme/creme_api/README +++ b/creme/creme_api/README @@ -1,29 +1,13 @@ -A way to use a specific block for the creme config - creme model administration ? - -ajouter une application - créer un token pour une application donnée (app id app secret) - -> génération d'un token - -appel api avec le app id et le token - - - - ################################### -# Configuration -- Une page spécifique à l'api pour la gestion des applications, des tokens ? -Pour l'accès à la documentation? - ## application -applications serveurs / application user +applications servers / application user filtre / IP utilisateurs autorisés configuration de la durée max des tokens restriction par scopes (resources) ##tokens restriction par scopes (resources) -durée de validité # Documentation diff --git a/creme/creme_api/api/authentication.py b/creme/creme_api/api/authentication.py index e0e8db2b71..728e4ab59a 100644 --- a/creme/creme_api/api/authentication.py +++ b/creme/creme_api/api/authentication.py @@ -13,7 +13,7 @@ class TokenAuthentication(BaseAuthentication): Token based authentication """ - keyword = "Token" + keyword = "token" errors = { "empty": _("Invalid token header. No credentials provided."), "too_long": _("Invalid token header. Token string should not contain spaces."), diff --git a/creme/creme_api/api/pagination.py b/creme/creme_api/api/pagination.py index 6c6abe55c3..c859a6e952 100644 --- a/creme/creme_api/api/pagination.py +++ b/creme/creme_api/api/pagination.py @@ -3,3 +3,5 @@ class CremeCursorPagination(CursorPagination): ordering = "id" + page_size_query_param = "page_size" + max_page_size = 200 diff --git a/creme/creme_api/api/persons/serializers.py b/creme/creme_api/api/persons/serializers.py index 004c4b7554..b51c0a54ae 100644 --- a/creme/creme_api/api/persons/serializers.py +++ b/creme/creme_api/api/persons/serializers.py @@ -2,8 +2,7 @@ from django.utils.translation import gettext as _ from rest_framework import serializers -from creme.creme_api.api.core.serializers import ( - # CremeEntityRelatedField, +from creme.creme_api.api.core.serializers import ( # CremeEntityRelatedField, CremeEntitySerializer, ) from creme.persons import ( diff --git a/creme/creme_api/api/tokens/serializers.py b/creme/creme_api/api/tokens/serializers.py index 38da01c356..7354c6cddf 100644 --- a/creme/creme_api/api/tokens/serializers.py +++ b/creme/creme_api/api/tokens/serializers.py @@ -9,19 +9,22 @@ class TokenSerializer(serializers.Serializer): "authentication_failure": _("Unable to log in with provided credentials.") } - client_id = serializers.UUIDField(label=_("Client ID"), write_only=True) - client_secret = serializers.CharField( - label=_("Client secret"), style={"input_type": "password"}, write_only=True + application_id = serializers.UUIDField(label=_("Application ID"), write_only=True) + application_secret = serializers.CharField( + label=_("Application secret"), style={"input_type": "password"}, write_only=True ) + token = serializers.CharField(label=_("Token"), read_only=True) + token_type = serializers.CharField(label=_("Token type"), read_only=True) + expires_in = serializers.IntegerField(label=_("Expires in"), read_only=True) def validate(self, attrs): - client_id = attrs["client_id"] - client_secret = attrs["client_secret"] + application_id = attrs["application_id"] + application_secret = attrs["application_secret"] application = Application.authenticate( - client_id, - client_secret, + application_id, + application_secret, request=self.context["request"], ) if not application: diff --git a/creme/creme_api/api/tokens/viewsets.py b/creme/creme_api/api/tokens/viewsets.py index ace5b8ab6e..4b381f65c8 100644 --- a/creme/creme_api/api/tokens/viewsets.py +++ b/creme/creme_api/api/tokens/viewsets.py @@ -1,6 +1,7 @@ from rest_framework import mixins, parsers, renderers, viewsets from rest_framework.response import Response +from creme.creme_api.api.authentication import TokenAuthentication from creme.creme_api.api.schemas import CremeSchema from creme.creme_api.models import Token @@ -35,4 +36,10 @@ def create(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) application = serializer.validated_data["application"] token = Token.objects.create(application=application) - return Response({"token": token.code}) + return Response( + { + "token": token.code, + "token_type": TokenAuthentication.keyword, + "expires_in": application.token_duration, + } + ) diff --git a/creme/creme_api/apps.py b/creme/creme_api/apps.py index 1a0674f04b..7490bd7eba 100644 --- a/creme/creme_api/apps.py +++ b/creme/creme_api/apps.py @@ -13,3 +13,12 @@ def register_bricks(self, brick_registry): from .bricks import ApplicationsBrick brick_registry.register(ApplicationsBrick) + + def register_menu_entries(self, menu_registry): + from creme.creme_config.menu import CremeConfigEntry, WorldConfigEntry + + from .menu import CremeApiEntry + + CremeConfigEntry.children_classes.insert( + CremeConfigEntry.children_classes.index(WorldConfigEntry), CremeApiEntry + ) diff --git a/creme/creme_api/menu.py b/creme/creme_api/menu.py new file mode 100644 index 0000000000..141243e2f0 --- /dev/null +++ b/creme/creme_api/menu.py @@ -0,0 +1,10 @@ +from django.utils.translation import gettext_lazy as _ + +from creme.creme_core.gui import menu + + +class CremeApiEntry(menu.FixedURLEntry): + permissions = "creme_api" + id = "creme_api-documentation" + label = _("Api") + url_name = "creme_api__documentation" diff --git a/creme/creme_api/migrations/0001_initial.py b/creme/creme_api/migrations/0001_initial.py index 59529158cd..a11da64aab 100644 --- a/creme/creme_api/migrations/0001_initial.py +++ b/creme/creme_api/migrations/0001_initial.py @@ -36,19 +36,19 @@ class Migration(migrations.Migration): ), ), ( - "client_id", + "application_id", models.UUIDField( db_index=True, default=uuid.uuid4, editable=False, unique=True, - verbose_name="Client ID", + verbose_name="Application ID", ), ), ( - "client_secret", + "application_secret", models.CharField( - blank=True, max_length=255, verbose_name="Client secret" + blank=True, max_length=255, verbose_name="Application secret" ), ), ("enabled", models.BooleanField(default=True, verbose_name="Enabled")), diff --git a/creme/creme_api/models.py b/creme/creme_api/models.py index c7d6196bcf..9e04d44c55 100644 --- a/creme/creme_api/models.py +++ b/creme/creme_api/models.py @@ -19,7 +19,7 @@ def generate_secret(length, chars=(string.ascii_letters + string.digits)): return "".join(secrets.choice(chars) for i in range(length)) -def default_application_client_secret(): +def default_application_application_secret(): return generate_secret(40) @@ -34,20 +34,20 @@ class Application(CremeModel): db_index=True, ) - client_id = models.UUIDField( - verbose_name=_("Client ID"), + application_id = models.UUIDField( + verbose_name=_("Application ID"), max_length=100, unique=True, db_index=True, default=uuid.uuid4, editable=False, ) - client_secret = models.CharField( - verbose_name=_("Client secret"), + application_secret = models.CharField( + verbose_name=_("Application secret"), max_length=255, blank=True, ) - _client_secret = None + _application_secret = None enabled = models.BooleanField( verbose_name=_("Enabled"), @@ -76,34 +76,34 @@ class Meta: def __str__(self): return self.name - def set_client_secret(self, raw_client_secret): - self.client_secret = make_password(raw_client_secret) - self._client_secret = raw_client_secret + def set_application_secret(self, raw_application_secret): + self.application_secret = make_password(raw_application_secret) + self._application_secret = raw_application_secret - def check_client_secret(self, raw_client_secret): + def check_application_secret(self, raw_application_secret): def setter(rcs): - self.set_client_secret(rcs) - self.save(update_fields=["client_secret"]) + self.set_application_secret(rcs) + self.save(update_fields=["application_secret"]) - return check_password(raw_client_secret, self.client_secret, setter) + return check_password(raw_application_secret, self.application_secret, setter) def save(self, **kwargs): if self.pk is None: - self.set_client_secret(default_application_client_secret()) + self.set_application_secret(default_application_application_secret()) return super().save(**kwargs) def can_authenticate(self, request=None): return self.enabled @staticmethod - def authenticate(client_id, client_secret, request=None): + def authenticate(application_id, application_secret, request=None): try: - application = Application.objects.get(client_id=client_id) + application = Application.objects.get(application_id=application_id) except (Application.DoesNotExist, ValidationError): - Application().set_client_secret(client_secret) + Application().set_application_secret(application_secret) else: - if application.check_client_secret( - client_secret + if application.check_application_secret( + application_secret ) and application.can_authenticate(request=request): return application @@ -131,6 +131,11 @@ class Token(models.Model): def is_expired(self): return timezone.now() >= self.expires + # def expires_in(self): + # if self.is_expired(): + # return 0 + # return int((self.expires - timezone.now()).total_seconds()) + def save(self, **kwargs): if self.expires is None: delta = timezone.timedelta(seconds=self.application.token_duration) diff --git a/creme/creme_api/templates/creme_api/bricks/applications.html b/creme/creme_api/templates/creme_api/bricks/applications.html index 81d4fe5161..bb50ebb45e 100644 --- a/creme/creme_api/templates/creme_api/bricks/applications.html +++ b/creme/creme_api/templates/creme_api/bricks/applications.html @@ -23,10 +23,10 @@ {% block brick_table_columns %} {% brick_table_column_for_field ctype=objects_ctype field='name' status='primary' %} - {% brick_table_column_for_field ctype=objects_ctype field='client_id' %} + {% brick_table_column_for_field ctype=objects_ctype field='application_id' %} {% brick_table_column_for_field ctype=objects_ctype field='enabled' %} {% brick_table_column_for_field ctype=objects_ctype field='token_duration' %} -{# {% brick_table_column_for_field ctype=objects_ctype field='_client_secret' %}#} +{# {% brick_table_column_for_field ctype=objects_ctype field='_application_secret' %}#} {% brick_table_column_for_field ctype=objects_ctype field='created' data_type='date' %} {% brick_table_column_for_field ctype=objects_ctype field='modified' data_type='date' %} {% brick_table_column title=_('Action') status='action' %} @@ -36,7 +36,7 @@ {% for application in page.object_list %} {% print_field object=application field='name' %} - {% print_field object=application field='client_id' %} + {% print_field object=application field='application_id' %} {% print_field object=application field='enabled' %} {% print_field object=application field='token_duration' %} {% trans 'seconds' %} {{application.created|date:"DATE_FORMAT"}} diff --git a/creme/creme_api/templates/creme_api/description.md b/creme/creme_api/templates/creme_api/description.md index 38dcbd82bf..5ed0668a74 100644 --- a/creme/creme_api/templates/creme_api/description.md +++ b/creme/creme_api/templates/creme_api/description.md @@ -1,13 +1,59 @@ # Drive Creme CRM from the outside -Some general description -[Base url]({{ creme_api_rool_url }}) +Welcome on the technical documentation of the Creme CRM API. + +This page introduces the technical characteristics of the API. + +All urls in this document are relative to the api base url: [{{ creme_root_url }}]({{ creme_root_url }}) ## Authentication -How to authenticate +In order to authenticate, you must first define an application. +An application represents an external tool or process which needs to access Creme through its API. +It's identified by its name, and defines various configuration variables related to the service +integration, and security options. + +At creation time, an application is assigned an **Application ID** and an **Application Secret**. +This id and password pair will allow you to create access tokens. +The application secret will be displayed only once. + +To create a token using the application ID and application secret, use the [Tokens endpoint](#/Tokens). + +``` +{ + "token": "long-token-string", + "token_type": "token", + "expires_in": 3600 +} +``` +Here we can see this token is valid for the next `3600` seconds. + +It must then be provided in each API call in the `Authorization` HTTP header. + +``` +Authorization: {token_type} {token} + +Authorization: token long-token-string +``` ## Pagination -A note about the pagination system +When listing resources you can control the number of results return per page using the `page_size` query parameter. +``` +GET {{ creme_root_url }}creme_api/{resource}/?page_size=10 +``` +results will be returned in that form: +``` +{ + "next": "{{ creme_root_url }}creme_api/{resource}/?cursor=cD05Mw%3D%3D&page_size=10", + "previous": null, + "results": [ + ... + ] +} +``` +To fetch the next page, use the "next" url, which defines a `cursor` query parameter: +``` +GET {{ creme_root_url }}creme_api/{resource}/?cursor=cD05Mw%3D%3D&page_size=10 +``` diff --git a/creme/creme_api/tests/test_documentation.py b/creme/creme_api/tests/test_documentation.py index fbb6f3fb04..ed47807bd2 100644 --- a/creme/creme_api/tests/test_documentation.py +++ b/creme/creme_api/tests/test_documentation.py @@ -79,4 +79,4 @@ def test_context(self): response.context["creme_api__tokens_url"], "http://testserver/creme_api/tokens/", ) - self.assertEqual(response.context["token_type"], "Token") + self.assertEqual(response.context["token_type"], "token") diff --git a/creme/creme_api/tests/test_models.py b/creme/creme_api/tests/test_models.py index 8848531747..d155b0f749 100644 --- a/creme/creme_api/tests/test_models.py +++ b/creme/creme_api/tests/test_models.py @@ -10,51 +10,57 @@ class ApplicationTestCase(TestCase): def test_init(self): application = Application(name="TestCase") - self.assertTrue(application.client_id) - self.assertEqual(len(application.client_secret), 0) + self.assertTrue(application.application_id) + self.assertEqual(len(application.application_secret), 0) self.assertTrue(application.enabled) self.assertEqual(application.token_duration, 3600) - self.assertEqual(application.client_secret, "") - self.assertIsNone(application._client_secret) + self.assertEqual(application.application_secret, "") + self.assertIsNone(application._application_secret) def test_str(self): application = Application(name="TestCase") self.assertEqual(str(application), "TestCase") - def test_set_client_secret(self): + def test_set_application_secret(self): application = Application(name="TestCase") - application.set_client_secret("Password") - self.assertEqual(application._client_secret, "Password") - self.assertIsNotNone(application.client_secret) - self.assertTrue(check_password("Password", application.client_secret)) + application.set_application_secret("Password") + self.assertEqual(application._application_secret, "Password") + self.assertIsNotNone(application.application_secret) + self.assertTrue(check_password("Password", application.application_secret)) def test_save01(self): application = Application(name="TestCase") application.save() - self.assertIsNotNone(application._client_secret) - self.assertIsNotNone(application.client_secret) + self.assertIsNotNone(application._application_secret) + self.assertIsNotNone(application.application_secret) self.assertTrue( - check_password(application._client_secret, application.client_secret) + check_password( + application._application_secret, application.application_secret + ) ) application.save() self.assertTrue( - check_password(application._client_secret, application.client_secret) + check_password( + application._application_secret, application.application_secret + ) ) def test_save02(self): application = Application.objects.create(name="TestCase") - self.assertIsNotNone(application._client_secret) - self.assertIsNotNone(application.client_secret) + self.assertIsNotNone(application._application_secret) + self.assertIsNotNone(application.application_secret) self.assertTrue( - check_password(application._client_secret, application.client_secret) + check_password( + application._application_secret, application.application_secret + ) ) - def test_check_client_secret(self): + def test_check_application_secret(self): application = Application(name="TestCase") - application.set_client_secret("Password") - self.assertTrue(application.check_client_secret("Password")) - self.assertFalse(application.check_client_secret("WrongPassword")) + application.set_application_secret("Password") + self.assertTrue(application.check_application_secret("Password")) + self.assertFalse(application.check_application_secret("WrongPassword")) def test_can_authenticate(self): application = Application(name="TestCase", enabled=True) @@ -63,7 +69,7 @@ def test_can_authenticate(self): self.assertFalse(application.can_authenticate()) def test_authenticate01(self): - self.assertIsNone(Application.authenticate("client_id", "Secret")) + self.assertIsNone(Application.authenticate("application_id", "Secret")) def test_authenticate02(self): self.assertIsNone(Application.authenticate(uuid4().hex, "WrongSecret")) @@ -71,13 +77,15 @@ def test_authenticate02(self): def test_authenticate03(self): application = Application.objects.create(name="TestCase", enabled=False) self.assertIsNone( - Application.authenticate(application.client_id, application._client_secret) + Application.authenticate( + application.application_id, application._application_secret + ) ) def test_authenticate04(self): application = Application.objects.create(name="TestCase") authenticated_application = Application.authenticate( - application.client_id, application._client_secret + application.application_id, application._application_secret ) self.assertEqual(authenticated_application.pk, application.pk) diff --git a/creme/creme_api/tests/test_tokens.py b/creme/creme_api/tests/test_tokens.py index dfca263adc..d303647f89 100644 --- a/creme/creme_api/tests/test_tokens.py +++ b/creme/creme_api/tests/test_tokens.py @@ -14,29 +14,29 @@ def test_create_token__missing(self): self.assertValidationErrors( response, { - "client_id": ["required"], - "client_secret": ["required"], + "application_id": ["required"], + "application_secret": ["required"], }, ) def test_create_token__empty(self): data = { - "client_id": "", # trim - "client_secret": "", + "application_id": "", # trim + "application_secret": "", } response = self.make_request(data=data, status_code=400) self.assertValidationErrors( response, { - "client_id": ["invalid"], # Must be a valid UUID. - "client_secret": ["blank"], + "application_id": ["invalid"], # Must be a valid UUID. + "application_secret": ["blank"], }, ) def test_create_token__no_application(self): data = { - "client_id": uuid4().hex, - "client_secret": "Secret", + "application_id": uuid4().hex, + "application_secret": "Secret", } response = self.make_request(data=data, status_code=400) self.assertValidationErrors( @@ -48,8 +48,8 @@ def test_create_token__no_application(self): def test_create_token(self): data = { - "client_id": self.application.client_id, - "client_secret": self.application._client_secret, + "application_id": self.application.application_id, + "application_secret": self.application._application_secret, } response = self.make_request(data=data, status_code=200) token = Token.objects.get(application=self.application) diff --git a/creme/creme_api/views.py b/creme/creme_api/views.py index 999da86343..ca2521b32e 100644 --- a/creme/creme_api/views.py +++ b/creme/creme_api/views.py @@ -1,4 +1,3 @@ -from django.apps import apps from django.contrib import messages from django.template.loader import render_to_string from django.urls import reverse @@ -39,11 +38,8 @@ def get_title(self): def initial(self, request, *args, **kwargs): super().initial(request, *args, **kwargs) - creme_api_app_config = apps.get_app_config("creme_api") - creme_api_rool_url = self.request.build_absolute_uri( - creme_api_app_config.url_root - ) - context = {"creme_api_rool_url": creme_api_rool_url} + creme_root_url = self.request.build_absolute_uri(location="/") + context = {"creme_root_url": creme_root_url} title = self.get_title() description = self.get_description(context=context, request=request) self.schema_generator = self.generator_class( @@ -91,8 +87,8 @@ class ApplicationCreation(generic.CremeModelCreationPopup): success_message = _( "The application «{application_name}» has been created. " "Identifiers have been generated, here they are: \n\n" - "Client ID : {client_id}\n" - "Client Secret : {client_secret}\n\n" + "Application ID : {application_id}\n" + "Application Secret : {application_secret}\n\n" "This is the first and last time this secret displayed!" ) permissions = "creme_api.can_admin" @@ -100,8 +96,8 @@ class ApplicationCreation(generic.CremeModelCreationPopup): def get_success_message(self): return self.success_message.format( application_name=self.object.name, - client_id=self.object.client_id, - client_secret=self.object._client_secret, + application_id=self.object.application_id, + application_secret=self.object._application_secret, ) def form_valid(self, form):