From 98ff00bc62ea2ee1cdcaa7273cf961e9425c724c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 29 Jun 2021 13:44:46 -0400 Subject: [PATCH 01/15] Fixes #6676: Fix device/VM counts per cluster under cluster type/group views --- docs/release-notes/version-2.11.md | 1 + netbox/virtualization/views.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index e1840319e..83987ec93 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -12,6 +12,7 @@ * [#6637](https://github.com/netbox-community/netbox/issues/6637) - Fix group assignment in "available VLANs" link under VLAN group view * [#6640](https://github.com/netbox-community/netbox/issues/6640) - Disallow numeric values in custom text fields * [#6652](https://github.com/netbox-community/netbox/issues/6652) - Fix exception when adding components in bulk to multiple devices +* [#6676](https://github.com/netbox-community/netbox/issues/6676) - Fix device/VM counts per cluster under cluster type/group views --- diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 421278d6e..fe0d5322f 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -34,6 +34,9 @@ class ClusterTypeView(generic.ObjectView): def get_extra_context(self, request, instance): clusters = Cluster.objects.restrict(request.user, 'view').filter( type=instance + ).annotate( + device_count=count_related(Device, 'cluster'), + vm_count=count_related(VirtualMachine, 'cluster') ) clusters_table = tables.ClusterTable(clusters) @@ -93,6 +96,9 @@ class ClusterGroupView(generic.ObjectView): def get_extra_context(self, request, instance): clusters = Cluster.objects.restrict(request.user, 'view').filter( group=instance + ).annotate( + device_count=count_related(Device, 'cluster'), + vm_count=count_related(VirtualMachine, 'cluster') ) clusters_table = tables.ClusterTable(clusters) From 18934bcc691fb5e1df28c20a26c5f83fd604dcc9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 29 Jun 2021 13:47:44 -0400 Subject: [PATCH 02/15] Closes #6666: Show management-only status under interface detail view --- docs/release-notes/version-2.11.md | 1 + netbox/templates/dcim/interface.html | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 83987ec93..3c99434a9 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -5,6 +5,7 @@ ### Enhancements * [#6620](https://github.com/netbox-community/netbox/issues/6620) - Show assigned VMs count under device role view +* [#6666](https://github.com/netbox-community/netbox/issues/6666) - Show management-only status under interface detail view ### Bug Fixes diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 9f85196fe..d8069da43 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -54,6 +54,16 @@ {% endif %} + + Management Only + + {% if object.mgmt_only %} + + {% else %} + + {% endif %} + + Parent @@ -88,7 +98,7 @@ 802.1Q Mode - {{ object.get_mode_display }} + {{ object.get_mode_display|placeholder }} From 18a9e39be60c29e8d19be275b86c4f9b189c1207 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 29 Jun 2021 14:00:16 -0400 Subject: [PATCH 03/15] Closes #6667: Display VM memory as GB/TB as appropriate --- docs/release-notes/version-2.11.md | 1 + .../templates/virtualization/virtualmachine.html | 2 +- netbox/utilities/templatetags/helpers.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 3c99434a9..296cf085c 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -6,6 +6,7 @@ * [#6620](https://github.com/netbox-community/netbox/issues/6620) - Show assigned VMs count under device role view * [#6666](https://github.com/netbox-community/netbox/issues/6666) - Show management-only status under interface detail view +* [#6667](https://github.com/netbox-community/netbox/issues/6667) - Display VM memory as GB/TB as appropriate ### Bug Fixes diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 03512d02f..a5ce03aab 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -138,7 +138,7 @@ Memory {% if object.memory %} - {{ object.memory }} MB + {{ object.memory|humanize_megabytes }} {% else %} {% endif %} diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 01dce8479..49d8323e9 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -129,6 +129,20 @@ def humanize_speed(speed): return '{} Kbps'.format(speed) +@register.filter() +def humanize_megabytes(mb): + """ + Express a number of megabytes in the most suitable unit (e.g. gigabytes or terabytes). + """ + if not mb: + return '' + if mb >= 1048576: + return f'{int(mb / 1048576)} TB' + if mb >= 1024: + return f'{int(mb / 1024)} GB' + return f'{mb} MB' + + @register.filter() def tzoffset(value): """ From add95292cef42eea640a8e137fbd56cb9096a634 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 1 Jul 2021 10:48:24 -0400 Subject: [PATCH 04/15] Fixes #6680: Allow setting custom field values for VM interfaces on intial creation --- docs/release-notes/version-2.11.md | 1 + .../templates/dcim/device_component_add.html | 24 ++++++++++-- .../virtualmachine_component_add.html | 38 ------------------- netbox/virtualization/forms.py | 10 +++-- netbox/virtualization/views.py | 2 +- 5 files changed, 28 insertions(+), 47 deletions(-) delete mode 100644 netbox/templates/virtualization/virtualmachine_component_add.html diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 296cf085c..6516bcd29 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -15,6 +15,7 @@ * [#6640](https://github.com/netbox-community/netbox/issues/6640) - Disallow numeric values in custom text fields * [#6652](https://github.com/netbox-community/netbox/issues/6652) - Fix exception when adding components in bulk to multiple devices * [#6676](https://github.com/netbox-community/netbox/issues/6676) - Fix device/VM counts per cluster under cluster type/group views +* [#6680](https://github.com/netbox-community/netbox/issues/6680) - Allow setting custom field values for VM interfaces on intial creation --- diff --git a/netbox/templates/dcim/device_component_add.html b/netbox/templates/dcim/device_component_add.html index 0b7200c1f..0937cc371 100644 --- a/netbox/templates/dcim/device_component_add.html +++ b/netbox/templates/dcim/device_component_add.html @@ -1,4 +1,5 @@ {% extends 'base.html' %} +{% load helpers %} {% load form_helpers %} {% block title %}Create {{ component_type }}{% endblock %} @@ -18,19 +19,34 @@ {% endif %}
- {{ component_type|title }} + {{ component_type|bettertitle }}
- {% render_form form %} + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + {% for field in form.visible_fields %} + {% if field.name not in form.custom_fields %} + {% render_field field %} + {% endif %} + {% endfor %}
-
+ {% if form.custom_fields %} +
+
Custom Fields
+
+ {% render_custom_fields form %} +
+
+ {% endif %} +
Cancel
-
+
diff --git a/netbox/templates/virtualization/virtualmachine_component_add.html b/netbox/templates/virtualization/virtualmachine_component_add.html deleted file mode 100644 index 11b120ee0..000000000 --- a/netbox/templates/virtualization/virtualmachine_component_add.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends 'base.html' %} -{% load helpers %} -{% load form_helpers %} - -{% block title %}Create {{ component_type }}{% endblock %} - -{% block content %} -
- {% csrf_token %} -
-
- {% if form.non_field_errors %} -
-
Errors
-
- {{ form.non_field_errors }} -
-
- {% endif %} -
-
- {{ component_type|bettertitle }} -
-
- {% render_form form %} -
-
-
-
- - - Cancel -
-
-
-
-
-{% endblock %} diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index f6bbd5b0d..f7b241c1a 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -8,7 +8,8 @@ from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN from dcim.forms import InterfaceCommonForm, INTERFACE_MODE_HELP_TEXT from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup from extras.forms import ( - AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm, + AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, + CustomFieldFilterForm, ) from extras.models import Tag from ipam.models import IPAddress, VLAN @@ -659,7 +660,8 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm) self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) -class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm): +class VMInterfaceCreateForm(BootstrapMixin, CustomFieldForm, InterfaceCommonForm): + model = VMInterface virtual_machine = DynamicModelChoiceField( queryset=VirtualMachine.objects.all() ) @@ -723,7 +725,7 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm): self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id) -class VMInterfaceCSVForm(CSVModelForm): +class VMInterfaceCSVForm(CustomFieldModelCSVForm): virtual_machine = CSVModelChoiceField( queryset=VirtualMachine.objects.all(), to_field_name='name' @@ -746,7 +748,7 @@ class VMInterfaceCSVForm(CSVModelForm): return self.cleaned_data['enabled'] -class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=VMInterface.objects.all(), widget=forms.MultipleHiddenInput() diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index fe0d5322f..4fdca3078 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -461,7 +461,7 @@ class VMInterfaceCreateView(generic.ComponentCreateView): queryset = VMInterface.objects.all() form = forms.VMInterfaceCreateForm model_form = forms.VMInterfaceForm - template_name = 'virtualization/virtualmachine_component_add.html' + template_name = 'dcim/device_component_add.html' class VMInterfaceEditView(generic.ObjectEditView): From 76a61195845b7d57c213f9376178d4df4196cbcc Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 1 Jul 2021 15:17:46 -0400 Subject: [PATCH 05/15] Closes #6138: Add an 'empty' filter modifier for character fields --- docs/release-notes/version-2.11.md | 1 + docs/rest-api/filtering.md | 33 +++++++++++++++++------------- netbox/extras/apps.py | 1 + netbox/extras/lookups.py | 17 +++++++++++++++ netbox/netbox/filtersets.py | 13 +++++------- netbox/utilities/constants.py | 3 ++- 6 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 netbox/extras/lookups.py diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 6516bcd29..5c09cbe96 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -4,6 +4,7 @@ ### Enhancements +* [#6138](https://github.com/netbox-community/netbox/issues/6138) - Add an `empty` filter modifier for character fields * [#6620](https://github.com/netbox-community/netbox/issues/6620) - Show assigned VMs count under device role view * [#6666](https://github.com/netbox-community/netbox/issues/6666) - Show management-only status under interface detail view * [#6667](https://github.com/netbox-community/netbox/issues/6667) - Display VM memory as GB/TB as appropriate diff --git a/docs/rest-api/filtering.md b/docs/rest-api/filtering.md index b77513297..471beffee 100644 --- a/docs/rest-api/filtering.md +++ b/docs/rest-api/filtering.md @@ -61,25 +61,30 @@ These lookup expressions can be applied by adding a suffix to the desired field' Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions: -- `n` - not equal to (negation) -- `lt` - less than -- `lte` - less than or equal -- `gt` - greater than -- `gte` - greater than or equal +| Filter | Description | +|--------|-------------| +| `n` | Not equal to | +| `lt` | Less than | +| `lte` | Less than or equal to | +| `gt` | Greater than | +| `gte` | Greater than or equal to | ### String Fields String based (char) fields (Name, Address, etc) support these lookup expressions: -- `n` - not equal to (negation) -- `ic` - case insensitive contains -- `nic` - negated case insensitive contains -- `isw` - case insensitive starts with -- `nisw` - negated case insensitive starts with -- `iew` - case insensitive ends with -- `niew` - negated case insensitive ends with -- `ie` - case insensitive exact match -- `nie` - negated case insensitive exact match +| Filter | Description | +|--------|-------------| +| `n` | Not equal to | +| `ic` | Contains (case-insensitive) | +| `nic` | Does not contain (case-insensitive) | +| `isw` | Starts with (case-insensitive) | +| `nisw` | Does not start with (case-insensitive) | +| `iew` | Ends with (case-insensitive) | +| `niew` | Does not end with (case-insensitive) | +| `ie` | Exact match (case-insensitive) | +| `nie` | Inverse exact match (case-insensitive) | +| `empty` | Is empty (boolean) | ### Foreign Keys & Other Fields diff --git a/netbox/extras/apps.py b/netbox/extras/apps.py index 3201c3bb2..7500157c0 100644 --- a/netbox/extras/apps.py +++ b/netbox/extras/apps.py @@ -5,4 +5,5 @@ class ExtrasConfig(AppConfig): name = "extras" def ready(self): + import extras.lookups import extras.signals diff --git a/netbox/extras/lookups.py b/netbox/extras/lookups.py new file mode 100644 index 000000000..7197efcfc --- /dev/null +++ b/netbox/extras/lookups.py @@ -0,0 +1,17 @@ +from django.db.models import CharField, Lookup + + +class Empty(Lookup): + """ + Filter on whether a string is empty. + """ + lookup_name = 'empty' + + def as_sql(self, qn, connection): + lhs, lhs_params = self.process_lhs(qn, connection) + rhs, rhs_params = self.process_rhs(qn, connection) + params = lhs_params + rhs_params + return 'CAST(LENGTH(%s) AS BOOLEAN) != %s' % (lhs, rhs), params + + +CharField.register_lookup(Empty) diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index aa9e15385..791c21d19 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -89,13 +89,13 @@ class BaseFilterSet(django_filters.FilterSet): filters.MultiValueNumberFilter, filters.MultiValueTimeFilter )): - lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP + return FILTER_NUMERIC_BASED_LOOKUP_MAP elif isinstance(existing_filter, ( filters.TreeNodeMultipleChoiceFilter, )): # TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression - lookup_map = FILTER_TREENODE_NEGATION_LOOKUP_MAP + return FILTER_TREENODE_NEGATION_LOOKUP_MAP elif isinstance(existing_filter, ( django_filters.ModelChoiceFilter, @@ -103,7 +103,7 @@ class BaseFilterSet(django_filters.FilterSet): TagFilter )) or existing_filter.extra.get('choices'): # These filter types support only negation - lookup_map = FILTER_NEGATION_LOOKUP_MAP + return FILTER_NEGATION_LOOKUP_MAP elif isinstance(existing_filter, ( django_filters.filters.CharFilter, @@ -111,12 +111,9 @@ class BaseFilterSet(django_filters.FilterSet): filters.MultiValueCharFilter, filters.MultiValueMACAddressFilter )): - lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP + return FILTER_CHAR_BASED_LOOKUP_MAP - else: - lookup_map = None - - return lookup_map + return None @classmethod def get_filters(cls): diff --git a/netbox/utilities/constants.py b/netbox/utilities/constants.py index 8cf047c42..c3fbd0687 100644 --- a/netbox/utilities/constants.py +++ b/netbox/utilities/constants.py @@ -11,7 +11,8 @@ FILTER_CHAR_BASED_LOOKUP_MAP = dict( isw='istartswith', nisw='istartswith', ie='iexact', - nie='iexact' + nie='iexact', + empty='empty', ) FILTER_NUMERIC_BASED_LOOKUP_MAP = dict( From 1be4a57bd4dc38873b59bca21781e92205c4e240 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 1 Jul 2021 15:33:39 -0400 Subject: [PATCH 06/15] Closes #6345: Introduce PermissionsViolation exception for use in generic views --- netbox/netbox/views/generic.py | 36 ++++++++++++++++++---------------- netbox/utilities/exceptions.py | 8 ++++++++ 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index 957979e4c..c43519e01 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -18,7 +18,7 @@ from django_tables2.export import TableExport from extras.models import CustomField, ExportTemplate from utilities.error_handlers import handle_protectederror -from utilities.exceptions import AbortTransaction +from utilities.exceptions import AbortTransaction, PermissionsViolation from utilities.forms import ( BootstrapMixin, BulkRenameForm, ConfirmationForm, CSVDataField, ImportForm, TableConfigForm, restrict_form_fields, ) @@ -290,7 +290,8 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): obj = form.save() # Check that the new object conforms with any assigned object-level permissions - self.queryset.get(pk=obj.pk) + if not self.queryset.filter(pk=obj.pk).first(): + raise PermissionsViolation() msg = '{} {}'.format( 'Created' if object_created else 'Modified', @@ -318,7 +319,7 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): else: return redirect(self.get_return_url(request, obj)) - except ObjectDoesNotExist: + except PermissionsViolation: msg = "Object save failed due to object-level permissions violation" logger.debug(msg) form.add_error(None, msg) @@ -480,7 +481,7 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): # Enforce object-level permissions if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs): - raise ObjectDoesNotExist + raise PermissionsViolation # If we make it to this point, validation has succeeded on all new objects. msg = "Added {} {}".format(len(new_objs), model._meta.verbose_name_plural) @@ -494,7 +495,7 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): except IntegrityError: pass - except ObjectDoesNotExist: + except PermissionsViolation: msg = "Object creation failed due to object-level permissions violation" logger.debug(msg) form.add_error(None, msg) @@ -565,7 +566,8 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): obj = model_form.save() # Enforce object-level permissions - self.queryset.get(pk=obj.pk) + if not self.queryset.filter(pk=obj.pk).first(): + raise PermissionsViolation() logger.debug(f"Created {obj} (PK: {obj.pk})") @@ -601,7 +603,7 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): except AbortTransaction: pass - except ObjectDoesNotExist: + except PermissionsViolation: msg = "Object creation failed due to object-level permissions violation" logger.debug(msg) form.add_error(None, msg) @@ -712,7 +714,7 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): # Enforce object-level permissions if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs): - raise ObjectDoesNotExist + raise PermissionsViolation # Compile a table containing the imported objects obj_table = self.table(new_objs) @@ -730,7 +732,7 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): except ValidationError: pass - except ObjectDoesNotExist: + except PermissionsViolation: msg = "Object import failed due to object-level permissions violation" logger.debug(msg) form.add_error(None, msg) @@ -845,7 +847,7 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): # Enforce object-level permissions if self.queryset.filter(pk__in=[obj.pk for obj in updated_objects]).count() != len(updated_objects): - raise ObjectDoesNotExist + raise PermissionsViolation if updated_objects: msg = 'Updated {} {}'.format(len(updated_objects), model._meta.verbose_name_plural) @@ -857,7 +859,7 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): except ValidationError as e: messages.error(self.request, "{} failed validation: {}".format(obj, e)) - except ObjectDoesNotExist: + except PermissionsViolation: msg = "Object update failed due to object-level permissions violation" logger.debug(msg) form.add_error(None, msg) @@ -952,7 +954,7 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): # Enforce constrained permissions if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects): - raise ObjectDoesNotExist + raise PermissionsViolation messages.success(request, "Renamed {} {}".format( len(selected_objects), @@ -960,7 +962,7 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): )) return redirect(self.get_return_url(request)) - except ObjectDoesNotExist: + except PermissionsViolation: msg = "Object update failed due to object-level permissions violation" logger.debug(msg) form.add_error(None, msg) @@ -1146,7 +1148,7 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View # Enforce object-level permissions if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs): - raise ObjectDoesNotExist + raise PermissionsViolation messages.success(request, "Added {} {}".format( len(new_components), self.queryset.model._meta.verbose_name_plural @@ -1156,7 +1158,7 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View else: return redirect(self.get_return_url(request)) - except ObjectDoesNotExist: + except PermissionsViolation: msg = "Component creation failed due to object-level permissions violation" logger.debug(msg) form.add_error(None, msg) @@ -1238,12 +1240,12 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, # Enforce object-level permissions if self.queryset.filter(pk__in=[obj.pk for obj in new_components]).count() != len(new_components): - raise ObjectDoesNotExist + raise PermissionsViolation except IntegrityError: pass - except ObjectDoesNotExist: + except PermissionsViolation: msg = "Component creation failed due to object-level permissions violation" logger.debug(msg) form.add_error(None, msg) diff --git a/netbox/utilities/exceptions.py b/netbox/utilities/exceptions.py index 77a915d9c..4ba62bc01 100644 --- a/netbox/utilities/exceptions.py +++ b/netbox/utilities/exceptions.py @@ -9,6 +9,14 @@ class AbortTransaction(Exception): pass +class PermissionsViolation(Exception): + """ + Raised when an operation was prevented because it would violate the + allowed permissions. + """ + pass + + class RQWorkerNotRunningException(APIException): """ Indicates the temporary inability to enqueue a new task (e.g. custom script execution) because no RQ worker From 631d991d8dc8cd8540bec6281583e8a4880f17a9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 1 Jul 2021 15:49:05 -0400 Subject: [PATCH 07/15] Closes #6368: Enable virtual chassis assignment during bulk import of devices --- docs/release-notes/version-2.11.md | 1 + netbox/dcim/forms.py | 15 +++++++++++++-- netbox/dcim/tests/test_views.py | 10 ++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 5c09cbe96..91a615966 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -5,6 +5,7 @@ ### Enhancements * [#6138](https://github.com/netbox-community/netbox/issues/6138) - Add an `empty` filter modifier for character fields +* [#6368](https://github.com/netbox-community/netbox/issues/6368) - Enable virtual chassis assignment during bulk import of devices * [#6620](https://github.com/netbox-community/netbox/issues/6620) - Show assigned VMs count under device role view * [#6666](https://github.com/netbox-community/netbox/issues/6666) - Show management-only status under interface detail view * [#6667](https://github.com/netbox-community/netbox/issues/6667) - Display VM memory as GB/TB as appropriate diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index eec828f13..dfe9fb066 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2236,6 +2236,12 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): choices=DeviceStatusChoices, help_text='Operational status' ) + virtual_chassis = CSVModelChoiceField( + queryset=VirtualChassis.objects.all(), + to_field_name='name', + required=False, + help_text='Virtual chassis' + ) cluster = CSVModelChoiceField( queryset=Cluster.objects.all(), to_field_name='name', @@ -2246,6 +2252,10 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm): class Meta: fields = [] model = Device + help_texts = { + 'vc_position': 'Virtual chassis position', + 'vc_priority': 'Virtual chassis priority', + } def __init__(self, data=None, *args, **kwargs): super().__init__(data, *args, **kwargs) @@ -2284,7 +2294,8 @@ class DeviceCSVForm(BaseDeviceCSVForm): class Meta(BaseDeviceCSVForm.Meta): fields = [ 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', - 'site', 'location', 'rack', 'position', 'face', 'cluster', 'comments', + 'site', 'location', 'rack', 'position', 'face', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', + 'comments', ] def __init__(self, data=None, *args, **kwargs): @@ -2319,7 +2330,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm): class Meta(BaseDeviceCSVForm.Meta): fields = [ 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', - 'parent', 'device_bay', 'cluster', 'comments', + 'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments', ] def __init__(self, data=None, *args, **kwargs): diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 5da1fcb5b..94cf2c9b3 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1023,6 +1023,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): tags = create_tags('Alpha', 'Bravo', 'Charlie') + VirtualChassis.objects.create(name='Virtual Chassis 1') + cls.form_data = { 'device_type': devicetypes[1].pk, 'device_role': deviceroles[1].pk, @@ -1048,10 +1050,10 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "device_role,manufacturer,device_type,status,name,site,location,rack,position,face", - "Device Role 1,Manufacturer 1,Device Type 1,active,Device 4,Site 1,Location 1,Rack 1,10,front", - "Device Role 1,Manufacturer 1,Device Type 1,active,Device 5,Site 1,Location 1,Rack 1,20,front", - "Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Location 1,Rack 1,30,front", + "device_role,manufacturer,device_type,status,name,site,location,rack,position,face,virtual_chassis,vc_position,vc_priority", + "Device Role 1,Manufacturer 1,Device Type 1,active,Device 4,Site 1,Location 1,Rack 1,10,front,Virtual Chassis 1,1,10", + "Device Role 1,Manufacturer 1,Device Type 1,active,Device 5,Site 1,Location 1,Rack 1,20,front,Virtual Chassis 1,2,20", + "Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Location 1,Rack 1,30,front,Virtual Chassis 1,3,30", ) cls.bulk_edit_data = { From 0ad9b83623547627201c748ebe6c482440f16022 Mon Sep 17 00:00:00 2001 From: Robin Schneider Date: Sun, 7 Feb 2021 23:54:21 +0100 Subject: [PATCH 08/15] Closes #5503: ISO 8601 date in UI and alternative format as tooltip With this commit all dates in the UI are now consistently displayed. I changed the long date format as suggested by @xkilian and confirmed by my own research. * DATETIME_FORMAT * Before July 20, 2020 4:52 p.m. * Now 20th July, 2020 16:52 "20th July, 2020" would be spoken as "the 20th of July, 2020" but the "the" and "of" are never written. The only exception is `object_list.html`. I tried it but there it does not work so easily because the dates are passed to Jinja as SafeString. --- netbox/extras/models/models.py | 5 ++- netbox/templates/base.html | 2 +- netbox/templates/circuits/circuit.html | 2 +- netbox/templates/dcim/rack.html | 2 +- netbox/templates/dcim/site.html | 6 +++- netbox/templates/extras/journalentry.html | 2 +- netbox/templates/extras/objectchange.html | 2 +- netbox/templates/extras/report.html | 2 +- netbox/templates/extras/report_list.html | 2 +- netbox/templates/extras/report_result.html | 2 +- netbox/templates/extras/script_list.html | 2 +- netbox/templates/extras/script_result.html | 4 +-- netbox/templates/generic/object.html | 4 +-- netbox/templates/home.html | 2 +- netbox/templates/inc/custom_fields_panel.html | 3 ++ netbox/templates/inc/image_attachments.html | 3 +- netbox/templates/ipam/aggregate.html | 2 +- netbox/templates/users/api_tokens.html | 4 +-- netbox/templates/users/profile.html | 2 +- netbox/templates/users/userkey.html | 7 ++-- netbox/utilities/templatetags/helpers.py | 36 +++++++++++++++++++ 21 files changed, 71 insertions(+), 25 deletions(-) diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index c2cebe163..6bce039fc 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -431,9 +431,8 @@ class JournalEntry(ChangeLoggedModel): verbose_name_plural = 'journal entries' def __str__(self): - created_date = timezone.localdate(self.created) - created_time = timezone.localtime(self.created) - return f"{date_format(created_date)} - {time_format(created_time)} ({self.get_kind_display()})" + created = timezone.localtime(self.created) + return f"{date_format(created, format='SHORT_DATETIME_FORMAT')} ({self.get_kind_display()})" def get_absolute_url(self): return reverse('extras:journalentry', args=[self.pk]) diff --git a/netbox/templates/base.html b/netbox/templates/base.html index 3a169d965..32ddc8c26 100644 --- a/netbox/templates/base.html +++ b/netbox/templates/base.html @@ -67,7 +67,7 @@

{{ settings.HOSTNAME }} (v{{ settings.VERSION }})

-

{% now 'Y-m-d H:i:s T' %}

+

{% annotated_now %} {% now 'T' %}

diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index ab8bd7394..5d57cd8d6 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -51,7 +51,7 @@ Install Date - {{ object.install_date|placeholder }} + {{ object.install_date|annotated_date|placeholder }} Commit Rate diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 3bb0084a3..ada93518f 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -268,7 +268,7 @@ {{ resv.description }}
- {{ resv.user }} · {{ resv.created }} + {{ resv.user }} · {{ resv.created|annotated_date }} {% if perms.dcim.change_rackreservation %} diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index e0c707a49..a2ad76d53 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -80,7 +80,11 @@ {% if object.time_zone %} {{ object.time_zone }} (UTC {{ object.time_zone|tzoffset }})
- Site time: {% timezone object.time_zone %}{% now "SHORT_DATETIME_FORMAT" %}{% endtimezone %} + Site time: + {% timezone object.time_zone %} + {% annotated_now %} + {% endtimezone %} + {% else %} {% endif %} diff --git a/netbox/templates/extras/journalentry.html b/netbox/templates/extras/journalentry.html index f64741f36..6e986be6c 100644 --- a/netbox/templates/extras/journalentry.html +++ b/netbox/templates/extras/journalentry.html @@ -25,7 +25,7 @@ Created - {{ object.created }} + {{ object.created|annotated_date }} diff --git a/netbox/templates/extras/objectchange.html b/netbox/templates/extras/objectchange.html index c49cea79b..b13491a55 100644 --- a/netbox/templates/extras/objectchange.html +++ b/netbox/templates/extras/objectchange.html @@ -44,7 +44,7 @@ Time - {{ object.time }} + {{ object.time|annotated_date }} diff --git a/netbox/templates/extras/report.html b/netbox/templates/extras/report.html index 82c3f3042..642272c8f 100644 --- a/netbox/templates/extras/report.html +++ b/netbox/templates/extras/report.html @@ -38,7 +38,7 @@

diff --git a/netbox/templates/extras/report_list.html b/netbox/templates/extras/report_list.html index 525a40d12..ac8171a9a 100644 --- a/netbox/templates/extras/report_list.html +++ b/netbox/templates/extras/report_list.html @@ -32,7 +32,7 @@ {{ report.description|render_markdown|placeholder }} {% if report.result %} - {{ report.result.created }} + {{ report.result.created|annotated_date }} {% else %} Never {% endif %} diff --git a/netbox/templates/extras/report_result.html b/netbox/templates/extras/report_result.html index 3d01ca38e..073edc803 100644 --- a/netbox/templates/extras/report_result.html +++ b/netbox/templates/extras/report_result.html @@ -8,7 +8,7 @@

- Run: {{ result.created }} + Run: {{ result.created|annotated_date }} {% if result.completed %} Duration: {{ result.duration }} {% else %} diff --git a/netbox/templates/extras/script_list.html b/netbox/templates/extras/script_list.html index e87860622..71a73d80e 100644 --- a/netbox/templates/extras/script_list.html +++ b/netbox/templates/extras/script_list.html @@ -29,7 +29,7 @@ {{ script.Meta.description|render_markdown }} {% if script.result %} - {{ script.result.created }} + {{ script.result.created|annotated_date }} {% else %} Never diff --git a/netbox/templates/extras/script_result.html b/netbox/templates/extras/script_result.html index 76e0613b7..816a2348d 100644 --- a/netbox/templates/extras/script_result.html +++ b/netbox/templates/extras/script_result.html @@ -13,7 +13,7 @@

  • Scripts
  • {{ script.module|bettertitle }}
  • {{ script }}
  • -
  • {{ result.created }}
  • +
  • {{ result.created|annotated_date }}
  • @@ -32,7 +32,7 @@

    - Run: {{ result.created }} + Run: {{ result.created|annotated_date }} {% if result.completed %} Duration: {{ result.duration }} {% else %} diff --git a/netbox/templates/generic/object.html b/netbox/templates/generic/object.html index b2f6926d7..648a936e0 100644 --- a/netbox/templates/generic/object.html +++ b/netbox/templates/generic/object.html @@ -42,8 +42,8 @@

    {% block title %}{{ object }}{% endblock %}

    - Created {{ object.created }} · - Updated {{ object.last_updated|timesince }} ago + Created {{ object.created|annotated_date }} · + Updated {{ object.last_updated|timesince }} ago {{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}

    diff --git a/netbox/templates/home.html b/netbox/templates/home.html index 273a78bc9..618deb8ca 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -291,7 +291,7 @@ {% for result in report_results %} {{ result.name }} - {% include 'extras/inc/job_label.html' %} + {% include 'extras/inc/job_label.html' %} {% endfor %} diff --git a/netbox/templates/inc/custom_fields_panel.html b/netbox/templates/inc/custom_fields_panel.html index bd80974eb..d938244ac 100644 --- a/netbox/templates/inc/custom_fields_panel.html +++ b/netbox/templates/inc/custom_fields_panel.html @@ -1,3 +1,4 @@ +{% load helpers %} {% with custom_fields=object.get_custom_fields %} {% if custom_fields %}
    @@ -17,6 +18,8 @@ {{ value|truncatechars:70 }} {% elif field.type == 'multiselect' and value %} {{ value|join:", " }} + {% elif field.type == 'date' and value %} + {{ value|annotated_date }} {% elif value is not None %} {{ value }} {% elif field.required %} diff --git a/netbox/templates/inc/image_attachments.html b/netbox/templates/inc/image_attachments.html index 38be9924d..d1f4e45dc 100644 --- a/netbox/templates/inc/image_attachments.html +++ b/netbox/templates/inc/image_attachments.html @@ -1,3 +1,4 @@ +{% load helpers %} {% if images %} @@ -13,7 +14,7 @@ {{ attachment }} - + - + diff --git a/netbox/templates/users/api_tokens.html b/netbox/templates/users/api_tokens.html index f14773293..ad2e3911a 100644 --- a/netbox/templates/users/api_tokens.html +++ b/netbox/templates/users/api_tokens.html @@ -24,12 +24,12 @@
    Created
    - {{ token.created|date }} + {{ token.created|annotated_date }}
    Expires
    {% if token.expires %} - {{ token.expires|date }} + {{ token.expires|annotated_date }} {% else %} Never {% endif %} diff --git a/netbox/templates/users/profile.html b/netbox/templates/users/profile.html index 35a94ac6f..2e575cca6 100644 --- a/netbox/templates/users/profile.html +++ b/netbox/templates/users/profile.html @@ -11,7 +11,7 @@ Email
    {{ request.user.email }}
    Registered -
    {{ request.user.date_joined }}
    +
    {{ request.user.date_joined|annotated_date }}
    Groups
    {{ request.user.groups.all|join:', ' }}
    Admin access diff --git a/netbox/templates/users/userkey.html b/netbox/templates/users/userkey.html index 3839a6925..95e9846db 100644 --- a/netbox/templates/users/userkey.html +++ b/netbox/templates/users/userkey.html @@ -1,4 +1,5 @@ {% extends 'users/base.html' %} +{% load helpers %} {% block title %}User Key{% endblock %} @@ -19,7 +20,9 @@ {% endif %}

    - Created {{ object.created }} · Updated {{ object.last_updated|timesince }} ago + + Created {{ object.created|annotated_date }} · + Updated {{ object.last_updated|timesince }} ago

    {% if not object.is_active %}

    Session key: Active

    - Created {{ object.session_key.created }} + Created {{ object.session_key.created|annotated_date }} {% else %}

    No active session key

    {% endif %} diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 49d8323e9..54725502a 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -4,8 +4,10 @@ import re import yaml from django import template +from django.template.defaultfilters import date from django.conf import settings from django.urls import NoReverseMatch, reverse +from django.utils import timezone from django.utils.html import strip_tags from django.utils.safestring import mark_safe from markdown import markdown @@ -151,6 +153,40 @@ def tzoffset(value): return datetime.datetime.now(value).strftime('%z') +@register.filter(expects_localtime=True) +def annotated_date(date_value): + """ + Returns date as HTML span with short date format as the content and the + (long) date format as the title. + """ + if not date_value: + return '' + + # ../../templates/inc/custom_fields_panel.html passes a string. + if type(date_value) == str: + date_value = datetime.datetime.strptime(date_value, "%Y-%m-%d").date() + + if type(date_value) == datetime.date: + long_ts = date(date_value, 'DATE_FORMAT') + short_ts = date(date_value, 'SHORT_DATE_FORMAT') + else: + long_ts = date(date_value, 'DATETIME_FORMAT') + short_ts = date(date_value, 'SHORT_DATETIME_FORMAT') + + span = f'{short_ts}' + + return mark_safe(span) + + +@register.simple_tag +def annotated_now(): + """ + Returns the current date piped through the annotated_date filter. + """ + tzinfo = timezone.get_current_timezone() if settings.USE_TZ else None + return annotated_date(datetime.datetime.now(tz=tzinfo)) + + @register.filter() def fgcolor(value): """ From 74f1b51b38059fe300cb80090abf378ed9109e51 Mon Sep 17 00:00:00 2001 From: Robin Schneider Date: Fri, 2 Jul 2021 22:11:50 +0200 Subject: [PATCH 09/15] Use annotated_date also for updated datetimes This changes the text from: Updated 5 months, 1 week ago to: Updated 2021-01-24 00:33 (5 months, 1 week ago) Co-authored-by: Jeremy Stretch --- netbox/templates/generic/object.html | 2 +- netbox/templates/users/userkey.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/generic/object.html b/netbox/templates/generic/object.html index 648a936e0..e7424aa56 100644 --- a/netbox/templates/generic/object.html +++ b/netbox/templates/generic/object.html @@ -43,7 +43,7 @@

    Created {{ object.created|annotated_date }} · - Updated {{ object.last_updated|timesince }} ago + Updated {{ object.last_updated|annotated_date }} ({{ object.last_updated|timesince }} ago) {{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}

    diff --git a/netbox/templates/users/userkey.html b/netbox/templates/users/userkey.html index 95e9846db..a024ee352 100644 --- a/netbox/templates/users/userkey.html +++ b/netbox/templates/users/userkey.html @@ -22,7 +22,7 @@

    Created {{ object.created|annotated_date }} · - Updated {{ object.last_updated|timesince }} ago + Updated {{ object.last_updated|annotated_date }} ({{ object.last_updated|timesince }} ago)

    {% if not object.is_active %}
    {{ attachment.size|filesizeformat }}{{ attachment.created }}{{ attachment.created|annotated_date }} {% if perms.extras.change_imageattachment %} diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index 468531b55..60a467644 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -54,7 +54,7 @@
    Date Added{{ object.date_added|placeholder }}{{ object.date_added|annotated_date|placeholder }}
    Description