diff --git a/docs/release-notes/version-2.9.md b/docs/release-notes/version-2.9.md index 7c63af51c..ce480035e 100644 --- a/docs/release-notes/version-2.9.md +++ b/docs/release-notes/version-2.9.md @@ -8,6 +8,10 @@ NetBox v2.9 replaces Django's built-in permissions framework with one that supports object-based assignment of permissions using arbitrary constraints. When granting a user or group to perform a certain action on one or more types of objects, an administrator can optionally specify a set of constraints. The permission will apply only to objects which match the specified constraints. For example, assigning permission to modify devices with the constraint `{"tenant__group__name": "Customers"}` would grant the permission only for devices assigned to a tenant belonging to the "Customers" group. +### Enhancements + +* [#4742](https://github.com/netbox-community/netbox/issues/4742) - Add tagging for cables, power panels, and rack reservations + ### Configuration Changes * `REMOTE_AUTH_DEFAULT_PERMISSIONS` now takes a dictionary rather than a list. This is a mapping of permission names to a dictionary of constraining attributes, or `None`. For example, `['dcim.add_site', 'dcim.change_site']` would become `{'dcim.add_site': None, 'dcim.change_site': None}`. diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 9ac58dc3a..9174085b8 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -165,10 +165,11 @@ class RackReservationSerializer(ValidatedModelSerializer): rack = NestedRackSerializer() user = NestedUserSerializer() tenant = NestedTenantSerializer(required=False, allow_null=True) + tags = TagListSerializerField(required=False) class Meta: model = RackReservation - fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description'] + fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags'] class RackElevationDetailFilterSerializer(serializers.Serializer): @@ -640,12 +641,13 @@ class CableSerializer(ValidatedModelSerializer): termination_b = serializers.SerializerMethodField(read_only=True) status = ChoiceField(choices=CableStatusChoices, required=False) length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False) + tags = TagListSerializerField(required=False) class Meta: model = Cable fields = [ 'id', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', 'termination_b_id', - 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', + 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', ] def _get_termination(self, obj, side): @@ -729,11 +731,12 @@ class PowerPanelSerializer(ValidatedModelSerializer): allow_null=True, default=None ) + tags = TagListSerializerField(required=False) powerfeed_count = serializers.IntegerField(read_only=True) class Meta: model = PowerPanel - fields = ['id', 'site', 'rack_group', 'name', 'powerfeed_count'] + fields = ['id', 'site', 'rack_group', 'name', 'tags', 'powerfeed_count'] class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer): diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 8c24180bb..92f7f8241 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -298,6 +298,7 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet): to_field_name='username', label='User (name)', ) + tag = TagFilter() class Meta: model = RackReservation @@ -1117,6 +1118,7 @@ class CableFilterSet(BaseFilterSet): method='filter_device', field_name='device__tenant__slug' ) + tag = TagFilter() class Meta: model = Cable @@ -1265,6 +1267,7 @@ class PowerPanelFilterSet(BaseFilterSet): lookup_expr='in', label='Rack group (ID)', ) + tag = TagFilter() class Meta: model = PowerPanel diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 94cf51fcd..cd728cd19 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -750,11 +750,14 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): ), widget=StaticSelect2() ) + tags = TagField( + required=False + ) class Meta: model = RackReservation fields = [ - 'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', + 'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'tags', ] def __init__(self, *args, **kwargs): @@ -825,7 +828,7 @@ class RackReservationCSVForm(CSVModelForm): self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) -class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): +class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RackReservation.objects.all(), widget=forms.MultipleHiddenInput() @@ -851,6 +854,7 @@ class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm): + model = RackReservation field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant'] q = forms.CharField( required=False, @@ -872,6 +876,7 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm): null_option=True, ) ) + tag = TagFilterField(model) # @@ -3662,11 +3667,14 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm): class CableForm(BootstrapMixin, forms.ModelForm): + tags = TagField( + required=False + ) class Meta: model = Cable fields = [ - 'type', 'status', 'label', 'color', 'length', 'length_unit', + 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', ] widgets = { 'status': StaticSelect2, @@ -3799,7 +3807,7 @@ class CableCSVForm(CSVModelForm): return length_unit if length_unit is not None else '' -class CableBulkEditForm(BootstrapMixin, BulkEditForm): +class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Cable.objects.all(), widget=forms.MultipleHiddenInput @@ -3912,6 +3920,7 @@ class CableFilterForm(BootstrapMixin, forms.Form): required=False, label='Device' ) + tag = TagFilterField(model) # @@ -4325,11 +4334,14 @@ class PowerPanelForm(BootstrapMixin, forms.ModelForm): queryset=RackGroup.objects.all(), required=False ) + tags = TagField( + required=False + ) class Meta: model = PowerPanel fields = [ - 'site', 'rack_group', 'name', + 'site', 'rack_group', 'name', 'tags', ] @@ -4359,7 +4371,7 @@ class PowerPanelCSVForm(CSVModelForm): self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params) -class PowerPanelBulkEditForm(BootstrapMixin, BulkEditForm): +class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=PowerPanel.objects.all(), widget=forms.MultipleHiddenInput @@ -4420,6 +4432,7 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + tag = TagFilterField(model) # diff --git a/netbox/dcim/migrations/0107_add_tags.py b/netbox/dcim/migrations/0107_add_tags.py new file mode 100644 index 000000000..2f7d29b96 --- /dev/null +++ b/netbox/dcim/migrations/0107_add_tags.py @@ -0,0 +1,30 @@ +# Generated by Django 3.0.6 on 2020-06-10 18:32 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0042_customfield_manager'), + ('dcim', '0106_role_default_color'), + ] + + operations = [ + migrations.AddField( + model_name='cable', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='powerpanel', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='rackreservation', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + ] diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 73220d1f0..f7411ca56 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -832,6 +832,7 @@ class RackReservation(ChangeLoggedModel): description = models.CharField( max_length=200 ) + tags = TaggableManager(through=TaggedItem) objects = RestrictedQuerySet.as_manager() @@ -1832,6 +1833,7 @@ class PowerPanel(ChangeLoggedModel): name = models.CharField( max_length=50 ) + tags = TaggableManager(through=TaggedItem) objects = RestrictedQuerySet.as_manager() @@ -2106,6 +2108,7 @@ class Cable(ChangeLoggedModel): blank=True, null=True ) + tags = TaggableManager(through=TaggedItem) objects = RestrictedQuerySet.as_manager() diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 9018625a0..0d7b4bb7a 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -399,6 +399,9 @@ class RackReservationTable(BaseTable): orderable=False, verbose_name='Units' ) + tags = TagColumn( + url_name='dcim:rackreservation_list' + ) actions = tables.TemplateColumn( template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, @@ -408,7 +411,8 @@ class RackReservationTable(BaseTable): class Meta(BaseTable.Meta): model = RackReservation fields = ( - 'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'actions', + 'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags', + 'actions', ) default_columns = ( 'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions', @@ -1086,12 +1090,15 @@ class CableTable(BaseTable): order_by='_abs_length' ) color = ColorColumn() + tags = TagColumn( + url_name='dcim:cable_list' + ) class Meta(BaseTable.Meta): model = Cable fields = ( 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', - 'status', 'type', 'color', 'length', + 'status', 'type', 'color', 'length', 'tags', ) default_columns = ( 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', @@ -1245,10 +1252,13 @@ class PowerPanelTable(BaseTable): template_code=POWERPANEL_POWERFEED_COUNT, verbose_name='Feeds' ) + tags = TagColumn( + url_name='dcim:powerpanel_list' + ) class Meta(BaseTable.Meta): model = PowerPanel - fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count') + fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count', 'tags') default_columns = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count') diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index cfbb2b95f..82657afb9 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -202,6 +202,7 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'user': user3.pk, 'tenant': None, 'description': 'Rack reservation', + 'tags': 'Alpha,Bravo,Charlie', } cls.csv_data = ( @@ -1510,6 +1511,7 @@ class CableTestCase( 'color': 'c0c0c0', 'length': 100, 'length_unit': CableLengthUnitChoices.UNIT_FOOT, + 'tags': 'Alpha,Bravo,Charlie', } cls.csv_data = ( @@ -1609,6 +1611,7 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'site': sites[1].pk, 'rack_group': rackgroups[1].pk, 'name': 'Power Panel X', + 'tags': 'Alpha,Bravo,Charlie', } cls.csv_data = ( diff --git a/netbox/templates/dcim/cable.html b/netbox/templates/dcim/cable.html index e6a2fa008..91c7b1a94 100644 --- a/netbox/templates/dcim/cable.html +++ b/netbox/templates/dcim/cable.html @@ -81,6 +81,7 @@ + {% include 'extras/inc/tags_panel.html' with tags=cable.tags.all url='dcim:cable_list' %} {% plugin_left_page cable %}
diff --git a/netbox/templates/dcim/inc/cable_form.html b/netbox/templates/dcim/inc/cable_form.html index a52cc302e..98eca17d2 100644 --- a/netbox/templates/dcim/inc/cable_form.html +++ b/netbox/templates/dcim/inc/cable_form.html @@ -29,5 +29,6 @@ {% endif %}
+ {% render_field form.tags %} diff --git a/netbox/templates/dcim/powerpanel.html b/netbox/templates/dcim/powerpanel.html index 3ee8d80e0..90956d2a3 100644 --- a/netbox/templates/dcim/powerpanel.html +++ b/netbox/templates/dcim/powerpanel.html @@ -82,6 +82,7 @@ + {% include 'extras/inc/tags_panel.html' with tags=powerpanel.tags.all url='dcim:powerpanel_list' %} {% plugin_left_page powerpanel %}
diff --git a/netbox/templates/dcim/rackreservation.html b/netbox/templates/dcim/rackreservation.html index d4bbbc97d..ab0fc0bba 100644 --- a/netbox/templates/dcim/rackreservation.html +++ b/netbox/templates/dcim/rackreservation.html @@ -124,6 +124,7 @@
+ {% include 'extras/inc/tags_panel.html' with tags=rackreservation.tags.all url='dcim:rackreservation_list' %} {% plugin_left_page rackreservation %}
diff --git a/netbox/templates/dcim/rackreservation_edit.html b/netbox/templates/dcim/rackreservation_edit.html index b2304974e..3db8f6d72 100644 --- a/netbox/templates/dcim/rackreservation_edit.html +++ b/netbox/templates/dcim/rackreservation_edit.html @@ -16,6 +16,7 @@ {% render_field form.tenant_group %} {% render_field form.tenant %} {% render_field form.description %} + {% render_field form.tags %}
{% endblock %}