stages/user_delete: add user delete stage, remove view from core

This commit is contained in:
Jens Langhammer 2020-05-12 14:50:00 +02:00
parent 137e90355b
commit e45b33c6c2
17 changed files with 226 additions and 24 deletions

View File

@ -15,7 +15,7 @@
<div class="pf-c-form__horizontal-group"> <div class="pf-c-form__horizontal-group">
<div class="pf-c-form__actions"> <div class="pf-c-form__actions">
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" /> <input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_core:user-delete' %}?back={{ request.get_full_path }}">{% trans "Delete account" %}</a> <a class="pf-c-button pf-m-danger" href="{% url 'passbook_flows:default-unenrollment' %}?back={{ request.get_full_path }}">{% trans "Delete account" %}</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -29,10 +29,3 @@ class TestUserViews(TestCase):
self.client.get(reverse("passbook_core:user-settings")).status_code, 200 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()

View File

@ -6,7 +6,6 @@ from passbook.core.views import overview, user
urlpatterns = [ urlpatterns = [
# User views # User views
path("-/user/", user.UserSettingsView.as_view(), name="user-settings"), path("-/user/", user.UserSettingsView.as_view(), name="user-settings"),
path("-/user/delete/", user.UserDeleteView.as_view(), name="user-delete"),
# Overview # Overview
path("", overview.OverviewView.as_view(), name="overview"), path("", overview.OverviewView.as_view(), name="overview"),
] ]

View File

@ -22,17 +22,3 @@ class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
def get_object(self): def get_object(self):
return self.request.user 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")

View File

@ -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),
),
]

View File

@ -15,10 +15,11 @@ class FlowDesignation(models.TextChoices):
should be replaced by a database entry.""" should be replaced by a database entry."""
AUTHENTICATION = "authentication" AUTHENTICATION = "authentication"
INVALIDATION = "invalidation"
ENROLLMENT = "enrollment" ENROLLMENT = "enrollment"
UNRENOLLMENT = "unenrollment"
RECOVERY = "recovery" RECOVERY = "recovery"
PASSWORD_CHANGE = "password_change" # nosec # noqa PASSWORD_CHANGE = "password_change" # nosec # noqa
INVALIDATION = "invalidation"
class Stage(UUIDModel): class Stage(UUIDModel):

View File

@ -30,6 +30,11 @@ urlpatterns = [
ToDefaultFlow.as_view(designation=FlowDesignation.ENROLLMENT), ToDefaultFlow.as_view(designation=FlowDesignation.ENROLLMENT),
name="default-enrollment", name="default-enrollment",
), ),
path(
"-/default/unenrollment/",
ToDefaultFlow.as_view(designation=FlowDesignation.UNRENOLLMENT),
name="default-unenrollment",
),
path( path(
"-/default/password_change/", "-/default/password_change/",
ToDefaultFlow.as_view(designation=FlowDesignation.PASSWORD_CHANGE), ToDefaultFlow.as_view(designation=FlowDesignation.PASSWORD_CHANGE),

View File

@ -109,6 +109,7 @@ INSTALLED_APPS = [
"passbook.stages.prompt.apps.PassbookStagPromptConfig", "passbook.stages.prompt.apps.PassbookStagPromptConfig",
"passbook.stages.identification.apps.PassbookStageIdentificationConfig", "passbook.stages.identification.apps.PassbookStageIdentificationConfig",
"passbook.stages.invitation.apps.PassbookStageUserInvitationConfig", "passbook.stages.invitation.apps.PassbookStageUserInvitationConfig",
"passbook.stages.user_delete.apps.PassbookStageUserDeleteConfig",
"passbook.stages.user_login.apps.PassbookStageUserLoginConfig", "passbook.stages.user_login.apps.PassbookStageUserLoginConfig",
"passbook.stages.user_logout.apps.PassbookStageUserLogoutConfig", "passbook.stages.user_logout.apps.PassbookStageUserLogoutConfig",
"passbook.stages.user_write.apps.PassbookStageUserWriteConfig", "passbook.stages.user_write.apps.PassbookStageUserWriteConfig",

View File

View File

@ -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

View File

@ -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"

View File

@ -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"""

View File

@ -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',),
),
]

View File

@ -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")

View File

@ -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()

View File

@ -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())