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

This commit is contained in:
Jeremy Stretch 2020-06-10 14:55:46 -04:00
parent 67784c0568
commit 88ae522c9a
13 changed files with 86 additions and 12 deletions

View File

@ -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. 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 ### 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}`. * `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}`.

View File

@ -165,10 +165,11 @@ class RackReservationSerializer(ValidatedModelSerializer):
rack = NestedRackSerializer() rack = NestedRackSerializer()
user = NestedUserSerializer() user = NestedUserSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
tags = TagListSerializerField(required=False)
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description'] fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags']
class RackElevationDetailFilterSerializer(serializers.Serializer): class RackElevationDetailFilterSerializer(serializers.Serializer):
@ -640,12 +641,13 @@ class CableSerializer(ValidatedModelSerializer):
termination_b = serializers.SerializerMethodField(read_only=True) termination_b = serializers.SerializerMethodField(read_only=True)
status = ChoiceField(choices=CableStatusChoices, required=False) status = ChoiceField(choices=CableStatusChoices, required=False)
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False) length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
tags = TagListSerializerField(required=False)
class Meta: class Meta:
model = Cable model = Cable
fields = [ fields = [
'id', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', 'termination_b_id', '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): def _get_termination(self, obj, side):
@ -729,11 +731,12 @@ class PowerPanelSerializer(ValidatedModelSerializer):
allow_null=True, allow_null=True,
default=None default=None
) )
tags = TagListSerializerField(required=False)
powerfeed_count = serializers.IntegerField(read_only=True) powerfeed_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = PowerPanel model = PowerPanel
fields = ['id', 'site', 'rack_group', 'name', 'powerfeed_count'] fields = ['id', 'site', 'rack_group', 'name', 'tags', 'powerfeed_count']
class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer): class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):

View File

@ -298,6 +298,7 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet):
to_field_name='username', to_field_name='username',
label='User (name)', label='User (name)',
) )
tag = TagFilter()
class Meta: class Meta:
model = RackReservation model = RackReservation
@ -1117,6 +1118,7 @@ class CableFilterSet(BaseFilterSet):
method='filter_device', method='filter_device',
field_name='device__tenant__slug' field_name='device__tenant__slug'
) )
tag = TagFilter()
class Meta: class Meta:
model = Cable model = Cable
@ -1265,6 +1267,7 @@ class PowerPanelFilterSet(BaseFilterSet):
lookup_expr='in', lookup_expr='in',
label='Rack group (ID)', label='Rack group (ID)',
) )
tag = TagFilter()
class Meta: class Meta:
model = PowerPanel model = PowerPanel

View File

@ -750,11 +750,14 @@ class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
), ),
widget=StaticSelect2() widget=StaticSelect2()
) )
tags = TagField(
required=False
)
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = [ fields = [
'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'tags',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -825,7 +828,7 @@ class RackReservationCSVForm(CSVModelForm):
self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params)
class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm): class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=RackReservation.objects.all(), queryset=RackReservation.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
@ -851,6 +854,7 @@ class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm): class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
model = RackReservation
field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant'] field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant']
q = forms.CharField( q = forms.CharField(
required=False, required=False,
@ -872,6 +876,7 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
null_option=True, null_option=True,
) )
) )
tag = TagFilterField(model)
# #
@ -3662,11 +3667,14 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm):
class CableForm(BootstrapMixin, forms.ModelForm): class CableForm(BootstrapMixin, forms.ModelForm):
tags = TagField(
required=False
)
class Meta: class Meta:
model = Cable model = Cable
fields = [ fields = [
'type', 'status', 'label', 'color', 'length', 'length_unit', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
] ]
widgets = { widgets = {
'status': StaticSelect2, 'status': StaticSelect2,
@ -3799,7 +3807,7 @@ class CableCSVForm(CSVModelForm):
return length_unit if length_unit is not None else '' return length_unit if length_unit is not None else ''
class CableBulkEditForm(BootstrapMixin, BulkEditForm): class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Cable.objects.all(), queryset=Cable.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -3912,6 +3920,7 @@ class CableFilterForm(BootstrapMixin, forms.Form):
required=False, required=False,
label='Device' label='Device'
) )
tag = TagFilterField(model)
# #
@ -4325,11 +4334,14 @@ class PowerPanelForm(BootstrapMixin, forms.ModelForm):
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
required=False required=False
) )
tags = TagField(
required=False
)
class Meta: class Meta:
model = PowerPanel model = PowerPanel
fields = [ 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) self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params)
class PowerPanelBulkEditForm(BootstrapMixin, BulkEditForm): class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=PowerPanel.objects.all(), queryset=PowerPanel.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -4420,6 +4432,7 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_option=True, null_option=True,
) )
) )
tag = TagFilterField(model)
# #

