diff --git a/config/facsimile/peeringdb.yaml b/config/facsimile/peeringdb.yaml
index 395023db..52815a24 100644
--- a/config/facsimile/peeringdb.yaml
+++ b/config/facsimile/peeringdb.yaml
@@ -160,6 +160,7 @@ install:
- $SRC_DIR$/peeringdb_server/management/commands/pdb_process_admin_tool_command.py
- $SRC_DIR$/peeringdb_server/management/commands/pdb_load_data.py
- $SRC_DIR$/peeringdb_server/management/commands/pdb_fix_status_history.py
+ - $SRC_DIR$/peeringdb_server/management/commands/pdb_migrate_ixlans.py
- $SRC_DIR$/peeringdb_server/migrations/__init__.py
- $SRC_DIR$/peeringdb_server/migrations/0001_initial.py
- $SRC_DIR$/peeringdb_server/migrations/0002_partnernship_model.py
diff --git a/peeringdb_server/admin.py b/peeringdb_server/admin.py
index d9d6c4f9..42d90436 100644
--- a/peeringdb_server/admin.py
+++ b/peeringdb_server/admin.py
@@ -10,7 +10,7 @@ import django.urls
from django.conf.urls import url
from django.shortcuts import redirect, Http404
from django.contrib.contenttypes.models import ContentType
-from django.contrib import admin
+from django.contrib import admin, messages
from django.contrib.auth import forms
from django.contrib.admin import helpers
from django.contrib.admin.actions import delete_selected
@@ -296,6 +296,15 @@ def soft_delete(modeladmin, request, queryset):
if request.user:
reversion.set_user(request.user)
+ if queryset.model.handleref.tag == "ixlan":
+ messages.error(
+ request,
+ _(
+ "Ixlans can no longer be directly deleted as they are now synced to the parent exchange"
+ ),
+ )
+ return
+
for row in queryset:
row.delete()
@@ -406,7 +415,13 @@ class IXLanInline(SanitizedAdmin, admin.StackedInline):
extra = 0
form = StatusForm
exclude = ["arp_sponge"]
- readonly_fields = ["ixf_import_attempt_info", "prefixes"]
+ readonly_fields = ["id", "ixf_import_attempt_info", "prefixes"]
+
+ def has_add_permission(self, request):
+ return False
+
+ def has_delete_permission(self, request, obj):
+ return False
def ixf_import_attempt_info(self, obj):
if obj.ixf_import_attempt:
@@ -553,6 +568,7 @@ class IXLanAdminForm(StatusForm):
class IXLanAdmin(SoftDeleteAdmin):
+ actions = []
list_display = ("ix", "name", "descr", "status")
search_fields = ("name", "ix__name")
list_filter = (StatusFilter,)
diff --git a/peeringdb_server/autocomplete_views.py b/peeringdb_server/autocomplete_views.py
index 9a64bfe2..0d8f356d 100644
--- a/peeringdb_server/autocomplete_views.py
+++ b/peeringdb_server/autocomplete_views.py
@@ -131,13 +131,12 @@ class IXLanAutocomplete(AutocompleteHTMLResponse):
def get_result_label(self, item):
return (
- ' %s
%s
'
+ ' %s
'
% (
item.pk,
html.escape(item.ix.name),
html.escape(item.ix.country.code),
html.escape(item.ix.name_long),
- html.escape(item.name),
)
)
diff --git a/peeringdb_server/client_adaptor/backend.py b/peeringdb_server/client_adaptor/backend.py
index 3a68dfb5..2b7575d0 100644
--- a/peeringdb_server/client_adaptor/backend.py
+++ b/peeringdb_server/client_adaptor/backend.py
@@ -119,7 +119,10 @@ class Backend(BaseBackend):
obj.clean()
def save(self, obj):
- obj.save()
+ if obj.HandleRef.tag == "ix":
+ obj.save(create_ixlan=False)
+ else:
+ obj.save()
def detect_uniqueness_error(self, exc):
"""
diff --git a/peeringdb_server/management/commands/pdb_api_test.py b/peeringdb_server/management/commands/pdb_api_test.py
index 6a5de782..5039c4d5 100644
--- a/peeringdb_server/management/commands/pdb_api_test.py
+++ b/peeringdb_server/management/commands/pdb_api_test.py
@@ -346,6 +346,7 @@ class TestJSON(unittest.TestCase):
def make_data_ixlan(self, **kwargs):
data = {
"ix_id": 1,
+ "id": 1,
"name": self.make_name("Test"),
"descr": NOTE,
"mtu": 12345,
@@ -353,6 +354,8 @@ class TestJSON(unittest.TestCase):
"rs_asn": 12345,
"arp_sponge": None,
}
+ if "ix_id" in kwargs:
+ data["id"] = kwargs.get("ix_id")
data.update(**kwargs)
return data
@@ -651,7 +654,8 @@ class TestJSON(unittest.TestCase):
kwargs_s = {"%s_%s" % (rel, qfld): getattr(SHARED["%s_r_ok" % rel], fld)}
kwargs_m = {"%s_%s__in" % (rel, qfld): ",".join([str(id) for id in ids])}
- if hasattr(REFTAG_MAP[target], "%s" % rel):
+ attr = getattr(REFTAG_MAP[target], rel, None)
+ if attr and not isinstance(attr, property):
valid_s = [
r.id
@@ -998,6 +1002,11 @@ class TestJSON(unittest.TestCase):
SHARED["ix_id"] = r_data.get("id")
+ # make sure ixlan was created and has matching id
+ ix = InternetExchange.objects.get(id=SHARED["ix_id"])
+ assert ix.ixlan
+ assert ix.ixlan.id == ix.id
+
self.assert_update(
self.db_org_admin,
"ix",
@@ -1330,23 +1339,23 @@ class TestJSON(unittest.TestCase):
def test_org_admin_002_POST_PUT_DELETE_ixlan(self):
data = self.make_data_ixlan(ix_id=SHARED["ix_rw_ok"].id)
- r_data = self.assert_create(
- self.db_org_admin,
- "ixlan",
- data,
- test_failures={
- "invalid": {"ix_id": ""},
- "perms": {"ix_id": SHARED["ix_r_ok"].id},
- "status": {"ix_id": SHARED["ix_rw_pending"].id},
- },
- )
-
- SHARED["ixlan_id"] = r_data["id"]
+ with self.assertRaises(Exception) as exc:
+ r_data = self.assert_create(
+ self.db_org_admin,
+ "ixlan",
+ data,
+ test_failures={
+ "invalid": {"ix_id": ""},
+ "perms": {"ix_id": SHARED["ix_r_ok"].id},
+ "status": {"ix_id": SHARED["ix_rw_pending"].id},
+ },
+ )
+ self.assertIn('Method "POST" not allowed', str(exc.exception))
self.assert_update(
self.db_org_admin,
"ixlan",
- SHARED["ixlan_id"],
+ SHARED["ixlan_rw_ok"].id,
{"name": self.make_name("Test")},
test_failures={
"invalid": {"mtu": "NEEDS TO BE INT"},
@@ -1354,12 +1363,14 @@ class TestJSON(unittest.TestCase):
},
)
- self.assert_delete(
- self.db_org_admin,
- "ixlan",
- test_success=SHARED["ixlan_id"],
- test_failure=SHARED["ixlan_r_ok"].id,
- )
+ with self.assertRaises(Exception) as exc:
+ self.assert_delete(
+ self.db_org_admin,
+ "ixlan",
+ test_success=SHARED["ixlan_rw_ok"].id,
+ test_failure=SHARED["ixlan_r_ok"].id,
+ )
+ self.assertIn('Method "DELETE" not allowed', str(exc.exception))
##########################################################################
@@ -2082,11 +2093,8 @@ class TestJSON(unittest.TestCase):
for i in range(0, 2)
]
- # create ixlan at each exchange
- ixlans = [
- IXLan.objects.create(status="ok", **self.make_data_ixlan(ix_id=ix.id))
- for ix in exchanges
- ]
+ # collect ixlans
+ ixlans = [ix.ixlan for ix in exchanges]
# all three networks peer at first exchange
for net in networks:
@@ -2591,13 +2599,15 @@ class TestJSON(unittest.TestCase):
def test_readonly_users_002_POST_ixlan(self):
for db in self.readonly_dbs():
- self.assert_create(
- db,
- "ixlan",
- self.make_data_ixlan(),
- test_failures={"perms": {}},
- test_success=False,
- )
+ with self.assertRaises(Exception) as exc:
+ self.assert_create(
+ db,
+ "ixlan",
+ self.make_data_ixlan(),
+ test_failures={"perms": {}},
+ test_success=False,
+ )
+ self.assertIn('Method "POST" not allowed', str(exc.exception))
##########################################################################
@@ -2616,9 +2626,14 @@ class TestJSON(unittest.TestCase):
def test_readonly_users_004_DELETE_ixlan(self):
for db in self.readonly_dbs():
- self.assert_delete(
- db, "ixlan", test_success=False, test_failure=SHARED["ixlan_r_ok"].id
- )
+ with self.assertRaises(Exception) as exc:
+ self.assert_delete(
+ db,
+ "ixlan",
+ test_success=False,
+ test_failure=SHARED["ixlan_r_ok"].id,
+ )
+ self.assertIn('Method "DELETE" not allowed', str(exc.exception))
##########################################################################
@@ -2712,16 +2727,6 @@ class TestJSON(unittest.TestCase):
test_failures={"perms": {"net_id": SHARED["net_rw2_ok"].id}},
)
- # user with create perms should not be able to create an ixlan under
- # net_rw_ix
- self.assert_create(
- self.db_crud_create,
- "ixlan",
- self.make_data_ixlan(ix_id=SHARED["ix_rw3_ok"].id),
- test_failures={"perms": {}},
- test_success=False,
- )
-
# other crud test users should not be able to create a new poc under
# net_rw3_ok
for p in ["delete", "update"]:
@@ -3134,7 +3139,9 @@ class Command(BaseCommand):
for k in unset:
if k in data:
del data[k]
- obj = model.objects.create(**data)
+ obj = model(**data)
+ obj.save()
+
cls.log(
"%s with status '%s' for %s testing created! (%s)"
% (tag.upper(), status, prefix.upper(), obj.updated)
@@ -3299,12 +3306,12 @@ class Command(BaseCommand):
for status in ["ok", "pending"]:
for prefix in ["r", "rw"]:
- cls.create_entity(
- IXLan,
- status=status,
- prefix=prefix,
- ix_id=SHARED["ix_%s_%s" % (prefix, status)].id,
- )
+ SHARED["ixlan_{}_{}".format(prefix, status)] = SHARED[
+ "ix_{}_{}".format(prefix, status)
+ ].ixlan
+
+ for status in ["ok", "pending"]:
+ for prefix in ["r", "rw"]:
cls.create_entity(
IXLanPrefix,
status=status,
diff --git a/peeringdb_server/management/commands/pdb_generate_test_data.py b/peeringdb_server/management/commands/pdb_generate_test_data.py
index 9fb56308..f9d4a267 100644
--- a/peeringdb_server/management/commands/pdb_generate_test_data.py
+++ b/peeringdb_server/management/commands/pdb_generate_test_data.py
@@ -58,7 +58,6 @@ class Command(BaseCommand):
"net",
"ix",
"fac",
- "ixlan",
"ixpfx",
"ixfac",
"netixlan",
@@ -104,6 +103,8 @@ class Command(BaseCommand):
params.update(protocol="IPv6")
entity = self.mock.create(reftag, **params)
self.entities[reftag].append(entity)
+ elif reftag == "ix":
+ self.entities["ixlan"].append(entity.ixlan)
self.entities["net"].append(self.mock.create("net"))
self.entities["ix"].append(self.mock.create("ix"))
diff --git a/peeringdb_server/migrations/0022_ixlan_remove_auto_increment.py b/peeringdb_server/migrations/0022_ixlan_remove_auto_increment.py
index adbb95ff..dacfca19 100644
--- a/peeringdb_server/migrations/0022_ixlan_remove_auto_increment.py
+++ b/peeringdb_server/migrations/0022_ixlan_remove_auto_increment.py
@@ -12,6 +12,9 @@ class Migration(migrations.Migration):
]
operations = [
- # this change was reverted, but we will keep this empty migration
- # so it does not break the migration chain
+ migrations.AlterField(
+ model_name="ixlan",
+ name="id",
+ field=models.IntegerField(primary_key=True, serialize=False),
+ ),
]
diff --git a/peeringdb_server/mock.py b/peeringdb_server/mock.py
index f496b80f..2cab404b 100644
--- a/peeringdb_server/mock.py
+++ b/peeringdb_server/mock.py
@@ -96,6 +96,10 @@ class Mock(object):
# these we don't care about
if field.name in ["id", "logo", "version", "created", "updated"]:
continue
+ # if reftag == "ixlan" and field.name != "id":
+ # continue
+ # elif reftag != "ixlan":
+ # continue
# this we dont care about either
if field.name.find("geocode") == 0:
@@ -140,7 +144,15 @@ class Mock(object):
# with the same name as the field name
else:
data[field.name] = getattr(self, field.name)(data, reftag=reftag)
- return model.objects.create(**data)
+ obj = model(**data)
+ obj.clean()
+ obj.save()
+ return obj
+
+ def id(self, data, reftag=None):
+ if reftag == "ixlan":
+ return data["ix"].id
+ return None
def status(self, data, reftag=None):
return "ok"
diff --git a/peeringdb_server/models.py b/peeringdb_server/models.py
index 1634c3c2..7b6bf7e3 100644
--- a/peeringdb_server/models.py
+++ b/peeringdb_server/models.py
@@ -1335,6 +1335,17 @@ class InternetExchange(pdb_models.InternetExchangeBase):
ix_id,
)
+ @property
+ def ixlan(self):
+ """
+ Returns the ixlan for this exchange
+
+ As per #21 each exchange will get one ixlan with a matching
+ id, but the schema is to remain unchanged until a major
+ version bump.
+ """
+ return self.ixlan_set.first()
+
@property
def networks(self):
"""
@@ -1422,6 +1433,29 @@ class InternetExchange(pdb_models.InternetExchangeBase):
ixpfx.status = "ok"
ixpfx.save()
+ def save(self, create_ixlan=True, **kwargs):
+ """
+ When an internet exchange is saved, make sure the ixlan for it
+ exists
+
+ Keyword Argument(s):
+
+ - create_ixlan (`bool`=True): if True and the ix is missing
+ it's ixlan, create it
+ """
+ r = super(InternetExchange, self).save(**kwargs)
+
+ if not self.ixlan and create_ixlan:
+ ixlan = IXLan(ix=self, status=self.status, mtu=0)
+
+ # ixlan id will be set to match ix id in ixlan's clean()
+ # call
+ ixlan.clean()
+
+ ixlan.save()
+
+ return r
+
def validate_phonenumbers(self):
self.tech_phone = validate_phonenumber(self.tech_phone, self.country.code)
self.policy_phone = validate_phonenumber(self.policy_phone, self.country.code)
@@ -1478,6 +1512,11 @@ class IXLan(pdb_models.IXLanBase):
Describes a LAN at an exchange
"""
+ # as we are preparing to drop IXLans from the schema, as an interim
+ # step (#21) we are giving each ix one ixlan with matching ids, so we need
+ # to have an id field that doesnt automatically increment
+ id = models.IntegerField(primary_key=True)
+
ix = models.ForeignKey(
InternetExchange, on_delete=models.CASCADE, default=0, related_name="ixlan_set"
)
@@ -1498,7 +1537,7 @@ class IXLan(pdb_models.IXLanBase):
"""
Returns a descriptive label of the ixlan for logging purposes
"""
- return "ixlan{} {} {}".format(self.id, self.name, self.ix.name)
+ return "ixlan{} {}".format(self.id, self.ix.name)
@classmethod
def nsp_namespace_from_id(cls, org_id, ix_id, id):
@@ -1566,6 +1605,27 @@ class IXLan(pdb_models.IXLanBase):
return True
return False
+ def clean(self):
+ # id is set and does not match the parent ix id
+
+ if self.id and self.id != self.ix.id:
+ raise ValidationError({"id": _("IXLan id needs to match parent ix id")})
+
+ # id is not set (new ixlan)
+
+ if not self.id:
+
+ # ixlan for ix already exists
+
+ if self.ix.ixlan:
+ raise ValidationError(_("Ixlan for exchange already exists"))
+
+ # enforce correct id moving forward
+
+ self.id = self.ix.id
+
+ return super(IXLan, self).clean()
+
@reversion.create_revision()
def add_netixlan(self, netixlan_info, save=True, save_others=True):
"""
diff --git a/peeringdb_server/rest.py b/peeringdb_server/rest.py
index b633ba08..8fd7ddc7 100644
--- a/peeringdb_server/rest.py
+++ b/peeringdb_server/rest.py
@@ -558,7 +558,7 @@ def ref_dict():
return {tag: view.model for tag, view, na in router.registry}
-def model_view_set(model):
+def model_view_set(model, methods=None):
"""
shortcut for peeringdb models to generate viewset and register in the API urls
"""
@@ -593,6 +593,9 @@ def model_view_set(model):
# create the type
viewset_t = type(model + "ViewSet", (ModelViewSet,), clsdict)
+ if methods:
+ viewset_t.http_method_names = methods
+
# register with the rest router for incoming requests
ref_tag = model_t.handleref.tag
router.register(ref_tag, viewset_t, basename=ref_tag)
@@ -603,7 +606,7 @@ def model_view_set(model):
FacilityViewSet = model_view_set("Facility")
InternetExchangeViewSet = model_view_set("InternetExchange")
InternetExchangeFacilityViewSet = model_view_set("InternetExchangeFacility")
-IXLanViewSet = model_view_set("IXLan")
+IXLanViewSet = model_view_set("IXLan", methods=["get", "put"])
IXLanPrefixViewSet = model_view_set("IXLanPrefix")
NetworkViewSet = model_view_set("Network")
NetworkContactViewSet = model_view_set("NetworkContact")
diff --git a/peeringdb_server/serializers.py b/peeringdb_server/serializers.py
index 11453ce5..76180086 100644
--- a/peeringdb_server/serializers.py
+++ b/peeringdb_server/serializers.py
@@ -2005,8 +2005,13 @@ class InternetExchangeSerializer(ModelSerializer):
# create ix
r = super(InternetExchangeSerializer, self).create(validated_data)
+ ixlan = r.ixlan
+
# create ixlan
- ixlan = IXLan.objects.create(name="Main", ix=r, status="pending")
+ # if False:# not ixlan:
+ # ixlan = IXLan(ix=r, status="pending")
+ # ixlan.clean()
+ # ixlan.save()
# see if prefix already exists in a deleted state
ixpfx = IXLanPrefix.objects.filter(prefix=prefix, status="deleted").first()
diff --git a/peeringdb_server/static/20c/twentyc.edit.js b/peeringdb_server/static/20c/twentyc.edit.js
index 91588a7d..d7a4d2f2 100644
--- a/peeringdb_server/static/20c/twentyc.edit.js
+++ b/peeringdb_server/static/20c/twentyc.edit.js
@@ -174,7 +174,7 @@ twentyc.editable.action.register(
modules = [],
targets = 1,
changed,
- status={"error":false},
+ status={"error":false, "data":{}},
i;
@@ -182,13 +182,16 @@ twentyc.editable.action.register(
targets--;
if(error)
status.error = true;
+
+ if(data) {
+ $.extend(status.data, data);
+ }
+
if(!targets) {
if(!status.error && !me.noToggle) {
- if(data)
- container.editable("toggle", { data:data });
- else
- container.editable("toggle");
+ container.editable("toggle", { data:status.data });
}
+
/*
if(!status.error && container.data("edit-always")) {
// if container is always toggled to edit mode
@@ -212,6 +215,8 @@ twentyc.editable.action.register(
var target = twentyc.editable.target.instantiate(container);
changed = target.data._changed;
+ $.extend(status.data, target.data);
+
// prepare modules
container.find("[data-edit-module]").
//editable("filter", { belongs : container }).
@@ -244,7 +249,14 @@ twentyc.editable.action.register(
}
var grouped = container.editable("filter", { grouped : true }).not("[data-edit-module]");
- targets += grouped.length;
+
+ grouped.each(function(idx) {
+ var target = twentyc.editable.target.instantiate($(this));
+ $.extend(status.data, target.data);
+ if(target.data._changed) {
+ targets += 1
+ }
+ });
if(changed || container.data("edit-always-submit") == "yes"){
$(target).on("success", function(ev, data) {
@@ -258,8 +270,9 @@ twentyc.editable.action.register(
// submit main target
var result = target.execute();
- } else
+ } else {
dec_targets({}, {});
+ }
// submit grouped targets
diff --git a/peeringdb_server/static/peeringdb.js b/peeringdb_server/static/peeringdb.js
index 98c495f3..edf0df70 100644
--- a/peeringdb_server/static/peeringdb.js
+++ b/peeringdb_server/static/peeringdb.js
@@ -252,6 +252,13 @@ PeeringDB.ViewActions = {
actions : {}
}
+PeeringDB.ViewActions.actions.ix_ixf_preview = function(netId) {
+ $("#ixf-preview-modal").modal("show");
+ var preview = new PeeringDB.IXFPreview()
+ preview.request(netId, $("#ixf-log"));
+}
+
+
PeeringDB.ViewActions.actions.net_ixf_preview = function(netId) {
$("#ixf-preview-modal").modal("show");
var preview = new PeeringDB.IXFNetPreview()
@@ -262,7 +269,6 @@ PeeringDB.ViewActions.actions.net_ixf_postmortem = function(netId) {
$("#ixf-postmortem-modal").modal("show");
var postmortem = new PeeringDB.IXFNetPostmortem()
postmortem.request(netId, $("#ixf-postmortem"));
-
}
@@ -1087,6 +1093,7 @@ twentyc.editable.target.register(
"base"
);
+
/*
* editable api listing module
*/
@@ -1345,34 +1352,6 @@ twentyc.editable.module.register(
},
- // FINALIZERS: IXLAN
-
- finalize_add_ixlan : function(data, callback, sentData) {
-
- // we currently do not publish ix-f setting fields on the API
- // so we need to set those from sent data
- data.ixf_ixp_member_list_url = sentData.ixf_ixp_member_list_url;
- data.ixf_ixp_import_enabled = sentData.ixf_ixp_import_enabled;
- callback(data);
- },
-
-
- finalize_row_ixlan : function(rowId, row, data) {
- row.editable("payload", {
- ix_id : data.ix_id
- })
- row.data("edit-label", gettext("IXLAN") + ": "+data.name); ///
-
- var modPrefix = row.find('[data-edit-module="api_listing"]');
- modPrefix.editable("sync");
- modPrefix.editable("toggle");
-
- var cmpPrefixAdd = row.find('[data-edit-component="add"]')
- cmpPrefixAdd.editable("payload", {
- ixlan_id : data.id
- });
- },
-
// FINALIZERS: IXLAN PREFIX
finalize_row_ixpfx : function(rowId, row, data) {
diff --git a/peeringdb_server/static/site.css b/peeringdb_server/static/site.css
index 132c6b10..8f9ac0e3 100644
--- a/peeringdb_server/static/site.css
+++ b/peeringdb_server/static/site.css
@@ -660,6 +660,7 @@ table.result {
div.list {
margin-left: 15px;
margin-right: 15px;
+ margin-top: 15px;
}
div.list div.header {
@@ -680,6 +681,7 @@ div.list h5 {
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
+ margin-left: -11px;
}
div.list div.empty-result {
diff --git a/peeringdb_server/templates/site/view.html b/peeringdb_server/templates/site/view.html
index 364f4b35..8db1d231 100644
--- a/peeringdb_server/templates/site/view.html
+++ b/peeringdb_server/templates/site/view.html
@@ -63,7 +63,27 @@
{% for row in data.fields %}
- {% if not row.value|dont_render %}
+ {% if row.type == "group" %}
+
+
+
+
+ {% for payload_row in row.payload %}
+
{{ payload_row.value }}
+ {% endfor %}
+
+
+ {% elif row.type == "group_end" %}
+
+ {% endif %}
+
+ {% if not row.value|dont_render and row.type != "group" and row.type != "group_end" %}
{% if not row.admin or permissions.can_write %}
diff --git a/peeringdb_server/templates/site/view_exchange_assets.html b/peeringdb_server/templates/site/view_exchange_assets.html
index 214b7331..f6da15a3 100644
--- a/peeringdb_server/templates/site/view_exchange_assets.html
+++ b/peeringdb_server/templates/site/view_exchange_assets.html
@@ -26,84 +26,13 @@
-
-
- {% if permissions.can_delete %}
-
×
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
- {% trans "Enable IX-F Import" %}
-
-
-
-
-
-
-
+
{% if permissions.can_delete %}
×
{% endif %}
-
+
diff --git a/peeringdb_server/templates/site/view_exchange_bottom.html b/peeringdb_server/templates/site/view_exchange_bottom.html
index 7ababeb2..871193d0 100644
--- a/peeringdb_server/templates/site/view_exchange_bottom.html
+++ b/peeringdb_server/templates/site/view_exchange_bottom.html
@@ -1,244 +1,81 @@
{% load util %}
{% load i18n %}
-
+
+
-{% if not data.lan_simple_view %}
+ data-edit-target="api:ixpfx">
+
+
{% trans "Prefixes" %}
-