mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-20 10:16:42 -06:00
commit
27a893a9a1
21
CHANGELOG.md
21
CHANGELOG.md
@ -1,3 +1,24 @@
|
|||||||
|
v2.5.1 (2018-12-13)
|
||||||
|
|
||||||
|
## Enhancements
|
||||||
|
|
||||||
|
* [#2655](https://github.com/digitalocean/netbox/issues/2655) - Add 128GFC Fibrechannel interface type
|
||||||
|
* [#2674](https://github.com/digitalocean/netbox/issues/2674) - Enable filtering changelog by object type under web UI
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
* [#2662](https://github.com/digitalocean/netbox/issues/2662) - Fix ImproperlyConfigured exception when rendering API docs
|
||||||
|
* [#2663](https://github.com/digitalocean/netbox/issues/2663) - Prevent duplicate interfaces from appearing under VLAN members view
|
||||||
|
* [#2666](https://github.com/digitalocean/netbox/issues/2666) - Correct display of length unit in cables list
|
||||||
|
* [#2676](https://github.com/digitalocean/netbox/issues/2676) - Fix exception when passing dictionary value to a ChoiceField
|
||||||
|
* [#2678](https://github.com/digitalocean/netbox/issues/2678) - Fix error when viewing webhook in admin UI without write permission
|
||||||
|
* [#2680](https://github.com/digitalocean/netbox/issues/2680) - Disallow POST requests to `/dcim/interface-connections/` API endpoint
|
||||||
|
* [#2683](https://github.com/digitalocean/netbox/issues/2683) - Fix exception when connecting a cable to a RearPort with no corresponding FrontPort
|
||||||
|
* [#2684](https://github.com/digitalocean/netbox/issues/2684) - Fix custom field filtering
|
||||||
|
* [#2687](https://github.com/digitalocean/netbox/issues/2687) - Correct naming of before/after filters for changelog entries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
v2.5.0 (2018-12-10)
|
v2.5.0 (2018-12-10)
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
@ -122,6 +122,52 @@ When a base serializer includes one or more nested serializers, the hierarchical
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Brief Format
|
||||||
|
|
||||||
|
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of the objects themselves without any related data, such as when populating a drop-down list in a form.
|
||||||
|
|
||||||
|
For example, the default (complete) format of an IP address looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/ipam/prefixes/13980/
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 13980,
|
||||||
|
"family": 4,
|
||||||
|
"prefix": "192.0.2.0/24",
|
||||||
|
"site": null,
|
||||||
|
"vrf": null,
|
||||||
|
"tenant": null,
|
||||||
|
"vlan": null,
|
||||||
|
"status": {
|
||||||
|
"value": 1,
|
||||||
|
"label": "Active"
|
||||||
|
},
|
||||||
|
"role": null,
|
||||||
|
"is_pool": false,
|
||||||
|
"description": "",
|
||||||
|
"tags": [],
|
||||||
|
"custom_fields": {},
|
||||||
|
"created": "2018-12-11",
|
||||||
|
"last_updated": "2018-12-11T16:27:55.073174-05:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The brief format is much more terse, but includes a link to the object's full representation:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/ipam/prefixes/13980/?brief=1
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 13980,
|
||||||
|
"url": "https://netbox/api/ipam/prefixes/13980/",
|
||||||
|
"family": 4,
|
||||||
|
"prefix": "192.0.2.0/24"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The brief format is supported for both lists and individual objects.
|
||||||
|
|
||||||
## Static Choice Fields
|
## Static Choice Fields
|
||||||
|
|
||||||
Some model fields, such as the `status` field in the above example, utilize static integers corresponding to static choices. The available choices can be retrieved from the read-only `_choices` endpoint within each app. A specific `model:field` tuple may optionally be specified in the URL.
|
Some model fields, such as the `status` field in the above example, utilize static integers corresponding to static choices. The available choices can be retrieved from the read-only `_choices` endpoint within each app. A specific `model:field` tuple may optionally be specified in the URL.
|
||||||
|
@ -176,7 +176,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
|||||||
unique_together = ['provider', 'cid']
|
unique_together = ['provider', 'cid']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} {}'.format(self.provider, self.cid)
|
return self.cid
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('circuits:circuit', args=[self.pk])
|
return reverse('circuits:circuit', args=[self.pk])
|
||||||
|
@ -484,7 +484,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
|||||||
filterset_class = filters.PowerConnectionFilter
|
filterset_class = filters.PowerConnectionFilter
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionViewSet(ModelViewSet):
|
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||||
queryset = Interface.objects.select_related(
|
queryset = Interface.objects.select_related(
|
||||||
'device', '_connected_interface', '_connected_circuittermination'
|
'device', '_connected_interface', '_connected_circuittermination'
|
||||||
).filter(
|
).filter(
|
||||||
|
@ -103,6 +103,7 @@ IFACE_FF_4GFC_SFP = 3040
|
|||||||
IFACE_FF_8GFC_SFP_PLUS = 3080
|
IFACE_FF_8GFC_SFP_PLUS = 3080
|
||||||
IFACE_FF_16GFC_SFP_PLUS = 3160
|
IFACE_FF_16GFC_SFP_PLUS = 3160
|
||||||
IFACE_FF_32GFC_SFP28 = 3320
|
IFACE_FF_32GFC_SFP28 = 3320
|
||||||
|
IFACE_FF_128GFC_QSFP28 = 3400
|
||||||
# Serial
|
# Serial
|
||||||
IFACE_FF_T1 = 4000
|
IFACE_FF_T1 = 4000
|
||||||
IFACE_FF_E1 = 4010
|
IFACE_FF_E1 = 4010
|
||||||
@ -188,6 +189,7 @@ IFACE_FF_CHOICES = [
|
|||||||
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
|
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
|
||||||
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
|
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
|
||||||
[IFACE_FF_32GFC_SFP28, 'SFP28 (32GFC)'],
|
[IFACE_FF_32GFC_SFP28, 'SFP28 (32GFC)'],
|
||||||
|
[IFACE_FF_128GFC_QSFP28, 'QSFP28 (128GFC)'],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
@ -110,11 +110,14 @@ class CableTermination(models.Model):
|
|||||||
raise Exception("Invalid position for {} ({} positions): {})".format(
|
raise Exception("Invalid position for {} ({} positions): {})".format(
|
||||||
termination, termination.positions, position
|
termination, termination.positions, position
|
||||||
))
|
))
|
||||||
|
try:
|
||||||
peer_port = FrontPort.objects.get(
|
peer_port = FrontPort.objects.get(
|
||||||
rear_port=termination,
|
rear_port=termination,
|
||||||
rear_port_position=position,
|
rear_port_position=position,
|
||||||
)
|
)
|
||||||
return peer_port, 1
|
return peer_port, 1
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return None, None
|
||||||
|
|
||||||
# Follow a circuit to its other termination
|
# Follow a circuit to its other termination
|
||||||
elif isinstance(termination, CircuitTermination) and follow_circuits:
|
elif isinstance(termination, CircuitTermination) and follow_circuits:
|
||||||
@ -2629,5 +2632,7 @@ class Cable(ChangeLoggedModel):
|
|||||||
path_status = CONNECTION_STATUS_PLANNED
|
path_status = CONNECTION_STATUS_PLANNED
|
||||||
break
|
break
|
||||||
|
|
||||||
# (A path end, B path end, connected/planned)
|
a_endpoint = a_path[-1][2]
|
||||||
return a_path[-1][2], b_path[-1][2], path_status
|
b_endpoint = b_path[-1][2]
|
||||||
|
|
||||||
|
return a_endpoint, b_endpoint, path_status
|
||||||
|
@ -62,7 +62,7 @@ def nullify_connected_endpoints(instance, **kwargs):
|
|||||||
instance.termination_b.save()
|
instance.termination_b.save()
|
||||||
|
|
||||||
# If this Cable was part of a complete path, tear it down
|
# If this Cable was part of a complete path, tear it down
|
||||||
if endpoint_a is not None and endpoint_b is not None:
|
if hasattr(endpoint_a, 'connected_endpoint') and hasattr(endpoint_b, 'connected_endpoint'):
|
||||||
endpoint_a.connected_endpoint = None
|
endpoint_a.connected_endpoint = None
|
||||||
endpoint_a.connection_status = None
|
endpoint_a.connection_status = None
|
||||||
endpoint_a.save()
|
endpoint_a.save()
|
||||||
|
@ -179,7 +179,7 @@ CABLE_TERMINATION_PARENT = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CABLE_LENGTH = """
|
CABLE_LENGTH = """
|
||||||
{% if record.length %}{{ record.length }}{{ record.length_unit }}{% else %}—{% endif %}
|
{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% else %}—{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ class WebhookForm(forms.ModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if 'obj_type' in self.fields:
|
||||||
order_content_types(self.fields['obj_type'])
|
order_content_types(self.fields['obj_type'])
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,12 +31,12 @@ class CustomFieldFilter(django_filters.Filter):
|
|||||||
# Treat 0 as None
|
# Treat 0 as None
|
||||||
if int(value) == 0:
|
if int(value) == 0:
|
||||||
return queryset.exclude(
|
return queryset.exclude(
|
||||||
custom_field_values__field__name=self.name,
|
custom_field_values__field__name=self.field_name,
|
||||||
)
|
)
|
||||||
# Match on exact CustomFieldChoice PK
|
# Match on exact CustomFieldChoice PK
|
||||||
else:
|
else:
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
custom_field_values__field__name=self.name,
|
custom_field_values__field__name=self.field_name,
|
||||||
custom_field_values__serialized_value=value,
|
custom_field_values__serialized_value=value,
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -45,12 +45,12 @@ class CustomFieldFilter(django_filters.Filter):
|
|||||||
# Apply the assigned filter logic (exact or loose)
|
# Apply the assigned filter logic (exact or loose)
|
||||||
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
|
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
custom_field_values__field__name=self.name,
|
custom_field_values__field__name=self.field_name,
|
||||||
custom_field_values__serialized_value=value
|
custom_field_values__serialized_value=value
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
custom_field_values__field__name=self.name,
|
custom_field_values__field__name=self.field_name,
|
||||||
custom_field_values__serialized_value__icontains=value
|
custom_field_values__serialized_value__icontains=value
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from taggit.models import Tag
|
|||||||
from dcim.models import DeviceRole, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, FilterChoiceField,
|
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect, FilterChoiceField,
|
||||||
FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
|
FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
|
||||||
)
|
)
|
||||||
from .constants import (
|
from .constants import (
|
||||||
@ -307,21 +307,20 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
|||||||
# Change logging
|
# Change logging
|
||||||
#
|
#
|
||||||
|
|
||||||
class ObjectChangeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||||
model = ObjectChange
|
model = ObjectChange
|
||||||
q = forms.CharField(
|
q = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
# TODO: Change time_0 and time_1 to time_after and time_before for django-filter==2.0
|
time_after = forms.DateTimeField(
|
||||||
time_0 = forms.DateTimeField(
|
|
||||||
label='After',
|
label='After',
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(
|
||||||
attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
|
attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
time_1 = forms.DateTimeField(
|
time_before = forms.DateTimeField(
|
||||||
label='Before',
|
label='Before',
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(
|
||||||
@ -336,3 +335,9 @@ class ObjectChangeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
queryset=User.objects.order_by('username'),
|
queryset=User.objects.order_by('username'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
changed_object_type = forms.ModelChoiceField(
|
||||||
|
queryset=ContentType.objects.order_by('model'),
|
||||||
|
required=False,
|
||||||
|
widget=ContentTypeSelect(),
|
||||||
|
label='Object Type'
|
||||||
|
)
|
||||||
|
@ -812,7 +812,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
|
|||||||
return Interface.objects.filter(
|
return Interface.objects.filter(
|
||||||
Q(untagged_vlan_id=self.pk) |
|
Q(untagged_vlan_id=self.pk) |
|
||||||
Q(tagged_vlans=self.pk)
|
Q(tagged_vlans=self.pk)
|
||||||
)
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
class Service(ChangeLoggedModel, CustomFieldModel):
|
class Service(ChangeLoggedModel, CustomFieldModel):
|
||||||
|
@ -430,7 +430,7 @@ class VLANDetailTable(VLANTable):
|
|||||||
|
|
||||||
class VLANMemberTable(BaseTable):
|
class VLANMemberTable(BaseTable):
|
||||||
parent = tables.LinkColumn(order_by=['device', 'virtual_machine'])
|
parent = tables.LinkColumn(order_by=['device', 'virtual_machine'])
|
||||||
name = tables.Column(verbose_name='Interface')
|
name = tables.LinkColumn(verbose_name='Interface')
|
||||||
untagged = tables.TemplateColumn(
|
untagged = tables.TemplateColumn(
|
||||||
template_code=VLAN_MEMBER_UNTAGGED,
|
template_code=VLAN_MEMBER_UNTAGGED,
|
||||||
orderable=False
|
orderable=False
|
||||||
|
@ -22,7 +22,7 @@ except ImportError:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
VERSION = '2.5.0'
|
VERSION = '2.5.1'
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-md-offset-3">
|
<div class="col-md-6 col-md-offset-3">
|
||||||
<h3>{% block title %}Circuit {{ obj.circuit }} - {{ form.term_side.value }} Side{% endblock %}</h3>
|
<h3>{% block title %}{{ obj.circuit.provider }} {{ obj.circuit }} - Side {{ form.term_side.value }}{% endblock %}</h3>
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
<div class="panel panel-danger">
|
<div class="panel panel-danger">
|
||||||
<div class="panel-heading"><strong>Errors</strong></div>
|
<div class="panel-heading"><strong>Errors</strong></div>
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Circuit</td>
|
<td>Circuit</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ termination.circuit.get_absolute_url }}">{{ termination.circuit }}</a> (Side {{ termination.term_side }})
|
<a href="{{ termination.circuit.get_absolute_url }}">{{ termination.circuit }}</a> ({{ termination }})
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{% if end.device %}
|
{% if end.device %}
|
||||||
<strong><a href="{{ end.device.get_absolute_url }}">{{ end.device }}</a></strong>
|
<strong><a href="{{ end.device.get_absolute_url }}">{{ end.device }}</a></strong>
|
||||||
{% else %}
|
{% else %}
|
||||||
<strong><a href="{{ end.circuit.get_absolute_url }}">{{ end.circuit }}</a></strong>
|
<strong><a href="{{ end.circuit.provider.get_absolute_url }}">{{ end.circuit.provider }}</a></strong>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body text-center">
|
<div class="panel-body text-center">
|
||||||
@ -21,7 +21,8 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{# Circuit termination #}
|
{# Circuit termination #}
|
||||||
<strong>Side {{ end.term_side }}</strong>
|
<strong><a href="{{ end.circuit.get_absolute_url }}">{{ end.circuit }}</a></strong><br/>
|
||||||
|
{{ end }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -75,10 +75,16 @@
|
|||||||
{% elif iface.connected_endpoint.name %}
|
{% elif iface.connected_endpoint.name %}
|
||||||
{# Connected to an Interface #}
|
{# Connected to an Interface #}
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=iface.connected_endpoint.device.pk %}">{{ iface.connected_endpoint.device }}</a>
|
<a href="{% url 'dcim:device' pk=iface.connected_endpoint.device.pk %}">
|
||||||
|
{{ iface.connected_endpoint.device }}
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:interface' pk=iface.connected_endpoint.pk %}"><span title="{{ iface.connected_endpoint.get_form_factor_display }}">{{ iface.connected_endpoint }}</span></a>
|
<a href="{% url 'dcim:interface' pk=iface.connected_endpoint.pk %}">
|
||||||
|
<span title="{{ iface.connected_endpoint.get_form_factor_display }}">
|
||||||
|
{{ iface.connected_endpoint }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
{% elif iface.connected_endpoint.term_side %}
|
{% elif iface.connected_endpoint.term_side %}
|
||||||
{# Connected to a CircuitTermination #}
|
{# Connected to a CircuitTermination #}
|
||||||
@ -86,22 +92,38 @@
|
|||||||
{% if peer_termination %}
|
{% if peer_termination %}
|
||||||
{% if peer_termination.connected_endpoint %}
|
{% if peer_termination.connected_endpoint %}
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=peer_termination.connected_endpoint.device.pk %}">{{ peer_termination.connected_endpoint.device }}</a><br/>
|
<a href="{% url 'dcim:device' pk=peer_termination.connected_endpoint.device.pk %}">
|
||||||
<small>via <i class="fa fa-fw fa-globe" title="Circuit"></i> <a href="{% url 'circuits:circuit' pk=iface.connected_endpoint.circuit_id %}">{{ iface.connected_endpoint.circuit }}</a></small>
|
{{ peer_termination.connected_endpoint.device }}
|
||||||
|
</a><br/>
|
||||||
|
<small>via <i class="fa fa-fw fa-globe" title="Circuit"></i>
|
||||||
|
<a href="{{ iface.connected_endpoint.circuit.get_absolure_url }}">
|
||||||
|
{{ iface.connected_endpoint.circuit.provider }}
|
||||||
|
{{ iface.connected_endpoint.circuit }}
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ peer_termination.connected_endpoint }}
|
{{ peer_termination.connected_endpoint }}
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a>
|
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">
|
||||||
via <i class="fa fa-fw fa-globe" title="Circuit"></i> <a href="{% url 'circuits:circuit' pk=iface.connected_endpoint.circuit_id %}">{{ iface.connected_endpoint.circuit }}</a>
|
{{ peer_termination.site }}
|
||||||
|
</a>
|
||||||
|
via <i class="fa fa-fw fa-globe" title="Circuit"></i>
|
||||||
|
<a href="{{ iface.connected_endpoint.circuit.get_absolute_url }}">
|
||||||
|
{{ iface.connected_endpoint.circuit.provider }}
|
||||||
|
{{ iface.connected_endpoint.circuit }}
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<i class="fa fa-fw fa-globe" title="Circuit"></i>
|
<i class="fa fa-fw fa-globe" title="Circuit"></i>
|
||||||
<a href="{% url 'circuits:circuit' pk=iface.connected_endpoint.circuit_id %}">{{ iface.connected_endpoint.circuit }}</a>
|
<a href="{{ iface.connected_endpoint.circuit.get_absolute_url }}">
|
||||||
|
{{ iface.connected_endpoint.circuit.provider }}
|
||||||
|
{{ iface.connected_endpoint.circuit }}
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -163,6 +163,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% elif connected_circuittermination %}
|
{% elif connected_circuittermination %}
|
||||||
{% with ct=connected_circuittermination %}
|
{% with ct=connected_circuittermination %}
|
||||||
|
<tr>
|
||||||
|
<td>Provider</td>
|
||||||
|
<td><a href="{{ ct.circuit.provider.get_absolute_url }}">{{ ct.circuit.provider }}</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Circuit</td>
|
<td>Circuit</td>
|
||||||
<td><a href="{{ ct.circuit.get_absolute_url }}">{{ ct.circuit }}</a></td>
|
<td><a href="{{ ct.circuit.get_absolute_url }}">{{ ct.circuit }}</a></td>
|
||||||
|
@ -78,17 +78,26 @@ class ChoiceField(Field):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
|
|
||||||
|
# Provide an explicit error message if the request is trying to write a dict
|
||||||
|
if type(data) is dict:
|
||||||
|
raise ValidationError('Value must be passed directly (e.g. "foo": 123); do not use a dictionary.')
|
||||||
|
|
||||||
|
# Check for string representations of boolean/integer values
|
||||||
if hasattr(data, 'lower'):
|
if hasattr(data, 'lower'):
|
||||||
# Hotwiring boolean values from string
|
|
||||||
if data.lower() == 'true':
|
if data.lower() == 'true':
|
||||||
return True
|
data = True
|
||||||
if data.lower() == 'false':
|
elif data.lower() == 'false':
|
||||||
return False
|
data = False
|
||||||
# Check for string representation of an integer (e.g. "123")
|
else:
|
||||||
try:
|
try:
|
||||||
data = int(data)
|
data = int(data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if data not in self._choices:
|
||||||
|
raise ValidationError("{} is not a valid choice.".format(data))
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user