stages/user_delete: add user delete stage, remove view from core
This commit is contained in:
parent
137e90355b
commit
e45b33c6c2
|
@ -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>
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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")
|
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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):
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
|
@ -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"
|
|
@ -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"""
|
|
@ -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',),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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")
|
|
@ -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()
|
|
@ -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())
|
Reference in New Issue