mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Merge branch 'develop-2.6' of github.com:digitalocean/netbox into develop-2.6
This commit is contained in:
commit
5991bd368c
27
CHANGELOG.md
27
CHANGELOG.md
@ -1,3 +1,30 @@
|
|||||||
|
v2.6.0 (FUTURE)
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### API Device/VM Config Context Included by Default ([#2350](https://github.com/digitalocean/netbox/issues/2350))
|
||||||
|
|
||||||
|
The rendered Config Context for Devices and VMs is now included by default in all API results (list and detail views).
|
||||||
|
Previously the rendered Config Context was only available in the detail view for objects. Users with large amounts of
|
||||||
|
context data may observe a performance drop when returning multiple objects. To combat this, in cases where the rendered
|
||||||
|
Config Context is not needed, the query parameter `?exclude=config_context` may be added to the request as to remove
|
||||||
|
the Config Context from being included in any results.
|
||||||
|
|
||||||
|
### Tag Permissions Changed
|
||||||
|
|
||||||
|
NetBox now makes use of its own `Tag` model instead of the vanilla model which ships with django-taggit. This new model
|
||||||
|
lives in the `extras` app and thus any permissions that you may have configured using "Taggit | Tag" should be changed
|
||||||
|
to now use "Extras | Tag."
|
||||||
|
|
||||||
|
## Enhancements
|
||||||
|
|
||||||
|
* [#2324](https://github.com/digitalocean/netbox/issues/2324) - Add color option for tags
|
||||||
|
* [#2643](https://github.com/digitalocean/netbox/issues/2643) - Add `description` field to console/power components and device bays
|
||||||
|
* [#2791](https://github.com/digitalocean/netbox/issues/2791) - Add a comment field for tags
|
||||||
|
* [#2926](https://github.com/digitalocean/netbox/issues/2926) - Add changelog to the Tag model
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
v2.5.8 (FUTURE)
|
v2.5.8 (FUTURE)
|
||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
25
netbox/circuits/migrations/0015_custom_tag_models.py
Normal file
25
netbox/circuits/migrations/0015_custom_tag_models.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 2.1.4 on 2019-02-20 06:56
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0014_circuittermination_description'),
|
||||||
|
('extras', '0017_tag_taggeditem'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='provider',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
]
|
@ -6,7 +6,7 @@ from taggit.managers import TaggableManager
|
|||||||
from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES
|
from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES
|
||||||
from dcim.fields import ASNField
|
from dcim.fields import ASNField
|
||||||
from dcim.models import CableTermination
|
from dcim.models import CableTermination
|
||||||
from extras.models import CustomFieldModel, ObjectChange
|
from extras.models import CustomFieldModel, ObjectChange, TaggedItem
|
||||||
from utilities.models import ChangeLoggedModel
|
from utilities.models import ChangeLoggedModel
|
||||||
from utilities.utils import serialize_object
|
from utilities.utils import serialize_object
|
||||||
from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
|
from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
|
||||||
@ -55,7 +55,7 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
|
|||||||
object_id_field='obj_id'
|
object_id_field='obj_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
|
||||||
|
|
||||||
@ -165,7 +165,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
|||||||
object_id_field='obj_id'
|
object_id_field='obj_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
|
||||||
|
@ -346,8 +346,8 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
|
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
|
||||||
'tags',
|
'cable', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -359,8 +359,8 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
|
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
|
||||||
'tags',
|
'cable', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -372,8 +372,8 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
|
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
|
||||||
'tags',
|
'cable', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -385,8 +385,8 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
|
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
|
||||||
'tags',
|
'cable', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -475,7 +475,7 @@ class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fields = ['id', 'device', 'name', 'installed_device', 'tags']
|
fields = ['id', 'device', 'name', 'description', 'installed_device', 'tags']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -291,16 +291,23 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
"""
|
"""
|
||||||
Include rendered config context when retrieving a single Device.
|
Select the specific serializer based on the request context.
|
||||||
|
|
||||||
|
If the `brief` query param equates to True, return the NestedDeviceSerializer
|
||||||
|
|
||||||
|
If the `exclude` query param includes `config_context` as a value, return the DeviceSerializer
|
||||||
|
|
||||||
|
Else, return the DeviceWithConfigContextSerializer
|
||||||
"""
|
"""
|
||||||
if self.action == 'retrieve':
|
|
||||||
return serializers.DeviceWithConfigContextSerializer
|
|
||||||
|
|
||||||
request = self.get_serializer_context()['request']
|
request = self.get_serializer_context()['request']
|
||||||
if request.query_params.get('brief', False):
|
if request.query_params.get('brief', False):
|
||||||
return serializers.NestedDeviceSerializer
|
return serializers.NestedDeviceSerializer
|
||||||
|
|
||||||
return serializers.DeviceSerializer
|
elif 'config_context' in request.query_params.get('exclude', []):
|
||||||
|
return serializers.DeviceSerializer
|
||||||
|
|
||||||
|
return serializers.DeviceWithConfigContextSerializer
|
||||||
|
|
||||||
@action(detail=True, url_path='napalm')
|
@action(detail=True, url_path='napalm')
|
||||||
def napalm(self, request, pk):
|
def napalm(self, request, pk):
|
||||||
|
@ -693,7 +693,8 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
|||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value)
|
Q(name__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -706,7 +707,7 @@ class ConsolePortFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = ['name', 'connection_status']
|
fields = ['name', 'description', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortFilter(DeviceComponentFilterSet):
|
class ConsoleServerPortFilter(DeviceComponentFilterSet):
|
||||||
@ -718,7 +719,7 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = ['name', 'connection_status']
|
fields = ['name', 'description', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class PowerPortFilter(DeviceComponentFilterSet):
|
class PowerPortFilter(DeviceComponentFilterSet):
|
||||||
@ -730,7 +731,7 @@ class PowerPortFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = ['name', 'connection_status']
|
fields = ['name', 'description', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletFilter(DeviceComponentFilterSet):
|
class PowerOutletFilter(DeviceComponentFilterSet):
|
||||||
@ -742,7 +743,7 @@ class PowerOutletFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = ['name', 'connection_status']
|
fields = ['name', 'description', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceFilter(django_filters.FilterSet):
|
class InterfaceFilter(django_filters.FilterSet):
|
||||||
@ -797,13 +798,14 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['name', 'connection_status', 'form_factor', 'enabled', 'mtu', 'mgmt_only']
|
fields = ['name', 'connection_status', 'form_factor', 'enabled', 'mtu', 'mgmt_only', 'description']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value)
|
Q(name__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
def filter_device(self, queryset, name, value):
|
def filter_device(self, queryset, name, value):
|
||||||
@ -861,7 +863,7 @@ class FrontPortFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = ['name', 'type']
|
fields = ['name', 'type', 'description']
|
||||||
|
|
||||||
|
|
||||||
class RearPortFilter(DeviceComponentFilterSet):
|
class RearPortFilter(DeviceComponentFilterSet):
|
||||||
@ -873,14 +875,14 @@ class RearPortFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fields = ['name', 'type']
|
fields = ['name', 'type', 'description']
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayFilter(DeviceComponentFilterSet):
|
class DeviceBayFilter(DeviceComponentFilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fields = ['name']
|
fields = ['name', 'description']
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemFilter(DeviceComponentFilterSet):
|
class InventoryItemFilter(DeviceComponentFilterSet):
|
||||||
|
@ -1854,7 +1854,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'tags',
|
'device', 'name', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
@ -1865,6 +1865,10 @@ class ConsolePortCreateForm(ComponentForm):
|
|||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
@ -1882,7 +1886,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'tags',
|
'device', 'name', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
@ -1893,11 +1897,31 @@ class ConsoleServerPortCreateForm(ComponentForm):
|
|||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=ConsoleServerPort.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput()
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = [
|
||||||
|
'description',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortBulkRenameForm(BulkRenameForm):
|
class ConsoleServerPortBulkRenameForm(BulkRenameForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=ConsoleServerPort.objects.all(),
|
queryset=ConsoleServerPort.objects.all(),
|
||||||
@ -1924,7 +1948,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'tags',
|
'device', 'name', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
@ -1935,6 +1959,10 @@ class PowerPortCreateForm(ComponentForm):
|
|||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
@ -1952,7 +1980,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'tags',
|
'device', 'name', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
@ -1963,11 +1991,31 @@ class PowerOutletCreateForm(ComponentForm):
|
|||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=PowerOutlet.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput()
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = [
|
||||||
|
'description',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletBulkRenameForm(BulkRenameForm):
|
class PowerOutletBulkRenameForm(BulkRenameForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=PowerOutlet.objects.all(),
|
queryset=PowerOutlet.objects.all(),
|
||||||
@ -2781,7 +2829,7 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
fields = [
|
fields = [
|
||||||
'device', 'name', 'tags',
|
'device', 'name', 'description', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
|
85
netbox/dcim/migrations/0070_custom_tag_models.py
Normal file
85
netbox/dcim/migrations/0070_custom_tag_models.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# Generated by Django 2.1.4 on 2019-02-20 06:56
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0069_deprecate_nullablecharfield'),
|
||||||
|
('extras', '0017_tag_taggeditem'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicebay',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='frontport',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventoryitem',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rack',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rearport',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='site',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='virtualchassis',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 2.1.7 on 2019-02-20 18:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0070_custom_tag_models'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicebay',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
]
|
@ -15,7 +15,7 @@ from mptt.models import MPTTModel, TreeForeignKey
|
|||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
from timezone_field import TimeZoneField
|
from timezone_field import TimeZoneField
|
||||||
|
|
||||||
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange
|
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
|
||||||
from utilities.fields import ColorField
|
from utilities.fields import ColorField
|
||||||
from utilities.managers import NaturalOrderingManager
|
from utilities.managers import NaturalOrderingManager
|
||||||
from utilities.models import ChangeLoggedModel
|
from utilities.models import ChangeLoggedModel
|
||||||
@ -46,6 +46,10 @@ class ComponentTemplateModel(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class ComponentModel(models.Model):
|
class ComponentModel(models.Model):
|
||||||
|
description = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
@ -319,7 +323,7 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
objects = NaturalOrderingManager()
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||||
@ -566,7 +570,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
objects = NaturalOrderingManager()
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
|
'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
|
||||||
@ -914,7 +918,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
|||||||
object_id_field='obj_id'
|
object_id_field='obj_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments',
|
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments',
|
||||||
@ -1455,7 +1459,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = NaturalOrderingManager()
|
objects = NaturalOrderingManager()
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||||
@ -1743,9 +1747,9 @@ class ConsolePort(CableTermination, ComponentModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = DeviceComponentManager()
|
objects = DeviceComponentManager()
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name']
|
csv_headers = ['device', 'name', 'description']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
ordering = ['device', 'name']
|
||||||
@ -1761,6 +1765,7 @@ class ConsolePort(CableTermination, ComponentModel):
|
|||||||
return (
|
return (
|
||||||
self.device.identifier,
|
self.device.identifier,
|
||||||
self.name,
|
self.name,
|
||||||
|
self.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1786,9 +1791,9 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = DeviceComponentManager()
|
objects = DeviceComponentManager()
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name']
|
csv_headers = ['device', 'name', 'description']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ['device', 'name']
|
unique_together = ['device', 'name']
|
||||||
@ -1803,6 +1808,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
|||||||
return (
|
return (
|
||||||
self.device.identifier,
|
self.device.identifier,
|
||||||
self.name,
|
self.name,
|
||||||
|
self.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1835,13 +1841,13 @@ class PowerPort(CableTermination, ComponentModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = DeviceComponentManager()
|
objects = DeviceComponentManager()
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name']
|
csv_headers = ['device', 'name']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
ordering = ['device', 'name']
|
||||||
unique_together = ['device', 'name']
|
unique_together = ['device', 'name', 'description']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -1853,6 +1859,7 @@ class PowerPort(CableTermination, ComponentModel):
|
|||||||
return (
|
return (
|
||||||
self.device.identifier,
|
self.device.identifier,
|
||||||
self.name,
|
self.name,
|
||||||
|
self.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1878,12 +1885,12 @@ class PowerOutlet(CableTermination, ComponentModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = DeviceComponentManager()
|
objects = DeviceComponentManager()
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name']
|
csv_headers = ['device', 'name']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ['device', 'name']
|
unique_together = ['device', 'name', 'description']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -1895,6 +1902,7 @@ class PowerOutlet(CableTermination, ComponentModel):
|
|||||||
return (
|
return (
|
||||||
self.device.identifier,
|
self.device.identifier,
|
||||||
self.name,
|
self.name,
|
||||||
|
self.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1973,10 +1981,6 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
verbose_name='OOB Management',
|
verbose_name='OOB Management',
|
||||||
help_text='This interface is used only for out-of-band management'
|
help_text='This interface is used only for out-of-band management'
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
|
||||||
max_length=100,
|
|
||||||
blank=True
|
|
||||||
)
|
|
||||||
mode = models.PositiveSmallIntegerField(
|
mode = models.PositiveSmallIntegerField(
|
||||||
choices=IFACE_MODE_CHOICES,
|
choices=IFACE_MODE_CHOICES,
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -1998,7 +2002,7 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = InterfaceManager()
|
objects = InterfaceManager()
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'device', 'virtual_machine', 'name', 'lag', 'form_factor', 'enabled', 'mac_address', 'mtu', 'mgmt_only',
|
'device', 'virtual_machine', 'name', 'lag', 'form_factor', 'enabled', 'mac_address', 'mtu', 'mgmt_only',
|
||||||
@ -2193,13 +2197,9 @@ class FrontPort(CableTermination, ComponentModel):
|
|||||||
default=1,
|
default=1,
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
|
||||||
max_length=100,
|
|
||||||
blank=True
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = DeviceComponentManager()
|
objects = DeviceComponentManager()
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
|
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
|
||||||
|
|
||||||
@ -2259,13 +2259,9 @@ class RearPort(CableTermination, ComponentModel):
|
|||||||
default=1,
|
default=1,
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
|
||||||
max_length=100,
|
|
||||||
blank=True
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = DeviceComponentManager()
|
objects = DeviceComponentManager()
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name', 'type', 'positions', 'description']
|
csv_headers = ['device', 'name', 'type', 'positions', 'description']
|
||||||
|
|
||||||
@ -2312,9 +2308,9 @@ class DeviceBay(ComponentModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = DeviceComponentManager()
|
objects = DeviceComponentManager()
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'name', 'installed_device']
|
csv_headers = ['device', 'name', 'installed_device', 'description']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
ordering = ['device', 'name']
|
||||||
@ -2331,6 +2327,7 @@ class DeviceBay(ComponentModel):
|
|||||||
self.device.identifier,
|
self.device.identifier,
|
||||||
self.name,
|
self.name,
|
||||||
self.installed_device.identifier if self.installed_device else None,
|
self.installed_device.identifier if self.installed_device else None,
|
||||||
|
self.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@ -2400,12 +2397,8 @@ class InventoryItem(ComponentModel):
|
|||||||
default=False,
|
default=False,
|
||||||
verbose_name='Discovered'
|
verbose_name='Discovered'
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
|
||||||
max_length=100,
|
|
||||||
blank=True
|
|
||||||
)
|
|
||||||
|
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description',
|
||||||
@ -2452,7 +2445,7 @@ class VirtualChassis(ChangeLoggedModel):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['master', 'domain']
|
csv_headers = ['master', 'domain']
|
||||||
|
|
||||||
|
@ -1791,6 +1791,16 @@ class DeviceTest(APITestCase):
|
|||||||
site=self.site1,
|
site=self.site1,
|
||||||
cluster=self.cluster1
|
cluster=self.cluster1
|
||||||
)
|
)
|
||||||
|
self.device_with_context_data = Device.objects.create(
|
||||||
|
device_type=self.devicetype1,
|
||||||
|
device_role=self.devicerole1,
|
||||||
|
name='Device with context data',
|
||||||
|
site=self.site1,
|
||||||
|
local_context_data={
|
||||||
|
'A': 1,
|
||||||
|
'B': 2
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_get_device(self):
|
def test_get_device(self):
|
||||||
|
|
||||||
@ -1806,7 +1816,7 @@ class DeviceTest(APITestCase):
|
|||||||
url = reverse('dcim-api:device-list')
|
url = reverse('dcim-api:device-list')
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
|
|
||||||
self.assertEqual(response.data['count'], 3)
|
self.assertEqual(response.data['count'], 4)
|
||||||
|
|
||||||
def test_list_devices_brief(self):
|
def test_list_devices_brief(self):
|
||||||
|
|
||||||
@ -1832,7 +1842,7 @@ class DeviceTest(APITestCase):
|
|||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(Device.objects.count(), 4)
|
self.assertEqual(Device.objects.count(), 5)
|
||||||
device4 = Device.objects.get(pk=response.data['id'])
|
device4 = Device.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(device4.device_type_id, data['device_type'])
|
self.assertEqual(device4.device_type_id, data['device_type'])
|
||||||
self.assertEqual(device4.device_role_id, data['device_role'])
|
self.assertEqual(device4.device_role_id, data['device_role'])
|
||||||
@ -1867,7 +1877,7 @@ class DeviceTest(APITestCase):
|
|||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(Device.objects.count(), 6)
|
self.assertEqual(Device.objects.count(), 7)
|
||||||
self.assertEqual(response.data[0]['name'], data[0]['name'])
|
self.assertEqual(response.data[0]['name'], data[0]['name'])
|
||||||
self.assertEqual(response.data[1]['name'], data[1]['name'])
|
self.assertEqual(response.data[1]['name'], data[1]['name'])
|
||||||
self.assertEqual(response.data[2]['name'], data[2]['name'])
|
self.assertEqual(response.data[2]['name'], data[2]['name'])
|
||||||
@ -1891,7 +1901,7 @@ class DeviceTest(APITestCase):
|
|||||||
response = self.client.put(url, data, format='json', **self.header)
|
response = self.client.put(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(Device.objects.count(), 3)
|
self.assertEqual(Device.objects.count(), 4)
|
||||||
device1 = Device.objects.get(pk=response.data['id'])
|
device1 = Device.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(device1.device_type_id, data['device_type'])
|
self.assertEqual(device1.device_type_id, data['device_type'])
|
||||||
self.assertEqual(device1.device_role_id, data['device_role'])
|
self.assertEqual(device1.device_role_id, data['device_role'])
|
||||||
@ -1906,7 +1916,21 @@ class DeviceTest(APITestCase):
|
|||||||
response = self.client.delete(url, **self.header)
|
response = self.client.delete(url, **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||||
self.assertEqual(Device.objects.count(), 2)
|
self.assertEqual(Device.objects.count(), 3)
|
||||||
|
|
||||||
|
def test_config_context_included_by_default_in_list_view(self):
|
||||||
|
|
||||||
|
url = reverse('dcim-api:device-list') + '?slug=device-with-context-data'
|
||||||
|
response = self.client.get(url, **self.header)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['results'][0].get('config_context', {}).get('A'), 1)
|
||||||
|
|
||||||
|
def test_config_context_excluded(self):
|
||||||
|
|
||||||
|
url = reverse('dcim-api:device-list') + '?exclude=config_context'
|
||||||
|
response = self.client.get(url, **self.header)
|
||||||
|
|
||||||
|
self.assertFalse('config_context' in response.data['results'][0])
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTest(APITestCase):
|
class ConsolePortTest(APITestCase):
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from taggit.models import Tag
|
|
||||||
|
|
||||||
from dcim.api.nested_serializers import (
|
from dcim.api.nested_serializers import (
|
||||||
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
|
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
|
||||||
@ -10,6 +9,7 @@ from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
|
|||||||
from extras.constants import *
|
from extras.constants import *
|
||||||
from extras.models import (
|
from extras.models import (
|
||||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||||
|
Tag
|
||||||
)
|
)
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
@ -80,7 +80,7 @@ class TagSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
fields = ['id', 'name', 'slug', 'tagged_items']
|
fields = ['id', 'name', 'slug', 'color', 'comments', 'tagged_items']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -6,11 +6,11 @@ from rest_framework.decorators import action
|
|||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
||||||
from taggit.models import Tag
|
|
||||||
|
|
||||||
from extras import filters
|
from extras import filters
|
||||||
from extras.models import (
|
from extras.models import (
|
||||||
ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||||
|
Tag
|
||||||
)
|
)
|
||||||
from extras.reports import get_report, get_reports
|
from extras.reports import get_report, get_reports
|
||||||
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
|
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
|
||||||
@ -115,7 +115,7 @@ class TopologyMapViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class TagViewSet(ModelViewSet):
|
class TagViewSet(ModelViewSet):
|
||||||
queryset = Tag.objects.annotate(tagged_items=Count('taggit_taggeditem_items'))
|
queryset = Tag.objects.annotate(tagged_items=Count('extras_taggeditem_items'))
|
||||||
serializer_class = serializers.TagSerializer
|
serializer_class = serializers.TagSerializer
|
||||||
filterset_class = filters.TagFilter
|
filterset_class = filters.TagFilter
|
||||||
|
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from taggit.models import Tag
|
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
|
||||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap
|
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag, TopologyMap
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldFilter(django_filters.Filter):
|
class CustomFieldFilter(django_filters.Filter):
|
||||||
|
@ -6,19 +6,18 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from mptt.forms import TreeNodeMultipleChoiceField
|
from mptt.forms import TreeNodeMultipleChoiceField
|
||||||
from taggit.forms import TagField
|
from taggit.forms import TagField
|
||||||
from taggit.models import Tag
|
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect,
|
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect,
|
||||||
FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
|
FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField, CommentField
|
||||||
)
|
)
|
||||||
from .constants import (
|
from .constants import (
|
||||||
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
|
||||||
OBJECTCHANGE_ACTION_CHOICES,
|
OBJECTCHANGE_ACTION_CHOICES,
|
||||||
)
|
)
|
||||||
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange
|
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -190,11 +189,12 @@ class CustomFieldFilterForm(forms.Form):
|
|||||||
|
|
||||||
class TagForm(BootstrapMixin, forms.ModelForm):
|
class TagForm(BootstrapMixin, forms.ModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
comments = CommentField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tag
|
model = Tag
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug',
|
'name', 'slug', 'color', 'comments'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
43
netbox/extras/migrations/0017_tag_taggeditem.py
Normal file
43
netbox/extras/migrations/0017_tag_taggeditem.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Generated by Django 2.1.4 on 2019-02-20 06:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import utilities.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('extras', '0016_exporttemplate_add_cable'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Tag',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('slug', models.SlugField(max_length=100, unique=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TaggedItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('object_id', models.IntegerField(db_index=True)),
|
||||||
|
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extras_taggeditem_tagged_items', to='contenttypes.ContentType')),
|
||||||
|
('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extras_taggeditem_items', to='extras.Tag')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterIndexTogether(
|
||||||
|
name='taggeditem',
|
||||||
|
index_together={('content_type', 'object_id')},
|
||||||
|
),
|
||||||
|
]
|
65
netbox/extras/migrations/0018_tag_data.py
Normal file
65
netbox/extras/migrations/0018_tag_data.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# Generated by Django 2.1.4 on 2019-02-20 06:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import utilities.fields
|
||||||
|
|
||||||
|
|
||||||
|
def copy_tags(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Copy data from taggit_tag to extras_tag
|
||||||
|
"""
|
||||||
|
TaggitTag = apps.get_model('taggit', 'Tag')
|
||||||
|
ExtrasTag = apps.get_model('extras', 'Tag')
|
||||||
|
|
||||||
|
tags_values = TaggitTag.objects.all().values('id', 'name', 'slug')
|
||||||
|
tags = [ExtrasTag(**tag) for tag in tags_values]
|
||||||
|
ExtrasTag.objects.bulk_create(tags)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_taggeditems(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Copy data from taggit_taggeditem to extras_taggeditem
|
||||||
|
"""
|
||||||
|
TaggitTaggedItem = apps.get_model('taggit', 'TaggedItem')
|
||||||
|
ExtrasTaggedItem = apps.get_model('extras', 'TaggedItem')
|
||||||
|
|
||||||
|
tagged_items_values = TaggitTaggedItem.objects.all().values('id', 'object_id', 'content_type_id', 'tag_id')
|
||||||
|
tagged_items = [ExtrasTaggedItem(**tagged_item) for tagged_item in tagged_items_values]
|
||||||
|
ExtrasTaggedItem.objects.bulk_create(tagged_items)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_taggit_taggeditems(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Delete all TaggedItem instances from taggit_taggeditem
|
||||||
|
"""
|
||||||
|
TaggitTaggedItem = apps.get_model('taggit', 'TaggedItem')
|
||||||
|
TaggitTaggedItem.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_taggit_tags(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Delete all Tag instances from taggit_tag
|
||||||
|
"""
|
||||||
|
TaggitTag = apps.get_model('taggit', 'Tag')
|
||||||
|
TaggitTag.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0017_tag_taggeditem'),
|
||||||
|
('circuits', '0015_custom_tag_models'),
|
||||||
|
('dcim', '0070_custom_tag_models'),
|
||||||
|
('ipam', '0025_custom_tag_models'),
|
||||||
|
('secrets', '0006_custom_tag_models'),
|
||||||
|
('tenancy', '0006_custom_tag_models'),
|
||||||
|
('virtualization', '0009_custom_tag_models'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(copy_tags),
|
||||||
|
migrations.RunPython(copy_taggeditems),
|
||||||
|
migrations.RunPython(delete_taggit_taggeditems),
|
||||||
|
migrations.RunPython(delete_taggit_tags),
|
||||||
|
]
|
@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 2.1.4 on 2019-02-20 07:38
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import utilities.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0018_tag_data'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tag',
|
||||||
|
name='color',
|
||||||
|
field=utilities.fields.ColorField(max_length=6, default='9e9e9e'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tag',
|
||||||
|
name='comments',
|
||||||
|
field=models.TextField(blank=True, default=''),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tag',
|
||||||
|
name='created',
|
||||||
|
field=models.DateField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tag',
|
||||||
|
name='last_updated',
|
||||||
|
field=models.DateTimeField(auto_now=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -12,8 +12,10 @@ from django.db.models import F, Q
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.template import Template, Context
|
from django.template import Template, Context
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from taggit.models import TagBase, GenericTaggedItemBase
|
||||||
|
|
||||||
from dcim.constants import CONNECTION_STATUS_CONNECTED
|
from dcim.constants import CONNECTION_STATUS_CONNECTED
|
||||||
|
from utilities.fields import ColorField
|
||||||
from utilities.utils import deepmerge, foreground_color
|
from utilities.utils import deepmerge, foreground_color
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .querysets import ConfigContextQuerySet
|
from .querysets import ConfigContextQuerySet
|
||||||
@ -860,3 +862,37 @@ class ObjectChange(models.Model):
|
|||||||
self.object_repr,
|
self.object_repr,
|
||||||
self.object_data,
|
self.object_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Tags
|
||||||
|
#
|
||||||
|
|
||||||
|
# TODO: figure out a way around this circular import for ObjectChange
|
||||||
|
from utilities.models import ChangeLoggedModel # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(TagBase, ChangeLoggedModel):
|
||||||
|
color = ColorField(
|
||||||
|
default='9e9e9e'
|
||||||
|
)
|
||||||
|
comments = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default=''
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('extras:tag', args=[self.slug])
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedItem(GenericTaggedItemBase):
|
||||||
|
tag = models.ForeignKey(
|
||||||
|
to=Tag,
|
||||||
|
related_name="%(app_label)s_%(class)s_items",
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
index_together = (
|
||||||
|
("content_type", "object_id")
|
||||||
|
)
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
from taggit.models import Tag, TaggedItem
|
|
||||||
|
|
||||||
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
|
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
|
||||||
from .models import ConfigContext, ObjectChange
|
from .models import ConfigContext, ObjectChange, Tag, TaggedItem
|
||||||
|
|
||||||
TAG_ACTIONS = """
|
TAG_ACTIONS = """
|
||||||
|
<a href="{% url 'extras:tag_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||||
|
<i class="fa fa-history"></i>
|
||||||
|
</a>
|
||||||
{% if perms.taggit.change_tag %}
|
{% if perms.taggit.change_tag %}
|
||||||
<a href="{% url 'extras:tag_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
<a href="{% url 'extras:tag_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -71,10 +73,11 @@ class TagTable(BaseTable):
|
|||||||
attrs={'td': {'class': 'text-right'}},
|
attrs={'td': {'class': 'text-right'}},
|
||||||
verbose_name=''
|
verbose_name=''
|
||||||
)
|
)
|
||||||
|
color = ColorColumn()
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Tag
|
model = Tag
|
||||||
fields = ('pk', 'name', 'items', 'slug', 'actions')
|
fields = ('pk', 'name', 'items', 'slug', 'color', 'actions')
|
||||||
|
|
||||||
|
|
||||||
class TaggedItemTable(BaseTable):
|
class TaggedItemTable(BaseTable):
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from taggit.models import Tag
|
|
||||||
|
|
||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Region, Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Region, Site
|
||||||
from extras.constants import GRAPH_TYPE_SITE
|
from extras.constants import GRAPH_TYPE_SITE
|
||||||
from extras.models import ConfigContext, Graph, ExportTemplate
|
from extras.models import ConfigContext, Graph, ExportTemplate, Tag
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.testing import APITestCase
|
from utilities.testing import APITestCase
|
||||||
|
|
||||||
|
@ -4,10 +4,9 @@ import uuid
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from taggit.models import Tag
|
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.models import ConfigContext, ObjectChange
|
from extras.models import ConfigContext, ObjectChange, Tag
|
||||||
|
|
||||||
|
|
||||||
class TagTestCase(TestCase):
|
class TagTestCase(TestCase):
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from extras import views
|
from extras import views
|
||||||
|
from extras.models import Tag
|
||||||
|
|
||||||
|
|
||||||
app_name = 'extras'
|
app_name = 'extras'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -11,6 +13,7 @@ urlpatterns = [
|
|||||||
url(r'^tags/(?P<slug>[\w-]+)/$', views.TagView.as_view(), name='tag'),
|
url(r'^tags/(?P<slug>[\w-]+)/$', views.TagView.as_view(), name='tag'),
|
||||||
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
|
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
|
||||||
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
|
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||||
|
url(r'^tags/(?P<slug>[\w-]+)/changelog/$', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
|
||||||
|
|
||||||
# Config contexts
|
# Config contexts
|
||||||
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
||||||
|
@ -9,7 +9,6 @@ from django.shortcuts import get_object_or_404, redirect, render
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
from taggit.models import Tag, TaggedItem
|
|
||||||
|
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.paginator import EnhancedPaginator
|
from utilities.paginator import EnhancedPaginator
|
||||||
@ -19,7 +18,7 @@ from .forms import (
|
|||||||
ConfigContextForm, ConfigContextBulkEditForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm,
|
ConfigContextForm, ConfigContextBulkEditForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm,
|
||||||
TagFilterForm, TagForm,
|
TagFilterForm, TagForm,
|
||||||
)
|
)
|
||||||
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
|
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem
|
||||||
from .reports import get_report, get_reports
|
from .reports import get_report, get_reports
|
||||||
from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable
|
from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable
|
||||||
|
|
||||||
@ -30,7 +29,7 @@ from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemT
|
|||||||
|
|
||||||
class TagListView(ObjectListView):
|
class TagListView(ObjectListView):
|
||||||
queryset = Tag.objects.annotate(
|
queryset = Tag.objects.annotate(
|
||||||
items=Count('taggit_taggeditem_items')
|
items=Count('extras_taggeditem_items')
|
||||||
).order_by(
|
).order_by(
|
||||||
'name'
|
'name'
|
||||||
)
|
)
|
||||||
@ -69,22 +68,23 @@ class TagView(View):
|
|||||||
|
|
||||||
|
|
||||||
class TagEditView(PermissionRequiredMixin, ObjectEditView):
|
class TagEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'taggit.change_tag'
|
permission_required = 'extras.change_tag'
|
||||||
model = Tag
|
model = Tag
|
||||||
model_form = TagForm
|
model_form = TagForm
|
||||||
default_return_url = 'extras:tag_list'
|
default_return_url = 'extras:tag_list'
|
||||||
|
template_name = 'extras/tag_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'taggit.delete_tag'
|
permission_required = 'extras.delete_tag'
|
||||||
model = Tag
|
model = Tag
|
||||||
default_return_url = 'extras:tag_list'
|
default_return_url = 'extras:tag_list'
|
||||||
|
|
||||||
|
|
||||||
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'taggit.delete_tag'
|
permission_required = 'extras.delete_tag'
|
||||||
queryset = Tag.objects.annotate(
|
queryset = Tag.objects.annotate(
|
||||||
items=Count('taggit_taggeditem_items')
|
items=Count('extras_taggeditem_items')
|
||||||
).order_by(
|
).order_by(
|
||||||
'name'
|
'name'
|
||||||
)
|
)
|
||||||
|
45
netbox/ipam/migrations/0025_custom_tag_models.py
Normal file
45
netbox/ipam/migrations/0025_custom_tag_models.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 2.1.4 on 2019-02-20 06:56
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0024_vrf_allow_null_rd'),
|
||||||
|
('extras', '0017_tag_taggeditem'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='aggregate',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ipaddress',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='service',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vlan',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vrf',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
]
|
@ -10,7 +10,7 @@ from django.urls import reverse
|
|||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
from extras.models import CustomFieldModel
|
from extras.models import CustomFieldModel, TaggedItem
|
||||||
from utilities.models import ChangeLoggedModel
|
from utilities.models import ChangeLoggedModel
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .fields import IPNetworkField, IPAddressField
|
from .fields import IPNetworkField, IPAddressField
|
||||||
@ -55,7 +55,7 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
|
|||||||
object_id_field='obj_id'
|
object_id_field='obj_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||||
|
|
||||||
@ -154,7 +154,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
|
|||||||
object_id_field='obj_id'
|
object_id_field='obj_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['prefix', 'rir', 'date_added', 'description']
|
csv_headers = ['prefix', 'rir', 'date_added', 'description']
|
||||||
|
|
||||||
@ -324,7 +324,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = PrefixQuerySet.as_manager()
|
objects = PrefixQuerySet.as_manager()
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
|
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
|
||||||
@ -583,7 +583,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
objects = IPAddressManager()
|
objects = IPAddressManager()
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
|
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
|
||||||
@ -790,7 +790,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
|
|||||||
object_id_field='obj_id'
|
object_id_field='obj_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||||
|
|
||||||
@ -892,7 +892,7 @@ class Service(ChangeLoggedModel, CustomFieldModel):
|
|||||||
object_id_field='obj_id'
|
object_id_field='obj_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'description']
|
csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'description']
|
||||||
|
|
||||||
|
@ -2,8 +2,17 @@ from django.conf import settings
|
|||||||
from django.contrib.admin import AdminSite
|
from django.contrib.admin import AdminSite
|
||||||
from django.contrib.auth.admin import GroupAdmin, UserAdmin
|
from django.contrib.auth.admin import GroupAdmin, UserAdmin
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from taggit.admin import TagAdmin
|
from taggit.admin import TagAdmin, TaggedItemInline
|
||||||
from taggit.models import Tag
|
|
||||||
|
from extras.models import Tag, TaggedItem
|
||||||
|
|
||||||
|
|
||||||
|
class NetBoxTaggedItemInline(TaggedItemInline):
|
||||||
|
model = TaggedItem
|
||||||
|
|
||||||
|
|
||||||
|
class NetBoxTagAdmin(TagAdmin):
|
||||||
|
inlines = [NetBoxTaggedItemInline]
|
||||||
|
|
||||||
|
|
||||||
class NetBoxAdminSite(AdminSite):
|
class NetBoxAdminSite(AdminSite):
|
||||||
@ -20,7 +29,7 @@ admin_site = NetBoxAdminSite(name='admin')
|
|||||||
# Register external models
|
# Register external models
|
||||||
admin_site.register(Group, GroupAdmin)
|
admin_site.register(Group, GroupAdmin)
|
||||||
admin_site.register(User, UserAdmin)
|
admin_site.register(User, UserAdmin)
|
||||||
admin_site.register(Tag, TagAdmin)
|
admin_site.register(Tag, NetBoxTagAdmin)
|
||||||
|
|
||||||
# Modify the template to include an RQ link if django_rq is installed (see RQ_SHOW_ADMIN_LINK)
|
# Modify the template to include an RQ link if django_rq is installed (see RQ_SHOW_ADMIN_LINK)
|
||||||
if settings.WEBHOOKS_ENABLED:
|
if settings.WEBHOOKS_ENABLED:
|
||||||
|
@ -22,7 +22,7 @@ except ImportError:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
VERSION = '2.5.8-dev'
|
VERSION = '2.6.0-dev'
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
20
netbox/secrets/migrations/0006_custom_tag_models.py
Normal file
20
netbox/secrets/migrations/0006_custom_tag_models.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 2.1.4 on 2019-02-20 06:56
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('secrets', '0005_change_logging'),
|
||||||
|
('extras', '0017_tag_taggeditem'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='secret',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
]
|
@ -14,7 +14,7 @@ from django.urls import reverse
|
|||||||
from django.utils.encoding import force_bytes
|
from django.utils.encoding import force_bytes
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from extras.models import CustomFieldModel
|
from extras.models import CustomFieldModel, TaggedItem
|
||||||
from utilities.models import ChangeLoggedModel
|
from utilities.models import ChangeLoggedModel
|
||||||
from .exceptions import InvalidKey
|
from .exceptions import InvalidKey
|
||||||
from .hashers import SecretValidationHasher
|
from .hashers import SecretValidationHasher
|
||||||
@ -345,7 +345,7 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
|||||||
object_id_field='obj_id'
|
object_id_field='obj_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
plaintext = None
|
plaintext = None
|
||||||
csv_headers = ['device', 'role', 'name', 'plaintext']
|
csv_headers = ['device', 'role', 'name', 'plaintext']
|
||||||
|
@ -445,6 +445,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Description</th>
|
||||||
<th colspan="2">Installed Device</th>
|
<th colspan="2">Installed Device</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -570,6 +571,7 @@
|
|||||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
<th>Cable</th>
|
<th>Cable</th>
|
||||||
<th colspan="2">Connection</th>
|
<th colspan="2">Connection</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
@ -625,6 +627,7 @@
|
|||||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
<th>Cable</th>
|
<th>Cable</th>
|
||||||
<th colspan="2">Connection</th>
|
<th colspan="2">Connection</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
@ -5,6 +5,11 @@
|
|||||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp }}
|
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{# Description #}
|
||||||
|
<td>
|
||||||
|
{{ cp.description }}
|
||||||
|
</td>
|
||||||
|
|
||||||
{# Cable #}
|
{# Cable #}
|
||||||
<td>
|
<td>
|
||||||
{% if cp.cable %}
|
{% if cp.cable %}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
<tr class="consoleserverport{% if csp.cable.status %} success{% elif csp.cable %} info{% endif %}">
|
<tr class="consoleserverport{% if csp.cable.status %} success{% elif csp.cable %} info{% endif %}">
|
||||||
|
|
||||||
{# Checkbox #}
|
{# Checkbox #}
|
||||||
@ -12,12 +14,17 @@
|
|||||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
|
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{# Description #}
|
||||||
|
<td>
|
||||||
|
{{ csp.description|placeholder }}
|
||||||
|
</td>
|
||||||
|
|
||||||
{# Cable #}
|
{# Cable #}
|
||||||
<td>
|
<td>
|
||||||
{% if csp.cable %}
|
{% if csp.cable %}
|
||||||
<a href="{{ csp.cable.get_absolute_url }}">{{ csp.cable }}</a>
|
<a href="{{ csp.cable.get_absolute_url }}">{{ csp.cable }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
—
|
<span class="text-muted">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
@ -1,16 +1,35 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
<tr class="devicebay">
|
<tr class="devicebay">
|
||||||
{% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
|
{% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
|
||||||
<td class="pk">
|
<td class="pk">
|
||||||
<input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
|
<input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{# Name #}
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
|
<i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{# Status #}
|
||||||
|
<td>
|
||||||
|
{% if devicebay.installed_device %}
|
||||||
|
<span class="label label-{{ devicebay.installed_device.get_status_class }}">
|
||||||
|
{{ devicebay.installed_device.get_status_display }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="label label-default">Vacant</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Description #}
|
||||||
|
<td>
|
||||||
|
{{ devicebay.description|placeholder }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Installed device #}
|
||||||
{% if devicebay.installed_device %}
|
{% if devicebay.installed_device %}
|
||||||
<td>
|
|
||||||
<span class="label label-{{ devicebay.installed_device.get_status_class }}">{{ devicebay.installed_device.get_status_display }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=devicebay.installed_device.pk %}">{{ devicebay.installed_device }}</a>
|
<a href="{% url 'dcim:device' pk=devicebay.installed_device.pk %}">{{ devicebay.installed_device }}</a>
|
||||||
</td>
|
</td>
|
||||||
@ -18,11 +37,10 @@
|
|||||||
<span>{{ devicebay.installed_device.device_type.display_name }}</span>
|
<span>{{ devicebay.installed_device.device_type.display_name }}</span>
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td></td>
|
<td colspan="2"></td>
|
||||||
<td colspan="2">
|
|
||||||
<span class="text-muted">Vacant</span>
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{# Actions #}
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.dcim.change_devicebay %}
|
{% if perms.dcim.change_devicebay %}
|
||||||
{% if devicebay.installed_device %}
|
{% if devicebay.installed_device %}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
<tr class="poweroutlet{% if po.cable.status %} success{% elif po.cable %} info{% endif %}">
|
<tr class="poweroutlet{% if po.cable.status %} success{% elif po.cable %} info{% endif %}">
|
||||||
|
|
||||||
{# Checkbox #}
|
{# Checkbox #}
|
||||||
@ -12,12 +14,17 @@
|
|||||||
<i class="fa fa-fw fa-bolt"></i> {{ po }}
|
<i class="fa fa-fw fa-bolt"></i> {{ po }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{# Description #}
|
||||||
|
<td>
|
||||||
|
{{ po.description|placeholder }}
|
||||||
|
</td>
|
||||||
|
|
||||||
{# Cable #}
|
{# Cable #}
|
||||||
<td>
|
<td>
|
||||||
{% if po.cable %}
|
{% if po.cable %}
|
||||||
<a href="{{ po.cable.get_absolute_url }}">{{ po.cable }}</a>
|
<a href="{{ po.cable.get_absolute_url }}">{{ po.cable }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
—
|
<span class="text-muted">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
@ -5,6 +5,11 @@
|
|||||||
<i class="fa fa-fw fa-bolt"></i> {{ pp }}
|
<i class="fa fa-fw fa-bolt"></i> {{ pp }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
{# Description #}
|
||||||
|
<td>
|
||||||
|
{{ pp.description }}
|
||||||
|
</td>
|
||||||
|
|
||||||
{# Cable #}
|
{# Cable #}
|
||||||
<td>
|
<td>
|
||||||
{% if pp.cable %}
|
{% if pp.cable %}
|
||||||
|
@ -31,6 +31,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}Tag: {{ tag }}{% endblock %}</h1>
|
<h1>{% block title %}Tag: {{ tag }}{% endblock %}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=tag %}
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||||
|
<a href="{{ tag.get_absolute_url }}">Tag</a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||||
|
<a href="{% url 'extras:tag_changelog' slug=tag.slug %}">Changelog</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -59,8 +68,26 @@
|
|||||||
{{ items_count }}
|
{{ items_count }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Color</td>
|
||||||
|
<td>
|
||||||
|
<span class="label color-block" style="background-color: #{{ tag.color }}"> </span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Comments</strong>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body rendered-markdown">
|
||||||
|
{% if tag.comments %}
|
||||||
|
{{ tag.comments|gfm }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% include 'panel_table.html' with table=items_table heading='Tagged Objects' %}
|
{% include 'panel_table.html' with table=items_table heading='Tagged Objects' %}
|
||||||
|
19
netbox/templates/extras/tag_edit.html
Normal file
19
netbox/templates/extras/tag_edit.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% extends 'utilities/obj_edit.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tag</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.name %}
|
||||||
|
{% render_field form.slug %}
|
||||||
|
{% render_field form.color %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Comments</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.comments %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -1,5 +1,7 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
{% if url_name %}
|
{% if url_name %}
|
||||||
<a href="{% url url_name %}?tag={{ tag.slug }}"><span class="label label-default">{{ tag }}</span></a>
|
<a href="{% url url_name %}?tag={{ tag.slug }}"><span class="label label-default" style="color: {{ tag.color|fgcolor }}; background-color: #{{ tag.color }}">{{ tag }}</span></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="label label-default">{{ tag }}</span>
|
<span class="label label-default">{{ tag }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
20
netbox/tenancy/migrations/0006_custom_tag_models.py
Normal file
20
netbox/tenancy/migrations/0006_custom_tag_models.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 2.1.4 on 2019-02-20 06:56
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tenancy', '0005_change_logging'),
|
||||||
|
('extras', '0017_tag_taggeditem'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tenant',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
]
|
@ -3,7 +3,7 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from extras.models import CustomFieldModel
|
from extras.models import CustomFieldModel, TaggedItem
|
||||||
from utilities.models import ChangeLoggedModel
|
from utilities.models import ChangeLoggedModel
|
||||||
|
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ class Tenant(ChangeLoggedModel, CustomFieldModel):
|
|||||||
object_id_field='obj_id'
|
object_id_field='obj_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['name', 'slug', 'group', 'description', 'comments']
|
csv_headers = ['name', 'slug', 'group', 'description', 'comments']
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from taggit.models import Tag
|
|
||||||
|
from extras.models import Tag
|
||||||
|
|
||||||
|
|
||||||
class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||||
|
@ -157,7 +157,7 @@ class ObjectListView(View):
|
|||||||
|
|
||||||
# Construct queryset for tags list
|
# Construct queryset for tags list
|
||||||
if hasattr(model, 'tags'):
|
if hasattr(model, 'tags'):
|
||||||
tags = model.tags.annotate(count=Count('taggit_taggeditem_items')).order_by('name')
|
tags = model.tags.annotate(count=Count('extras_taggeditem_items')).order_by('name')
|
||||||
else:
|
else:
|
||||||
tags = None
|
tags = None
|
||||||
|
|
||||||
|
@ -50,16 +50,23 @@ class VirtualMachineViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
"""
|
"""
|
||||||
Include rendered config context when retrieving a single VirtualMachine.
|
Select the specific serializer based on the request context.
|
||||||
|
|
||||||
|
If the `brief` query param equates to True, return the NestedVirtualMachineSerializer
|
||||||
|
|
||||||
|
If the `exclude` query param includes `config_context` as a value, return the VirtualMachineSerializer
|
||||||
|
|
||||||
|
Else, return the VirtualMachineWithConfigContextSerializer
|
||||||
"""
|
"""
|
||||||
if self.action == 'retrieve':
|
|
||||||
return serializers.VirtualMachineWithConfigContextSerializer
|
|
||||||
|
|
||||||
request = self.get_serializer_context()['request']
|
request = self.get_serializer_context()['request']
|
||||||
if request.query_params.get('brief', False):
|
if request.query_params.get('brief', False):
|
||||||
return serializers.NestedVirtualMachineSerializer
|
return serializers.NestedVirtualMachineSerializer
|
||||||
|
|
||||||
return serializers.VirtualMachineSerializer
|
elif 'config_context' in request.query_params.get('exclude', []):
|
||||||
|
return serializers.VirtualMachineSerializer
|
||||||
|
|
||||||
|
return serializers.VirtualMachineWithConfigContextSerializer
|
||||||
|
|
||||||
|
|
||||||
class InterfaceViewSet(ModelViewSet):
|
class InterfaceViewSet(ModelViewSet):
|
||||||
|
25
netbox/virtualization/migrations/0009_custom_tag_models.py
Normal file
25
netbox/virtualization/migrations/0009_custom_tag_models.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 2.1.4 on 2019-02-20 06:56
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('virtualization', '0008_virtualmachine_local_context_data'),
|
||||||
|
('extras', '0017_tag_taggeditem'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='virtualmachine',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
]
|
@ -6,7 +6,7 @@ from django.urls import reverse
|
|||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from extras.models import ConfigContextModel, CustomFieldModel
|
from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
|
||||||
from utilities.models import ChangeLoggedModel
|
from utilities.models import ChangeLoggedModel
|
||||||
from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES
|
from .constants import DEVICE_STATUS_ACTIVE, VM_STATUS_CHOICES, VM_STATUS_CLASSES
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
|
|||||||
object_id_field='obj_id'
|
object_id_field='obj_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = ['name', 'type', 'group', 'site', 'comments']
|
csv_headers = ['name', 'type', 'group', 'site', 'comments']
|
||||||
|
|
||||||
@ -238,7 +238,7 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
|||||||
object_id_field='obj_id'
|
object_id_field='obj_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TaggableManager()
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
csv_headers = [
|
csv_headers = [
|
||||||
'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
||||||
|
@ -337,6 +337,14 @@ class VirtualMachineTest(APITestCase):
|
|||||||
self.virtualmachine1 = VirtualMachine.objects.create(name='Test Virtual Machine 1', cluster=self.cluster1)
|
self.virtualmachine1 = VirtualMachine.objects.create(name='Test Virtual Machine 1', cluster=self.cluster1)
|
||||||
self.virtualmachine2 = VirtualMachine.objects.create(name='Test Virtual Machine 2', cluster=self.cluster1)
|
self.virtualmachine2 = VirtualMachine.objects.create(name='Test Virtual Machine 2', cluster=self.cluster1)
|
||||||
self.virtualmachine3 = VirtualMachine.objects.create(name='Test Virtual Machine 3', cluster=self.cluster1)
|
self.virtualmachine3 = VirtualMachine.objects.create(name='Test Virtual Machine 3', cluster=self.cluster1)
|
||||||
|
self.virtualmachine_with_context_data = VirtualMachine.objects.create(
|
||||||
|
name='VM with context data',
|
||||||
|
cluster=self.cluster1,
|
||||||
|
local_context_data={
|
||||||
|
'A': 1,
|
||||||
|
'B': 2
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def test_get_virtualmachine(self):
|
def test_get_virtualmachine(self):
|
||||||
|
|
||||||
@ -350,7 +358,7 @@ class VirtualMachineTest(APITestCase):
|
|||||||
url = reverse('virtualization-api:virtualmachine-list')
|
url = reverse('virtualization-api:virtualmachine-list')
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
|
|
||||||
self.assertEqual(response.data['count'], 3)
|
self.assertEqual(response.data['count'], 4)
|
||||||
|
|
||||||
def test_list_virtualmachines_brief(self):
|
def test_list_virtualmachines_brief(self):
|
||||||
|
|
||||||
@ -373,7 +381,7 @@ class VirtualMachineTest(APITestCase):
|
|||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(VirtualMachine.objects.count(), 4)
|
self.assertEqual(VirtualMachine.objects.count(), 5)
|
||||||
virtualmachine4 = VirtualMachine.objects.get(pk=response.data['id'])
|
virtualmachine4 = VirtualMachine.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(virtualmachine4.name, data['name'])
|
self.assertEqual(virtualmachine4.name, data['name'])
|
||||||
self.assertEqual(virtualmachine4.cluster.pk, data['cluster'])
|
self.assertEqual(virtualmachine4.cluster.pk, data['cluster'])
|
||||||
@ -388,7 +396,7 @@ class VirtualMachineTest(APITestCase):
|
|||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertEqual(VirtualMachine.objects.count(), 3)
|
self.assertEqual(VirtualMachine.objects.count(), 4)
|
||||||
|
|
||||||
def test_create_virtualmachine_bulk(self):
|
def test_create_virtualmachine_bulk(self):
|
||||||
|
|
||||||
@ -411,7 +419,7 @@ class VirtualMachineTest(APITestCase):
|
|||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(VirtualMachine.objects.count(), 6)
|
self.assertEqual(VirtualMachine.objects.count(), 7)
|
||||||
self.assertEqual(response.data[0]['name'], data[0]['name'])
|
self.assertEqual(response.data[0]['name'], data[0]['name'])
|
||||||
self.assertEqual(response.data[1]['name'], data[1]['name'])
|
self.assertEqual(response.data[1]['name'], data[1]['name'])
|
||||||
self.assertEqual(response.data[2]['name'], data[2]['name'])
|
self.assertEqual(response.data[2]['name'], data[2]['name'])
|
||||||
@ -438,7 +446,7 @@ class VirtualMachineTest(APITestCase):
|
|||||||
response = self.client.put(url, data, format='json', **self.header)
|
response = self.client.put(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(VirtualMachine.objects.count(), 3)
|
self.assertEqual(VirtualMachine.objects.count(), 4)
|
||||||
virtualmachine1 = VirtualMachine.objects.get(pk=response.data['id'])
|
virtualmachine1 = VirtualMachine.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(virtualmachine1.name, data['name'])
|
self.assertEqual(virtualmachine1.name, data['name'])
|
||||||
self.assertEqual(virtualmachine1.cluster.pk, data['cluster'])
|
self.assertEqual(virtualmachine1.cluster.pk, data['cluster'])
|
||||||
@ -451,7 +459,22 @@ class VirtualMachineTest(APITestCase):
|
|||||||
response = self.client.delete(url, **self.header)
|
response = self.client.delete(url, **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||||
self.assertEqual(VirtualMachine.objects.count(), 2)
|
self.assertEqual(VirtualMachine.objects.count(), 3)
|
||||||
|
|
||||||
|
def test_config_context_included_by_default_in_list_view(self):
|
||||||
|
|
||||||
|
url = reverse('virtualization-api:virtualmachine-list')
|
||||||
|
url = '{}?id__in={}'.format(url, self.virtualmachine_with_context_data.pk)
|
||||||
|
response = self.client.get(url, **self.header)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['results'][0].get('config_context', {}).get('A'), 1)
|
||||||
|
|
||||||
|
def test_config_context_excluded(self):
|
||||||
|
|
||||||
|
url = reverse('virtualization-api:virtualmachine-list') + '?exclude=config_context'
|
||||||
|
response = self.client.get(url, **self.header)
|
||||||
|
|
||||||
|
self.assertFalse('config_context' in response.data['results'][0])
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTest(APITestCase):
|
class InterfaceTest(APITestCase):
|
||||||
|
Loading…
Reference in New Issue
Block a user