From 446acbdf8285ad76a885a909c408350372c9ad6b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 6 Dec 2019 16:13:52 -0500 Subject: [PATCH] Closes #33: Add ability to clone objects (pre-populate form fields) --- docs/release-notes/version-2.7.md | 1 + netbox/circuits/models.py | 10 ++- netbox/dcim/models.py | 18 ++++++ netbox/ipam/models.py | 15 +++++ netbox/templates/circuits/circuit.html | 4 ++ netbox/templates/circuits/provider.html | 4 ++ netbox/templates/dcim/device.html | 6 ++ netbox/templates/dcim/devicetype.html | 62 ++++++++++--------- netbox/templates/dcim/powerfeed.html | 4 ++ netbox/templates/dcim/rack.html | 4 ++ netbox/templates/dcim/site.html | 8 ++- netbox/templates/ipam/aggregate.html | 4 ++ netbox/templates/ipam/ipaddress.html | 4 ++ netbox/templates/ipam/prefix.html | 4 ++ netbox/templates/ipam/vlan.html | 4 ++ netbox/templates/ipam/vrf.html | 4 ++ netbox/templates/tenancy/tenant.html | 4 ++ netbox/templates/virtualization/cluster.html | 4 ++ .../virtualization/virtualmachine.html | 4 ++ netbox/tenancy/models.py | 3 + netbox/utilities/templates/buttons/clone.html | 3 + netbox/utilities/templatetags/buttons.py | 40 +++++++++++- netbox/virtualization/models.py | 6 ++ 23 files changed, 186 insertions(+), 34 deletions(-) create mode 100644 netbox/utilities/templates/buttons/clone.html diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index b3f102b9f..4e70f73a7 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -127,6 +127,7 @@ PATCH) to maintain backward compatibility. This behavior will be discontinued be ## Enhancements +* [#33](https://github.com/digitalocean/netbox/issues/33) - Add ability to clone objects (pre-populate form fields) * [#792](https://github.com/digitalocean/netbox/issues/792) - Add power port and power outlet types * [#1865](https://github.com/digitalocean/netbox/issues/1865) - Add console port and console server port types * [#2902](https://github.com/digitalocean/netbox/issues/2902) - Replace `supervisord` with `systemd` diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 5f80a4bfe..672e18b62 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -57,7 +57,12 @@ class Provider(ChangeLoggedModel, CustomFieldModel): 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', + ] + clone_fields = [ + 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', + ] class Meta: ordering = ['name'] @@ -171,6 +176,9 @@ class Circuit(ChangeLoggedModel, CustomFieldModel): csv_headers = [ 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', ] + clone_fields = [ + 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', + ] STATUS_CLASS_MAP = { CircuitStatusChoices.STATUS_DEPROVISIONING: 'warning', diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 6f46c0c96..2297909b4 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -331,6 +331,10 @@ class Site(ChangeLoggedModel, CustomFieldModel): 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments', ] + clone_fields = [ + 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', + 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', + ] STATUS_CLASS_MAP = { SiteStatusChoices.STATUS_ACTIVE: 'success', @@ -559,6 +563,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel): 'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', ] + clone_fields = [ + 'site', 'group', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width', + 'outer_depth', 'outer_unit', + ] STATUS_CLASS_MAP = { RackStatusChoices.STATUS_RESERVED: 'warning', @@ -948,6 +956,9 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): csv_headers = [ 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments', ] + clone_fields = [ + 'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', + ] class Meta: ordering = ['manufacturer', 'model'] @@ -1617,6 +1628,9 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', 'site', 'rack_group', 'rack_name', 'position', 'face', 'comments', ] + clone_fields = [ + 'device_type', 'device_role', 'tenant', 'platform', 'site', 'rack', 'status', 'cluster', + ] STATUS_CLASS_MAP = { DeviceStatusChoices.STATUS_OFFLINE: 'warning', @@ -3159,6 +3173,10 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): 'site', 'panel_name', 'rack_group', 'rack_name', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'comments', ] + clone_fields = [ + 'power_panel', 'rack', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', + 'available_power', + ] STATUS_CLASS_MAP = { PowerFeedStatusChoices.STATUS_OFFLINE: 'warning', diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 79a6f48ad..68cefe77f 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -78,6 +78,9 @@ class VRF(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description'] + clone_fields = [ + 'tenant', 'enforce_unique', 'description', + ] class Meta: ordering = ['name', 'rd'] @@ -177,6 +180,9 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = ['prefix', 'rir', 'date_added', 'description'] + clone_fields = [ + 'rir', 'date_added', 'description', + ] class Meta: ordering = ['family', 'prefix'] @@ -350,6 +356,9 @@ class Prefix(ChangeLoggedModel, CustomFieldModel): csv_headers = [ 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description', ] + clone_fields = [ + 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', + ] STATUS_CLASS_MAP = { 'container': 'default', @@ -627,6 +636,9 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary', 'dns_name', 'description', ] + clone_fields = [ + 'vrf', 'tenant', 'status', 'role', 'description', + ] STATUS_CLASS_MAP = { 'active': 'primary', @@ -898,6 +910,9 @@ class VLAN(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description'] + clone_fields = [ + 'site', 'group', 'tenant', 'status', 'role', 'description', + ] STATUS_CLASS_MAP = { 'active': 'primary', diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index a29a2ed7c..41a195bed 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load buttons %} {% load custom_links %} {% load helpers %} @@ -27,6 +28,9 @@
+ {% if perms.circuits.add_circuit %} + {% clone_button 'circuits:circuit_add' circuit %} + {% endif %} {% if perms.circuits.change_circuit %} diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index a83a5337a..8a72f6ba5 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load buttons %} {% load static %} {% load custom_links %} {% load helpers %} @@ -33,6 +34,9 @@ Graphs {% endif %} + {% if perms.circuits.add_provider %} + {% clone_button 'circuits:provider_add' provider %} + {% endif %} {% if perms.circuits.change_provider %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 0caddb7b1..93be6ebca 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load buttons %} {% load static %} {% load helpers %} {% load custom_links %} @@ -57,6 +58,11 @@ {% if perms.dcim.add_devicebay %}
  • Device Bays
  • {% endif %}
    + {% endif %} + {% if perms.dcim.add_device %} + {% clone_button 'dcim:device_add' device %} + {% endif %} + {% if perms.dcim.change_device %} Edit this device diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 2e244ac55..b7d5a013c 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load buttons %} {% load custom_links %} {% load helpers %} @@ -14,37 +15,40 @@ - {% if perms.dcim.change_devicetype or perms.dcim.delete_devicetype %} -
    - {% if perms.dcim.change_devicetype %} -
    - - -
    - - - Edit this device type - - {% endif %} - {% if perms.dcim.delete_devicetype %} - +
    + {% if perms.dcim.change_devicetype %} +
    + + +
    + {% endif %} + {% if perms.dcim.add_devicetype %} + {% clone_button 'dcim:devicetype_add' devicetype %} + {% endif %} + {% if perms.dcim.change_devicetype %} + + + Edit this device type + + {% endif %} + {% if perms.dcim.delete_devicetype %} + Delete this device type - - {% endif %} -
    - {% endif %} + + {% endif %} +

    {{ devicetype.manufacturer }} {{ devicetype.model }}

    {% include 'inc/created_updated.html' with obj=devicetype %}
    diff --git a/netbox/templates/dcim/powerfeed.html b/netbox/templates/dcim/powerfeed.html index a8ab302eb..e192b837e 100644 --- a/netbox/templates/dcim/powerfeed.html +++ b/netbox/templates/dcim/powerfeed.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load buttons %} {% load static %} {% load custom_links %} {% load helpers %} @@ -30,6 +31,9 @@
    + {% if perms.dcim.add_powerfeed %} + {% clone_button 'dcim:powerfeed_add' powerfeed %} + {% endif %} {% if perms.dcim.change_powerfeed %} diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 2a347e6e6..f82ee0d4f 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load buttons %} {% load custom_links %} {% load helpers %} @@ -31,6 +32,9 @@ Next Rack + {% if perms.dcim.add_rack %} + {% clone_button 'dcim:rack_add' rack %} + {% endif %} {% if perms.dcim.change_rack %} Edit this rack diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 8af48968c..8c7cc1915 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -1,8 +1,9 @@ {% extends '_base.html' %} -{% load static %} -{% load tz %} +{% load buttons %} {% load custom_links %} {% load helpers %} +{% load static %} +{% load tz %} {% block header %}
    + {% if perms.ipam.add_aggregate %} + {% clone_button 'ipam:aggregate_add' aggregate %} + {% endif %} {% if perms.ipam.change_aggregate %} diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index cb04e14d5..ad334fecc 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load buttons %} {% load custom_links %} {% load helpers %} @@ -27,6 +28,9 @@
    + {% if perms.ipam.add_ipaddress %} + {% clone_button 'ipam:ipaddress_add' ipaddress %} + {% endif %} {% if perms.ipam.change_ipaddress %} diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 9ea34804e..a74bc595e 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load buttons %} {% load custom_links %} {% load helpers %} @@ -38,6 +39,9 @@ Add an IP Address {% endif %} + {% if perms.ipam.add_prefix %} + {% clone_button 'ipam:prefix_add' prefix %} + {% endif %} {% if perms.ipam.change_prefix %} diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 20e9f39e9..2f751bd5f 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load buttons %} {% load custom_links %} {% load helpers %} @@ -30,6 +31,9 @@
    + {% if perms.ipam.add_vlan %} + {% clone_button 'ipam:vlan_add' vlan %} + {% endif %} {% if perms.ipam.change_vlan %} diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index 6c3ce013b..dd52671fb 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load buttons %} {% load custom_links %} {% load helpers %} @@ -24,6 +25,9 @@
    + {% if perms.ipam.add_vrf %} + {% clone_button 'ipam:vrf_add' vrf %} + {% endif %} {% if perms.ipam.change_vrf %} diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index a03d60523..cbb09b7a3 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load buttons %} {% load custom_links %} {% load helpers %} @@ -27,6 +28,9 @@
    + {% if perms.tenancy.add_tenant %} + {% clone_button 'tenancy:tenant_add' tenant %} + {% endif %} {% if perms.tenancy.change_tenant %} diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html index 264538237..0385c8923 100644 --- a/netbox/templates/virtualization/cluster.html +++ b/netbox/templates/virtualization/cluster.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load buttons %} {% load custom_links %} {% load helpers %} @@ -27,6 +28,9 @@
    + {% if perms.virtualization.add_cluster %} + {% clone_button 'virtualization:cluster_add' cluster %} + {% endif %} {% if perms.virtualization.change_cluster %} diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 2498039ff..8f51a0caf 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -1,4 +1,5 @@ {% extends '_base.html' %} +{% load buttons %} {% load custom_links %} {% load helpers %} @@ -26,6 +27,9 @@
    + {% if perms.virtualization.add_virtualmachine %} + {% clone_button 'virtualization:virtualmachine_add' virtualmachine %} + {% endif %} {% if perms.virtualization.change_virtualmachine %} diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index bc67804d6..dde4039da 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -73,6 +73,9 @@ class Tenant(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = ['name', 'slug', 'group', 'description', 'comments'] + clone_fields = [ + 'group', 'description', + ] class Meta: ordering = ['group', 'name'] diff --git a/netbox/utilities/templates/buttons/clone.html b/netbox/utilities/templates/buttons/clone.html new file mode 100644 index 000000000..b8f3638c4 --- /dev/null +++ b/netbox/utilities/templates/buttons/clone.html @@ -0,0 +1,3 @@ + + Clone + diff --git a/netbox/utilities/templatetags/buttons.py b/netbox/utilities/templatetags/buttons.py index b9a8bf6ec..d7c0998c3 100644 --- a/netbox/utilities/templatetags/buttons.py +++ b/netbox/utilities/templatetags/buttons.py @@ -1,4 +1,5 @@ from django import template +from django.urls import reverse from extras.models import ExportTemplate @@ -7,12 +8,47 @@ register = template.Library() @register.inclusion_tag('buttons/add.html') def add_button(url): - return {'add_url': url} + return { + 'add_url': url, + } @register.inclusion_tag('buttons/import.html') def import_button(url): - return {'import_url': url} + return { + 'import_url': url, + } + + +@register.inclusion_tag('buttons/clone.html') +def clone_button(url, instance): + + url = reverse(url) + + # Populate form field values + params = {} + for field_name in getattr(instance, 'clone_fields', []): + field = instance._meta.get_field(field_name) + field_value = field.value_from_object(instance) + + # Swap out False with URL-friendly value + if field_value is False: + field_value = '' + + # Omit empty values + if field_value not in (None, ''): + params[field_name] = field_value + + # TODO: Tag replication + + # Append parameters to URL + param_string = '&'.join(['{}={}'.format(k, v) for k, v in params.items()]) + if param_string: + url = '{}?{}'.format(url, param_string) + + return { + 'url': url, + } @register.inclusion_tag('buttons/export.html', takes_context=True) diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index f47172f1d..aa84c403c 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -129,6 +129,9 @@ class Cluster(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) csv_headers = ['name', 'type', 'group', 'site', 'comments'] + clone_fields = [ + 'type', 'group', 'tenant', 'site', + ] class Meta: ordering = ['name'] @@ -252,6 +255,9 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): csv_headers = [ 'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', ] + clone_fields = [ + 'cluster', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk', + ] STATUS_CLASS_MAP = { 'active': 'success',