diff --git a/horizon/templates/horizon/common/_modal.html b/horizon/templates/horizon/common/_modal.html new file mode 100644 index 000000000..34863bf59 --- /dev/null +++ b/horizon/templates/horizon/common/_modal.html @@ -0,0 +1,10 @@ + diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 3db80395e..68a0d2f21 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -167,6 +167,14 @@ class SecurityGroupRule(APIResourceWrapper): return _('ALLOW %(from)s:%(to)s from %(cidr)s') % vals +class FlavorExtraSpec(object): + def __init__(self, flavor_id, key, val): + self.flavor_id = flavor_id + self.id = key + self.key = key + self.value = val + + def novaclient(request): insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) LOG.debug('novaclient connection created using token "%s" and url "%s"' % @@ -206,6 +214,28 @@ def flavor_list(request): return novaclient(request).flavors.list() +def flavor_get_extras(request, flavor_id, raw=False): + """Get flavor extra specs.""" + flavor = novaclient(request).flavors.get(flavor_id) + extras = flavor.get_keys() + if raw: + return extras + return [FlavorExtraSpec(flavor_id, key, value) for + key, value in extras.items()] + + +def flavor_extra_delete(request, flavor_id, keys): + """Unset the flavor extra spec keys.""" + flavor = novaclient(request).flavors.get(flavor_id) + return flavor.unset_keys(keys) + + +def flavor_extra_set(request, flavor_id, metadata): + """Set the flavor extra spec keys.""" + flavor = novaclient(request).flavors.get(flavor_id) + return flavor.set_keys(metadata) + + def tenant_floating_ip_list(request): """Fetches a list of all floating ips.""" return novaclient(request).floating_ips.list() diff --git a/openstack_dashboard/dashboards/admin/flavors/extras/__init__.py b/openstack_dashboard/dashboards/admin/flavors/extras/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack_dashboard/dashboards/admin/flavors/extras/forms.py b/openstack_dashboard/dashboards/admin/flavors/extras/forms.py new file mode 100644 index 000000000..5cada8ec4 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/extras/forms.py @@ -0,0 +1,66 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright (c) 2012 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django.utils.translation import ugettext_lazy as _ + +from openstack_dashboard import api +from horizon import exceptions +from horizon import forms +from horizon import messages + +LOG = logging.getLogger(__name__) + + +class CreateExtraSpec(forms.SelfHandlingForm): + key = forms.CharField(max_length="25", label=_("Key")) + value = forms.CharField(max_length="25", label=_("Value")) + flavor_id = forms.IntegerField(widget=forms.widgets.HiddenInput) + + def handle(self, request, data): + try: + api.nova.flavor_extra_set(request, + data['flavor_id'], + {data['key']: data['value']}) + msg = _('Created extra spec "%s".') % data['key'] + messages.success(request, msg) + return True + except: + exceptions.handle(request, + _("Unable to create flavor extra spec.")) + + +class EditExtraSpec(forms.SelfHandlingForm): + key = forms.CharField(max_length="25", label=_("Key")) + value = forms.CharField(max_length="25", label=_("Value")) + flavor_id = forms.IntegerField(widget=forms.widgets.HiddenInput) + + def handle(self, request, data): + flavor_id = data['flavor_id'] + try: + api.nova.flavor_extra_set(request, + flavor_id, + {data['key']: data['value']}) + msg = _('Saved extra spec "%s".') % data['key'] + messages.success(request, msg) + return True + except: + exceptions.handle(request, _("Unable to edit extra spec.")) diff --git a/openstack_dashboard/dashboards/admin/flavors/extras/tables.py b/openstack_dashboard/dashboards/admin/flavors/extras/tables.py new file mode 100644 index 000000000..abb325a57 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/extras/tables.py @@ -0,0 +1,75 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import re + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables + +from openstack_dashboard import api + + +LOG = logging.getLogger(__name__) + + +class ExtraSpecDelete(tables.DeleteAction): + data_type_singular = _("ExtraSpec") + data_type_plural = _("ExtraSpecs") + + def delete(self, request, obj_ids): + flavor = api.nova.flavor_get(request, self.table.kwargs['id']) + flavor.unset_keys([obj_ids]) + + +class ExtraSpecCreate(tables.LinkAction): + name = "create" + verbose_name = _("Create") + url = "horizon:admin:flavors:extras:create" + classes = ("btn-create", "ajax-modal") + + def get_link_url(self, extra_spec=None): + return reverse(self.url, args=[self.table.kwargs['id']]) + + +class ExtraSpecEdit(tables.LinkAction): + name = "edit" + verbose_name = _("Edit") + url = "horizon:admin:flavors:extras:edit" + classes = ("btn-edit", "ajax-modal") + + def get_link_url(self, extra_spec): + return reverse(self.url, args=[self.table.kwargs['id'], + extra_spec.key]) + + +class ExtraSpecsTable(tables.DataTable): + key = tables.Column('key', verbose_name=_('Key')) + value = tables.Column('value', verbose_name=_('Value')) + + class Meta: + name = "extras" + verbose_name = _("Extra Specs") + table_actions = (ExtraSpecCreate, ExtraSpecDelete) + row_actions = (ExtraSpecEdit, ExtraSpecDelete) + + def get_object_id(self, datum): + return datum.key + + def get_object_display(self, datum): + return datum.key diff --git a/openstack_dashboard/dashboards/admin/flavors/extras/tests.py b/openstack_dashboard/dashboards/admin/flavors/extras/tests.py new file mode 100644 index 000000000..a8df96896 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/extras/tests.py @@ -0,0 +1,65 @@ +from django import http +from django.core.urlresolvers import reverse + +from mox import IsA + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + + +class FlavorExtrasTests(test.BaseAdminViewTests): + + def test_list_extras_when_none_exists(self): + flavor = self.flavors.first() + extras = [api.FlavorExtraSpec(flavor.id, 'k1', 'v1')] + + self.mox.StubOutWithMock(api.nova, 'flavor_get') + self.mox.StubOutWithMock(api.nova, 'flavor_get_extras') + + # GET -- to determine correctness of output + api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor) + api.nova.flavor_get_extras(IsA(http.HttpRequest), + flavor.id).AndReturn(extras) + self.mox.ReplayAll() + url = reverse('horizon:admin:flavors:extras:index', args=[flavor.id]) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, "admin/flavors/extras/index.html") + + def test_extra_create_post(self): + flavor = self.flavors.first() + create_url = reverse('horizon:admin:flavors:extras:create', + args=[flavor.id]) + index_url = reverse('horizon:admin:flavors:extras:index', + args=[flavor.id]) + + self.mox.StubOutWithMock(api.nova, 'flavor_extra_set') + + # GET to display the flavor_name + api.nova.flavor_extra_set(IsA(http.HttpRequest), + int(flavor.id), + {'k1': 'v1'}) + self.mox.ReplayAll() + + data = {'flavor_id': flavor.id, + 'key': 'k1', + 'value': 'v1'} + resp = self.client.post(create_url, data) + self.assertNoFormErrors(resp) + self.assertMessageCount(success=1) + self.assertRedirectsNoFollow(resp, index_url) + + def test_extra_create_get(self): + flavor = self.flavors.first() + create_url = reverse('horizon:admin:flavors:extras:create', + args=[flavor.id]) + + self.mox.StubOutWithMock(api.nova, 'flavor_get') + + api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor) + self.mox.ReplayAll() + + resp = self.client.get(create_url) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, + 'admin/flavors/extras/create.html') diff --git a/openstack_dashboard/dashboards/admin/flavors/extras/urls.py b/openstack_dashboard/dashboards/admin/flavors/extras/urls.py new file mode 100644 index 000000000..b39211a7a --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/extras/urls.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls.defaults import patterns, url + +from .views import IndexView, EditView, CreateView + +urlpatterns = patterns('', + url(r'^$', IndexView.as_view(), name='index'), + url(r'^create/$', CreateView.as_view(), name='create'), + url(r'^(?P[^/]+)/edit/$', EditView.as_view(), name='edit') +) diff --git a/openstack_dashboard/dashboards/admin/flavors/extras/views.py b/openstack_dashboard/dashboards/admin/flavors/extras/views.py new file mode 100644 index 000000000..87caa7221 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/extras/views.py @@ -0,0 +1,93 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright (c) 2012 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import tables + +from openstack_dashboard import api +from .tables import ExtraSpecsTable +from .forms import CreateExtraSpec, EditExtraSpec + + +LOG = logging.getLogger(__name__) + + +class ExtraSpecMixin(object): + def get_context_data(self, **kwargs): + context = super(ExtraSpecMixin, self).get_context_data(**kwargs) + try: + context['flavor'] = api.nova.flavor_get(self.request, + self.kwargs['id']) + except: + exceptions.handle(self.request, + _("Unable to retrieve flavor data.")) + return context + + +class IndexView(ExtraSpecMixin, forms.ModalFormMixin, tables.DataTableView): + table_class = ExtraSpecsTable + template_name = 'admin/flavors/extras/index.html' + + def get_data(self): + try: + flavor_id = self.kwargs['id'] + extras_list = api.nova.flavor_get_extras(self.request, flavor_id) + extras_list.sort(key=lambda es: (es.key,)) + except: + extras_list = [] + exceptions.handle(self.request, + _('Unable to retrieve extra spec list.')) + return extras_list + + +class CreateView(ExtraSpecMixin, forms.ModalFormView): + form_class = CreateExtraSpec + template_name = 'admin/flavors/extras/create.html' + + def get_initial(self): + return {'flavor_id': self.kwargs['id']} + + def get_success_url(self): + return "/admin/flavors/%s/extras/" % (self.kwargs['id']) + + +class EditView(ExtraSpecMixin, forms.ModalFormView): + form_class = EditExtraSpec + template_name = 'admin/flavors/extras/edit.html' + + def get_initial(self): + flavor_id = self.kwargs['id'] + key = self.kwargs['key'] + try: + extra_specs = api.nova.flavor_get_extras(self.request, + flavor_id, + raw=True) + except: + extra_specs = {} + exceptions.handle(self.request, + _("Unable to retrieve flavor extra spec data.")) + return {'flavor_id': flavor_id, + 'key': key, + 'value': extra_specs.get(key, '')} diff --git a/openstack_dashboard/dashboards/admin/flavors/forms.py b/openstack_dashboard/dashboards/admin/flavors/forms.py index 0f68ee7c5..a7d8d1d3b 100644 --- a/openstack_dashboard/dashboards/admin/flavors/forms.py +++ b/openstack_dashboard/dashboards/admin/flavors/forms.py @@ -61,19 +61,26 @@ class EditFlavor(CreateFlavor): def handle(self, request, data): try: + flavor_id = data['flavor_id'] + # grab any existing extra specs, because flavor edit currently + # implemented as a delete followed by a create + extras_dict = api.nova.flavor_get_extras(self.request, flavor_id) # First mark the existing flavor as deleted. api.nova.flavor_delete(request, data['flavor_id']) # Then create a new flavor with the same name but a new ID. # This is in the same try/except block as the delete call # because if the delete fails the API will error out because # active flavors can't have the same name. + new_flavor_id = uuid.uuid4() flavor = api.nova.flavor_create(request, data['name'], data['memory_mb'], data['vcpus'], data['disk_gb'], - uuid.uuid4(), + new_flavor_id, ephemeral=data['eph_gb']) + if (len(extras_dict) > 0): + api.nova.flavor_extra_set(request, new_flavor_id, extras_dict) msg = _('Updated flavor "%s".') % data['name'] messages.success(request, msg) return flavor diff --git a/openstack_dashboard/dashboards/admin/flavors/tables.py b/openstack_dashboard/dashboards/admin/flavors/tables.py index 0658589d8..1b3c3ffc6 100644 --- a/openstack_dashboard/dashboards/admin/flavors/tables.py +++ b/openstack_dashboard/dashboards/admin/flavors/tables.py @@ -32,6 +32,13 @@ class EditFlavor(tables.LinkAction): classes = ("ajax-modal", "btn-edit") +class ViewFlavorExtras(tables.LinkAction): + name = "extras" + verbose_name = _("View Extra Specs") + url = "horizon:admin:flavors:extras:index" + classes = ("btn-edit",) + + def get_size(flavor): return _("%sMB") % flavor.ram @@ -51,4 +58,4 @@ class FlavorsTable(tables.DataTable): name = "flavors" verbose_name = _("Flavors") table_actions = (CreateFlavor, DeleteFlavor) - row_actions = (EditFlavor, DeleteFlavor) + row_actions = (EditFlavor, ViewFlavorExtras, DeleteFlavor) diff --git a/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/_create.html b/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/_create.html new file mode 100644 index 000000000..4c8d4d1ca --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/_create.html @@ -0,0 +1,27 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}extra_spec_create_form{% endblock %} +{% block form_action %}{% url horizon:admin:flavors:extras:create flavor.id %}{% endblock %} + + +{% block modal_id %}extra_spec_create_modal{% endblock %} +{% block modal-header %}{% trans "Create Flavor Extra Spec" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans 'Create a new "extra spec" key-value pair for a flavor.' %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} + diff --git a/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/_edit.html b/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/_edit.html new file mode 100644 index 000000000..d44e0a263 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/_edit.html @@ -0,0 +1,27 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}extra_spec_edit_form{% endblock %} +{% block form_action %}{% url horizon:admin:flavors:extras:create flavor.id %}{% endblock %} + + +{% block modal_id %}extra_spec_edit_modal{% endblock %} +{% block modal-header %}{% trans "Edit Flavor Extra Spec" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans 'Update an "extra spec" key-value pair for a flavor.' %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} + diff --git a/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/_index.html b/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/_index.html new file mode 100644 index 000000000..ff3fb33c0 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/_index.html @@ -0,0 +1,14 @@ +{% extends "horizon/common/_modal.html" %} +{% load i18n %} + +{% block modal_id %}extra_specs_modal{% endblock %} +{% block modal-header %}{% trans "Flavor Extra Specs" %}{% endblock %} + +{% block modal-body %} + {{ table.render }} +{% endblock %} + +{% block modal-footer %} + {% trans "Close" %} +{% endblock %} + diff --git a/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/create.html b/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/create.html new file mode 100644 index 000000000..20461837e --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/create.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Create Flavor Extra Spec" %}{% endblock %} + +{% block page_header %} +

{% trans "Flavor" %}: {{flavor.name}}

+{% endblock page_header %} + +{% block main %} + {% include "admin/flavors/extras/_create.html" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/edit.html b/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/edit.html new file mode 100644 index 000000000..c471844ef --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/edit.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Edit Flavor Extra Spec" %}{% endblock %} + +{% block page_header %} +

{% trans "Flavor" %}: {{flavor.name}}

+{% endblock page_header %} + +{% block main %} + {% include "admin/flavors/extras/_edit.html" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/index.html b/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/index.html new file mode 100644 index 000000000..fbe91effc --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/templates/flavors/extras/index.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Flavor Extra Specs" %}{% endblock %} + +{% block page_header %} +

{% trans "Flavor" %}: {{flavor.name}}

+{% endblock page_header %} + +{% block main %} + {% include "admin/flavors/extras/_index.html" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/flavors/tests.py b/openstack_dashboard/dashboards/admin/flavors/tests.py index 0b9ac4353..6104d6432 100644 --- a/openstack_dashboard/dashboards/admin/flavors/tests.py +++ b/openstack_dashboard/dashboards/admin/flavors/tests.py @@ -42,7 +42,9 @@ class FlavorsTests(test.BaseAdminViewTests): def test_edit_flavor(self): flavor = self.flavors.first() eph = getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral') + extras = {} self.mox.StubOutWithMock(api.nova, 'flavor_list') + self.mox.StubOutWithMock(api.nova, 'flavor_get_extras') self.mox.StubOutWithMock(api.nova, 'flavor_get') self.mox.StubOutWithMock(api.nova, 'flavor_delete') self.mox.StubOutWithMock(api.nova, 'flavor_create') @@ -52,6 +54,8 @@ class FlavorsTests(test.BaseAdminViewTests): # POST api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor) + api.nova.flavor_get_extras(IsA(http.HttpRequest), int(flavor.id))\ + .AndReturn(extras) api.nova.flavor_delete(IsA(http.HttpRequest), int(flavor.id)) api.nova.flavor_create(IsA(http.HttpRequest), flavor.name, @@ -62,11 +66,13 @@ class FlavorsTests(test.BaseAdminViewTests): ephemeral=eph).AndReturn(flavor) self.mox.ReplayAll() + #get_test url = reverse('horizon:admin:flavors:edit', args=[flavor.id]) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed(resp, "admin/flavors/edit.html") + #post test data = {'flavor_id': flavor.id, 'name': flavor.name, 'vcpus': flavor.vcpus + 1, @@ -75,4 +81,4 @@ class FlavorsTests(test.BaseAdminViewTests): 'eph_gb': eph} resp = self.client.post(url, data) self.assertRedirectsNoFollow(resp, - reverse("horizon:admin:flavors:index")) + reverse("horizon:admin:flavors:index")) diff --git a/openstack_dashboard/dashboards/admin/flavors/urls.py b/openstack_dashboard/dashboards/admin/flavors/urls.py index 9a15a7026..522367e0e 100644 --- a/openstack_dashboard/dashboards/admin/flavors/urls.py +++ b/openstack_dashboard/dashboards/admin/flavors/urls.py @@ -18,12 +18,13 @@ # License for the specific language governing permissions and limitations # under the License. -from django.conf.urls.defaults import patterns, url +from django.conf.urls.defaults import patterns, url, include from .views import IndexView, CreateView, EditView - +from .extras import urls as extras_urls urlpatterns = patterns('openstack_dashboard.dashboards.admin.flavors.views', url(r'^$', IndexView.as_view(), name='index'), url(r'^create/$', CreateView.as_view(), name='create'), - url(r'^(?P[^/]+)/edit/$', EditView.as_view(), name='edit') + url(r'^(?P[^/]+)/edit/$', EditView.as_view(), name='edit'), + url(r'^(?P[^/]+)/extras/', include(extras_urls, namespace='extras')), )