mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge pull request #3621 from netbox-community/451-devicetype-import
Enable YAML/JSON-based DeviceType import
This commit is contained in:
@ -1,4 +1,3 @@
|
||||
|
||||
# Rack types
|
||||
RACK_TYPE_2POST = 100
|
||||
RACK_TYPE_4POST = 200
|
||||
@ -58,7 +57,10 @@ SUBDEVICE_ROLE_CHOICES = (
|
||||
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
||||
)
|
||||
|
||||
# Interface types
|
||||
#
|
||||
# Numeric interface types
|
||||
#
|
||||
|
||||
# Virtual
|
||||
IFACE_TYPE_VIRTUAL = 0
|
||||
IFACE_TYPE_LAG = 200
|
||||
@ -113,15 +115,15 @@ IFACE_TYPE_16GFC_SFP_PLUS = 3160
|
||||
IFACE_TYPE_32GFC_SFP28 = 3320
|
||||
IFACE_TYPE_128GFC_QSFP28 = 3400
|
||||
# InfiniBand
|
||||
IFACE_FF_INFINIBAND_SDR = 7010
|
||||
IFACE_FF_INFINIBAND_DDR = 7020
|
||||
IFACE_FF_INFINIBAND_QDR = 7030
|
||||
IFACE_FF_INFINIBAND_FDR10 = 7040
|
||||
IFACE_FF_INFINIBAND_FDR = 7050
|
||||
IFACE_FF_INFINIBAND_EDR = 7060
|
||||
IFACE_FF_INFINIBAND_HDR = 7070
|
||||
IFACE_FF_INFINIBAND_NDR = 7080
|
||||
IFACE_FF_INFINIBAND_XDR = 7090
|
||||
IFACE_TYPE_INFINIBAND_SDR = 7010
|
||||
IFACE_TYPE_INFINIBAND_DDR = 7020
|
||||
IFACE_TYPE_INFINIBAND_QDR = 7030
|
||||
IFACE_TYPE_INFINIBAND_FDR10 = 7040
|
||||
IFACE_TYPE_INFINIBAND_FDR = 7050
|
||||
IFACE_TYPE_INFINIBAND_EDR = 7060
|
||||
IFACE_TYPE_INFINIBAND_HDR = 7070
|
||||
IFACE_TYPE_INFINIBAND_NDR = 7080
|
||||
IFACE_TYPE_INFINIBAND_XDR = 7090
|
||||
# Serial
|
||||
IFACE_TYPE_T1 = 4000
|
||||
IFACE_TYPE_E1 = 4010
|
||||
@ -227,15 +229,15 @@ IFACE_TYPE_CHOICES = [
|
||||
[
|
||||
'InfiniBand',
|
||||
[
|
||||
[IFACE_FF_INFINIBAND_SDR, 'SDR (2 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_DDR, 'DDR (4 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_QDR, 'QDR (8 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_FDR, 'FDR (13.5 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_EDR, 'EDR (25 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_HDR, 'HDR (50 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_NDR, 'NDR (100 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_XDR, 'XDR (250 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_SDR, 'SDR (2 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_DDR, 'DDR (4 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_QDR, 'QDR (8 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_FDR, 'FDR (13.5 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_EDR, 'EDR (25 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_HDR, 'HDR (50 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_NDR, 'NDR (100 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_XDR, 'XDR (250 Gbps)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
@ -382,7 +384,8 @@ CONNECTION_STATUS_CHOICES = [
|
||||
|
||||
# Cable endpoint types
|
||||
CABLE_TERMINATION_TYPES = [
|
||||
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination',
|
||||
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport',
|
||||
'circuittermination',
|
||||
]
|
||||
|
||||
# Cable types
|
||||
@ -510,3 +513,379 @@ POWERFEED_LEG_CHOICES = (
|
||||
(POWERFEED_LEG_B, 'B'),
|
||||
(POWERFEED_LEG_C, 'C'),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Interface type values
|
||||
#
|
||||
|
||||
class InterfaceTypes:
|
||||
"""
|
||||
Interface.type slugs
|
||||
"""
|
||||
# Virtual
|
||||
TYPE_VIRTUAL = 'virtual'
|
||||
TYPE_LAG = 'lag'
|
||||
|
||||
# Ethernet
|
||||
TYPE_100ME_FIXED = '100base-tx'
|
||||
TYPE_1GE_FIXED = '1000base-t'
|
||||
TYPE_1GE_GBIC = '1000base-x-gbic'
|
||||
TYPE_1GE_SFP = '1000base-x-sfp'
|
||||
TYPE_2GE_FIXED = '2.5gbase-t'
|
||||
TYPE_5GE_FIXED = '5gbase-t'
|
||||
TYPE_10GE_FIXED = '10gbase-t'
|
||||
TYPE_10GE_CX4 = '10gbase-cx4'
|
||||
TYPE_10GE_SFP_PLUS = '10gbase-x-sfpp'
|
||||
TYPE_10GE_XFP = '10gbase-x-xfp'
|
||||
TYPE_10GE_XENPAK = '10gbase-x-xenpak'
|
||||
TYPE_10GE_X2 = '10gbase-x-x2'
|
||||
TYPE_25GE_SFP28 = '25gbase-x-sfp28'
|
||||
TYPE_40GE_QSFP_PLUS = '40gbase-x-qsfpp'
|
||||
TYPE_50GE_QSFP28 = '50gbase-x-sfp28'
|
||||
TYPE_100GE_CFP = '100gbase-x-cfp'
|
||||
TYPE_100GE_CFP2 = '100gbase-x-cfp2'
|
||||
TYPE_100GE_CFP4 = '100gbase-x-cfp4'
|
||||
TYPE_100GE_CPAK = '100gbase-x-cpak'
|
||||
TYPE_100GE_QSFP28 = '100gbase-x-qsfp28'
|
||||
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
|
||||
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
|
||||
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
|
||||
|
||||
# Wireless
|
||||
TYPE_80211A = 'ieee802.11a'
|
||||
TYPE_80211G = 'ieee802.11g'
|
||||
TYPE_80211N = 'ieee802.11n'
|
||||
TYPE_80211AC = 'ieee802.11ac'
|
||||
TYPE_80211AD = 'ieee802.11ad'
|
||||
|
||||
# Cellular
|
||||
TYPE_GSM = 'gsm'
|
||||
TYPE_CDMA = 'cdma'
|
||||
TYPE_LTE = 'lte'
|
||||
|
||||
# SONET
|
||||
TYPE_SONET_OC3 = 'sonet-oc3'
|
||||
TYPE_SONET_OC12 = 'sonet-oc12'
|
||||
TYPE_SONET_OC48 = 'sonet-oc48'
|
||||
TYPE_SONET_OC192 = 'sonet-oc192'
|
||||
TYPE_SONET_OC768 = 'sonet-oc768'
|
||||
TYPE_SONET_OC1920 = 'sonet-oc1920'
|
||||
TYPE_SONET_OC3840 = 'sonet-oc3840'
|
||||
|
||||
# Fibrechannel
|
||||
TYPE_1GFC_SFP = '1gfc-sfp'
|
||||
TYPE_2GFC_SFP = '2gfc-sfp'
|
||||
TYPE_4GFC_SFP = '4gfc-sfp'
|
||||
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
|
||||
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
|
||||
TYPE_32GFC_SFP28 = '32gfc-sfp28'
|
||||
TYPE_128GFC_QSFP28 = '128gfc-sfp28'
|
||||
|
||||
# InfiniBand
|
||||
TYPE_INFINIBAND_SDR = 'inifiband-sdr'
|
||||
TYPE_INFINIBAND_DDR = 'inifiband-ddr'
|
||||
TYPE_INFINIBAND_QDR = 'inifiband-qdr'
|
||||
TYPE_INFINIBAND_FDR10 = 'inifiband-fdr10'
|
||||
TYPE_INFINIBAND_FDR = 'inifiband-fdr'
|
||||
TYPE_INFINIBAND_EDR = 'inifiband-edr'
|
||||
TYPE_INFINIBAND_HDR = 'inifiband-hdr'
|
||||
TYPE_INFINIBAND_NDR = 'inifiband-ndr'
|
||||
TYPE_INFINIBAND_XDR = 'inifiband-xdr'
|
||||
|
||||
# Serial
|
||||
TYPE_T1 = 't1'
|
||||
TYPE_E1 = 'e1'
|
||||
TYPE_T3 = 't3'
|
||||
TYPE_E3 = 'e3'
|
||||
|
||||
# Stacking
|
||||
TYPE_STACKWISE = 'cisco-stackwise'
|
||||
TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus'
|
||||
TYPE_FLEXSTACK = 'cisco-flexstack'
|
||||
TYPE_FLEXSTACK_PLUS = 'cisco-flexstack-plus'
|
||||
TYPE_JUNIPER_VCP = 'juniper-vcp'
|
||||
TYPE_SUMMITSTACK = 'extreme-summitstack'
|
||||
TYPE_SUMMITSTACK128 = 'extreme-summitstack-128'
|
||||
TYPE_SUMMITSTACK256 = 'extreme-summitstack-256'
|
||||
TYPE_SUMMITSTACK512 = 'extreme-summitstack-512'
|
||||
|
||||
# Other
|
||||
TYPE_OTHER = 'other'
|
||||
|
||||
TYPE_CHOICES = (
|
||||
(
|
||||
'Virtual interfaces',
|
||||
(
|
||||
(TYPE_VIRTUAL, 'Virtual'),
|
||||
(TYPE_LAG, 'Link Aggregation Group (LAG)'),
|
||||
),
|
||||
),
|
||||
(
|
||||
'Ethernet (fixed)',
|
||||
(
|
||||
(TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'),
|
||||
(TYPE_1GE_FIXED, '1000BASE-T (1GE)'),
|
||||
(TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'),
|
||||
(TYPE_5GE_FIXED, '5GBASE-T (5GE)'),
|
||||
(TYPE_10GE_FIXED, '10GBASE-T (10GE)'),
|
||||
(TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Ethernet (modular)',
|
||||
(
|
||||
(TYPE_1GE_GBIC, 'GBIC (1GE)'),
|
||||
(TYPE_1GE_SFP, 'SFP (1GE)'),
|
||||
(TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'),
|
||||
(TYPE_10GE_XFP, 'XFP (10GE)'),
|
||||
(TYPE_10GE_XENPAK, 'XENPAK (10GE)'),
|
||||
(TYPE_10GE_X2, 'X2 (10GE)'),
|
||||
(TYPE_25GE_SFP28, 'SFP28 (25GE)'),
|
||||
(TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'),
|
||||
(TYPE_50GE_QSFP28, 'QSFP28 (50GE)'),
|
||||
(TYPE_100GE_CFP, 'CFP (100GE)'),
|
||||
(TYPE_100GE_CFP2, 'CFP2 (100GE)'),
|
||||
(TYPE_200GE_CFP2, 'CFP2 (200GE)'),
|
||||
(TYPE_100GE_CFP4, 'CFP4 (100GE)'),
|
||||
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
|
||||
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
|
||||
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
||||
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Wireless',
|
||||
(
|
||||
(TYPE_80211A, 'IEEE 802.11a'),
|
||||
(TYPE_80211G, 'IEEE 802.11b/g'),
|
||||
(TYPE_80211N, 'IEEE 802.11n'),
|
||||
(TYPE_80211AC, 'IEEE 802.11ac'),
|
||||
(TYPE_80211AD, 'IEEE 802.11ad'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Cellular',
|
||||
(
|
||||
(TYPE_GSM, 'GSM'),
|
||||
(TYPE_CDMA, 'CDMA'),
|
||||
(TYPE_LTE, 'LTE'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'SONET',
|
||||
(
|
||||
(TYPE_SONET_OC3, 'OC-3/STM-1'),
|
||||
(TYPE_SONET_OC12, 'OC-12/STM-4'),
|
||||
(TYPE_SONET_OC48, 'OC-48/STM-16'),
|
||||
(TYPE_SONET_OC192, 'OC-192/STM-64'),
|
||||
(TYPE_SONET_OC768, 'OC-768/STM-256'),
|
||||
(TYPE_SONET_OC1920, 'OC-1920/STM-640'),
|
||||
(TYPE_SONET_OC3840, 'OC-3840/STM-1234'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'FibreChannel',
|
||||
(
|
||||
(TYPE_1GFC_SFP, 'SFP (1GFC)'),
|
||||
(TYPE_2GFC_SFP, 'SFP (2GFC)'),
|
||||
(TYPE_4GFC_SFP, 'SFP (4GFC)'),
|
||||
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
|
||||
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
|
||||
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
|
||||
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'InfiniBand',
|
||||
(
|
||||
(TYPE_INFINIBAND_SDR, 'SDR (2 Gbps)'),
|
||||
(TYPE_INFINIBAND_DDR, 'DDR (4 Gbps)'),
|
||||
(TYPE_INFINIBAND_QDR, 'QDR (8 Gbps)'),
|
||||
(TYPE_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'),
|
||||
(TYPE_INFINIBAND_FDR, 'FDR (13.5 Gbps)'),
|
||||
(TYPE_INFINIBAND_EDR, 'EDR (25 Gbps)'),
|
||||
(TYPE_INFINIBAND_HDR, 'HDR (50 Gbps)'),
|
||||
(TYPE_INFINIBAND_NDR, 'NDR (100 Gbps)'),
|
||||
(TYPE_INFINIBAND_XDR, 'XDR (250 Gbps)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Serial',
|
||||
(
|
||||
(TYPE_T1, 'T1 (1.544 Mbps)'),
|
||||
(TYPE_E1, 'E1 (2.048 Mbps)'),
|
||||
(TYPE_T3, 'T3 (45 Mbps)'),
|
||||
(TYPE_E3, 'E3 (34 Mbps)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Stacking',
|
||||
(
|
||||
(TYPE_STACKWISE, 'Cisco StackWise'),
|
||||
(TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'),
|
||||
(TYPE_FLEXSTACK, 'Cisco FlexStack'),
|
||||
(TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'),
|
||||
(TYPE_JUNIPER_VCP, 'Juniper VCP'),
|
||||
(TYPE_SUMMITSTACK, 'Extreme SummitStack'),
|
||||
(TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'),
|
||||
(TYPE_SUMMITSTACK256, 'Extreme SummitStack-256'),
|
||||
(TYPE_SUMMITSTACK512, 'Extreme SummitStack-512'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Other',
|
||||
(
|
||||
(TYPE_OTHER, 'Other'),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def slug_to_integer(cls, slug):
|
||||
"""
|
||||
Provide backward-compatible mapping of the type slug to integer.
|
||||
"""
|
||||
return {
|
||||
# Slug: integer
|
||||
cls.TYPE_VIRTUAL: IFACE_TYPE_VIRTUAL,
|
||||
cls.TYPE_LAG: IFACE_TYPE_LAG,
|
||||
cls.TYPE_100ME_FIXED: IFACE_TYPE_100ME_FIXED,
|
||||
cls.TYPE_1GE_FIXED: IFACE_TYPE_1GE_FIXED,
|
||||
cls.TYPE_1GE_GBIC: IFACE_TYPE_1GE_GBIC,
|
||||
cls.TYPE_1GE_SFP: IFACE_TYPE_1GE_SFP,
|
||||
cls.TYPE_2GE_FIXED: IFACE_TYPE_2GE_FIXED,
|
||||
cls.TYPE_5GE_FIXED: IFACE_TYPE_5GE_FIXED,
|
||||
cls.TYPE_10GE_FIXED: IFACE_TYPE_10GE_FIXED,
|
||||
cls.TYPE_10GE_CX4: IFACE_TYPE_10GE_CX4,
|
||||
cls.TYPE_10GE_SFP_PLUS: IFACE_TYPE_10GE_SFP_PLUS,
|
||||
cls.TYPE_10GE_XFP: IFACE_TYPE_10GE_XFP,
|
||||
cls.TYPE_10GE_XENPAK: IFACE_TYPE_10GE_XENPAK,
|
||||
cls.TYPE_10GE_X2: IFACE_TYPE_10GE_X2,
|
||||
cls.TYPE_25GE_SFP28: IFACE_TYPE_25GE_SFP28,
|
||||
cls.TYPE_40GE_QSFP_PLUS: IFACE_TYPE_40GE_QSFP_PLUS,
|
||||
cls.TYPE_50GE_QSFP28: IFACE_TYPE_50GE_QSFP28,
|
||||
cls.TYPE_100GE_CFP: IFACE_TYPE_100GE_CFP,
|
||||
cls.TYPE_100GE_CFP2: IFACE_TYPE_100GE_CFP2,
|
||||
cls.TYPE_100GE_CFP4: IFACE_TYPE_100GE_CFP4,
|
||||
cls.TYPE_100GE_CPAK: IFACE_TYPE_100GE_CPAK,
|
||||
cls.TYPE_100GE_QSFP28: IFACE_TYPE_100GE_QSFP28,
|
||||
cls.TYPE_200GE_CFP2: IFACE_TYPE_200GE_CFP2,
|
||||
cls.TYPE_200GE_QSFP56: IFACE_TYPE_200GE_QSFP56,
|
||||
cls.TYPE_400GE_QSFP_DD: IFACE_TYPE_400GE_QSFP_DD,
|
||||
cls.TYPE_80211A: IFACE_TYPE_80211A,
|
||||
cls.TYPE_80211G: IFACE_TYPE_80211G,
|
||||
cls.TYPE_80211N: IFACE_TYPE_80211N,
|
||||
cls.TYPE_80211AC: IFACE_TYPE_80211AC,
|
||||
cls.TYPE_80211AD: IFACE_TYPE_80211AD,
|
||||
cls.TYPE_GSM: IFACE_TYPE_GSM,
|
||||
cls.TYPE_CDMA: IFACE_TYPE_CDMA,
|
||||
cls.TYPE_LTE: IFACE_TYPE_LTE,
|
||||
cls.TYPE_SONET_OC3: IFACE_TYPE_SONET_OC3,
|
||||
cls.TYPE_SONET_OC12: IFACE_TYPE_SONET_OC12,
|
||||
cls.TYPE_SONET_OC48: IFACE_TYPE_SONET_OC48,
|
||||
cls.TYPE_SONET_OC192: IFACE_TYPE_SONET_OC192,
|
||||
cls.TYPE_SONET_OC768: IFACE_TYPE_SONET_OC768,
|
||||
cls.TYPE_SONET_OC1920: IFACE_TYPE_SONET_OC1920,
|
||||
cls.TYPE_SONET_OC3840: IFACE_TYPE_SONET_OC3840,
|
||||
cls.TYPE_1GFC_SFP: IFACE_TYPE_1GFC_SFP,
|
||||
cls.TYPE_2GFC_SFP: IFACE_TYPE_2GFC_SFP,
|
||||
cls.TYPE_4GFC_SFP: IFACE_TYPE_4GFC_SFP,
|
||||
cls.TYPE_8GFC_SFP_PLUS: IFACE_TYPE_8GFC_SFP_PLUS,
|
||||
cls.TYPE_16GFC_SFP_PLUS: IFACE_TYPE_16GFC_SFP_PLUS,
|
||||
cls.TYPE_32GFC_SFP28: IFACE_TYPE_32GFC_SFP28,
|
||||
cls.TYPE_128GFC_QSFP28: IFACE_TYPE_128GFC_QSFP28,
|
||||
cls.TYPE_INFINIBAND_SDR: IFACE_TYPE_INFINIBAND_SDR,
|
||||
cls.TYPE_INFINIBAND_DDR: IFACE_TYPE_INFINIBAND_DDR,
|
||||
cls.TYPE_INFINIBAND_QDR: IFACE_TYPE_INFINIBAND_QDR,
|
||||
cls.TYPE_INFINIBAND_FDR10: IFACE_TYPE_INFINIBAND_FDR10,
|
||||
cls.TYPE_INFINIBAND_FDR: IFACE_TYPE_INFINIBAND_FDR,
|
||||
cls.TYPE_INFINIBAND_EDR: IFACE_TYPE_INFINIBAND_EDR,
|
||||
cls.TYPE_INFINIBAND_HDR: IFACE_TYPE_INFINIBAND_HDR,
|
||||
cls.TYPE_INFINIBAND_NDR: IFACE_TYPE_INFINIBAND_NDR,
|
||||
cls.TYPE_INFINIBAND_XDR: IFACE_TYPE_INFINIBAND_XDR,
|
||||
cls.TYPE_T1: IFACE_TYPE_T1,
|
||||
cls.TYPE_E1: IFACE_TYPE_E1,
|
||||
cls.TYPE_T3: IFACE_TYPE_T3,
|
||||
cls.TYPE_E3: IFACE_TYPE_E3,
|
||||
cls.TYPE_STACKWISE: IFACE_TYPE_STACKWISE,
|
||||
cls.TYPE_STACKWISE_PLUS: IFACE_TYPE_STACKWISE_PLUS,
|
||||
cls.TYPE_FLEXSTACK: IFACE_TYPE_FLEXSTACK,
|
||||
cls.TYPE_FLEXSTACK_PLUS: IFACE_TYPE_FLEXSTACK_PLUS,
|
||||
cls.TYPE_JUNIPER_VCP: IFACE_TYPE_JUNIPER_VCP,
|
||||
cls.TYPE_SUMMITSTACK: IFACE_TYPE_SUMMITSTACK,
|
||||
cls.TYPE_SUMMITSTACK128: IFACE_TYPE_SUMMITSTACK128,
|
||||
cls.TYPE_SUMMITSTACK256: IFACE_TYPE_SUMMITSTACK256,
|
||||
cls.TYPE_SUMMITSTACK512: IFACE_TYPE_SUMMITSTACK512,
|
||||
}.get(slug)
|
||||
|
||||
|
||||
#
|
||||
# Port type values
|
||||
#
|
||||
|
||||
class PortTypes:
|
||||
"""
|
||||
FrontPort/RearPort.type slugs
|
||||
"""
|
||||
TYPE_8P8C = '8p8c'
|
||||
TYPE_110_PUNCH = '110-punch'
|
||||
TYPE_BNC = 'bnc'
|
||||
TYPE_ST = 'st'
|
||||
TYPE_SC = 'sc'
|
||||
TYPE_SC_APC = 'sc-apc'
|
||||
TYPE_FC = 'fc'
|
||||
TYPE_LC = 'lc'
|
||||
TYPE_LC_APC = 'lc-apc'
|
||||
TYPE_MTRJ = 'mtrj'
|
||||
TYPE_MPO = 'mpo'
|
||||
TYPE_LSH = 'lsh'
|
||||
TYPE_LSH_APC = 'lsh-apc'
|
||||
|
||||
TYPE_CHOICES = (
|
||||
(
|
||||
'Copper',
|
||||
(
|
||||
(TYPE_8P8C, '8P8C'),
|
||||
(TYPE_110_PUNCH, '110 Punch'),
|
||||
(TYPE_BNC, 'BNC'),
|
||||
),
|
||||
),
|
||||
(
|
||||
'Fiber Optic',
|
||||
(
|
||||
(TYPE_FC, 'FC'),
|
||||
(TYPE_LC, 'LC'),
|
||||
(TYPE_LC_APC, 'LC/APC'),
|
||||
(TYPE_LSH, 'LSH'),
|
||||
(TYPE_LSH_APC, 'LSH/APC'),
|
||||
(TYPE_MPO, 'MPO'),
|
||||
(TYPE_MTRJ, 'MTRJ'),
|
||||
(TYPE_SC, 'SC'),
|
||||
(TYPE_SC_APC, 'SC/APC'),
|
||||
(TYPE_ST, 'ST'),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def slug_to_integer(cls, slug):
|
||||
"""
|
||||
Provide backward-compatible mapping of the type slug to integer.
|
||||
"""
|
||||
return {
|
||||
# Slug: integer
|
||||
cls.TYPE_8P8C: PORT_TYPE_8P8C,
|
||||
cls.TYPE_110_PUNCH: PORT_TYPE_8P8C,
|
||||
cls.TYPE_BNC: PORT_TYPE_BNC,
|
||||
cls.TYPE_ST: PORT_TYPE_ST,
|
||||
cls.TYPE_SC: PORT_TYPE_SC,
|
||||
cls.TYPE_SC_APC: PORT_TYPE_SC_APC,
|
||||
cls.TYPE_FC: PORT_TYPE_FC,
|
||||
cls.TYPE_LC: PORT_TYPE_LC,
|
||||
cls.TYPE_LC_APC: PORT_TYPE_LC_APC,
|
||||
cls.TYPE_MTRJ: PORT_TYPE_MTRJ,
|
||||
cls.TYPE_MPO: PORT_TYPE_MPO,
|
||||
cls.TYPE_LSH: PORT_TYPE_LSH,
|
||||
cls.TYPE_LSH_APC: PORT_TYPE_LSH_APC,
|
||||
}.get(slug)
|
||||
|
@ -23,7 +23,7 @@ from utilities.forms import (
|
||||
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
||||
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
|
||||
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
|
||||
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
|
||||
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .constants import *
|
||||
@ -828,29 +828,17 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class DeviceTypeCSVForm(forms.ModelForm):
|
||||
class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=True,
|
||||
to_field_name='name',
|
||||
help_text='Manufacturer name',
|
||||
error_messages={
|
||||
'invalid_choice': 'Manufacturer not found.',
|
||||
}
|
||||
)
|
||||
subdevice_role = CSVChoiceField(
|
||||
choices=SUBDEVICE_ROLE_CHOICES,
|
||||
required=False,
|
||||
help_text='Parent/child status'
|
||||
to_field_name='name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = DeviceType.csv_headers
|
||||
help_texts = {
|
||||
'model': 'Model name',
|
||||
'slug': 'URL-friendly slug',
|
||||
}
|
||||
fields = [
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
]
|
||||
|
||||
|
||||
class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
@ -1232,6 +1220,139 @@ class DeviceBayTemplateCreateForm(ComponentForm):
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Component template import forms
|
||||
#
|
||||
|
||||
class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
def __init__(self, device_type, data=None, *args, **kwargs):
|
||||
|
||||
# Must pass the parent DeviceType on form initialization
|
||||
data.update({
|
||||
'device_type': device_type.pk,
|
||||
})
|
||||
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
def clean_device_type(self):
|
||||
|
||||
data = self.cleaned_data['device_type']
|
||||
|
||||
# Limit fields referencing other components to the parent DeviceType
|
||||
for field_name, field in self.fields.items():
|
||||
if isinstance(field, forms.ModelChoiceField) and field_name != 'device_type':
|
||||
field.queryset = field.queryset.filter(device_type=data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
'device_type', 'name',
|
||||
]
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name',
|
||||
]
|
||||
|
||||
|
||||
class PowerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'maximum_draw', 'allocated_draw',
|
||||
]
|
||||
|
||||
|
||||
class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'power_port', 'feed_leg',
|
||||
]
|
||||
|
||||
|
||||
class InterfaceTemplateImportForm(ComponentTemplateImportForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=InterfaceTypes.TYPE_CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'mgmt_only',
|
||||
]
|
||||
|
||||
def clean_type(self):
|
||||
# Convert slug value to field integer value
|
||||
slug = self.cleaned_data['type']
|
||||
return InterfaceTypes.slug_to_integer(slug)
|
||||
|
||||
|
||||
class FrontPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypes.TYPE_CHOICES
|
||||
)
|
||||
rear_port = forms.ModelChoiceField(
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
|
||||
]
|
||||
|
||||
def clean_type(self):
|
||||
# Convert slug value to field integer value
|
||||
slug = self.cleaned_data['type']
|
||||
return PortTypes.slug_to_integer(slug)
|
||||
|
||||
|
||||
class RearPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypes.TYPE_CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'positions',
|
||||
]
|
||||
|
||||
def clean_type(self):
|
||||
# Convert slug value to field integer value
|
||||
slug = self.cleaned_data['type']
|
||||
return PortTypes.slug_to_integer(slug)
|
||||
|
||||
|
||||
class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = [
|
||||
'device_type', 'name',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Device roles
|
||||
#
|
||||
|
@ -3,10 +3,11 @@ import urllib.parse
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.constants import CABLE_TYPE_CAT6, IFACE_TYPE_1GE_FIXED
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup,
|
||||
RackReservation, RackRole, Site, Region, VirtualChassis,
|
||||
Cable, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
||||
FrontPortTemplate, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerPortTemplate,
|
||||
PowerOutletTemplate, Rack, RackGroup, RackReservation, RackRole, RearPortTemplate, Site, Region, VirtualChassis,
|
||||
)
|
||||
from utilities.testing import create_test_user
|
||||
|
||||
@ -221,6 +222,132 @@ class DeviceTypeTestCase(TestCase):
|
||||
response = self.client.get(devicetype.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_devicetype_import(self):
|
||||
|
||||
IMPORT_DATA = """
|
||||
manufacturer: Generic
|
||||
model: TEST-1000
|
||||
slug: test-1000
|
||||
u_height: 2
|
||||
console-ports:
|
||||
- name: Console Port 1
|
||||
- name: Console Port 2
|
||||
- name: Console Port 3
|
||||
console-server-ports:
|
||||
- name: Console Server Port 1
|
||||
- name: Console Server Port 2
|
||||
- name: Console Server Port 3
|
||||
power-ports:
|
||||
- name: Power Port 1
|
||||
- name: Power Port 2
|
||||
- name: Power Port 3
|
||||
power-outlets:
|
||||
- name: Power Outlet 1
|
||||
power_port: Power Port 1
|
||||
feed_leg: 1
|
||||
- name: Power Outlet 2
|
||||
power_port: Power Port 1
|
||||
feed_leg: 1
|
||||
- name: Power Outlet 3
|
||||
power_port: Power Port 1
|
||||
feed_leg: 1
|
||||
interfaces:
|
||||
- name: Interface 1
|
||||
type: 1000base-t
|
||||
mgmt_only: true
|
||||
- name: Interface 2
|
||||
type: 1000base-t
|
||||
- name: Interface 3
|
||||
type: 1000base-t
|
||||
rear-ports:
|
||||
- name: Rear Port 1
|
||||
type: 8p8c
|
||||
- name: Rear Port 2
|
||||
type: 8p8c
|
||||
- name: Rear Port 3
|
||||
type: 8p8c
|
||||
front-ports:
|
||||
- name: Front Port 1
|
||||
type: 8p8c
|
||||
rear_port: Rear Port 1
|
||||
- name: Front Port 2
|
||||
type: 8p8c
|
||||
rear_port: Rear Port 2
|
||||
- name: Front Port 3
|
||||
type: 8p8c
|
||||
rear_port: Rear Port 3
|
||||
device-bays:
|
||||
- name: Device Bay 1
|
||||
- name: Device Bay 2
|
||||
- name: Device Bay 3
|
||||
"""
|
||||
|
||||
# Create the manufacturer
|
||||
Manufacturer(name='Generic', slug='generic').save()
|
||||
|
||||
# Authenticate as user with necessary permissions
|
||||
user = create_test_user(username='testuser2', permissions=[
|
||||
'dcim.view_devicetype',
|
||||
'dcim.add_devicetype',
|
||||
'dcim.add_consoleporttemplate',
|
||||
'dcim.add_consoleserverporttemplate',
|
||||
'dcim.add_powerporttemplate',
|
||||
'dcim.add_poweroutlettemplate',
|
||||
'dcim.add_interfacetemplate',
|
||||
'dcim.add_frontporttemplate',
|
||||
'dcim.add_rearporttemplate',
|
||||
'dcim.add_devicebaytemplate',
|
||||
])
|
||||
self.client.force_login(user)
|
||||
|
||||
form_data = {
|
||||
'data': IMPORT_DATA,
|
||||
'format': 'yaml'
|
||||
}
|
||||
response = self.client.post(reverse('dcim:devicetype_import'), data=form_data, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
dt = DeviceType.objects.get(model='TEST-1000')
|
||||
|
||||
# Verify all of the components were created
|
||||
self.assertEqual(dt.consoleport_templates.count(), 3)
|
||||
cp1 = ConsolePortTemplate.objects.first()
|
||||
self.assertEqual(cp1.name, 'Console Port 1')
|
||||
|
||||
self.assertEqual(dt.consoleserverport_templates.count(), 3)
|
||||
csp1 = ConsoleServerPortTemplate.objects.first()
|
||||
self.assertEqual(csp1.name, 'Console Server Port 1')
|
||||
|
||||
self.assertEqual(dt.powerport_templates.count(), 3)
|
||||
pp1 = PowerPortTemplate.objects.first()
|
||||
self.assertEqual(pp1.name, 'Power Port 1')
|
||||
|
||||
self.assertEqual(dt.poweroutlet_templates.count(), 3)
|
||||
po1 = PowerOutletTemplate.objects.first()
|
||||
self.assertEqual(po1.name, 'Power Outlet 1')
|
||||
self.assertEqual(po1.power_port, pp1)
|
||||
self.assertEqual(po1.feed_leg, POWERFEED_LEG_A)
|
||||
|
||||
self.assertEqual(dt.interface_templates.count(), 3)
|
||||
iface1 = InterfaceTemplate.objects.first()
|
||||
self.assertEqual(iface1.name, 'Interface 1')
|
||||
self.assertEqual(iface1.type, IFACE_TYPE_1GE_FIXED)
|
||||
self.assertTrue(iface1.mgmt_only)
|
||||
|
||||
self.assertEqual(dt.rearport_templates.count(), 3)
|
||||
rp1 = RearPortTemplate.objects.first()
|
||||
self.assertEqual(rp1.name, 'Rear Port 1')
|
||||
|
||||
self.assertEqual(dt.frontport_templates.count(), 3)
|
||||
fp1 = FrontPortTemplate.objects.first()
|
||||
self.assertEqual(fp1.name, 'Front Port 1')
|
||||
self.assertEqual(fp1.rear_port, rp1)
|
||||
self.assertEqual(fp1.rear_port_position, 1)
|
||||
|
||||
self.assertEqual(dt.device_bay_templates.count(), 3)
|
||||
db1 = DeviceBayTemplate.objects.first()
|
||||
self.assertEqual(db1.name, 'Device Bay 1')
|
||||
|
||||
|
||||
class DeviceRoleTestCase(TestCase):
|
||||
|
||||
|
@ -82,7 +82,7 @@ urlpatterns = [
|
||||
# Device types
|
||||
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||
path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
||||
path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
|
||||
path(r'device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'),
|
||||
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||
path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||
|
@ -1,3 +1,4 @@
|
||||
from collections import OrderedDict
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
@ -26,7 +27,7 @@ from utilities.paginator import EnhancedPaginator
|
||||
from utilities.utils import csv_format
|
||||
from utilities.views import (
|
||||
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
|
||||
ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import filters, forms, tables
|
||||
@ -653,11 +654,31 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
default_return_url = 'dcim:devicetype_list'
|
||||
|
||||
|
||||
class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_devicetype'
|
||||
model_form = forms.DeviceTypeCSVForm
|
||||
table = tables.DeviceTypeTable
|
||||
default_return_url = 'dcim:devicetype_list'
|
||||
class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView):
|
||||
permission_required = [
|
||||
'dcim.add_devicetype',
|
||||
'dcim.add_consoleporttemplate',
|
||||
'dcim.add_consoleserverporttemplate',
|
||||
'dcim.add_powerporttemplate',
|
||||
'dcim.add_poweroutlettemplate',
|
||||
'dcim.add_interfacetemplate',
|
||||
'dcim.add_frontporttemplate',
|
||||
'dcim.add_rearporttemplate',
|
||||
'dcim.add_devicebaytemplate',
|
||||
]
|
||||
model = DeviceType
|
||||
model_form = forms.DeviceTypeImportForm
|
||||
related_object_forms = OrderedDict((
|
||||
('console-ports', forms.ConsolePortTemplateImportForm),
|
||||
('console-server-ports', forms.ConsoleServerPortTemplateImportForm),
|
||||
('power-ports', forms.PowerPortTemplateImportForm),
|
||||
('power-outlets', forms.PowerOutletTemplateImportForm),
|
||||
('interfaces', forms.InterfaceTemplateImportForm),
|
||||
('rear-ports', forms.RearPortTemplateImportForm),
|
||||
('front-ports', forms.FrontPortTemplateImportForm),
|
||||
('device-bays', forms.DeviceBayTemplateImportForm),
|
||||
))
|
||||
default_return_url = 'dcim:devicetype_import'
|
||||
|
||||
|
||||
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% extends 'utilities/obj_bulk_import.html' %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include 'dcim/inc/device_import_header.html' %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% extends 'utilities/obj_bulk_import.html' %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% extends 'utilities/obj_bulk_import.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
|
60
netbox/templates/utilities/obj_bulk_import.html
Normal file
60
netbox/templates/utilities/obj_bulk_import.html
Normal file
@ -0,0 +1,60 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% block title %}{{ obj_type|bettertitle }} Bulk Import{% endblock %}</h1>
|
||||
{% block tabs %}{% endblock %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-12 text-right">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
{% if return_url %}
|
||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
{% if fields %}
|
||||
<h4 class="text-center">CSV Format</h4>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for name, field in fields.items %}
|
||||
<tr>
|
||||
<td><code>{{ name }}</code></td>
|
||||
<td>{% if field.required %}<i class="glyphicon glyphicon-ok" title="Required"></i>{% endif %}</td>
|
||||
<td>
|
||||
{{ field.help_text|default:field.label }}
|
||||
{% if field.choices %}
|
||||
<br /><small class="text-muted">Choices: {{ field|example_choices }}</small>
|
||||
{% elif field|widget_type == 'dateinput' %}
|
||||
<br /><small class="text-muted">Format: YYYY-MM-DD</small>
|
||||
{% elif field|widget_type == 'checkboxinput' %}
|
||||
<br /><small class="text-muted">Specify "true" or "false"</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -6,7 +6,7 @@
|
||||
<h1>{% block title %}{{ obj_type|bettertitle }} Import{% endblock %}</h1>
|
||||
{% block tabs %}{% endblock %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
@ -15,12 +15,13 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="form">
|
||||
<form action="" method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-12 text-right">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<button type="submit" name="_create" class="btn btn-primary">Submit</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Submit and Import Another</button>
|
||||
{% if return_url %}
|
||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||
{% endif %}
|
||||
@ -28,33 +29,5 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
{% if fields %}
|
||||
<h4 class="text-center">CSV Format</h4>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for name, field in fields.items %}
|
||||
<tr>
|
||||
<td><code>{{ name }}</code></td>
|
||||
<td>{% if field.required %}<i class="glyphicon glyphicon-ok" title="Required"></i>{% endif %}</td>
|
||||
<td>
|
||||
{{ field.help_text|default:field.label }}
|
||||
{% if field.choices %}
|
||||
<br /><small class="text-muted">Choices: {{ field|example_choices }}</small>
|
||||
{% elif field|widget_type == 'dateinput' %}
|
||||
<br /><small class="text-muted">Format: YYYY-MM-DD</small>
|
||||
{% elif field|widget_type == 'checkboxinput' %}
|
||||
<br /><small class="text-muted">Specify "true" or "false"</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -2,6 +2,7 @@ import csv
|
||||
import json
|
||||
import re
|
||||
from io import StringIO
|
||||
import yaml
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
@ -722,3 +723,41 @@ class BulkEditForm(forms.Form):
|
||||
# Copy any nullable fields defined in Meta
|
||||
if hasattr(self.Meta, 'nullable_fields'):
|
||||
self.nullable_fields = self.Meta.nullable_fields
|
||||
|
||||
|
||||
class ImportForm(BootstrapMixin, forms.Form):
|
||||
"""
|
||||
Generic form for creating an object from JSON/YAML data
|
||||
"""
|
||||
data = forms.CharField(
|
||||
widget=forms.Textarea,
|
||||
help_text="Enter object data in JSON or YAML format."
|
||||
)
|
||||
format = forms.ChoiceField(
|
||||
choices=(
|
||||
('json', 'JSON'),
|
||||
('yaml', 'YAML')
|
||||
),
|
||||
initial='yaml'
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
||||
data = self.cleaned_data['data']
|
||||
format = self.cleaned_data['format']
|
||||
|
||||
# Process JSON/YAML data
|
||||
if format == 'json':
|
||||
try:
|
||||
self.cleaned_data['data'] = json.loads(data)
|
||||
except json.decoder.JSONDecodeError as err:
|
||||
raise forms.ValidationError({
|
||||
'data': "Invalid JSON data: {}".format(err)
|
||||
})
|
||||
else:
|
||||
try:
|
||||
self.cleaned_data['data'] = yaml.load(data, Loader=yaml.SafeLoader)
|
||||
except yaml.scanner.ScannerError as err:
|
||||
raise forms.ValidationError({
|
||||
'data': "Invalid YAML data: {}".format(err)
|
||||
})
|
||||
|
@ -1,4 +1,6 @@
|
||||
import json
|
||||
import sys
|
||||
import yaml
|
||||
from copy import deepcopy
|
||||
|
||||
from django.conf import settings
|
||||
@ -24,10 +26,11 @@ from django_tables2 import RequestConfig
|
||||
|
||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate
|
||||
from extras.querysets import CustomFieldQueryset
|
||||
from utilities.exceptions import AbortTransaction
|
||||
from utilities.forms import BootstrapMixin, CSVDataField
|
||||
from utilities.utils import csv_format
|
||||
from .error_handlers import handle_protectederror
|
||||
from .forms import ConfirmationForm
|
||||
from .forms import ConfirmationForm, ImportForm
|
||||
from .paginator import EnhancedPaginator
|
||||
|
||||
|
||||
@ -394,6 +397,106 @@ class BulkCreateView(GetReturnURLMixin, View):
|
||||
})
|
||||
|
||||
|
||||
class ObjectImportView(GetReturnURLMixin, View):
|
||||
"""
|
||||
Import a single object (YAML or JSON format).
|
||||
"""
|
||||
model = None
|
||||
model_form = None
|
||||
related_object_forms = dict()
|
||||
template_name = 'utilities/obj_import.html'
|
||||
|
||||
def get(self, request):
|
||||
|
||||
form = ImportForm()
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'obj_type': self.model._meta.verbose_name,
|
||||
'return_url': self.get_return_url(request),
|
||||
})
|
||||
|
||||
def post(self, request):
|
||||
|
||||
form = ImportForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
# Initialize model form
|
||||
data = form.cleaned_data['data']
|
||||
model_form = self.model_form(data)
|
||||
|
||||
# Assign default values for any fields which were not specified. We have to do this manually because passing
|
||||
# 'initial=' to the form on initialization merely sets default values for the widgets. Since widgets are not
|
||||
# used for YAML/JSON import, we first bind the imported data normally, then update the form's data with the
|
||||
# applicable field defaults as needed prior to form validation.
|
||||
for field_name, field in model_form.fields.items():
|
||||
if field_name not in data and hasattr(field, 'initial'):
|
||||
model_form.data[field_name] = field.initial
|
||||
|
||||
if model_form.is_valid():
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
|
||||
# Save the primary object
|
||||
obj = model_form.save()
|
||||
|
||||
# Iterate through the related object forms (if any), validating and saving each instance.
|
||||
for field_name, related_object_form in self.related_object_forms.items():
|
||||
|
||||
for i, rel_obj_data in enumerate(data.get(field_name, list())):
|
||||
|
||||
f = related_object_form(obj, rel_obj_data)
|
||||
|
||||
for subfield_name, field in f.fields.items():
|
||||
if subfield_name not in rel_obj_data and hasattr(field, 'initial'):
|
||||
f.data[subfield_name] = field.initial
|
||||
|
||||
if f.is_valid():
|
||||
f.save()
|
||||
else:
|
||||
# Replicate errors on the related object form to the primary form for display
|
||||
for subfield_name, errors in f.errors.items():
|
||||
for err in errors:
|
||||
err_msg = "{}[{}] {}: {}".format(field_name, i, subfield_name, err)
|
||||
model_form.add_error(None, err_msg)
|
||||
raise AbortTransaction()
|
||||
|
||||
except AbortTransaction:
|
||||
pass
|
||||
|
||||
if not model_form.errors:
|
||||
|
||||
messages.success(request, mark_safe('Imported object: <a href="{}">{}</a>'.format(
|
||||
obj.get_absolute_url(), obj
|
||||
)))
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
return_url = form.cleaned_data.get('return_url')
|
||||
if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
|
||||
return redirect(return_url)
|
||||
else:
|
||||
return redirect(self.get_return_url(request, obj))
|
||||
|
||||
else:
|
||||
|
||||
# Replicate model form errors for display
|
||||
for field, errors in model_form.errors.items():
|
||||
for err in errors:
|
||||
if field == '__all__':
|
||||
form.add_error(None, err)
|
||||
else:
|
||||
form.add_error(None, "{}: {}".format(field, err))
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'obj_type': self.model._meta.verbose_name,
|
||||
'return_url': self.get_return_url(request),
|
||||
})
|
||||
|
||||
|
||||
class BulkImportView(GetReturnURLMixin, View):
|
||||
"""
|
||||
Import objects in bulk (CSV format).
|
||||
@ -405,7 +508,7 @@ class BulkImportView(GetReturnURLMixin, View):
|
||||
"""
|
||||
model_form = None
|
||||
table = None
|
||||
template_name = 'utilities/obj_import.html'
|
||||
template_name = 'utilities/obj_bulk_import.html'
|
||||
widget_attrs = {}
|
||||
|
||||
def _import_form(self, *args, **kwargs):
|
||||
|
Reference in New Issue
Block a user