Skip to content

Commit

Permalink
Merge branch 'develop' into 13843-fix-vlangroup-bulk-edit-scope
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch committed Sep 26, 2023
2 parents d8d930d + b759d69 commit b500cec
Show file tree
Hide file tree
Showing 14 changed files with 96 additions and 82 deletions.
13 changes: 13 additions & 0 deletions docs/release-notes/version-3.6.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

## v3.6.3 (FUTURE)

### Enhancements

* [#12732](https://github.com/netbox-community/netbox/issues/12732) - Add toggle to hide disconnected interfaces under device view

### Bug Fixes

* [#13506](https://github.com/netbox-community/netbox/issues/13506) - Enable creating a config template which references a data file via the REST API
* [#13666](https://github.com/netbox-community/netbox/issues/13666) - Cleanly handle reports without any test methods defined
* [#13839](https://github.com/netbox-community/netbox/issues/13839) - Restore original text color for HTML code elements
* [#13845](https://github.com/netbox-community/netbox/issues/13845) - Fix `AttributeError` exception when attaching front/rear images to a device type
* [#13871](https://github.com/netbox-community/netbox/issues/13871) - Fix rack filtering for empty location during device bulk import
* [#13891](https://github.com/netbox-community/netbox/issues/13891) - Allow designating an IP address as primary for device/VM while assigning it to an interface

---

## v3.6.2 (2023-09-20)
Expand Down
9 changes: 5 additions & 4 deletions netbox/dcim/models/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from functools import cached_property

from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import F, ProtectedError
Expand Down Expand Up @@ -332,10 +333,10 @@ def save(self, *args, **kwargs):
ret = super().save(*args, **kwargs)

# Delete any previously uploaded image files that are no longer in use
if self.front_image != self._original_front_image:
self._original_front_image.delete(save=False)
if self.rear_image != self._original_rear_image:
self._original_rear_image.delete(save=False)
if self._original_front_image and self.front_image != self._original_front_image:
default_storage.delete(self._original_front_image)
if self._original_rear_image and self.rear_image != self._original_rear_image:
default_storage.delete(self._original_rear_image)

return ret

Expand Down
15 changes: 13 additions & 2 deletions netbox/dcim/tables/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,19 @@ def get_interface_state_attribute(record):
Get interface enabled state as string to attach to <tr/> DOM element.
"""
if record.enabled:
return "enabled"
return 'enabled'
else:
return "disabled"
return 'disabled'


def get_interface_connected_attribute(record):
"""
Get interface disconnected state as string to attach to <tr/> DOM element.
"""
if record.mark_connected or record.cable:
return 'connected'
else:
return 'disconnected'


#
Expand Down Expand Up @@ -674,6 +684,7 @@ class Meta(DeviceComponentTable.Meta):
'data-name': lambda record: record.name,
'data-enabled': get_interface_state_attribute,
'data-type': lambda record: record.type,
'data-connected': get_interface_connected_attribute
}


Expand Down
2 changes: 1 addition & 1 deletion netbox/extras/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer
required=False
)
data_file = NestedDataFileSerializer(
read_only=True
required=False
)

class Meta:
Expand Down
5 changes: 4 additions & 1 deletion netbox/extras/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ def choices(self, request, pk):
data = [
{'id': c[0], 'display': c[1]} for c in page
]
return self.get_paginated_response(data)
else:
data = []

return self.get_paginated_response(data)


#
Expand Down
9 changes: 7 additions & 2 deletions netbox/extras/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,6 @@ def __init__(self):
'failure': 0,
'log': [],
}
if not test_methods:
raise Exception("A report must contain at least one test method.")
self.test_methods = test_methods

@classproperty
Expand Down Expand Up @@ -137,6 +135,13 @@ def filename(self):
def source(self):
return inspect.getsource(self.__class__)

@property
def is_valid(self):
"""
Indicates whether the report can be run.
"""
return bool(self.test_methods)

#
# Logging methods
#
Expand Down
2 changes: 1 addition & 1 deletion netbox/ipam/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ def clean(self):
})
elif selected_objects:
assigned_object = self.cleaned_data[selected_objects[0]]
if self.instance.pk and self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object:
if self.instance.pk and self.instance.assigned_object and self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object:
raise ValidationError(
_("Cannot reassign IP address while it is designated as the primary IP for the parent object")
)
Expand Down
13 changes: 7 additions & 6 deletions netbox/netbox/api/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ def validate_empty_values(self, data):
return super().validate_empty_values(data)

def to_representation(self, obj):
if obj == '':
return None
return {
'value': obj,
'label': self._choices[obj],
}
if obj != '':
# Use an empty string in place of the choice label if it cannot be resolved (i.e. because a previously
# configured choice has been removed from FIELD_CHOICES).
return {
'value': obj,
'label': self._choices.get(obj, ''),
}

def to_internal_value(self, data):
if data == '':
Expand Down
18 changes: 9 additions & 9 deletions netbox/project-static/dist/netbox.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion netbox/project-static/dist/netbox.js.map

Large diffs are not rendered by default.

69 changes: 17 additions & 52 deletions netbox/project-static/src/tables/interfaceTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,10 @@ class TableState {
private virtualButton: ButtonState;

/**
* Underlying DOM Table Caption Element.
* Instance of ButtonState for the 'show/hide virtual rows' button.
*/
private caption: Nullable<HTMLTableCaptionElement> = null;
// @ts-expect-error null handling is performed in the constructor
private disconnectedButton: ButtonState;

/**
* All table rows in table
Expand All @@ -166,9 +167,10 @@ class TableState {
this.table,
'button.toggle-virtual',
);

const caption = this.table.querySelector('caption');
this.caption = caption;
const toggleDisconnectedButton = findFirstAdjacent<HTMLButtonElement>(
this.table,
'button.toggle-disconnected',
);

if (toggleEnabledButton === null) {
throw new TableStateError("Table is missing a 'toggle-enabled' button.", table);
Expand All @@ -182,10 +184,15 @@ class TableState {
throw new TableStateError("Table is missing a 'toggle-virtual' button.", table);
}

if (toggleDisconnectedButton === null) {
throw new TableStateError("Table is missing a 'toggle-disconnected' button.", table);
}

// Attach event listeners to the buttons elements.
toggleEnabledButton.addEventListener('click', event => this.handleClick(event, this));
toggleDisabledButton.addEventListener('click', event => this.handleClick(event, this));
toggleVirtualButton.addEventListener('click', event => this.handleClick(event, this));
toggleDisconnectedButton.addEventListener('click', event => this.handleClick(event, this));

// Instantiate ButtonState for each button for state management.
this.enabledButton = new ButtonState(
Expand All @@ -200,6 +207,10 @@ class TableState {
toggleVirtualButton,
table.querySelectorAll<HTMLTableRowElement>('tr[data-type="virtual"]'),
);
this.disconnectedButton = new ButtonState(
toggleDisconnectedButton,
table.querySelectorAll<HTMLTableRowElement>('tr[data-connected="disconnected"]'),
);
} catch (err) {
if (err instanceof TableStateError) {
// This class is useless for tables that don't have toggle buttons.
Expand All @@ -211,52 +222,6 @@ class TableState {
}
}

/**
* Get the table caption's text.
*/
private get captionText(): string {
if (this.caption !== null) {
return this.caption.innerText;
}
return '';
}

/**
* Set the table caption's text.
*/
private set captionText(value: string) {
if (this.caption !== null) {
this.caption.innerText = value;
}
}

/**
* Update the table caption's text based on the state of each toggle button.
*/
private toggleCaption(): void {
const showEnabled = this.enabledButton.buttonState === 'show';
const showDisabled = this.disabledButton.buttonState === 'show';
const showVirtual = this.virtualButton.buttonState === 'show';

if (showEnabled && !showDisabled && !showVirtual) {
this.captionText = 'Showing Enabled Interfaces';
} else if (showEnabled && showDisabled && !showVirtual) {
this.captionText = 'Showing Enabled & Disabled Interfaces';
} else if (!showEnabled && showDisabled && !showVirtual) {
this.captionText = 'Showing Disabled Interfaces';
} else if (!showEnabled && !showDisabled && !showVirtual) {
this.captionText = 'Hiding Enabled, Disabled & Virtual Interfaces';
} else if (!showEnabled && !showDisabled && showVirtual) {
this.captionText = 'Showing Virtual Interfaces';
} else if (showEnabled && !showDisabled && showVirtual) {
this.captionText = 'Showing Enabled & Virtual Interfaces';
} else if (showEnabled && showDisabled && showVirtual) {
this.captionText = 'Showing Enabled, Disabled & Virtual Interfaces';
} else {
this.captionText = '';
}
}

/**
* When toggle buttons are clicked, reapply visability all rows and
* pass the event to all button handlers
Expand All @@ -272,7 +237,7 @@ class TableState {
instance.enabledButton.handleClick(event);
instance.disabledButton.handleClick(event);
instance.virtualButton.handleClick(event);
instance.toggleCaption();
instance.disconnectedButton.handleClick(event);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
<button type="button" class="dropdown-item toggle-enabled" data-state="show">{% trans "Hide Enabled" %}</button>
<button type="button" class="dropdown-item toggle-disabled" data-state="show">{% trans "Hide Disabled" %}</button>
<button type="button" class="dropdown-item toggle-virtual" data-state="show">{% trans "Hide Virtual" %}</button>
<button type="button" class="dropdown-item toggle-disconnected" data-state="show">{% trans "Hide Disconnected" %}</button>
</ul>
{% endblock extra_table_controls %}
8 changes: 7 additions & 1 deletion netbox/templates/extras/report.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@
{% if perms.extras.run_report %}
<div class="row">
<div class="col">
{% if not report.is_valid %}
<div class="alert alert-warning">
<i class="mdi mdi-alert"></i>
{% trans "This report is invalid and cannot be run." %}
</div>
{% endif %}
<form action="{% url 'extras:report' module=report.module name=report.class_name %}" method="post" class="form-object-edit">
{% csrf_token %}
{% render_form form %}
<div class="float-end">
<button type="submit" name="_run" class="btn btn-primary">
<button type="submit" name="_run" class="btn btn-primary"{% if not report.is_valid %} disabled{% endif %}>
{% if report.result %}
<i class="mdi mdi-replay"></i> {% trans "Run Again" %}
{% else %}
Expand Down
12 changes: 10 additions & 2 deletions netbox/templates/extras/report_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,18 @@ <h5 class="card-header" id="module{{ module.pk }}">
</td>
{% else %}
<td class="text-muted">{% trans "Never" %}</td>
<td>{{ ''|placeholder }}</td>
<td>
{% if report.is_valid %}
{{ ''|placeholder }}
{% else %}
<span class="badge bg-danger" title="{% trans "Report has no test methods" %}">
{% trans "Invalid" %}
</span>
{% endif %}
</td>
{% endif %}
<td>
{% if perms.extras.run_report %}
{% if perms.extras.run_report and report.is_valid %}
<div class="float-end noprint">
<form action="{% url 'extras:report' module=report.module name=report.class_name %}" method="post">
{% csrf_token %}
Expand Down

0 comments on commit b500cec

Please sign in to comment.