aboutsummaryrefslogtreecommitdiff
path: root/rpkid
diff options
context:
space:
mode:
authorMichael Elkins <melkins@tislabs.com>2011-03-24 21:20:02 +0000
committerMichael Elkins <melkins@tislabs.com>2011-03-24 21:20:02 +0000
commitf8438285d83f460d6fdde9ad084bc8cf8a48673d (patch)
treead3567305a1c9300e604438f5c63063a9b656528 /rpkid
parentb9db5dadd233bb3faeae65eb0e6010cd73297f19 (diff)
add support to the portal gui for generating ghostbuster requests
svn path=/rpkid/rpki/gui/app/admin.py; revision=3738
Diffstat (limited to 'rpkid')
-rw-r--r--rpkid/rpki/gui/app/admin.py10
-rw-r--r--rpkid/rpki/gui/app/forms.py56
-rw-r--r--rpkid/rpki/gui/app/glue.py35
-rw-r--r--rpkid/rpki/gui/app/models.py38
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/dashboard.html108
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html14
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html44
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html17
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html23
-rw-r--r--rpkid/rpki/gui/app/urls.py5
-rw-r--r--rpkid/rpki/gui/app/views.py70
11 files changed, 380 insertions, 40 deletions
diff --git a/rpkid/rpki/gui/app/admin.py b/rpkid/rpki/gui/app/admin.py
index 8d2b7824..8b7cd24a 100644
--- a/rpkid/rpki/gui/app/admin.py
+++ b/rpkid/rpki/gui/app/admin.py
@@ -46,11 +46,15 @@ class ResourceCertAdmin(admin.ModelAdmin):
class RoaRequestAdmin(admin.ModelAdmin):
pass
-admin.site.register(models.Conf, ConfAdmin)
-admin.site.register(models.Child, ChildAdmin)
+class GhostbusterAdmin(admin.ModelAdmin):
+ pass
+
admin.site.register(models.AddressRange, AddressRangeAdmin)
+admin.site.register(models.Child, ChildAdmin)
+admin.site.register(models.Conf, ConfAdmin)
admin.site.register(models.Asn, AsnAdmin)
+admin.site.register(models.Ghostbuster, GhostbusterAdmin)
admin.site.register(models.Parent, ParentAdmin)
+admin.site.register(models.ResourceCert, ResourceCertAdmin)
admin.site.register(models.Roa, RoaAdmin)
admin.site.register(models.RoaRequest, RoaRequestAdmin)
-admin.site.register(models.ResourceCert, ResourceCertAdmin)
diff --git a/rpkid/rpki/gui/app/forms.py b/rpkid/rpki/gui/app/forms.py
index f7f51936..86c3d6d8 100644
--- a/rpkid/rpki/gui/app/forms.py
+++ b/rpkid/rpki/gui/app/forms.py
@@ -144,4 +144,60 @@ def PrefixDeleteForm(prefix, *args, **kwargs):
return _wrapped(*args, **kwargs)
+def GhostbusterForm(parent_qs, conf=None):
+ """
+ Generate a ModelForm with the subset of parents for the current
+ resource handle.
+
+ The 'conf' argument is required when creating a new object, in
+ order to specify the value of the 'conf' field in the new
+ Ghostbuster object.
+ """
+ class wrapped(forms.ModelForm):
+ # override parent
+ parent = forms.ModelMultipleChoiceField(queryset=parent_qs, required=False,
+ help_text='use this record for a specific parent, or leave blank for all parents')
+ # override full_name. it is required in the db schema, but we allow the
+ # user to skip it and default from family+given name
+ full_name = forms.CharField(max_length=40, required=False,
+ help_text='automatically generated from family and given names if left blank')
+
+ class Meta:
+ model = models.Ghostbuster
+ exclude = [ 'conf' ]
+
+ def clean(self):
+ family_name = self.cleaned_data.get('family_name')
+ given_name = self.cleaned_data.get('given_name')
+ if not all([family_name, given_name]):
+ raise forms.ValidationError, 'Family and Given names must be specified'
+
+ email = self.cleaned_data.get('email_address')
+ postal = self.cleaned_data.get('postal_address')
+ telephone = self.cleaned_data.get('telephone')
+ if not any([email, postal, telephone]):
+ raise forms.ValidationError, 'One of telephone, email or postal address must be specified'
+
+ # if the full name is not specified, default to given+family
+ fn = self.cleaned_data.get('full_name')
+ if not fn:
+ self.cleaned_data['full_name'] = '%s %s' % (given_name, family_name)
+
+ return self.cleaned_data
+
+ def save(self, *args, **kwargs):
+ if conf:
+ # the generic create_object view doesn't allow us to set
+ # the conf field, so wrap the save() method and set it
+ # here
+ kwargs['commit'] = False
+ obj = super(wrapped, self).save(*args, **kwargs)
+ obj.conf = conf
+ obj.save()
+ return obj
+ else:
+ return super(wrapped, self).save(*args, **kwargs)
+
+ return wrapped
+
# vim:sw=4 ts=8 expandtab
diff --git a/rpkid/rpki/gui/app/glue.py b/rpkid/rpki/gui/app/glue.py
index a26910d8..70ec255e 100644
--- a/rpkid/rpki/gui/app/glue.py
+++ b/rpkid/rpki/gui/app/glue.py
@@ -95,6 +95,29 @@ def build_rpkid_caller(cfg, verbose=False):
url = rpkid_base + "left-right",
debug = verbose))
+def ghostbuster_to_vcard(gbr):
+ """
+ Convert a Ghostbuster object into a vCard object.
+ """
+ import vobject
+
+ vcard = vobject.vCard()
+ vcard.add('N').value = vobject.vcard.Name(family=gbr.family_name, given=gbr.given_name)
+ # mapping from vCard type to Ghostbuster model field
+ # the ORG type is a sequence of organization unit names, so
+ # transform the org name into a tuple before stuffing into the
+ # vCard object
+ attrs = [ ('FN', 'full_name', None),
+ ('ADR', 'postal_address', None),
+ ('TEL', 'telephone', None),
+ ('ORG', 'organization', lambda x: (x,)),
+ ('EMAIL', 'email_address', None) ]
+ for vtype, field, transform in attrs:
+ v = getattr(gbr, field)
+ if v:
+ vcard.add(vtype).value = transform(v) if transform else v
+ return vcard.serialize()
+
def configure_resources(log, handle):
"""
This function should be called when resources for this resource
@@ -142,8 +165,18 @@ def configure_resources(log, handle):
valid_until = rpki.sundial.datetime.fromdatetime(child.valid_until)
children.append((child.handle, asns, v4, v6, valid_until))
+ ghostbusters = []
+ for gbr in handle.ghostbusters.all():
+ vcard = ghostbuster_to_vcard(gbr)
+ parent_set = gbr.parent.all()
+ if parent_set:
+ for p in parent_set:
+ ghostbusters.append((p, vcard))
+ else:
+ ghostbusters.append((None, vcard))
+
irdb = IRDB(cfg)
- irdb.update(handle, roa_requests, children)
+ irdb.update(handle, roa_requests, children, ghostbusters)
irdb.close()
# for hosted handles, get the config for the rpkid host
diff --git a/rpkid/rpki/gui/app/models.py b/rpkid/rpki/gui/app/models.py
index 3b161a59..0a5b12ad 100644
--- a/rpkid/rpki/gui/app/models.py
+++ b/rpkid/rpki/gui/app/models.py
@@ -33,6 +33,10 @@ class IPAddressField(models.CharField):
def __init__( self, **kwargs ):
models.CharField.__init__(self, max_length=40, **kwargs)
+class TelephoneField(models.CharField):
+ def __init__( self, **kwargs ):
+ models.CharField.__init__(self, max_length=40, **kwargs)
+
class Conf(models.Model):
'''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>
@@ -222,4 +226,38 @@ class Roa(models.Model):
def get_absolute_url(self):
return ('rpki.gui.app.views.roa_view', [str(self.pk)])
+class Ghostbuster(models.Model):
+ """
+ Stores the information require to fill out a vCard entry to populate
+ a ghostbusters record.
+ """
+ full_name = models.CharField(max_length=40)
+
+ # components of the vCard N type
+ family_name = models.CharField(max_length=20)
+ given_name = models.CharField(max_length=20)
+ additional_name = models.CharField(max_length=20, blank=True, null=True)
+ honorific_prefix = models.CharField(max_length=10, blank=True, null=True)
+ honorific_suffix = models.CharField(max_length=10, blank=True, null=True)
+
+ email_address = models.EmailField(blank=True, null=True)
+ postal_address = models.CharField(blank=True, null=True, max_length=255)
+ organization = models.CharField(blank=True, null=True, max_length=255)
+ telephone = TelephoneField(blank=True, null=True)
+
+ conf = models.ForeignKey(Conf, related_name='ghostbusters')
+ # parent can be null when using the same record for all parents
+ parent = models.ManyToManyField(Parent, related_name='ghostbusters',
+ blank=True, null=True, help_text='use this record for a specific parent, or leave blank for all parents')
+
+ def __unicode__(self):
+ return u"%s's GBR: %s" % (self.conf, self.full_name)
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.app.views.ghostbuster_view', [str(self.pk)])
+
+ class Meta:
+ ordering = [ 'family_name', 'given_name' ]
+
# vim:sw=4 ts=8 expandtab
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html b/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html
index dea03c31..b3e9fab2 100644
--- a/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html
+++ b/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html
@@ -4,16 +4,34 @@
table { border-collapse: collapse }
th { border: solid 1px; padding: 1em }
td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1em }
+h2 { text-align:center; background-color:#dddddd }
+{% endblock %}
+
+{% block sidebar %}
+<ul class='compact'>
+ <li><a href="#parents">parents</a></li>
+ <li><a href="#children">children</a></li>
+ <li><a href="#roas">roas</a></li>
+ <li><a href="#ghostbusters">ghostbusters</a></li>
+ <li><a href="#unallocated">unallocated</a></li>
+</ul>
+
+<ul class='compact'>
+ <li><a href="{% url rpki.gui.app.views.conf_export %}">export identity</a></li>
+ <li><a href="{% url rpki.gui.app.views.conf_list %}">select identity</a></li>
+</ul>
{% endblock %}
{% block content %}
-<p>Handle: {{ request.session.handle }}
-| <a href="{% url rpki.gui.app.views.conf_export %}">export identity</a>
-| <a href="{% url rpki.gui.app.views.conf_list %}">select</a>
-<div style="border: inset">
-<h1 style="text-align: center">Parents</h1>
+<p id='breadcrumb'>{{ request.session.handle }} &gt; Dashboard</p>
+
+<h1>Dashboard</h1>
+
+<div class='separator'>
+<a name='parents'><h2>Parents</h2></a>
+{% if request.session.handle.parents.all %}
<ul>
{% for parent in request.session.handle.parents.all %}
<li><a href="{{ parent.get_absolute_url }}">{{ parent.handle }}</a>
@@ -42,15 +60,15 @@ td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1e
{% endfor %}
</ul>
+{% else %}
+<p style='font-style:italic'>none</p>
+{% endif %}
-<!--
-<a href="/myrpki/import/parent">[add]</a>
--->
-</div>
+</div><!--parents-->
+
+<div class='separator'>
+ <a name='children'><h2>Children</h2></a>
-<span>
-<div style="border: outset">
-<h1 style="text-align: center">Children</h1>
{% if request.session.handle.children.all %}
<ul>
{% for child in request.session.handle.children.all %}
@@ -73,7 +91,7 @@ td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1e
<a href="/myrpki/import/child">[add]</a>
-->
{% else %}
-<p>-- none --
+<p style='font-style:italic'>none</p>
{% endif %}
<p>
@@ -82,32 +100,52 @@ Export (csv): <a href="{% url rpki.gui.app.views.download_asns request.session.h
</div>
-<div style="border: outset"> <!-- ROAs -->
-<h1 style="text-align: center">My ROA [request]s</h1>
+<div class='separator'> <!-- ROAs -->
+ <a name='roas'><h2>ROA Requests</h2></a>
-<table>
-<tr> <th>Prefix</th> <th>ASN</th> </tr>
+ {% if request.session.handle.roas.all %}
+ <table>
+ <tr> <th>Prefix</th> <th>ASN</th> </tr>
-{% for roa in request.session.handle.roas.all %}
-<tr>
- <td style='text-align: left'>
+ {% for roa in request.session.handle.roas.all %}
+ <tr>
+ <td style='text-align: left'>
<ul style='list-style-position: outside'>
-{% for req in roa.from_roa_request.all %}
- <li><a href="{{ req.prefix.get_absolute_url }}">{{ req.as_roa_prefix }}</a>
-{% endfor %}
+ {% for req in roa.from_roa_request.all %}
+ <li><a href="{{ req.prefix.get_absolute_url }}">{{ req.as_roa_prefix }}</a>
+ {% endfor %}
</ul>
- </td>
- <td>{{ roa.asn }}</td>
-</tr>
-{% endfor %}
-</table>
-
-<p><a href="{% url rpki.gui.app.views.download_roas request.session.handle %}">export (csv)</a>
-
+ </td>
+ <td>{{ roa.asn }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% else %}
+ <p style='font-style:italic'>none</p>
+ {% endif %}
+
+ <p><a href="{% url rpki.gui.app.views.download_roas request.session.handle %}">export (csv)</a>
</div><!-- roas -->
-<div style="border: outset">
- <h1 style="text-align: center">Unallocated Resources</h1>
+<div class='separator'><!-- ghostbusters -->
+<a name='ghostbusters'><h2>Ghostbuster Requests</h2></a>
+ {% if request.session.handle.ghostbusters.all %}
+ <ul>
+ {% for gbr in request.session.handle.ghostbusters.all %}
+ <li><a href="{{ gbr.get_absolute_url }}">{{ gbr.full_name }}</a> |
+ <a href="{{ gbr.get_absolute_url }}/edit">edit</a> |
+ <a href="{{ gbr.get_absolute_url }}/delete">delete</a>
+ </li>
+ {% endfor %}
+ {% else %}
+<p style='font-style:italic'>none</p>
+ {% endif %}
+</ul>
+<p><a href='{% url rpki.gui.app.views.ghostbuster_create %}'>add</a></p>
+</div>
+
+<div class='separator'>
+<a name='unallocated'><h2>Unallocated Resources</h2></a>
{% if asns or ars %}
{% if asns %}
@@ -127,10 +165,10 @@ Export (csv): <a href="{% url rpki.gui.app.views.download_asns request.session.h
{% endif %}
{% else %}
- <p>-- none --
+<p style='font-style:italic'>none</p>
{% endif %}
</ul>
</div>
-</span>
+
{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html
new file mode 100644
index 00000000..6abd315a
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html
@@ -0,0 +1,14 @@
+{% extends "rpkigui/ghostbuster_detail.html" %}
+
+{% block extra %}
+
+<p>
+Please confirm that you really want to delete this object by clicking Submit.
+</p>
+
+<form method=POST action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ <input type='submit'>
+</form>
+
+{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html
new file mode 100644
index 00000000..cb03ec4e
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html
@@ -0,0 +1,44 @@
+{% extends "base.html" %}
+
+{% block css %}
+td { padding-right: 1em }
+td.label { font-weight:bold }
+{% endblock %}
+
+{% block sidebar %}
+<ul class='compact'>
+ <li><a href='{{ object.get_absolute_url }}/edit'>edit</a></li>
+ <li><a href='{{ object.get_absolute_url }}/delete'>delete</a></li>
+</ul>
+{% endblock %}
+
+{% block content %}
+<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> &gt; <a href="{% url rpki.gui.app.views.ghostbusters_list %}">Ghostbuster Request</a> &gt; {{ object.full_name }}</p>
+
+<h1>Ghostbuster View</h1>
+
+<table>
+ <tr><td class='label'>Full Name</td><td>{{ object.full_name }}</td></tr>
+
+ {% if object.honorific_prefix %}
+ <tr><td class='label'>Honorific Prefix</td><td>{{ object.honorific_prefix }}</td></tr>
+ {% endif %}
+
+ {% if object.organization %}
+ <tr><td class='label'>Organization</td><td>{{ object.organization }}</td></tr>
+ {% endif %}
+
+ {% if object.telephone %}
+ <tr><td class='label'>Telephone</td><td>{{ object.telephone }}</td></tr>
+ {% endif %}
+
+ {% if object.email_address %}
+ <tr><td class='label'>Email</td><td>{{ object.email_address }}</td></tr>
+ {% endif %}
+
+ {% if object.postal_address %}
+ <tr><td class='label'>Postal Address</td><td>{{ object.postal_address }}</td></tr>
+ {% endif %}
+</table>
+{% block extra %}{% endblock %}
+{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html
new file mode 100644
index 00000000..8a1d32ff
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html
@@ -0,0 +1,17 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{request.session.handle}}</a> &gt; <a href="{% url rpki.gui.app.views.ghostbusters_list %}">Ghostbusters</a> &gt; Edit</p>
+
+<h1>Edit Ghostbuster Request</h1>
+
+<form action='{{ request.get_full_path }}' method='POST'>
+ {% csrf_token %}
+ <table>
+{{ form.as_table }}
+
+</table>
+ <input type='submit' />
+</form>
+{% endblock %}
diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html
new file mode 100644
index 00000000..6890782d
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+
+{% block sidebar %}
+<ul class='compact'>
+ <li><a href='{% url rpki.gui.app.views.ghostbuster_create %}'>add</a></li>
+</ul>
+{% endblock %}
+
+{% block content %}
+<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> &gt; Ghostbusters</p>
+
+<h1>Ghostbuster Requests</h1>
+
+{% if object_list %}
+<ul>
+ {% for obj in object_list %}
+ <li><a href="{{ obj.get_absolute_url }}">{{ obj.full_name }}</a> | <a href="{{obj.get_absolute_url}}/edit">edit</a> | <a href="{{obj.get_absolute_url}}/delete">delete</a></li>
+ {% endfor %}
+</ul>
+{% else %}
+<p style='font-style:italic'>none</p>
+{% endif %}
+{% endblock %}
diff --git a/rpkid/rpki/gui/app/urls.py b/rpkid/rpki/gui/app/urls.py
index f71dc9d3..f2020d0d 100644
--- a/rpkid/rpki/gui/app/urls.py
+++ b/rpkid/rpki/gui/app/urls.py
@@ -33,6 +33,11 @@ urlpatterns = patterns('',
(r'^address/(?P<pk>\d+)/delete$', views.prefix_delete_view),
(r'^asn/(?P<pk>\d+)$', views.asn_view),
(r'^asn/(?P<pk>\d+)/allocate$', views.asn_allocate_view),
+ (r'^gbr/$', views.ghostbusters_list),
+ (r'^gbr/create$', views.ghostbuster_create),
+ (r'^gbr/(?P<pk>\d+)$', views.ghostbuster_view),
+ (r'^gbr/(?P<pk>\d+)/edit$', views.ghostbuster_edit),
+ (r'^gbr/(?P<pk>\d+)/delete$', views.ghostbuster_delete),
(r'^roa/(?P<pk>\d+)$', views.roa_view),
(r'^roareq/(?P<pk>\d+)$', views.roa_request_view),
(r'^roareq/(?P<pk>\d+)/delete$', views.roa_request_delete_view),
diff --git a/rpkid/rpki/gui/app/views.py b/rpkid/rpki/gui/app/views.py
index ad5f87a6..936a9108 100644
--- a/rpkid/rpki/gui/app/views.py
+++ b/rpkid/rpki/gui/app/views.py
@@ -25,7 +25,8 @@ from django.shortcuts import get_object_or_404, render_to_response
from django.utils.http import urlquote
from django.template import RequestContext
from django import http
-from django.views.generic.list_detail import object_list
+from django.views.generic.list_detail import object_list, object_detail
+from django.views.generic.create_update import delete_object, update_object, create_object
from django.core.urlresolvers import reverse
from rpki.gui.app import models, forms, glue, misc, AllocationTree, settings
@@ -531,4 +532,71 @@ def roa_view(request, pk):
"""not yet implemented"""
return
+@handle_required
+def ghostbusters_list(request):
+ """
+ Display a list of all ghostbuster requests for the current Conf.
+ """
+ conf = request.session['handle']
+
+ return object_list(request, queryset=conf.ghostbusters.all(), template_name='rpkigui/ghostbuster_list.html')
+
+@handle_required
+def ghostbuster_view(request, pk):
+ """
+ Display an individual ghostbuster request.
+ """
+ conf = request.session['handle']
+
+ return object_detail(request, queryset=conf.ghostbusters.all(), object_id=pk, template_name='rpkigui/ghostbuster_detail.html')
+
+@handle_required
+def ghostbuster_delete(request, pk):
+ conf = request.session['handle']
+
+ # verify that the object is owned by this conf
+ obj = get_object_or_404(models.Ghostbuster, pk=pk, conf=conf)
+
+ # modeled loosely on the generic delete_object() view.
+ if request.method == 'POST':
+ obj.delete()
+ glue.configure_resources(request.META['wsgi.errors'], conf)
+ return http.HttpResponseRedirect(reverse(ghostbusters_list))
+ else:
+ return render('rpkigui/ghostbuster_confirm_delete.html', { 'object': obj }, request)
+
+def _ghostbuster_edit(request, obj=None):
+ """
+ Common code for create/edit.
+ """
+ conf = request.session['handle']
+ form_class = forms.GhostbusterForm(conf.parents.all())
+ if request.method == 'POST':
+ form = form_class(request.POST, request.FILES, instance=obj)
+ if form.is_valid():
+ # use commit=False for the creation case, otherwise form.save()
+ # will fail due to schema constraint violation because conf is
+ # NULL
+ obj = form.save(commit=False)
+ obj.conf = conf
+ obj.save()
+ glue.configure_resources(request.META['wsgi.errors'], conf)
+ return http.HttpResponseRedirect(obj.get_absolute_url())
+ else:
+ form = form_class(instance=obj)
+ return render('rpkigui/ghostbuster_form.html', { 'form': form }, request)
+
+@handle_required
+def ghostbuster_edit(request, pk):
+ conf = request.session['handle']
+
+ # verify that the object is owned by this conf
+ obj = get_object_or_404(models.Ghostbuster, pk=pk, conf=conf)
+
+ return _ghostbuster_edit(request, obj)
+
+@handle_required
+def ghostbuster_create(request):
+ return _ghostbuster_edit(request)
+
# vim:sw=4 ts=8 expandtab