mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Closes #4742: Add tagging for cables, power panels, and rack reservations
This commit is contained in:
parent
67784c0568
commit
88ae522c9a
@ -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}`.
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
30
netbox/dcim/migrations/0107_add_tags.py
Normal file
30
netbox/dcim/migrations/0107_add_tags.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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()
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 = (
|
||||||
|
@ -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">
|
||||||
|
@ -29,5 +29,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% render_field form.tags %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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 %}
|
||||||
|
Loading…
Reference in New Issue
Block a user