View File

@ -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'),
),
]

View File

@ -832,6 +832,7 @@ class RackReservation(ChangeLoggedModel):
description = models.CharField( description = models.CharField(
max_length=200 max_length=200
) )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
@ -1832,6 +1833,7 @@ class PowerPanel(ChangeLoggedModel):
name = models.CharField( name = models.CharField(
max_length=50 max_length=50
) )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
@ -2106,6 +2108,7 @@ class Cable(ChangeLoggedModel):
blank=True, blank=True,
null=True null=True
) )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()

View File

@ -399,6 +399,9 @@ class RackReservationTable(BaseTable):
orderable=False, orderable=False,
verbose_name='Units' verbose_name='Units'
) )
tags = TagColumn(
url_name='dcim:rackreservation_list'
)
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=RACKRESERVATION_ACTIONS, template_code=RACKRESERVATION_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -408,7 +411,8 @@ class RackReservationTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RackReservation model = RackReservation
fields = ( 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 = ( default_columns = (
'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions', 'pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description', 'actions',
@ -1086,12 +1090,15 @@ class CableTable(BaseTable):
order_by='_abs_length' order_by='_abs_length'
) )
color = ColorColumn() color = ColorColumn()
tags = TagColumn(
url_name='dcim:cable_list'
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Cable model = Cable
fields = ( fields = (
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', 'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
'status', 'type', 'color', 'length', 'status', 'type', 'color', 'length', 'tags',
) )
default_columns = ( default_columns = (
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b', '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, template_code=POWERPANEL_POWERFEED_COUNT,
verbose_name='Feeds' verbose_name='Feeds'
) )
tags = TagColumn(
url_name='dcim:powerpanel_list'
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerPanel 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') default_columns = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count')

View File

@ -202,6 +202,7 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'user': user3.pk, 'user': user3.pk,
'tenant': None, 'tenant': None,
'description': 'Rack reservation', 'description': 'Rack reservation',
'tags': 'Alpha,Bravo,Charlie',
} }
cls.csv_data = ( cls.csv_data = (
@ -1510,6 +1511,7 @@ class CableTestCase(
'color': 'c0c0c0', 'color': 'c0c0c0',
'length': 100, 'length': 100,
'length_unit': CableLengthUnitChoices.UNIT_FOOT, 'length_unit': CableLengthUnitChoices.UNIT_FOOT,
'tags': 'Alpha,Bravo,Charlie',
} }
cls.csv_data = ( cls.csv_data = (
@ -1609,6 +1611,7 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'site': sites[1].pk, 'site': sites[1].pk,
'rack_group': rackgroups[1].pk, 'rack_group': rackgroups[1].pk,
'name': 'Power Panel X', 'name': 'Power Panel X',
'tags': 'Alpha,Bravo,Charlie',
} }
cls.csv_data = ( cls.csv_data = (

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@
{% render_field form.tenant_group %} {% render_field form.tenant_group %}
{% render_field form.tenant %} {% render_field form.tenant %}
{% render_field form.description %} {% render_field form.description %}
{% render_field form.tags %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}