Просмотр исходного кода

Closes #4742: Add tagging for cables, power panels, and rack reservations

Jeremy Stretch 5 лет назад
Родитель
Сommit
88ae522c9a

+ 4 - 0
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}`.

+ 6 - 3
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):

+ 3 - 0
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

+ 19 - 6
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)
 
 
 #

+ 30 - 0
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'),
+        ),
+    ]

+ 3 - 0
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()
 

+ 13 - 3
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')
 
 

+ 3 - 0
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 = (

+ 1 - 0
netbox/templates/dcim/cable.html

@@ -81,6 +81,7 @@
                     </tr>
                 </table>
             </div>
+            {% include 'extras/inc/tags_panel.html' with tags=cable.tags.all url='dcim:cable_list' %}
             {% plugin_left_page cable %}
         </div>
         <div class="col-md-6">

+ 1 - 0
netbox/templates/dcim/inc/cable_form.html

@@ -29,5 +29,6 @@
                 {% endif %}
             </div>
         </div>
+        {% render_field form.tags %}
     </div>
 </div>

+ 1 - 0
netbox/templates/dcim/powerpanel.html

@@ -82,6 +82,7 @@
                 </tr>
             </table>
         </div>
+        {% include 'extras/inc/tags_panel.html' with tags=powerpanel.tags.all url='dcim:powerpanel_list' %}
         {% plugin_left_page powerpanel %}
     </div>
 	<div class="col-md-9">

+ 1 - 0
netbox/templates/dcim/rackreservation.html

@@ -124,6 +124,7 @@
                 </tr>
             </table>
         </div>
+        {% include 'extras/inc/tags_panel.html' with tags=rackreservation.tags.all url='dcim:rackreservation_list' %}
         {% plugin_left_page rackreservation %}
 	</div>
     <div class="col-md-6">

+ 1 - 0
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 %}
         </div>
     </div>
 {% endblock %}