diff options
author | Michael Elkins <melkins@tislabs.com> | 2012-02-02 22:58:01 +0000 |
---|---|---|
committer | Michael Elkins <melkins@tislabs.com> | 2012-02-02 22:58:01 +0000 |
commit | 9dda8df7ef73871b014caa780b6169852988e5c7 (patch) | |
tree | ebdd4bd786a2f3d1d95b9a75ff4d0531e20a7760 | |
parent | 884bb262e0124efd8d313d2b4b19a53c6216a5a8 (diff) |
add support for add/revoke resources to child
add support for editing child model
add child creation wizard
svn path=/branches/tk161/; revision=4282
-rw-r--r-- | rpkid/rpki/gui/app/forms.py | 125 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/models.py | 21 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/bootstrap_form.html | 4 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/child_add_resource_form.html | 16 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/child_detail.html | 52 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/child_form.html | 23 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/child_view.html | 61 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/child_wizard_form.html | 17 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/object_detail.html | 8 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/views.py | 138 |
10 files changed, 299 insertions, 166 deletions
diff --git a/rpkid/rpki/gui/app/forms.py b/rpkid/rpki/gui/app/forms.py index 0755b6a0..65b84e89 100644 --- a/rpkid/rpki/gui/app/forms.py +++ b/rpkid/rpki/gui/app/forms.py @@ -1,10 +1,10 @@ # Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions # Copyright (C) 2012 SPARTA, Inc. a Parsons Company -# +# # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. -# +# # THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY # AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, @@ -15,11 +15,14 @@ __version__ = '$Id$' -from django import forms -from rpki import resource_set +from django.contrib.auth.models import User +from django import forms +from rpki.resource_set import (resource_range_as, resource_range_ipv4, + resource_range_ipv6) from rpki.gui.app import models from rpki.exceptions import BadIPResource +from rpki.gui.bootstrap.widgets import CheckboxSelectMultiple class AddConfForm(forms.Form): @@ -110,19 +113,32 @@ class ImportClientForm(forms.Form): widget=forms.FileInput(attrs={'class': 'input-file'})) -def ChildWizardForm(parent, *args, **kwargs): - class wrapped(forms.Form): - handle = forms.CharField(max_length=30, help_text='handle for new child') - #create_user = forms.BooleanField(help_text='create a new user account for this handle?') - #password = forms.CharField(widget=forms.PasswordInput, help_text='password for new user', required=False) - #password2 = forms.CharField(widget=forms.PasswordInput, help_text='repeat password', required=False) +class ChildWizardForm(forms.Form): + handle = forms.CharField(max_length=30, help_text='handle for new child') + email = forms.CharField(max_length=30, + help_text='email address for new user') + password = forms.CharField(widget=forms.PasswordInput) + password2 = forms.CharField(widget=forms.PasswordInput, + label='Confirm Password') + parent = forms.ModelChoiceField(queryset=models.Conf.objects.all()) - def clean_handle(self): - if parent.children.filter(handle=self.cleaned_data['handle']): - raise forms.ValidationError, 'a child with that handle already exists' - return self.cleaned_data['handle'] + def clean_handle(self): + handle = self.cleaned_data.get('handle') + if (models.Conf.objects.filter(handle=handle).exists() or + User.objects.filter(username=handle).exists()): + raise forms.ValidationError('user already exists') + return handle - return wrapped(*args, **kwargs) + def clean(self): + p1 = self.cleaned_data.get('password') + p2 = self.cleaned_data.get('password2') + if p1 != p2: + raise forms.ValidationError('passwords do not match') + handle = self.cleaned_data.get('handle') + parent = self.cleaned_data.get('parent') + if parent.children.filter(handle=handle).exists(): + raise forms.ValidationError('parent already has a child by that name') + return self.cleaned_data class ROARequest(forms.Form): @@ -139,9 +155,9 @@ class ROARequest(forms.Form): def _as_resource_range(self): prefix = self.cleaned_data.get('prefix') try: - r = resource_set.resource_range_ipv4.parse_str(prefix) + r = resource_range_ipv4.parse_str(prefix) except BadIPResource: - r = resource_set.resource_range_ipv6.parse_str(prefix) + r = resource_range_ipv6.parse_str(prefix) return r def clean_asn(self): @@ -182,4 +198,77 @@ class ROARequest(forms.Form): return self.cleaned_data -# vim:sw=4 ts=8 expandtab + +def AddASNForm(qs): + """ + Generate a form class which only allows specification of ASNs contained + within the specified queryset. `qs` should be a QuerySet of + irdb.models.ChildASN. + + """ + + class _wrapped(forms.Form): + asns = forms.CharField(label='ASNs', help_text='single ASN or range') + + def clean_asns(self): + try: + r = resource_range_as.parse_str(self.cleaned_data.get('asns')) + except: + raise forms.ValidationError('invalid AS or range') + if not qs.filter(min__lte=r.min, max__gte=r.max).exists(): + raise forms.ValidationError('AS or range is not delegated to you') + return str(r) + + return _wrapped + + +def AddNetForm(qsv4, qsv6): + """ + Generate a form class which only allows specification of prefixes contained + within the specified queryset. `qs` should be a QuerySet of + irdb.models.ChildNet. + + """ + + class _wrapped(forms.Form): + address_range = forms.CharField(help_text='CIDR or range') + + def clean_address_range(self): + address_range = self.cleaned_data.get('address_range') + try: + r = resource_range_ipv4.parse_str(address_range) + if not qsv4.filter(prefix_min__lte=r.min, prefix_max__gte=r.max).exists(): + raise forms.ValidationError('IP address range is not delegated to you') + except BadIPResource: + try: + r = resource_range_ipv6.parse_str(address_range) + if not qsv6.filter(prefix_min__lte=r.min, prefix_max__gte=r.max).exists(): + raise forms.ValidationError('IP address range is not delegated to you') + except BadIPResource: + raise forms.ValidationError('invalid IP address range') + return str(r) + + return _wrapped + + +def ChildForm(instance): + """ + Form for editing a Child model. + + This is roughly based on the equivalent ModelForm, but uses Form as a base + class so that selection boxes for the AS and Prefixes can be edited in a + single form. + + """ + + class _wrapped(forms.Form): + valid_until = forms.DateTimeField(initial=instance.valid_until) + as_ranges = forms.ModelMultipleChoiceField(queryset=models.ChildASN.objects.filter(child=instance), + required=False, + label='AS Ranges', + help_text='deselect to remove delegation') + address_ranges = forms.ModelMultipleChoiceField(queryset=models.ChildNet.objects.filter(child=instance), + required=False, + help_text='deselect to remove delegation') + + return _wrapped diff --git a/rpkid/rpki/gui/app/models.py b/rpkid/rpki/gui/app/models.py index eb6c6a47..a44e21d7 100644 --- a/rpkid/rpki/gui/app/models.py +++ b/rpkid/rpki/gui/app/models.py @@ -57,6 +57,27 @@ class Child(rpki.irdb.models.Child): proxy = True verbose_name_plural = 'children' + +class ChildASN(rpki.irdb.models.ChildASN): + """Proxy model for irdb ChildASN.""" + + class Meta: + proxy = True + + def __unicode__(self): + return u'AS%s' % self.as_resource_range() + + +class ChildNet(rpki.irdb.models.ChildNet): + """Proxy model for irdb ChildNet.""" + + class Meta: + proxy = True + + def __unicode__(self): + return u'%s' % self.as_resource_range() + + class Conf(rpki.irdb.models.ResourceHolderCA): '''This is the center of the universe, also known as a place to have a handle on a resource-holding entity. It's the <self> diff --git a/rpkid/rpki/gui/app/templates/app/bootstrap_form.html b/rpkid/rpki/gui/app/templates/app/bootstrap_form.html index f30cb4ad..d3df1081 100644 --- a/rpkid/rpki/gui/app/templates/app/bootstrap_form.html +++ b/rpkid/rpki/gui/app/templates/app/bootstrap_form.html @@ -1,3 +1,5 @@ +{# vim:set ft=htmldjango #} + {% if form.non_field_errors %} <div class='alert-message error'> {{ form.non_field_errors }} @@ -29,5 +31,3 @@ {% endif %} {% endfor %} - -<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/child_add_resource_form.html b/rpkid/rpki/gui/app/templates/app/child_add_resource_form.html new file mode 100644 index 00000000..98789191 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/child_add_resource_form.html @@ -0,0 +1,16 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-header'> + <h1>Add Resource: {{ object.handle }}</h1> +</div> + +<form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type='submit' value='Save'> + <a class='btn' href='{{ object.get_absolute_url }}'>Cancel</a> + </div> +</form> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/child_detail.html b/rpkid/rpki/gui/app/templates/app/child_detail.html new file mode 100644 index 00000000..7a6918eb --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/child_detail.html @@ -0,0 +1,52 @@ +{% extends "app/object_detail.html" %} + +{% block object_detail %} +<div class='row'> + <div class='span2'> + <p><strong>Child Handle</strong> + </div> + <div class='span2'> + <p>{{ object.handle }} + </div> +</div> +<div class='row'> + <div class='span2'> + <p><strong>Valid until</strong> + </div> + <div class='span4'> + <p>{{ object.valid_until }} + </div> +</div> + +<div class='row'> + <div class='span8'> + <strong>Addresses</strong> + {% if object.address_ranges.all %} + <ul class='unstyled'> + {% for a in object.address_ranges.all %} + <li>{{ a.as_resource_range }}</li> + {% endfor %} + </ul> + {% else %} + <p style='font-style:italic'>none</p> + {% endif %} + </div> + <div class='span8'> + <strong>ASNs</strong> + {% if object.asns.all %} + <ul class='unstyled'> + {% for a in object.asns.all %} + <li>{{ a.as_resource_range }}</li> + {% endfor %} + </ul> + {% else %} + <p style='font-style:italic'>none</p> + {% endif %} + </div> +</div> +{% endblock object_detail %} + +{% block actions %} +<a class='btn' href="{{ object.get_absolute_url }}/add_asn">Delegate AS</a> +<a class='btn' href="{{ object.get_absolute_url }}/add_address">Delegate Prefix</a> +{% endblock actions %} diff --git a/rpkid/rpki/gui/app/templates/app/child_form.html b/rpkid/rpki/gui/app/templates/app/child_form.html index 0e5a5ac2..cd9b2a8c 100644 --- a/rpkid/rpki/gui/app/templates/app/child_form.html +++ b/rpkid/rpki/gui/app/templates/app/child_form.html @@ -1,20 +1,17 @@ -{% extends "base.html" %} +{% extends "app/app_base.html" %} {% block content %} - -<p id='breadcrumb'> -<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle.handle }}</a> > -<a href="{{ child.get_absolute_url }}">{{ child.handle }}</a> > Edit -</p> - -<h1>Edit Child</h1> - -<p><span style='font-weight:bold'>Child:</span> {{ child.handle }}</p> +<div class='page-header'> + <h1>Edit Child: {{ object.handle }}</h1> +</div> <form method='POST' action='{{ request.get_full_path }}'> - {% csrf_token %} - {{ form.as_p }} - <input type='submit'/ value='Save'> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type='submit' value='Save'> + <a class='btn' href="{{ object.get_absolute_url }}">Cancel</a> + </div> </form> {% endblock %} diff --git a/rpkid/rpki/gui/app/templates/app/child_view.html b/rpkid/rpki/gui/app/templates/app/child_view.html deleted file mode 100644 index 7d3f5d8a..00000000 --- a/rpkid/rpki/gui/app/templates/app/child_view.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends "app/app_base.html" %} - -{% block content %} - -<div class='page-header'> - <h1>Child Detail</h1> -</div> - -<table style='condensed-table'> - <tr> - <td>Child</td> - <td>{{ child.handle }}</td> - </tr> - <tr> - <td>Valid until</td> - <td>{{ child.valid_until }}</td> - </tr> -</table> - -<h2>Delegated Addresses</h2> -{% if child.address_ranges.all %} -<ul> -{% for a in child.address_ranges.all %} -<li>{{ a.as_resource_range }}</li> -{% endfor %} -</ul> -{% else %} -<p style='font-style:italic'>none</p> -{% endif %} - -<h2>Delegated ASNs</h2> -{% if child.asns.all %} -<ul> -{% for a in child.asns.all %} -<li>{{ a.as_resource_range }}</li> -{% endfor %} -</ul> -{% else %} -<p style='font-style:italic'>none</p> -{% endif %} - -<hr> - -{% if form %} -<form method='POST' action='{{ request.get_full_path }}'> - {% csrf_token %} - <input type='submit'/> -</form> -{% else %} - -<div class='actions'> - <a class='btn' href="{{ child.get_absolute_url }}/edit">Edit</a> - <a class='btn' href="{{ child.get_absolute_url }}/export" title="download XML response file to return to child">Export child response</a></li> - <a class='btn' href="{{ child.get_absolute_url }}/export_repo" title="download XML response to publication client request">Export repo response</a></li> - <a class='btn danger' href="{{ child.get_absolute_url }}/delete" title="remove this handle as a RPKI child">Delete</a></li> - <a class='btn danger' href="{{ child.get_absolute_url }}/destroy" title="completely remove a locally hosted resource handle and gui account">Destroy</a></li> -</div> - -{% endif %} - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/app/child_wizard_form.html b/rpkid/rpki/gui/app/templates/app/child_wizard_form.html index 85c85ed5..1a07402f 100644 --- a/rpkid/rpki/gui/app/templates/app/child_wizard_form.html +++ b/rpkid/rpki/gui/app/templates/app/child_wizard_form.html @@ -1,13 +1,16 @@ -{% extends "base.html" %} +{% extends "app/app_base.html" %} {% block content %} +<div class='page-title'> + <h1>Create User</h1> +</div> <form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> - {% csrf_token %} - <table> -{{ form.as_table }} -</table> -<input type="submit" value="Create"> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type="submit" value="Create"> + <a class='btn' href="{% url rpki.gui.app.views.child_list %}">Cancel</a> + </div> </form> - {% endblock %} diff --git a/rpkid/rpki/gui/app/templates/app/object_detail.html b/rpkid/rpki/gui/app/templates/app/object_detail.html index 3fd12e82..7def9a61 100644 --- a/rpkid/rpki/gui/app/templates/app/object_detail.html +++ b/rpkid/rpki/gui/app/templates/app/object_detail.html @@ -2,20 +2,20 @@ {% load app_extras %} {% block content %} - <div class='page-header'> <h1>{% verbose_name object %}</h1> </div> {% block object_detail %} {{ object }} -{% endblock %} +{% endblock object_detail %} {% if confirm_delete %} <div class='alert-message box-message warning'> <p><strong>Please confirm</strong> that you would like to delete this object. <div class='alert-actions'> <form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} <input class='btn danger' type='submit' value='Delete'/> <a class='btn' href='{{ object.get_absolute_url }}'>Cancel</a> </form> @@ -27,9 +27,7 @@ <a class='btn' href='{{ object.get_absolute_url }}/edit'>Edit</a> {% endif %} <a class='btn danger' href='{{ object.get_absolute_url }}/delete'>Delete</a> + {% block actions %}{% endblock actions %} </div> {% endif %} - {% endblock content %} - -<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/views.py b/rpkid/rpki/gui/app/views.py index b1fdec33..57439aa6 100644 --- a/rpkid/rpki/gui/app/views.py +++ b/rpkid/rpki/gui/app/views.py @@ -33,6 +33,7 @@ from django import http from django.views.generic.list_detail import object_list, object_detail from django.views.generic.create_update import delete_object from django.core.urlresolvers import reverse +from django.contrib.auth.models import User from rpki.irdb import Zookeeper, ChildASN, ChildNet from rpki.gui.app import models, forms, glue, range_list @@ -314,7 +315,7 @@ def child_list(request): def child_add_resource(request, pk, form_class, unused_list, callback, template_name='app/child_add_resource_form.html'): conf = request.session['handle'] - child = models.Child.objects.filter(issuer=conf, pk=pk) + child = models.Child.objects.get(issuer=conf, pk=pk) if request.method == 'POST': form = form_class(request.POST, request.FILES) if form.is_valid(): @@ -328,55 +329,73 @@ def child_add_resource(request, pk, form_class, unused_list, callback, def add_asn_callback(child, form): - r = resource_range_as.parse_str(form.as_range) - child.asns.create(min=r.min, max=r.max) + asns = form.cleaned_data.get('asns') + r = resource_range_as.parse_str(asns) + child.asns.create(start_as=r.min, end_as=r.max) def child_add_asn(request, pk): - return child_add_resource(request, pk, form_class=forms.AddASNForm, - callback=add_asn_callback) + conf = request.session['handle'] + get_object_or_404(models.Child, issuer=conf, pk=pk) + qs = models.ResourceRangeAS.objects.filter(cert__parent__issuer=conf) + return child_add_resource(request, pk, forms.AddASNForm(qs), [], + add_asn_callback) def add_address_callback(child, form): + address_range = form.cleaned_data.get('address_range') try: - r = resource_range_ipv4.parse_str(form.prefix) - family = 4 + r = resource_range_ipv4.parse_str(address_range) + version = 'IPv4' except BadIPResource: - r = resource_range_ipv6.parse_str(form.prefix) - family = 6 - child.address_ranges.create(min=str(r.min), max=str(r.max), family=family) + r = resource_range_ipv6.parse_str(address_range) + version = 'IPv6' + child.address_ranges.create(start_ip=str(r.min), end_ip=str(r.max), + version=version) def child_add_address(request, pk): - return child_add_resource(request, pk, form_class=forms.AddAddressForm, - callback=add_address_callback) + conf = request.session['handle'] + get_object_or_404(models.Child, issuer=conf, pk=pk) + qsv4 = models.ResourceRangeAddressV4.objects.filter(cert__parent__issuer=conf) + qsv6 = models.ResourceRangeAddressV6.objects.filter(cert__parent__issuer=conf) + return child_add_resource(request, pk, + forms.AddNetForm(qsv4, qsv6), + [], + callback=add_address_callback) @handle_required def child_view(request, pk): - '''Detail view of child for the currently selected handle.''' - handle = request.session['handle'] - child = get_object_or_404(handle.children.all(), pk=pk) - return render(request, 'app/child_view.html', {'child': child}) + """Detail view of child for the currently selected handle.""" + conf = request.session['handle'] + child = get_object_or_404(conf.children.all(), pk=pk) + return render(request, 'app/child_detail.html', + {'object': child, 'can_edit': True}) @handle_required def child_edit(request, pk): """Edit the end validity date for a resource handle's child.""" - handle = request.session['handle'] - child = get_object_or_404(handle.children.all(), pk=pk) - + conf = request.session['handle'] + child = get_object_or_404(conf.children.all(), pk=pk) + form_class = forms.ChildForm(child) if request.method == 'POST': - form = forms.ChildForm(request.POST, request.FILES, instance=child) + form = form_class(request.POST, request.FILES) if form.is_valid(): - form.save() - glue.configure_resources(request.META['wsgi.errors'], handle) + child.valid_until = form.cleaned_data.get('valid_until') + child.save() + # remove AS & prefixes that are not selected in the form + models.ChildASN.objects.filter(child=child).exclude(pk__in=form.cleaned_data.get('as_ranges')).delete() + models.ChildNet.objects.filter(child=child).exclude(pk__in=form.cleaned_data.get('address_ranges')).delete() return http.HttpResponseRedirect(child.get_absolute_url()) else: - form = forms.ChildForm(instance=child) + form = form_class(initial={ + 'as_ranges': child.asns.all(), + 'address_ranges': child.address_ranges.all()}) return render(request, 'app/child_form.html', - {'child': child, 'form': form}) + {'object': child, 'form': form}) @handle_required @@ -593,42 +612,45 @@ def refresh(request): return http.HttpResponseRedirect(reverse(dashboard)) -@login_required -def initialize(request): - """ - Initialize a new user account. - - """ - if request.method == 'POST': - form = forms.GenericConfirmationForm(request.POST) - if form.is_valid(): - glue.initialize_handle(request.META['wsgi.errors'], - handle=request.user.username, owner=request.user) - return http.HttpResponseRedirect(reverse(dashboard)) - else: - form = forms.GenericConfirmationForm() - - return render(request, 'app/initialize_form.html', {'form': form}) - - @handle_required def child_wizard(request): """ Wizard mode to create a new locally hosted child. """ - conf = request.session['handle'] - log = request.META['wsgi.errors'] if not request.user.is_superuser: return http.HttpResponseForbidden() if request.method == 'POST': - form = forms.ChildWizardForm(conf, request.POST) + form = forms.ChildWizardForm(request.POST, request.FILES) if form.is_valid(): - glue.create_child(log, conf, form.cleaned_data['handle']) + handle = form.cleaned_data.get('handle') + pw = form.cleaned_data.get('password') + email = form.cleaned_data.get('email') + + User.objects.create_user(handle, email, pw) + + # FIXME etree_wrapper should allow us to deal with file objects + t = NamedTemporaryFile(delete=False) + t.close() + + zk_child = Zookeeper(handle=handle) + identity_xml = zk_child.initialize() + identity_xml.save(t.name) + parent = form.cleaned_data.get('parent') + zk_parent = Zookeeper(handle=parent.handle) + parent_response, _ = zk_parent.configure_child(t.name) + parent_response.save(t.name) + repo_req, _ = zk_child.configure_parent(t.name) + repo_req.save(t.name) + repo_resp, _ = zk_parent.configure_publication_client(t.name) + repo_resp.save(t.name) + zk_child.configure_repository(t.name) + os.remove(t.name) + return http.HttpResponseRedirect(reverse(dashboard)) else: - form = forms.ChildWizardForm(conf) + form = forms.ChildWizardForm() return render(request, 'app/child_wizard_form.html', {'form': form}) @@ -677,18 +699,12 @@ def update_bpki(request): @handle_required def child_delete(request, pk): conf = request.session['handle'] - child = get_object_or_404(conf.children, pk=pk) - - if request.method == 'POST': - form = forms.GenericConfirmationForm(request.POST, request.FILES) - if form.is_valid(): - child.delete() - return http.HttpResponseRedirect(reverse(child_list)) - else: - form = forms.GenericConfirmationForm() - - return render(request, 'app/child_delete_form.html', - {'form': form, 'object': child}) + # verify this child belongs to the current user + get_object_or_404(conf.children, pk=pk) + return delete_object(request, model=models.Child, object_id=pk, + post_delete_redirect=reverse(child_list), + template_name='app/child_detail.html', + extra_context={'confirm_delete': True}) @login_required @@ -857,7 +873,9 @@ def client_detail(request, pk): @superuser_required def client_delete(request, pk): return delete_object(request, model=models.Client, object_id=pk, - template_name='app/client_detail.html') + post_delete_redirect=reverse(client_list), + template_name='app/client_detail.html', + extra_context={'confirm_delete': True}) @superuser_required |