From a6d937b366931670d8cc19b5dd6564f2efee3d85 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 10 May 2021 11:23:21 -0400 Subject: [PATCH 1/6] Closes #5806: Add kilometer and mile as choices for cable length unit --- docs/release-notes/version-2.12.md | 4 ++++ netbox/dcim/choices.py | 7 +++++++ netbox/utilities/utils.py | 6 +++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.12.md b/docs/release-notes/version-2.12.md index 598237e76fb..c6992358308 100644 --- a/docs/release-notes/version-2.12.md +++ b/docs/release-notes/version-2.12.md @@ -2,6 +2,10 @@ ## v2.12-beta1 (FUTURE) +### Enhancements + +* [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit + ### Other Changes * [#5532](https://github.com/netbox-community/netbox/issues/5532) - Drop support for Python 3.6 diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index c5646cf2b3d..be35a24c549 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1064,14 +1064,21 @@ class CableStatusChoices(ChoiceSet): class CableLengthUnitChoices(ChoiceSet): + # Metric + UNIT_KILOMETER = 'km' UNIT_METER = 'm' UNIT_CENTIMETER = 'cm' + + # Imperial + UNIT_MILE = 'mi' UNIT_FOOT = 'ft' UNIT_INCH = 'in' CHOICES = ( + (UNIT_KILOMETER, 'Kilometers'), (UNIT_METER, 'Meters'), (UNIT_CENTIMETER, 'Centimeters'), + (UNIT_MILE, 'Miles'), (UNIT_FOOT, 'Feet'), (UNIT_INCH, 'Inches'), ) diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index ce6753877e9..a14a468b635 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -198,15 +198,19 @@ def to_meters(length, unit): "Unknown unit {}. Must be one of the following: {}".format(unit, ', '.join(valid_units)) ) + if unit == CableLengthUnitChoices.UNIT_KILOMETER: + return length * 1000 if unit == CableLengthUnitChoices.UNIT_METER: return length if unit == CableLengthUnitChoices.UNIT_CENTIMETER: return length / 100 + if unit == CableLengthUnitChoices.UNIT_MILE: + return length * 1609.344 if unit == CableLengthUnitChoices.UNIT_FOOT: return length * 0.3048 if unit == CableLengthUnitChoices.UNIT_INCH: return length * 0.3048 * 12 - raise ValueError("Unknown unit {}. Must be 'm', 'cm', 'ft', or 'in'.".format(unit)) + raise ValueError(f"Unknown unit {unit}. Must be 'km', 'm', 'cm', 'mi', 'ft', or 'in'.") def render_jinja2(template_code, context): From f3abf84e1732ea6f748f921c720afb9538903fc7 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 10 May 2021 14:14:54 -0400 Subject: [PATCH 2/6] Closes #6154: Allow decimal values for cable lengths --- docs/release-notes/version-2.12.md | 3 +++ netbox/dcim/migrations/0132_cable_length.py | 16 ++++++++++++++++ netbox/dcim/models/cables.py | 4 +++- netbox/templates/dcim/cable.html | 2 +- netbox/templates/dcim/trace/cable.html | 2 +- 5 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 netbox/dcim/migrations/0132_cable_length.py diff --git a/docs/release-notes/version-2.12.md b/docs/release-notes/version-2.12.md index c6992358308..c4402d689e3 100644 --- a/docs/release-notes/version-2.12.md +++ b/docs/release-notes/version-2.12.md @@ -5,6 +5,7 @@ ### Enhancements * [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit +* [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths ### Other Changes @@ -13,6 +14,8 @@ ### REST API Changes +* dcim.Cable + * `length` is now a decimal value * dcim.Device * Removed the `display_name` attribute (use `display` instead) * dcim.DeviceType diff --git a/netbox/dcim/migrations/0132_cable_length.py b/netbox/dcim/migrations/0132_cable_length.py new file mode 100644 index 00000000000..e20a8b8aaa2 --- /dev/null +++ b/netbox/dcim/migrations/0132_cable_length.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0131_consoleport_speed'), + ] + + operations = [ + migrations.AlterField( + model_name='cable', + name='length', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), + ), + ] diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 28d21ff68fc..2b8f052061f 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -74,7 +74,9 @@ class Cable(PrimaryModel): color = ColorField( blank=True ) - length = models.PositiveSmallIntegerField( + length = models.DecimalField( + max_digits=8, + decimal_places=2, blank=True, null=True ) diff --git a/netbox/templates/dcim/cable.html b/netbox/templates/dcim/cable.html index 61524d345e4..af55cd70b7f 100644 --- a/netbox/templates/dcim/cable.html +++ b/netbox/templates/dcim/cable.html @@ -60,7 +60,7 @@ Length {% if object.length %} - {{ object.length }} {{ object.get_length_unit_display }} + {{ object.length|floatformat }} {{ object.get_length_unit_display }} {% else %} {% endif %} diff --git a/netbox/templates/dcim/trace/cable.html b/netbox/templates/dcim/trace/cable.html index 43b4910f476..5f8fb01eb8d 100644 --- a/netbox/templates/dcim/trace/cable.html +++ b/netbox/templates/dcim/trace/cable.html @@ -10,7 +10,7 @@ {{ cable.get_type_display|default:"" }} {% endif %} {% if cable.length %} - ({{ cable.length }} {{ cable.get_length_unit_display }})
+ ({{ cable.length|floatformat }} {{ cable.get_length_unit_display }})
{% endif %} {{ cable.get_status_display }}
{% for tag in cable.tags.all %} From f6823b57045269117c510ada02cc390ef1184d2b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 10 May 2021 14:22:01 -0400 Subject: [PATCH 3/6] Closes #6338: Decimal fields are no longer coerced to strings in REST API --- docs/release-notes/version-2.12.md | 5 +++++ netbox/netbox/settings.py | 1 + 2 files changed, 6 insertions(+) diff --git a/docs/release-notes/version-2.12.md b/docs/release-notes/version-2.12.md index c4402d689e3..3adff772827 100644 --- a/docs/release-notes/version-2.12.md +++ b/docs/release-notes/version-2.12.md @@ -11,6 +11,7 @@ * [#5532](https://github.com/netbox-community/netbox/issues/5532) - Drop support for Python 3.6 * [#5994](https://github.com/netbox-community/netbox/issues/5994) - Drop support for `display_field` argument on ObjectVar +* [#6338](https://github.com/netbox-community/netbox/issues/6338) - Decimal fields are no longer coerced to strings in REST API ### REST API Changes @@ -22,9 +23,13 @@ * Removed the `display_name` attribute (use `display` instead) * dcim.Rack * Removed the `display_name` attribute (use `display` instead) +* dcim.Site + * `latitude` and `longitude` are now decimal fields rather than strings * extras.ContentType * Removed the `display_name` attribute (use `display` instead) * ipam.VLAN * Removed the `display_name` attribute (use `display` instead) * ipam.VRF * Removed the `display_name` attribute (use `display` instead) +* virtualization.VirtualMachine + * `vcpus` is now a decimal field rather than a string diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 71edae57338..0d79bca256a 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -464,6 +464,7 @@ FILTERS_NULL_CHOICE_VALUE = 'null' REST_FRAMEWORK_VERSION = VERSION.rsplit('.', 1)[0] # Use major.minor as API version REST_FRAMEWORK = { 'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION], + 'COERCE_DECIMAL_TO_STRING': False, 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', 'netbox.api.authentication.TokenAuthentication', From bf56145a093063d3c634601e0b024f14f7ad5e9b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 11 May 2021 14:03:50 -0400 Subject: [PATCH 4/6] Closes #4609: Allow marking prefixes as fully utilized --- docs/release-notes/version-2.12.md | 3 +++ netbox/ipam/api/serializers.py | 2 +- netbox/ipam/filtersets.py | 2 +- netbox/ipam/forms.py | 20 +++++++++++++++---- .../migrations/0047_prefix_mark_utilized.py | 16 +++++++++++++++ netbox/ipam/models/ip.py | 16 +++++++++++++-- netbox/ipam/tables.py | 8 ++++++-- netbox/ipam/tests/test_filtersets.py | 10 ++++++++-- netbox/templates/ipam/prefix.html | 15 +++++++++----- 9 files changed, 75 insertions(+), 17 deletions(-) create mode 100644 netbox/ipam/migrations/0047_prefix_mark_utilized.py diff --git a/docs/release-notes/version-2.12.md b/docs/release-notes/version-2.12.md index 3adff772827..1d884de72a5 100644 --- a/docs/release-notes/version-2.12.md +++ b/docs/release-notes/version-2.12.md @@ -4,6 +4,7 @@ ### Enhancements +* [#4609](https://github.com/netbox-community/netbox/issues/4609) - Allow marking prefixes as fully utilized * [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit * [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths @@ -27,6 +28,8 @@ * `latitude` and `longitude` are now decimal fields rather than strings * extras.ContentType * Removed the `display_name` attribute (use `display` instead) +* ipam.Prefix + * Added the `mark_utilized` boolean field * ipam.VLAN * Removed the `display_name` attribute (use `display` instead) * ipam.VRF diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 324c4de03f1..203cdc3fbbe 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -202,7 +202,7 @@ class PrefixSerializer(PrimaryModelSerializer): model = Prefix fields = [ 'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', - 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'mark_utilized', 'description', 'tags', 'custom_fields', 'created', 'last_updated', ] read_only_fields = ['family'] diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 5ab4994ea6f..63165d8d24c 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -304,7 +304,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet): class Meta: model = Prefix - fields = ['id', 'is_pool'] + fields = ['id', 'is_pool', 'mark_utilized'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 6a375385902..eed92dffdae 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -454,11 +454,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): class Meta: model = Prefix fields = [ - 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant', - 'tags', + 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', + 'tenant_group', 'tenant', 'tags', ] fieldsets = ( - ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'description', 'tags')), + ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')), ('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')), ('Tenancy', ('tenant_group', 'tenant')), ) @@ -582,6 +582,11 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF widget=BulkEditNullBooleanSelect(), label='Is a pool' ) + mark_utilized = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect(), + label='Treat as 100% utilized' + ) description = forms.CharField( max_length=100, required=False @@ -597,7 +602,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) model = Prefix field_order = [ 'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'region_id', - 'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', 'is_pool', + 'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', 'is_pool', 'mark_utilized', ] mask_length__lte = forms.IntegerField( widget=forms.HiddenInput() @@ -675,6 +680,13 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + mark_utilized = forms.NullBooleanField( + required=False, + label=_('Marked as 100% utilized'), + widget=StaticSelect2( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) tag = TagFilterField(model) diff --git a/netbox/ipam/migrations/0047_prefix_mark_utilized.py b/netbox/ipam/migrations/0047_prefix_mark_utilized.py new file mode 100644 index 00000000000..332066b0433 --- /dev/null +++ b/netbox/ipam/migrations/0047_prefix_mark_utilized.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0046_set_vlangroup_scope_types'), + ] + + operations = [ + migrations.AddField( + model_name='prefix', + name='mark_utilized', + field=models.BooleanField(default=False), + ), + ] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 2490a0c5ae9..cf469c930fe 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -288,6 +288,10 @@ class Prefix(PrimaryModel): default=False, help_text='All IP addresses within this prefix are considered usable' ) + mark_utilized = models.BooleanField( + default=False, + help_text="Treat as 100% utilized" + ) description = models.CharField( max_length=200, blank=True @@ -296,10 +300,11 @@ class Prefix(PrimaryModel): objects = PrefixQuerySet.as_manager() csv_headers = [ - 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'description', + 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', + 'description', ] clone_fields = [ - 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', + 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', ] class Meta: @@ -364,6 +369,7 @@ class Prefix(PrimaryModel): self.get_status_display(), self.role.name if self.role else None, self.is_pool, + self.mark_utilized, self.description, ) @@ -422,6 +428,9 @@ class Prefix(PrimaryModel): """ Return all available IPs within this prefix as an IPSet. """ + if self.mark_utilized: + return list() + prefix = netaddr.IPSet(self.prefix) child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()]) available_ips = prefix - child_ips @@ -461,6 +470,9 @@ class Prefix(PrimaryModel): Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of "container", calculate utilization based on child prefixes. For all others, count child IP addresses. """ + if self.mark_utilized: + return 100 + if self.status == PrefixStatusChoices.STATUS_CONTAINER: queryset = Prefix.objects.filter( prefix__net_contained=str(self.prefix), diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 0bbaddb5297..aa4119fa74b 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -283,11 +283,15 @@ class PrefixTable(BaseTable): is_pool = BooleanColumn( verbose_name='Pool' ) + mark_utilized = BooleanColumn( + verbose_name='Marked Utilized' + ) class Meta(BaseTable.Meta): model = Prefix fields = ( - 'pk', 'prefix', 'status', 'children', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'description', + 'pk', 'prefix', 'status', 'children', 'vrf', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'mark_utilized', + 'description', ) default_columns = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description') row_attrs = { @@ -308,7 +312,7 @@ class PrefixDetailTable(PrefixTable): class Meta(PrefixTable.Meta): fields = ( 'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool', - 'description', 'tags', + 'mark_utilized', 'description', 'tags', ) default_columns = ( 'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description', diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index f43a44c6289..e668215ad49 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -389,11 +389,11 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests): Tenant.objects.bulk_create(tenants) prefixes = ( - Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True), + Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True), Prefix(prefix='10.0.1.0/24', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), Prefix(prefix='10.0.2.0/24', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), Prefix(prefix='10.0.3.0/24', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), - Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True), + Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True), Prefix(prefix='2001:db8:0:1::/64', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), Prefix(prefix='2001:db8:0:2::/64', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), Prefix(prefix='2001:db8:0:3::/64', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), @@ -417,6 +417,12 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'is_pool': 'false'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + def test_mark_utilized(self): + params = {'mark_utilized': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'mark_utilized': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + def test_within(self): params = {'within': '10.0.0.0/16'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index bf003931565..3b69f1423e9 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -7,10 +7,10 @@
- Prefix + Prefix
- +
- @@ -101,9 +100,15 @@ - + -
{{ object.get_status_display }} @@ -20,7 +20,6 @@ Not a Pool {% endif %}
Family
Utilization{% utilization_graph object.get_utilization %} + {% if object.marked_utilized %} + {% utilization_graph 100 %} + {% else %} + {% utilization_graph object.get_utilization %} + {% endif %} +
+
{% include 'inc/custom_fields_panel.html' %} From ecf51406c57d27b8bf1ebc290953fb0bb025d47f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 11 May 2021 14:52:27 -0400 Subject: [PATCH 5/6] Permit disabling utilization graph warning/danger thresholds --- netbox/ipam/tables.py | 17 ++++++- netbox/templates/ipam/prefix.html | 5 +- .../templatetags/utilization_graph.html | 50 +++++-------------- netbox/utilities/templatetags/helpers.py | 11 +++- 4 files changed, 41 insertions(+), 42 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index aa4119fa74b..e0f63a5cabc 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -256,6 +256,21 @@ class RoleTable(BaseTable): # Prefixes # +class PrefixUtilizationColumn(UtilizationColumn): + """ + Extend UtilizationColumn to allow disabling the warning & danger thresholds for prefixes + marked as fully utilized. + """ + template_code = """ + {% load helpers %} + {% if record.pk and record.mark_utilized %} + {% utilization_graph value warning_threshold=0 danger_threshold=0 %} + {% elif record.pk %} + {% utilization_graph value %} + {% endif %} + """ + + class PrefixTable(BaseTable): pk = ToggleColumn() prefix = tables.TemplateColumn( @@ -300,7 +315,7 @@ class PrefixTable(BaseTable): class PrefixDetailTable(PrefixTable): - utilization = UtilizationColumn( + utilization = PrefixUtilizationColumn( accessor='get_utilization', orderable=False ) diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 3b69f1423e9..36675aa138e 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -101,8 +101,9 @@ Utilization - {% if object.marked_utilized %} - {% utilization_graph 100 %} + {% if object.mark_utilized %} + {% utilization_graph 100 warning_threshold=0 danger_threshold=0 %} + (Marked fully utilized) {% else %} {% utilization_graph object.get_utilization %} {% endif %} diff --git a/netbox/templates/utilities/templatetags/utilization_graph.html b/netbox/templates/utilities/templatetags/utilization_graph.html index c4a33911f9a..7f722c50e23 100644 --- a/netbox/templates/utilities/templatetags/utilization_graph.html +++ b/netbox/templates/utilities/templatetags/utilization_graph.html @@ -1,42 +1,18 @@ {% if utilization == 0 %} -
+
{{ utilization }}% -
+
{% else %} -
- {% if utilization >= danger_threshold %} -
+
- {{ utilization }}% + {{ utilization }}%
- {% elif utilization >= warning_threshold %} -
- {{ utilization }}% -
- {% else %} -
- {{ utilization }}% -
- {% endif %} -
-{% endif %} \ No newline at end of file +
+{% endif %} diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 6abbec619ae..78189ec495b 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -276,10 +276,17 @@ def utilization_graph(utilization, warning_threshold=75, danger_threshold=90): """ Display a horizontal bar graph indicating a percentage of utilization. """ + if danger_threshold and utilization >= danger_threshold: + bar_class = 'bg-danger' + elif warning_threshold and utilization >= warning_threshold: + bar_class = 'bg-warning' + elif warning_threshold or danger_threshold: + bar_class = 'bg-success' + else: + bar_class = 'bg-default' return { 'utilization': utilization, - 'warning_threshold': warning_threshold, - 'danger_threshold': danger_threshold, + 'bar_class': bar_class, } From 43a823d0ac42a1f3cf35c3631e1c0685e1471e9a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 11 May 2021 15:51:36 -0400 Subject: [PATCH 6/6] Closes #3665: Enable rendering export templates via REST API --- docs/additional-features/export-templates.md | 10 ++++++++++ docs/release-notes/version-2.12.md | 1 + netbox/extras/models/models.py | 9 ++++----- netbox/netbox/api/views.py | 15 +++++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/additional-features/export-templates.md b/docs/additional-features/export-templates.md index b3f585beeec..c80d5b8a1e2 100644 --- a/docs/additional-features/export-templates.md +++ b/docs/additional-features/export-templates.md @@ -33,6 +33,16 @@ The `as_attachment` attribute of an export template controls its behavior when r A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`. +## REST API Integration + +When it is necessary to provide authentication credentials (such as when [`LOGIN_REQUIRED`](../configuration/optional-settings.md#login_required) has been enabled), it is recommended to render export templates via the REST API. This allows the client to specify an authentication token. To render an export template via the REST API, make a `GET` request to the model's list endpoint and append the `export` parameter specifying the export template name. For example: + +``` +GET /api/dcim/sites/?export=MyTemplateName +``` + +Note that the body of the response will contain only the rendered export template content, as opposed to a JSON object or list. + ## Example Here's an example device export template that will generate a simple Nagios configuration from a list of devices. diff --git a/docs/release-notes/version-2.12.md b/docs/release-notes/version-2.12.md index 1d884de72a5..573cd68fe8e 100644 --- a/docs/release-notes/version-2.12.md +++ b/docs/release-notes/version-2.12.md @@ -4,6 +4,7 @@ ### Enhancements +* [#3665](https://github.com/netbox-community/netbox/issues/3665) - Enable rendering export templates via REST API * [#4609](https://github.com/netbox-community/netbox/issues/4609) - Allow marking prefixes as fully utilized * [#5806](https://github.com/netbox-community/netbox/issues/5806) - Add kilometer and mile as choices for cable length unit * [#6154](https://github.com/netbox-community/netbox/issues/6154) - Allow decimal values for cable lengths diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index c2cebe1631c..ab9cbe9f39e 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -300,13 +300,12 @@ class ExportTemplate(BigIDModel): # Build the response response = HttpResponse(output, content_type=mime_type) - filename = 'netbox_{}{}'.format( - queryset.model._meta.verbose_name_plural, - '.{}'.format(self.file_extension) if self.file_extension else '' - ) if self.as_attachment: - response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) + basename = queryset.model._meta.verbose_name_plural.replace(' ', '_') + extension = f'.{self.file_extension}' if self.file_extension else '' + filename = f'netbox_{basename}{extension}' + response['Content-Disposition'] = f'attachment; filename="{filename}"' return response diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py index 585b75686ee..56566dcd7d3 100644 --- a/netbox/netbox/api/views.py +++ b/netbox/netbox/api/views.py @@ -5,9 +5,11 @@ from collections import OrderedDict from django import __version__ as DJANGO_VERSION from django.apps import apps from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django.db.models import ProtectedError +from django.shortcuts import get_object_or_404 from django_rq.queues import get_connection from rest_framework import status from rest_framework.response import Response @@ -16,6 +18,7 @@ from rest_framework.views import APIView from rest_framework.viewsets import ModelViewSet as ModelViewSet_ from rq.worker import Worker +from extras.models import ExportTemplate from netbox.api import BulkOperationSerializer from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired from netbox.api.exceptions import SerializerNotFound @@ -222,6 +225,18 @@ class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_): # Check that the instance is matched by the view's queryset self.queryset.get(pk=instance.pk) + def list(self, request, *args, **kwargs): + """ + Overrides ListModelMixin to allow processing ExportTemplates. + """ + if 'export' in request.GET: + content_type = ContentType.objects.get_for_model(self.serializer_class.Meta.model) + et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export']) + queryset = self.filter_queryset(self.get_queryset()) + return et.render_to_response(queryset) + + return super().list(request, *args, **kwargs) + def perform_create(self, serializer): model = self.queryset.model logger = logging.getLogger('netbox.api.views.ModelViewSet')