From e45b33c6c237a8b64c2e7d5703895252e06bf0e1 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 12 May 2020 14:50:00 +0200 Subject: [PATCH] stages/user_delete: add user delete stage, remove view from core --- passbook/core/templates/user/settings.html | 2 +- passbook/core/tests/test_views_user.py | 7 -- passbook/core/urls.py | 1 - passbook/core/views/user.py | 14 ---- .../migrations/0005_auto_20200512_1158.py | 18 ++++++ passbook/flows/models.py | 3 +- passbook/flows/urls.py | 5 ++ passbook/root/settings.py | 1 + passbook/stages/user_delete/__init__.py | 0 passbook/stages/user_delete/api.py | 24 +++++++ passbook/stages/user_delete/apps.py | 10 +++ passbook/stages/user_delete/forms.py | 20 ++++++ .../user_delete/migrations/0001_initial.py | 27 ++++++++ .../stages/user_delete/migrations/__init__.py | 0 passbook/stages/user_delete/models.py | 19 ++++++ passbook/stages/user_delete/stage.py | 35 ++++++++++ passbook/stages/user_delete/tests.py | 64 +++++++++++++++++++ 17 files changed, 226 insertions(+), 24 deletions(-) create mode 100644 passbook/flows/migrations/0005_auto_20200512_1158.py create mode 100644 passbook/stages/user_delete/__init__.py create mode 100644 passbook/stages/user_delete/api.py create mode 100644 passbook/stages/user_delete/apps.py create mode 100644 passbook/stages/user_delete/forms.py create mode 100644 passbook/stages/user_delete/migrations/0001_initial.py create mode 100644 passbook/stages/user_delete/migrations/__init__.py create mode 100644 passbook/stages/user_delete/models.py create mode 100644 passbook/stages/user_delete/stage.py create mode 100644 passbook/stages/user_delete/tests.py diff --git a/passbook/core/templates/user/settings.html b/passbook/core/templates/user/settings.html index 09c8a2ec5..7c7cb3976 100644 --- a/passbook/core/templates/user/settings.html +++ b/passbook/core/templates/user/settings.html @@ -15,7 +15,7 @@
diff --git a/passbook/core/tests/test_views_user.py b/passbook/core/tests/test_views_user.py index 1a49dff3f..ab4a75654 100644 --- a/passbook/core/tests/test_views_user.py +++ b/passbook/core/tests/test_views_user.py @@ -29,10 +29,3 @@ class TestUserViews(TestCase): self.client.get(reverse("passbook_core:user-settings")).status_code, 200 ) - def test_user_delete(self): - """Test UserDeleteView""" - self.assertEqual( - self.client.post(reverse("passbook_core:user-delete")).status_code, 302 - ) - self.assertEqual(User.objects.filter(username="unittest user").exists(), False) - self.setUp() diff --git a/passbook/core/urls.py b/passbook/core/urls.py index b4d7bd5fa..df29adde5 100644 --- a/passbook/core/urls.py +++ b/passbook/core/urls.py @@ -6,7 +6,6 @@ from passbook.core.views import overview, user urlpatterns = [ # User views path("-/user/", user.UserSettingsView.as_view(), name="user-settings"), - path("-/user/delete/", user.UserDeleteView.as_view(), name="user-delete"), # Overview path("", overview.OverviewView.as_view(), name="overview"), ] diff --git a/passbook/core/views/user.py b/passbook/core/views/user.py index 946819fb9..a6c6e0ada 100644 --- a/passbook/core/views/user.py +++ b/passbook/core/views/user.py @@ -22,17 +22,3 @@ class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView): def get_object(self): return self.request.user - - -class UserDeleteView(LoginRequiredMixin, DeleteView): - """Delete user account""" - - template_name = "generic/delete.html" - - def get_object(self): - return self.request.user - - def get_success_url(self): - messages.success(self.request, _("Successfully deleted user.")) - logout(self.request) - return reverse("passbook_flows:default-auth") diff --git a/passbook/flows/migrations/0005_auto_20200512_1158.py b/passbook/flows/migrations/0005_auto_20200512_1158.py new file mode 100644 index 000000000..f6968079b --- /dev/null +++ b/passbook/flows/migrations/0005_auto_20200512_1158.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-05-12 11:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('passbook_flows', '0004_auto_20200510_2310'), + ] + + operations = [ + migrations.AlterField( + model_name='flow', + name='designation', + field=models.CharField(choices=[('authentication', 'Authentication'), ('invalidation', 'Invalidation'), ('enrollment', 'Enrollment'), ('unenrollment', 'Unrenollment'), ('recovery', 'Recovery'), ('password_change', 'Password Change')], max_length=100), + ), + ] diff --git a/passbook/flows/models.py b/passbook/flows/models.py index c82ab6e19..11735de39 100644 --- a/passbook/flows/models.py +++ b/passbook/flows/models.py @@ -15,10 +15,11 @@ class FlowDesignation(models.TextChoices): should be replaced by a database entry.""" AUTHENTICATION = "authentication" + INVALIDATION = "invalidation" ENROLLMENT = "enrollment" + UNRENOLLMENT = "unenrollment" RECOVERY = "recovery" PASSWORD_CHANGE = "password_change" # nosec # noqa - INVALIDATION = "invalidation" class Stage(UUIDModel): diff --git a/passbook/flows/urls.py b/passbook/flows/urls.py index 96d1eb8ed..ea7072792 100644 --- a/passbook/flows/urls.py +++ b/passbook/flows/urls.py @@ -30,6 +30,11 @@ urlpatterns = [ ToDefaultFlow.as_view(designation=FlowDesignation.ENROLLMENT), name="default-enrollment", ), + path( + "-/default/unenrollment/", + ToDefaultFlow.as_view(designation=FlowDesignation.UNRENOLLMENT), + name="default-unenrollment", + ), path( "-/default/password_change/", ToDefaultFlow.as_view(designation=FlowDesignation.PASSWORD_CHANGE), diff --git a/passbook/root/settings.py b/passbook/root/settings.py index 25d70f256..ae80d333e 100644 --- a/passbook/root/settings.py +++ b/passbook/root/settings.py @@ -109,6 +109,7 @@ INSTALLED_APPS = [ "passbook.stages.prompt.apps.PassbookStagPromptConfig", "passbook.stages.identification.apps.PassbookStageIdentificationConfig", "passbook.stages.invitation.apps.PassbookStageUserInvitationConfig", + "passbook.stages.user_delete.apps.PassbookStageUserDeleteConfig", "passbook.stages.user_login.apps.PassbookStageUserLoginConfig", "passbook.stages.user_logout.apps.PassbookStageUserLogoutConfig", "passbook.stages.user_write.apps.PassbookStageUserWriteConfig", diff --git a/passbook/stages/user_delete/__init__.py b/passbook/stages/user_delete/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/stages/user_delete/api.py b/passbook/stages/user_delete/api.py new file mode 100644 index 000000000..5b2b84942 --- /dev/null +++ b/passbook/stages/user_delete/api.py @@ -0,0 +1,24 @@ +"""User Delete Stage API Views""" +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import ModelViewSet + +from passbook.stages.user_delete.models import UserDeleteStage + + +class UserDeleteStageSerializer(ModelSerializer): + """UserDeleteStage Serializer""" + + class Meta: + + model = UserDeleteStage + fields = [ + "pk", + "name", + ] + + +class UserDeleteStageViewSet(ModelViewSet): + """UserDeleteStage Viewset""" + + queryset = UserDeleteStage.objects.all() + serializer_class = UserDeleteStageSerializer diff --git a/passbook/stages/user_delete/apps.py b/passbook/stages/user_delete/apps.py new file mode 100644 index 000000000..c9119d295 --- /dev/null +++ b/passbook/stages/user_delete/apps.py @@ -0,0 +1,10 @@ +"""passbook delete stage app config""" +from django.apps import AppConfig + + +class PassbookStageUserDeleteConfig(AppConfig): + """passbook delete stage config""" + + name = "passbook.stages.user_delete" + label = "passbook_stages_user_delete" + verbose_name = "passbook Stages.User Delete" diff --git a/passbook/stages/user_delete/forms.py b/passbook/stages/user_delete/forms.py new file mode 100644 index 000000000..c226f1f4f --- /dev/null +++ b/passbook/stages/user_delete/forms.py @@ -0,0 +1,20 @@ +"""passbook flows delete forms""" +from django import forms + +from passbook.stages.user_delete.models import UserDeleteStage + + +class UserDeleteStageForm(forms.ModelForm): + """Form to delete/edit UserDeleteStage instances""" + + class Meta: + + model = UserDeleteStage + fields = ["name"] + widgets = { + "name": forms.TextInput(), + } + + +class UserDeleteForm(forms.Form): + """Confirmation form to ensure user knows they are deleting their profile""" diff --git a/passbook/stages/user_delete/migrations/0001_initial.py b/passbook/stages/user_delete/migrations/0001_initial.py new file mode 100644 index 000000000..ca80b5565 --- /dev/null +++ b/passbook/stages/user_delete/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.5 on 2020-05-12 11:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('passbook_flows', '0005_auto_20200512_1158'), + ] + + operations = [ + migrations.CreateModel( + name='UserDeleteStage', + fields=[ + ('stage_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_flows.Stage')), + ], + options={ + 'verbose_name': 'User Delete Stage', + 'verbose_name_plural': 'User Delete Stages', + }, + bases=('passbook_flows.stage',), + ), + ] diff --git a/passbook/stages/user_delete/migrations/__init__.py b/passbook/stages/user_delete/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/stages/user_delete/models.py b/passbook/stages/user_delete/models.py new file mode 100644 index 000000000..f7ad5a520 --- /dev/null +++ b/passbook/stages/user_delete/models.py @@ -0,0 +1,19 @@ +"""delete stage models""" +from django.utils.translation import gettext_lazy as _ + +from passbook.flows.models import Stage + + +class UserDeleteStage(Stage): + """Delete stage, delete a user from saved data.""" + + type = "passbook.stages.user_delete.stage.UserDeleteStageView" + form = "passbook.stages.user_delete.forms.UserDeleteStageForm" + + def __str__(self): + return f"User Delete Stage {self.name}" + + class Meta: + + verbose_name = _("User Delete Stage") + verbose_name_plural = _("User Delete Stages") diff --git a/passbook/stages/user_delete/stage.py b/passbook/stages/user_delete/stage.py new file mode 100644 index 000000000..4c4d02fa1 --- /dev/null +++ b/passbook/stages/user_delete/stage.py @@ -0,0 +1,35 @@ +"""Delete stage logic""" +from django.contrib import messages +from django.contrib.auth.backends import ModelBackend +from django.http import HttpRequest, HttpResponse +from django.utils.translation import gettext as _ +from structlog import get_logger + +from django.views.generic import FormView +from passbook.core.models import User +from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER +from passbook.flows.stage import AuthenticationStage +from passbook.stages.user_delete.forms import UserDeleteForm + +LOGGER = get_logger() + + +class UserDeleteStageView(FormView, AuthenticationStage): + """Finalise Enrollment flow by creating a user object.""" + + form_class = UserDeleteForm + + def get(self, request: HttpRequest) -> HttpResponse: + if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context: + message = _("No Pending User.") + messages.error(request, message) + LOGGER.debug(message) + return self.executor.stage_invalid() + return super().get(request) + + def form_valid(self, form: UserDeleteForm) -> HttpResponse: + user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] + user.delete() + LOGGER.debug("Deleted user", user=user) + del self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] + return self.executor.stage_ok() diff --git a/passbook/stages/user_delete/tests.py b/passbook/stages/user_delete/tests.py new file mode 100644 index 000000000..040da90b0 --- /dev/null +++ b/passbook/stages/user_delete/tests.py @@ -0,0 +1,64 @@ +"""delete tests""" +import string +from random import SystemRandom + +from django.shortcuts import reverse +from django.test import Client, TestCase + +from passbook.core.models import User +from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding +from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan +from passbook.flows.views import SESSION_KEY_PLAN +from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT +from passbook.stages.user_delete.forms import UserDeleteStageForm +from passbook.stages.user_delete.models import UserDeleteStage + + +class TestUserDeleteStage(TestCase): + """Delete tests""" + + def setUp(self): + super().setUp() + self.username = 'qerqwerqrwqwerwq' + self.user = User.objects.create(username=self.username, email="test@beryju.org") + self.client = Client() + + self.flow = Flow.objects.create( + name="test-delete", + slug="test-delete", + designation=FlowDesignation.AUTHENTICATION, + ) + self.stage = UserDeleteStage.objects.create(name="delete") + FlowStageBinding.objects.create(flow=self.flow, stage=self.stage, order=2) + + def test_user_delete_get(self): + """Test Form render""" + plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan.context[PLAN_CONTEXT_PENDING_USER] = self.user + session = self.client.session + session[SESSION_KEY_PLAN] = plan + session.save() + + response = self.client.get( + reverse( + "passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + ) + ) + self.assertEqual(response.status_code, 200) + + def test_user_delete_post(self): + """Test User delete (actual)""" + plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage]) + plan.context[PLAN_CONTEXT_PENDING_USER] = self.user + session = self.client.session + session[SESSION_KEY_PLAN] = plan + session.save() + + response = self.client.post( + reverse( + "passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} + ), + {} + ) + self.assertEqual(response.status_code, 302) + self.assertFalse(User.objects.filter(username=self.username).exists())