From e3bf6210a1c73601fdbb6403cc3f6ae1ec968d9c Mon Sep 17 00:00:00 2001 From: Fluturecode Date: Fri, 26 May 2023 11:42:41 -0400 Subject: [PATCH 1/5] added migrations --- ...oricaluser_disabled_at_user_disabled_at.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 {{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/migrations/0002_historicaluser_disabled_at_user_disabled_at.py diff --git a/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/migrations/0002_historicaluser_disabled_at_user_disabled_at.py b/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/migrations/0002_historicaluser_disabled_at_user_disabled_at.py new file mode 100644 index 0000000..eb868e1 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/migrations/0002_historicaluser_disabled_at_user_disabled_at.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.2 on 2023-04-18 18:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="historicaluser", + name="disabled_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="user", + name="disabled_at", + field=models.DateTimeField(blank=True, null=True), + ), + ] From e2e7b0cba1efd2d5c971ef4b7ed017b6c77e2d27 Mon Sep 17 00:00:00 2001 From: Fluturecode Date: Fri, 26 May 2023 11:43:23 -0400 Subject: [PATCH 2/5] updated users/models.py --- .../{{ cookiecutter.project_slug }}/users/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/models.py b/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/models.py index ee845b0..3cae39c 100644 --- a/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/models.py +++ b/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/models.py @@ -66,7 +66,7 @@ class User(AbstractUser, TimeStampedModel): "Unselect this instead of deleting accounts." ), ) - + disabled_at = models.DateTimeField(null=True, blank=True) objects = UserManager() history = HistoricalRecords(excluded_fields=["password"]) @@ -89,6 +89,12 @@ class Meta: def activate(self): self.activated_at = timezone.now() self.is_active = True + self.disabled_at = None + self.save() + + def deactivate(self): + self.is_active = False + self.disabled_at = timezone.now() self.save() def can_access_role(self, role): From 65e80a01b0f9e09e089fb095a4d2802295bfc20e Mon Sep 17 00:00:00 2001 From: Fluturecode Date: Fri, 26 May 2023 11:44:35 -0400 Subject: [PATCH 3/5] added tests for models.py --- .../users/test/test_models.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/test/test_models.py b/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/test/test_models.py index 3cb80ad..b9b25e1 100644 --- a/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/test/test_models.py +++ b/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/test/test_models.py @@ -28,5 +28,16 @@ def test_user_activate(): user.activate() - assert user.is_active - assert user.activated_at + assert user.is_active == True + assert user.activated_at is not None + assert user.disabled_at is None + +@pytest.mark.django_db +def test_user_deactivate(): + user = UserFactory() + + user.activate() + user.deactivate() + + assert user.is_active is False + assert user.disabled_at is not None From df3db2dc366d99174559e58a1b68f7b1a3f29e97 Mon Sep 17 00:00:00 2001 From: Fluturecode Date: Fri, 26 May 2023 11:46:10 -0400 Subject: [PATCH 4/5] added disable and enable endpoints to user/views.py --- .../users/views.py | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/views.py b/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/views.py index 4c7881d..d7233f1 100644 --- a/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/views.py +++ b/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/views.py @@ -1,11 +1,11 @@ from rest_framework_extensions.mixins import NestedViewSetMixin -from {{ cookiecutter.project_slug }}.users.email import ChangeEmailRequestEmail -from {{ cookiecutter.project_slug }}.core.filters import ( +from dj_starter_demo.users.email import ChangeEmailRequestEmail +from dj_starter_demo.core.filters import ( CamelCaseDjangoFilterBackend, CamelCaseOrderingFilter, ) -from {{ cookiecutter.project_slug }}.core.serializers import NullSerializer, serialize_email -from {{ cookiecutter.project_slug }}.core.tasks import send_email_later +from dj_starter_demo.core.serializers import NullSerializer, serialize_email +from dj_starter_demo.core.tasks import send_email_later from djoser.serializers import UidAndTokenSerializer from django.db import transaction from rest_framework import filters, status, viewsets, mixins @@ -26,6 +26,8 @@ ) from .permissions import IsAdmin, IsUserOrAdmin from .models import HistoricalUser +from .models import User +from django.utils import timezone class UserViewSet(DjoserUserViewSet): @@ -44,17 +46,7 @@ class UserViewSet(DjoserUserViewSet): } search_fields = ["email", "last_name", "first_name"] - def make_role_read_only(self, serializer): - serializer.validated_data.pop('role', None) - def perform_update(self, serializer): - user_being_accessed = self.get_object() - user_making_request = self.request.user - is_same_user = (user_being_accessed == user_making_request) - - if is_same_user or user_being_accessed.role == "USER" or user_being_accessed.role == "EDITOR": - self.make_role_read_only(serializer) - serializer.save() @action( @@ -94,7 +86,6 @@ def cancel_change_email_request(self, request, *args, **kwargs): serialized_user = UserSerializer(user, context={"request": request}) return Response(data=serialized_user.data, status=status.HTTP_200_OK) - @action( detail=False, methods=["post"], @@ -193,7 +184,6 @@ def invitation(self, request): user = serializer.save(is_active=False) - # Send invitation email context = {"user": user} to = [user.email] serialized_email = serialize_email(settings.EMAIL.activation(self.request, context), to) @@ -201,6 +191,38 @@ def invitation(self, request): return Response(status=status.HTTP_204_NO_CONTENT) + @action( + detail=True, + methods=["post"], + serializer_class=NullSerializer, + permission_classes=[IsAdmin], + ) + def disable(self, request, id): + try: + user = User.objects.get(id=id) + except User.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + user.deactivate() + serialized_user = UserSerializer(user, context={"request": request}) + return Response(data=serialized_user.data, status=status.HTTP_200_OK) + + @action( + detail=True, + methods=["post"], + serializer_class=NullSerializer, + permission_classes=[IsAdmin], + ) + def enable(self, request, id): + try: + user = User.objects.get(id=id) + except User.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + user = User.objects.get(id=id) + user.activate() + serialized_user = UserSerializer(user, context={"request": request}) + return Response(data=serialized_user.data, status=status.HTTP_200_OK) class UserHistoryViewSet( NestedViewSetMixin, viewsets.GenericViewSet, mixins.ListModelMixin From e31e5f8c567f4d1300304ef4147ed8e376383e28 Mon Sep 17 00:00:00 2001 From: Fluturecode Date: Fri, 26 May 2023 11:50:16 -0400 Subject: [PATCH 5/5] updated dj_starter_demo to cookiecutter --- .../{{ cookiecutter.project_slug }}/users/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/views.py b/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/views.py index d7233f1..7dea28f 100644 --- a/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/views.py +++ b/{{ cookiecutter.project_slug }}/{{ cookiecutter.project_slug }}/users/views.py @@ -1,11 +1,11 @@ from rest_framework_extensions.mixins import NestedViewSetMixin -from dj_starter_demo.users.email import ChangeEmailRequestEmail -from dj_starter_demo.core.filters import ( +from {{ cookiecutter.project_slug }}.users.email import ChangeEmailRequestEmail +from {{ cookiecutter.project_slug }}.core.filters import ( CamelCaseDjangoFilterBackend, CamelCaseOrderingFilter, ) -from dj_starter_demo.core.serializers import NullSerializer, serialize_email -from dj_starter_demo.core.tasks import send_email_later +from {{ cookiecutter.project_slug }}.core.serializers import NullSerializer, serialize_email +from {{ cookiecutter.project_slug }}.core.tasks import send_email_later from djoser.serializers import UidAndTokenSerializer from django.db import transaction from rest_framework import filters, status, viewsets, mixins