mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-20 02:06: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)
|
||||
|
||||
## Notes
|
||||
|
@ -104,7 +104,7 @@ The base serializer is used to represent the default view of a model. This inclu
|
||||
}
|
||||
```
|
||||
|
||||
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
|
||||
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
|
||||
|
||||
When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object.
|
||||
|
||||
@ -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
|
||||
|
||||
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']
|
||||
|
||||
def __str__(self):
|
||||
return '{} {}'.format(self.provider, self.cid)
|
||||
return self.cid
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:circuit', args=[self.pk])
|
||||
|
@ -484,7 +484,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
filterset_class = filters.PowerConnectionFilter
|
||||
|
||||
|
||||
class InterfaceConnectionViewSet(ModelViewSet):
|
||||
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = Interface.objects.select_related(
|
||||
'device', '_connected_interface', '_connected_circuittermination'
|
||||
).filter(
|
||||
|
@ -103,6 +103,7 @@ IFACE_FF_4GFC_SFP = 3040
|
||||
IFACE_FF_8GFC_SFP_PLUS = 3080
|
||||
IFACE_FF_16GFC_SFP_PLUS = 3160
|
||||
IFACE_FF_32GFC_SFP28 = 3320
|
||||
IFACE_FF_128GFC_QSFP28 = 3400
|
||||
# Serial
|
||||
IFACE_FF_T1 = 4000
|
||||
IFACE_FF_E1 = 4010
|
||||
@ -188,6 +189,7 @@ IFACE_FF_CHOICES = [
|
||||
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
|
||||
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
|
||||
[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(
|
||||
termination, termination.positions, position
|
||||
))
|
||||
peer_port = FrontPort.objects.get(
|
||||
rear_port=termination,
|
||||
rear_port_position=position,
|
||||
)
|
||||
return peer_port, 1
|
||||
try:
|
||||
peer_port = FrontPort.objects.get(
|
||||
rear_port=termination,
|
||||
rear_port_position=position,
|
||||
)
|
||||
return peer_port, 1
|
||||
except ObjectDoesNotExist:
|
||||
return None, None
|
||||
|
||||
# Follow a circuit to its other termination
|
||||
elif isinstance(termination, CircuitTermination) and follow_circuits:
|
||||
@ -2629,5 +2632,7 @@ class Cable(ChangeLoggedModel):
|
||||
path_status = CONNECTION_STATUS_PLANNED
|
||||
break
|
||||
|
||||
# (A path end, B path end, connected/planned)
|
||||
return a_path[-1][2], b_path[-1][2], path_status
|
||||
a_endpoint = a_path[-1][2]
|
||||
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()
|
||||
|
||||
# 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.connection_status = None
|
||||
endpoint_a.save()
|
||||
|
@ -179,7 +179,7 @@ CABLE_TERMINATION_PARENT = """
|
||||
"""
|
||||
|
||||
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,7 +30,8 @@ class WebhookForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
order_content_types(self.fields['obj_type'])
|
||||
if 'obj_type' in self.fields:
|
||||
order_content_types(self.fields['obj_type'])
|
||||
|
||||
|
||||
@admin.register(Webhook, site=admin_site)
|
||||
|
@ -31,12 +31,12 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
# Treat 0 as None
|
||||
if int(value) == 0:
|
||||
return queryset.exclude(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__field__name=self.field_name,
|
||||
)
|
||||
# Match on exact CustomFieldChoice PK
|
||||
else:
|
||||
return queryset.filter(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__field__name=self.field_name,
|
||||
custom_field_values__serialized_value=value,
|
||||
)
|
||||
except ValueError:
|
||||
@ -45,12 +45,12 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
# Apply the assigned filter logic (exact or loose)
|
||||
if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
|
||||
queryset = queryset.filter(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__field__name=self.field_name,
|
||||
custom_field_values__serialized_value=value
|
||||
)
|
||||
else:
|
||||
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
|
||||
)
|
||||
|
||||
|
@ -11,7 +11,7 @@ from taggit.models import Tag
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, FilterChoiceField,
|
||||
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect, FilterChoiceField,
|
||||
FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
|
||||
)
|
||||
from .constants import (
|
||||
@ -307,21 +307,20 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
|
||||
# Change logging
|
||||
#
|
||||
|
||||
class ObjectChangeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
model = ObjectChange
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
# TODO: Change time_0 and time_1 to time_after and time_before for django-filter==2.0
|
||||
time_0 = forms.DateTimeField(
|
||||
time_after = forms.DateTimeField(
|
||||
label='After',
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'}
|
||||
)
|
||||
)
|
||||
time_1 = forms.DateTimeField(
|
||||
time_before = forms.DateTimeField(
|
||||
label='Before',
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
@ -336,3 +335,9 @@ class ObjectChangeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
queryset=User.objects.order_by('username'),
|
||||
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(
|
||||
Q(untagged_vlan_id=self.pk) |
|
||||
Q(tagged_vlans=self.pk)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
|
||||
class Service(ChangeLoggedModel, CustomFieldModel):
|
||||
|
@ -430,7 +430,7 @@ class VLANDetailTable(VLANTable):
|
||||
|
||||
class VLANMemberTable(BaseTable):
|
||||
parent = tables.LinkColumn(order_by=['device', 'virtual_machine'])
|
||||
name = tables.Column(verbose_name='Interface')
|
||||
name = tables.LinkColumn(verbose_name='Interface')
|
||||
untagged = tables.TemplateColumn(
|
||||
template_code=VLAN_MEMBER_UNTAGGED,
|
||||
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__)))
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<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 %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
|
@ -29,7 +29,7 @@
|
||||
<tr>
|
||||
<td>Circuit</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>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
@ -5,7 +5,7 @@
|
||||
{% if end.device %}
|
||||
<strong><a href="{{ end.device.get_absolute_url }}">{{ end.device }}</a></strong>
|
||||
{% 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 %}
|
||||
</div>
|
||||
<div class="panel-body text-center">
|
||||
@ -21,7 +21,8 @@
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{# Circuit termination #}
|
||||
<strong>Side {{ end.term_side }}</strong>
|
||||
<strong><a href="{{ end.circuit.get_absolute_url }}">{{ end.circuit }}</a></strong><br/>
|
||||
{{ end }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -75,10 +75,16 @@
|
||||
{% elif iface.connected_endpoint.name %}
|
||||
{# Connected to an Interface #}
|
||||
<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>
|
||||
<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>
|
||||
{% elif iface.connected_endpoint.term_side %}
|
||||
{# Connected to a CircuitTermination #}
|
||||
@ -86,22 +92,38 @@
|
||||
{% if peer_termination %}
|
||||
{% if peer_termination.connected_endpoint %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=peer_termination.connected_endpoint.device.pk %}">{{ peer_termination.connected_endpoint.device }}</a><br/>
|
||||
<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>
|
||||
<a href="{% url 'dcim:device' pk=peer_termination.connected_endpoint.device.pk %}">
|
||||
{{ 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>
|
||||
{{ peer_termination.connected_endpoint }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td colspan="2">
|
||||
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a>
|
||||
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>
|
||||
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">
|
||||
{{ 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>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<td colspan="2">
|
||||
<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>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
@ -163,6 +163,10 @@
|
||||
</tr>
|
||||
{% elif 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>
|
||||
<td>Circuit</td>
|
||||
<td><a href="{{ ct.circuit.get_absolute_url }}">{{ ct.circuit }}</a></td>
|
||||
|
@ -78,17 +78,26 @@ class ChoiceField(Field):
|
||||
return 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'):
|
||||
# Hotwiring boolean values from string
|
||||
if data.lower() == 'true':
|
||||
return True
|
||||
if data.lower() == 'false':
|
||||
return False
|
||||
# Check for string representation of an integer (e.g. "123")
|
||||
try:
|
||||
data = int(data)
|
||||
except ValueError:
|
||||
pass
|
||||
data = True
|
||||
elif data.lower() == 'false':
|
||||
data = False
|
||||
else:
|
||||
try:
|
||||
data = int(data)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if data not in self._choices:
|
||||
raise ValidationError("{} is not a valid choice.".format(data))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user