7376 csv tags (#10802)

* 7376 add tags to CSV import

* 7376 change help text

* 7376 validate tags

* 7376 fix tests

* 7376 add tag validation tests

* Introduce CSVModelMultipleChoiceField for CSV import tag assignment

* Clean up CSVImportTestCase

Co-authored-by: jeremystretch <jstretch@ns1.com>
This commit is contained in:
Arthur Hanson 2022-11-04 07:50:43 -07:00 committed by GitHub
parent bc6b5bc4be
commit cdeb65e2fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 165 additions and 61 deletions

View File

@ -18,7 +18,7 @@ class ProviderCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Provider model = Provider
fields = ( fields = (
'name', 'slug', 'account', 'description', 'comments', 'name', 'slug', 'account', 'description', 'comments', 'tags',
) )
@ -32,7 +32,7 @@ class ProviderNetworkCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = ProviderNetwork model = ProviderNetwork
fields = [ fields = [
'provider', 'name', 'service_id', 'description', 'comments', 'provider', 'name', 'service_id', 'description', 'comments', 'tags'
] ]
@ -41,7 +41,7 @@ class CircuitTypeCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = ('name', 'slug', 'description') fields = ('name', 'slug', 'description', 'tags')
help_texts = { help_texts = {
'name': 'Name of circuit type', 'name': 'Name of circuit type',
} }
@ -73,5 +73,5 @@ class CircuitCSVForm(NetBoxModelCSVForm):
model = Circuit model = Circuit
fields = [ fields = [
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
'description', 'comments', 'description', 'comments', 'tags'
] ]

View File

@ -56,7 +56,7 @@ class RegionCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Region model = Region
fields = ('name', 'slug', 'parent', 'description') fields = ('name', 'slug', 'parent', 'description', 'tags')
class SiteGroupCSVForm(NetBoxModelCSVForm): class SiteGroupCSVForm(NetBoxModelCSVForm):
@ -100,7 +100,7 @@ class SiteCSVForm(NetBoxModelCSVForm):
model = Site model = Site
fields = ( fields = (
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description',
'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags'
) )
help_texts = { help_texts = {
'time_zone': mark_safe( 'time_zone': mark_safe(
@ -137,7 +137,7 @@ class LocationCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Location model = Location
fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'description') fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'description', 'tags')
class RackRoleCSVForm(NetBoxModelCSVForm): class RackRoleCSVForm(NetBoxModelCSVForm):
@ -145,7 +145,7 @@ class RackRoleCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = RackRole model = RackRole
fields = ('name', 'slug', 'color', 'description') fields = ('name', 'slug', 'color', 'description', 'tags')
help_texts = { help_texts = {
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'), 'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
} }
@ -197,7 +197,7 @@ class RackCSVForm(NetBoxModelCSVForm):
fields = ( fields = (
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag',
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
'description', 'comments', 'description', 'comments', 'tags',
) )
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
@ -241,7 +241,7 @@ class RackReservationCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ('site', 'location', 'rack', 'units', 'tenant', 'description', 'comments') fields = ('site', 'location', 'rack', 'units', 'tenant', 'description', 'comments', 'tags')
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs) super().__init__(data, *args, **kwargs)
@ -264,7 +264,7 @@ class ManufacturerCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = ('name', 'slug', 'description') fields = ('name', 'slug', 'description', 'tags')
class DeviceRoleCSVForm(NetBoxModelCSVForm): class DeviceRoleCSVForm(NetBoxModelCSVForm):
@ -272,7 +272,7 @@ class DeviceRoleCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ('name', 'slug', 'color', 'vm_role', 'description') fields = ('name', 'slug', 'color', 'vm_role', 'description', 'tags')
help_texts = { help_texts = {
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'), 'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
} }
@ -289,7 +289,7 @@ class PlatformCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Platform model = Platform
fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description') fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags')
class BaseDeviceCSVForm(NetBoxModelCSVForm): class BaseDeviceCSVForm(NetBoxModelCSVForm):
@ -388,7 +388,7 @@ class DeviceCSVForm(BaseDeviceCSVForm):
fields = [ fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
'site', 'location', 'rack', 'position', 'face', 'airflow', 'virtual_chassis', 'vc_position', 'vc_priority', 'site', 'location', 'rack', 'position', 'face', 'airflow', 'virtual_chassis', 'vc_position', 'vc_priority',
'cluster', 'description', 'comments', 'cluster', 'description', 'comments', 'tags',
] ]
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
@ -425,7 +425,7 @@ class ModuleCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Module model = Module
fields = ( fields = (
'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'description', 'comments', 'device', 'module_bay', 'module_type', 'serial', 'asset_tag', 'description', 'comments', 'tags',
) )
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
@ -452,7 +452,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
class Meta(BaseDeviceCSVForm.Meta): class Meta(BaseDeviceCSVForm.Meta):
fields = [ fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments', 'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments', 'tags'
] ]
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
@ -503,7 +503,7 @@ class ConsolePortCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description') fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags')
class ConsoleServerPortCSVForm(NetBoxModelCSVForm): class ConsoleServerPortCSVForm(NetBoxModelCSVForm):
@ -526,7 +526,7 @@ class ConsoleServerPortCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description') fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags')
class PowerPortCSVForm(NetBoxModelCSVForm): class PowerPortCSVForm(NetBoxModelCSVForm):
@ -543,7 +543,7 @@ class PowerPortCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = ( fields = (
'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description', 'device', 'name', 'label', 'type', 'mark_connected', 'maximum_draw', 'allocated_draw', 'description', 'tags'
) )
@ -571,7 +571,7 @@ class PowerOutletCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = ('device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description') fields = ('device', 'name', 'label', 'type', 'mark_connected', 'power_port', 'feed_leg', 'description', 'tags')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -659,7 +659,7 @@ class InterfaceCSVForm(NetBoxModelCSVForm):
fields = ( fields = (
'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled', 'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'speed', 'duplex', 'enabled',
'mark_connected', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode', 'mark_connected', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'description', 'poe_mode', 'poe_type', 'mode',
'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'tags'
) )
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
@ -702,7 +702,7 @@ class FrontPortCSVForm(NetBoxModelCSVForm):
model = FrontPort model = FrontPort
fields = ( fields = (
'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position', 'device', 'name', 'label', 'type', 'color', 'mark_connected', 'rear_port', 'rear_port_position',
'description', 'description', 'tags'
) )
help_texts = { help_texts = {
'rear_port_position': 'Mapped position on corresponding rear port', 'rear_port_position': 'Mapped position on corresponding rear port',
@ -743,7 +743,7 @@ class RearPortCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = RearPort model = RearPort
fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description') fields = ('device', 'name', 'label', 'type', 'color', 'mark_connected', 'positions', 'description', 'tags')
help_texts = { help_texts = {
'positions': 'Number of front ports which may be mapped' 'positions': 'Number of front ports which may be mapped'
} }
@ -757,7 +757,7 @@ class ModuleBayCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = ModuleBay model = ModuleBay
fields = ('device', 'name', 'label', 'position', 'description') fields = ('device', 'name', 'label', 'position', 'description', 'tags')
class DeviceBayCSVForm(NetBoxModelCSVForm): class DeviceBayCSVForm(NetBoxModelCSVForm):
@ -777,7 +777,7 @@ class DeviceBayCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = DeviceBay model = DeviceBay
fields = ('device', 'name', 'label', 'installed_device', 'description') fields = ('device', 'name', 'label', 'installed_device', 'description', 'tags')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -832,7 +832,7 @@ class InventoryItemCSVForm(NetBoxModelCSVForm):
model = InventoryItem model = InventoryItem
fields = ( fields = (
'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description', 'description', 'tags'
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -928,7 +928,7 @@ class CableCSVForm(NetBoxModelCSVForm):
model = Cable model = Cable
fields = [ fields = [
'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type', 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'description', 'comments', 'tags',
] ]
help_texts = { help_texts = {
'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'), 'color': mark_safe('RGB color in hexadecimal (e.g. <code>00ff00</code>)'),
@ -985,7 +985,7 @@ class VirtualChassisCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = ('name', 'domain', 'master', 'description') fields = ('name', 'domain', 'master', 'description', 'comments', 'tags')
# #
@ -1006,7 +1006,7 @@ class PowerPanelCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = PowerPanel model = PowerPanel
fields = ('site', 'location', 'name', 'description', 'comments') fields = ('site', 'location', 'name', 'description', 'comments', 'tags')
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs) super().__init__(data, *args, **kwargs)
@ -1062,7 +1062,7 @@ class PowerFeedCSVForm(NetBoxModelCSVForm):
model = PowerFeed model = PowerFeed
fields = ( fields = (
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
'voltage', 'amperage', 'max_utilization', 'description', 'comments', 'voltage', 'amperage', 'max_utilization', 'description', 'comments', 'tags',
) )
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):

View File

@ -41,7 +41,7 @@ class VRFCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = VRF model = VRF
fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments') fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments', 'tags')
class RouteTargetCSVForm(NetBoxModelCSVForm): class RouteTargetCSVForm(NetBoxModelCSVForm):
@ -54,7 +54,7 @@ class RouteTargetCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = RouteTarget model = RouteTarget
fields = ('name', 'tenant', 'description', 'comments') fields = ('name', 'tenant', 'description', 'comments', 'tags')
class RIRCSVForm(NetBoxModelCSVForm): class RIRCSVForm(NetBoxModelCSVForm):
@ -62,7 +62,7 @@ class RIRCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = RIR model = RIR
fields = ('name', 'slug', 'is_private', 'description') fields = ('name', 'slug', 'is_private', 'description', 'tags')
help_texts = { help_texts = {
'name': 'RIR name', 'name': 'RIR name',
} }
@ -83,7 +83,7 @@ class AggregateCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Aggregate model = Aggregate
fields = ('prefix', 'rir', 'tenant', 'date_added', 'description', 'comments') fields = ('prefix', 'rir', 'tenant', 'date_added', 'description', 'comments', 'tags')
class ASNCSVForm(NetBoxModelCSVForm): class ASNCSVForm(NetBoxModelCSVForm):
@ -101,8 +101,7 @@ class ASNCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = ASN model = ASN
fields = ('asn', 'rir', 'tenant', 'description', 'comments') fields = ('asn', 'rir', 'tenant', 'description', 'comments', 'tags')
help_texts = {}
class RoleCSVForm(NetBoxModelCSVForm): class RoleCSVForm(NetBoxModelCSVForm):
@ -110,7 +109,7 @@ class RoleCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Role model = Role
fields = ('name', 'slug', 'weight', 'description') fields = ('name', 'slug', 'weight', 'description', 'tags')
class PrefixCSVForm(NetBoxModelCSVForm): class PrefixCSVForm(NetBoxModelCSVForm):
@ -159,7 +158,7 @@ class PrefixCSVForm(NetBoxModelCSVForm):
model = Prefix model = Prefix
fields = ( fields = (
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized',
'description', 'comments', 'description', 'comments', 'tags',
) )
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
@ -204,7 +203,7 @@ class IPRangeCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = IPRange model = IPRange
fields = ( fields = (
'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'description', 'comments', 'start_address', 'end_address', 'vrf', 'tenant', 'status', 'role', 'description', 'comments', 'tags',
) )
@ -257,7 +256,7 @@ class IPAddressCSVForm(NetBoxModelCSVForm):
model = IPAddress model = IPAddress
fields = [ fields = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary', 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary',
'dns_name', 'description', 'comments', 'dns_name', 'description', 'comments', 'tags',
] ]
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
@ -326,7 +325,7 @@ class FHRPGroupCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = FHRPGroup model = FHRPGroup
fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'comments') fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'comments', 'tags')
class VLANGroupCSVForm(NetBoxModelCSVForm): class VLANGroupCSVForm(NetBoxModelCSVForm):
@ -351,7 +350,7 @@ class VLANGroupCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = VLANGroup model = VLANGroup
fields = ('name', 'slug', 'scope_type', 'scope_id', 'min_vid', 'max_vid', 'description') fields = ('name', 'slug', 'scope_type', 'scope_id', 'min_vid', 'max_vid', 'description', 'tags')
labels = { labels = {
'scope_id': 'Scope ID', 'scope_id': 'Scope ID',
} }
@ -389,7 +388,7 @@ class VLANCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = VLAN model = VLAN
fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'comments') fields = ('site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'comments', 'tags')
help_texts = { help_texts = {
'vid': 'Numeric VLAN ID (1-4094)', 'vid': 'Numeric VLAN ID (1-4094)',
'name': 'VLAN name', 'name': 'VLAN name',
@ -404,7 +403,7 @@ class ServiceTemplateCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = ServiceTemplate model = ServiceTemplate
fields = ('name', 'protocol', 'ports', 'description', 'comments') fields = ('name', 'protocol', 'ports', 'description', 'comments', 'tags')
class ServiceCSVForm(NetBoxModelCSVForm): class ServiceCSVForm(NetBoxModelCSVForm):
@ -427,7 +426,7 @@ class ServiceCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Service model = Service
fields = ('device', 'virtual_machine', 'name', 'protocol', 'ports', 'description', 'comments') fields = ('device', 'virtual_machine', 'name', 'protocol', 'ports', 'description', 'comments', 'tags')
class L2VPNCSVForm(NetBoxModelCSVForm): class L2VPNCSVForm(NetBoxModelCSVForm):
@ -443,7 +442,7 @@ class L2VPNCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = L2VPN model = L2VPN
fields = ('identifier', 'name', 'slug', 'type', 'description', 'comments') fields = ('identifier', 'name', 'slug', 'type', 'description', 'comments', 'tags')
class L2VPNTerminationCSVForm(NetBoxModelCSVForm): class L2VPNTerminationCSVForm(NetBoxModelCSVForm):
@ -480,7 +479,7 @@ class L2VPNTerminationCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = L2VPNTermination model = L2VPNTermination
fields = ('l2vpn', 'device', 'virtual_machine', 'interface', 'vlan') fields = ('l2vpn', 'device', 'virtual_machine', 'interface', 'vlan', 'tags')
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs) super().__init__(data, *args, **kwargs)

View File

@ -1,12 +1,13 @@
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Q from django.db.models import Q
from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices
from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin
from extras.models import CustomField, Tag from extras.models import CustomField, Tag
from utilities.forms import BootstrapMixin, CSVModelForm from utilities.forms import BootstrapMixin, CSVModelForm
from utilities.forms.fields import DynamicModelMultipleChoiceField from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField
__all__ = ( __all__ = (
'NetBoxModelForm', 'NetBoxModelForm',
@ -61,7 +62,12 @@ class NetBoxModelCSVForm(CSVModelForm, NetBoxModelForm):
""" """
Base form for creating a NetBox objects from CSV data. Used for bulk importing. Base form for creating a NetBox objects from CSV data. Used for bulk importing.
""" """
tags = None # Temporary fix in lieu of tag import support (see #9158) tags = CSVModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False,
to_field_name='slug',
help_text='Tag slugs separated by commas, encased with double quotes (e.g. "tag1,tag2,tag3")'
)
def _get_custom_fields(self, content_type): def _get_custom_fields(self, content_type):
return CustomField.objects.filter(content_types=content_type).filter( return CustomField.objects.filter(content_types=content_type).filter(

View File

@ -0,0 +1,84 @@
from django.contrib.contenttypes.models import ContentType
from django.test import override_settings
from dcim.models import *
from users.models import ObjectPermission
from utilities.testing import ModelViewTestCase, create_tags
class CSVImportTestCase(ModelViewTestCase):
model = Region
@classmethod
def setUpTestData(cls):
create_tags('Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo')
def _get_csv_data(self, csv_data):
return '\n'.join(csv_data)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_valid_tags(self):
csv_data = (
'name,slug,tags',
'Region 1,region-1,"alpha,bravo"',
'Region 2,region-2,"charlie,delta"',
'Region 3,region-3,echo',
'Region 4,region-4,',
)
data = {
'csv': self._get_csv_data(csv_data),
}
# Assign model-level permission
obj_perm = ObjectPermission(name='Test permission', actions=['add'])
obj_perm.save()
obj_perm.users.add(self.user)
obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
# Try GET with model-level permission
self.assertHttpStatus(self.client.get(self._get_url('import')), 200)
# Test POST with permission
self.assertHttpStatus(self.client.post(self._get_url('import'), data), 200)
regions = Region.objects.all()
self.assertEqual(regions.count(), 4)
region = Region.objects.get(slug="region-4")
self.assertEqual(
list(regions[0].tags.values_list('name', flat=True)),
['Alpha', 'Bravo']
)
self.assertEqual(
list(regions[1].tags.values_list('name', flat=True)),
['Charlie', 'Delta']
)
self.assertEqual(
list(regions[2].tags.values_list('name', flat=True)),
['Echo']
)
self.assertEqual(regions[3].tags.count(), 0)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_invalid_tags(self):
csv_data = (
'name,slug,tags',
'Region 1,region-1,"Alpha,Bravo"', # Valid
'Region 2,region-2,"Alpha,Tango"', # Invalid
)
data = {
'csv': self._get_csv_data(csv_data),
}
# Assign model-level permission
obj_perm = ObjectPermission(name='Test permission', actions=['add'])
obj_perm.save()
obj_perm.users.add(self.user)
obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
# Try GET with model-level permission
self.assertHttpStatus(self.client.get(self._get_url('import')), 200)
# Test POST with permission
self.assertHttpStatus(self.client.post(self._get_url('import'), data), 200)
self.assertEqual(Region.objects.count(), 0)

View File

@ -26,7 +26,7 @@ class TenantGroupCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = TenantGroup model = TenantGroup
fields = ('name', 'slug', 'parent', 'description') fields = ('name', 'slug', 'parent', 'description', 'tags')
class TenantCSVForm(NetBoxModelCSVForm): class TenantCSVForm(NetBoxModelCSVForm):
@ -40,7 +40,7 @@ class TenantCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Tenant model = Tenant
fields = ('name', 'slug', 'group', 'description', 'comments') fields = ('name', 'slug', 'group', 'description', 'comments', 'tags')
# #
@ -58,7 +58,7 @@ class ContactGroupCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = ContactGroup model = ContactGroup
fields = ('name', 'slug', 'parent', 'description') fields = ('name', 'slug', 'parent', 'description', 'tags')
class ContactRoleCSVForm(NetBoxModelCSVForm): class ContactRoleCSVForm(NetBoxModelCSVForm):
@ -79,4 +79,4 @@ class ContactCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Contact model = Contact
fields = ('name', 'title', 'phone', 'email', 'address', 'link', 'group', 'description', 'comments') fields = ('name', 'title', 'phone', 'email', 'address', 'link', 'group', 'description', 'comments', 'tags')

View File

@ -16,6 +16,7 @@ __all__ = (
'CSVDataField', 'CSVDataField',
'CSVFileField', 'CSVFileField',
'CSVModelChoiceField', 'CSVModelChoiceField',
'CSVModelMultipleChoiceField',
'CSVMultipleChoiceField', 'CSVMultipleChoiceField',
'CSVMultipleContentTypeField', 'CSVMultipleContentTypeField',
'CSVTypedChoiceField', 'CSVTypedChoiceField',
@ -142,7 +143,7 @@ class CSVModelChoiceField(forms.ModelChoiceField):
Extends Django's `ModelChoiceField` to provide additional validation for CSV values. Extends Django's `ModelChoiceField` to provide additional validation for CSV values.
""" """
default_error_messages = { default_error_messages = {
'invalid_choice': 'Object not found.', 'invalid_choice': 'Object not found: %(value)s',
} }
def to_python(self, value): def to_python(self, value):
@ -154,6 +155,19 @@ class CSVModelChoiceField(forms.ModelChoiceField):
) )
class CSVModelMultipleChoiceField(forms.ModelMultipleChoiceField):
"""
Extends Django's `ModelMultipleChoiceField` to support comma-separated values.
"""
default_error_messages = {
'invalid_choice': 'Object not found: %(value)s',
}
def clean(self, value):
value = value.split(',') if value else []
return super().clean(value)
class CSVContentTypeField(CSVModelChoiceField): class CSVContentTypeField(CSVModelChoiceField):
""" """
CSV field for referencing a single content type, in the form `<app>.<model>`. CSV field for referencing a single content type, in the form `<app>.<model>`.

View File

@ -21,7 +21,7 @@ class ClusterTypeCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = ClusterType model = ClusterType
fields = ('name', 'slug', 'description') fields = ('name', 'slug', 'description', 'tags')
class ClusterGroupCSVForm(NetBoxModelCSVForm): class ClusterGroupCSVForm(NetBoxModelCSVForm):
@ -29,7 +29,7 @@ class ClusterGroupCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = ClusterGroup model = ClusterGroup
fields = ('name', 'slug', 'description') fields = ('name', 'slug', 'description', 'tags')
class ClusterCSVForm(NetBoxModelCSVForm): class ClusterCSVForm(NetBoxModelCSVForm):
@ -63,7 +63,7 @@ class ClusterCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Cluster model = Cluster
fields = ('name', 'type', 'group', 'status', 'site', 'description', 'comments') fields = ('name', 'type', 'group', 'status', 'site', 'description', 'comments', 'tags')
class VirtualMachineCSVForm(NetBoxModelCSVForm): class VirtualMachineCSVForm(NetBoxModelCSVForm):
@ -114,7 +114,7 @@ class VirtualMachineCSVForm(NetBoxModelCSVForm):
model = VirtualMachine model = VirtualMachine
fields = ( fields = (
'name', 'status', 'role', 'site', 'cluster', 'device', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'name', 'status', 'role', 'site', 'cluster', 'device', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
'description', 'comments', 'description', 'comments', 'tags',
) )
@ -151,7 +151,7 @@ class VMInterfaceCSVForm(NetBoxModelCSVForm):
model = VMInterface model = VMInterface
fields = ( fields = (
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
'vrf', 'vrf', 'tags'
) )
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):

View File

@ -25,7 +25,7 @@ class WirelessLANGroupCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = WirelessLANGroup model = WirelessLANGroup
fields = ('name', 'slug', 'parent', 'description') fields = ('name', 'slug', 'parent', 'description', 'tags')
class WirelessLANCSVForm(NetBoxModelCSVForm): class WirelessLANCSVForm(NetBoxModelCSVForm):
@ -62,6 +62,7 @@ class WirelessLANCSVForm(NetBoxModelCSVForm):
model = WirelessLAN model = WirelessLAN
fields = ( fields = (
'ssid', 'group', 'vlan', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments', 'ssid', 'group', 'vlan', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments',
'tags',
) )
@ -97,5 +98,5 @@ class WirelessLinkCSVForm(NetBoxModelCSVForm):
model = WirelessLink model = WirelessLink
fields = ( fields = (
'interface_a', 'interface_b', 'ssid', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'interface_a', 'interface_b', 'ssid', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description',
'comments', 'comments', 'tags',
) )