diff --git a/config/facsimile/peeringdb.yaml b/config/facsimile/peeringdb.yaml
index 52815a24..395023db 100644
--- a/config/facsimile/peeringdb.yaml
+++ b/config/facsimile/peeringdb.yaml
@@ -160,7 +160,6 @@ 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 42d90436..d9d6c4f9 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, messages
+from django.contrib import admin
from django.contrib.auth import forms
from django.contrib.admin import helpers
from django.contrib.admin.actions import delete_selected
@@ -296,15 +296,6 @@ 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()
@@ -415,13 +406,7 @@ class IXLanInline(SanitizedAdmin, admin.StackedInline):
extra = 0
form = StatusForm
exclude = ["arp_sponge"]
- 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
+ readonly_fields = ["ixf_import_attempt_info", "prefixes"]
def ixf_import_attempt_info(self, obj):
if obj.ixf_import_attempt:
@@ -568,7 +553,6 @@ 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 0d8f356d..9a64bfe2 100644
--- a/peeringdb_server/autocomplete_views.py
+++ b/peeringdb_server/autocomplete_views.py
@@ -131,12 +131,13 @@ 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 2b7575d0..3a68dfb5 100644
--- a/peeringdb_server/client_adaptor/backend.py
+++ b/peeringdb_server/client_adaptor/backend.py
@@ -119,10 +119,7 @@ class Backend(BaseBackend):
obj.clean()
def save(self, obj):
- if obj.HandleRef.tag == "ix":
- obj.save(create_ixlan=False)
- else:
- obj.save()
+ 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 5039c4d5..6a5de782 100644
--- a/peeringdb_server/management/commands/pdb_api_test.py
+++ b/peeringdb_server/management/commands/pdb_api_test.py
@@ -346,7 +346,6 @@ 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,
@@ -354,8 +353,6 @@ 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
@@ -654,8 +651,7 @@ 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])}
- attr = getattr(REFTAG_MAP[target], rel, None)
- if attr and not isinstance(attr, property):
+ if hasattr(REFTAG_MAP[target], "%s" % rel):
valid_s = [
r.id
@@ -1002,11 +998,6 @@ 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",
@@ -1339,23 +1330,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)
- 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))
+ 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"]
self.assert_update(
self.db_org_admin,
"ixlan",
- SHARED["ixlan_rw_ok"].id,
+ SHARED["ixlan_id"],
{"name": self.make_name("Test")},
test_failures={
"invalid": {"mtu": "NEEDS TO BE INT"},
@@ -1363,14 +1354,12 @@ class TestJSON(unittest.TestCase):
},
)
- 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))
+ self.assert_delete(
+ self.db_org_admin,
+ "ixlan",
+ test_success=SHARED["ixlan_id"],
+ test_failure=SHARED["ixlan_r_ok"].id,
+ )
##########################################################################
@@ -2093,8 +2082,11 @@ class TestJSON(unittest.TestCase):
for i in range(0, 2)
]
- # collect ixlans
- ixlans = [ix.ixlan for ix in exchanges]
+ # create ixlan at each exchange
+ ixlans = [
+ IXLan.objects.create(status="ok", **self.make_data_ixlan(ix_id=ix.id))
+ for ix in exchanges
+ ]
# all three networks peer at first exchange
for net in networks:
@@ -2599,15 +2591,13 @@ class TestJSON(unittest.TestCase):
def test_readonly_users_002_POST_ixlan(self):
for db in self.readonly_dbs():
- 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))
+ self.assert_create(
+ db,
+ "ixlan",
+ self.make_data_ixlan(),
+ test_failures={"perms": {}},
+ test_success=False,
+ )
##########################################################################
@@ -2626,14 +2616,9 @@ class TestJSON(unittest.TestCase):
def test_readonly_users_004_DELETE_ixlan(self):
for db in self.readonly_dbs():
- 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))
+ self.assert_delete(
+ db, "ixlan", test_success=False, test_failure=SHARED["ixlan_r_ok"].id
+ )
##########################################################################
@@ -2727,6 +2712,16 @@ 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"]:
@@ -3139,9 +3134,7 @@ class Command(BaseCommand):
for k in unset:
if k in data:
del data[k]
- obj = model(**data)
- obj.save()
-
+ obj = model.objects.create(**data)
cls.log(
"%s with status '%s' for %s testing created! (%s)"
% (tag.upper(), status, prefix.upper(), obj.updated)
@@ -3306,12 +3299,12 @@ class Command(BaseCommand):
for status in ["ok", "pending"]:
for prefix in ["r", "rw"]:
- SHARED["ixlan_{}_{}".format(prefix, status)] = SHARED[
- "ix_{}_{}".format(prefix, status)
- ].ixlan
-
- 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,
+ )
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 f9d4a267..9fb56308 100644
--- a/peeringdb_server/management/commands/pdb_generate_test_data.py
+++ b/peeringdb_server/management/commands/pdb_generate_test_data.py
@@ -58,6 +58,7 @@ class Command(BaseCommand):
"net",
"ix",
"fac",
+ "ixlan",
"ixpfx",
"ixfac",
"netixlan",
@@ -103,8 +104,6 @@ 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 dacfca19..adbb95ff 100644
--- a/peeringdb_server/migrations/0022_ixlan_remove_auto_increment.py
+++ b/peeringdb_server/migrations/0022_ixlan_remove_auto_increment.py
@@ -12,9 +12,6 @@ class Migration(migrations.Migration):
]
operations = [
- migrations.AlterField(
- model_name="ixlan",
- name="id",
- field=models.IntegerField(primary_key=True, serialize=False),
- ),
+ # this change was reverted, but we will keep this empty migration
+ # so it does not break the migration chain
]
diff --git a/peeringdb_server/mock.py b/peeringdb_server/mock.py
index 2cab404b..f496b80f 100644
--- a/peeringdb_server/mock.py
+++ b/peeringdb_server/mock.py
@@ -96,10 +96,6 @@ 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:
@@ -144,15 +140,7 @@ class Mock(object):
# with the same name as the field name
else:
data[field.name] = getattr(self, field.name)(data, reftag=reftag)
- obj = model(**data)
- obj.clean()
- obj.save()
- return obj
-
- def id(self, data, reftag=None):
- if reftag == "ixlan":
- return data["ix"].id
- return None
+ return model.objects.create(**data)
def status(self, data, reftag=None):
return "ok"
diff --git a/peeringdb_server/models.py b/peeringdb_server/models.py
index 7b6bf7e3..1634c3c2 100644
--- a/peeringdb_server/models.py
+++ b/peeringdb_server/models.py
@@ -1335,17 +1335,6 @@ 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):
"""
@@ -1433,29 +1422,6 @@ 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)
@@ -1512,11 +1478,6 @@ 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"
)
@@ -1537,7 +1498,7 @@ class IXLan(pdb_models.IXLanBase):
"""
Returns a descriptive label of the ixlan for logging purposes
"""
- return "ixlan{} {}".format(self.id, self.ix.name)
+ return "ixlan{} {} {}".format(self.id, self.name, self.ix.name)
@classmethod
def nsp_namespace_from_id(cls, org_id, ix_id, id):
@@ -1605,27 +1566,6 @@ 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 8fd7ddc7..b633ba08 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, methods=None):
+def model_view_set(model):
"""
shortcut for peeringdb models to generate viewset and register in the API urls
"""
@@ -593,9 +593,6 @@ def model_view_set(model, methods=None):
# 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)
@@ -606,7 +603,7 @@ def model_view_set(model, methods=None):
FacilityViewSet = model_view_set("Facility")
InternetExchangeViewSet = model_view_set("InternetExchange")
InternetExchangeFacilityViewSet = model_view_set("InternetExchangeFacility")
-IXLanViewSet = model_view_set("IXLan", methods=["get", "put"])
+IXLanViewSet = model_view_set("IXLan")
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 76180086..11453ce5 100644
--- a/peeringdb_server/serializers.py
+++ b/peeringdb_server/serializers.py
@@ -2005,13 +2005,8 @@ class InternetExchangeSerializer(ModelSerializer):
# create ix
r = super(InternetExchangeSerializer, self).create(validated_data)
- ixlan = r.ixlan
-
# create ixlan
- # if False:# not ixlan:
- # ixlan = IXLan(ix=r, status="pending")
- # ixlan.clean()
- # ixlan.save()
+ ixlan = IXLan.objects.create(name="Main", ix=r, status="pending")
# 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 d7a4d2f2..91588a7d 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, "data":{}},
+ status={"error":false},
i;
@@ -182,16 +182,13 @@ twentyc.editable.action.register(
targets--;
if(error)
status.error = true;
-
- if(data) {
- $.extend(status.data, data);
- }
-
if(!targets) {
if(!status.error && !me.noToggle) {
- container.editable("toggle", { data:status.data });
+ if(data)
+ container.editable("toggle", { data:data });
+ else
+ container.editable("toggle");
}
-
/*
if(!status.error && container.data("edit-always")) {
// if container is always toggled to edit mode
@@ -215,8 +212,6 @@ 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 }).
@@ -249,14 +244,7 @@ twentyc.editable.action.register(
}
var grouped = container.editable("filter", { grouped : true }).not("[data-edit-module]");
-
- grouped.each(function(idx) {
- var target = twentyc.editable.target.instantiate($(this));
- $.extend(status.data, target.data);
- if(target.data._changed) {
- targets += 1
- }
- });
+ targets += grouped.length;
if(changed || container.data("edit-always-submit") == "yes"){
$(target).on("success", function(ev, data) {
@@ -270,9 +258,8 @@ 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 edf0df70..98c495f3 100644
--- a/peeringdb_server/static/peeringdb.js
+++ b/peeringdb_server/static/peeringdb.js
@@ -252,13 +252,6 @@ 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()
@@ -269,6 +262,7 @@ PeeringDB.ViewActions.actions.net_ixf_postmortem = function(netId) {
$("#ixf-postmortem-modal").modal("show");
var postmortem = new PeeringDB.IXFNetPostmortem()
postmortem.request(netId, $("#ixf-postmortem"));
+
}
@@ -1093,7 +1087,6 @@ twentyc.editable.target.register(
"base"
);
-
/*
* editable api listing module
*/
@@ -1352,6 +1345,34 @@ 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 8f9ac0e3..132c6b10 100644
--- a/peeringdb_server/static/site.css
+++ b/peeringdb_server/static/site.css
@@ -660,7 +660,6 @@ table.result {
div.list {
margin-left: 15px;
margin-right: 15px;
- margin-top: 15px;
}
div.list div.header {
@@ -681,7 +680,6 @@ 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 8db1d231..364f4b35 100644
--- a/peeringdb_server/templates/site/view.html
+++ b/peeringdb_server/templates/site/view.html
@@ -63,27 +63,7 @@
{% for row in data.fields %}
- {% 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.value|dont_render %}
{% 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 f6da15a3..214b7331 100644
--- a/peeringdb_server/templates/site/view_exchange_assets.html
+++ b/peeringdb_server/templates/site/view_exchange_assets.html
@@ -26,13 +26,84 @@
+
+
+ {% 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 871193d0..7ababeb2 100644
--- a/peeringdb_server/templates/site/view_exchange_bottom.html
+++ b/peeringdb_server/templates/site/view_exchange_bottom.html
@@ -1,81 +1,244 @@
{% load util %}
{% load i18n %}
-
-
+
+{% if not data.lan_simple_view %}
-
-
{% trans "Prefixes" %}
+ data-edit-target="api:ixlan">
+