Merge branch 'develop-2.6' of github.com:digitalocean/netbox into develop-2.6

This commit is contained in:
John Anderson 2019-03-03 19:06:05 -05:00
commit 5991bd368c
47 changed files with 810 additions and 137 deletions

View File

@ -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

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

View File

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

View File

@ -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']
# #

View File

@ -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):

View File

@ -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):

View File

@ -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(),

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

View File

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

View File

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

View File

@ -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):

View File

@ -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']
# #

View File

@ -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

View File

@ -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):

View File

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

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

View 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),
]

View File

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

View File

@ -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")
)

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

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

View File

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

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

View File

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

View File

@ -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:

View File

@ -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__)))

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

View File

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

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}
&mdash; <span class="text-muted">&mdash;</span>
{% endif %} {% endif %}
</td> </td>

View File

@ -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 %}

View File

@ -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 %}
&mdash; <span class="text-muted">&mdash;</span>
{% endif %} {% endif %}
</td> </td>

View File

@ -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 %}

View File

@ -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 }}">&nbsp;</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' %}

View 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 %}

View File

@ -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 %}

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

View File

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

View File

@ -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):

View File

@ -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

View File

@ -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):

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

View File

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

View File

@ -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):