Closes #33: Add ability to clone objects (pre-populate form fields)

This commit is contained in:
Jeremy Stretch 2019-12-06 16:13:52 -05:00
parent 47fefbec07
commit 446acbdf82
23 changed files with 186 additions and 34 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
@ -27,6 +28,9 @@
</div>
</div>
<div class="pull-right noprint">
{% if perms.circuits.add_circuit %}
{% clone_button 'circuits:circuit_add' circuit %}
{% endif %}
{% if perms.circuits.change_circuit %}
<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load static %}
{% load custom_links %}
{% load helpers %}
@ -33,6 +34,9 @@
Graphs
</button>
{% endif %}
{% if perms.circuits.add_provider %}
{% clone_button 'circuits:provider_add' provider %}
{% endif %}
{% if perms.circuits.change_provider %}
<a href="{% url 'circuits:provider_edit' slug=provider.slug %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>

View File

@ -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 %}<li><a href="{% url 'dcim:devicebay_add' pk=device.pk %}">Device Bays</a></li>{% endif %}
</ul>
</div>
{% endif %}
{% if perms.dcim.add_device %}
{% clone_button 'dcim:device_add' device %}
{% endif %}
{% if perms.dcim.change_device %}
<a href="{% url 'dcim:device_edit' pk=device.pk %}" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
Edit this device

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
@ -14,37 +15,40 @@
</ol>
</div>
</div>
{% if perms.dcim.change_devicetype or perms.dcim.delete_devicetype %}
<div class="pull-right noprint">
{% if perms.dcim.change_devicetype %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_consoleporttemplate %}<li><a href="{% url 'dcim:devicetype_add_consoleport' pk=devicetype.pk %}">Console Ports</a></li>{% endif %}
{% if perms.dcim.add_consoleserverporttemplate %}<li><a href="{% url 'dcim:devicetype_add_consoleserverport' pk=devicetype.pk %}">Console Server Ports</a></li>{% endif %}
{% if perms.dcim.add_powerporttemplate %}<li><a href="{% url 'dcim:devicetype_add_powerport' pk=devicetype.pk %}">Power Ports</a></li>{% endif %}
{% if perms.dcim.add_poweroutlettemplate %}<li><a href="{% url 'dcim:devicetype_add_poweroutlet' pk=devicetype.pk %}">Power Outlets</a></li>{% endif %}
{% if perms.dcim.add_interfacetemplate %}<li><a href="{% url 'dcim:devicetype_add_interface' pk=devicetype.pk %}">Interfaces</a></li>{% endif %}
{% if perms.dcim.add_frontporttemplate %}<li><a href="{% url 'dcim:devicetype_add_frontport' pk=devicetype.pk %}">Front Ports</a></li>{% endif %}
{% if perms.dcim.add_rearporttemplate %}<li><a href="{% url 'dcim:devicetype_add_rearport' pk=devicetype.pk %}">Rear Ports</a></li>{% endif %}
{% if perms.dcim.add_devicebaytemplate %}<li><a href="{% url 'dcim:devicetype_add_devicebay' pk=devicetype.pk %}">Device Bays</a></li>{% endif %}
</ul>
</div>
<a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this device type
</a>
{% endif %}
{% if perms.dcim.delete_devicetype %}
<a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
<div class="pull-right noprint">
{% if perms.dcim.change_devicetype %}
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_consoleporttemplate %}<li><a href="{% url 'dcim:devicetype_add_consoleport' pk=devicetype.pk %}">Console Ports</a></li>{% endif %}
{% if perms.dcim.add_consoleserverporttemplate %}<li><a href="{% url 'dcim:devicetype_add_consoleserverport' pk=devicetype.pk %}">Console Server Ports</a></li>{% endif %}
{% if perms.dcim.add_powerporttemplate %}<li><a href="{% url 'dcim:devicetype_add_powerport' pk=devicetype.pk %}">Power Ports</a></li>{% endif %}
{% if perms.dcim.add_poweroutlettemplate %}<li><a href="{% url 'dcim:devicetype_add_poweroutlet' pk=devicetype.pk %}">Power Outlets</a></li>{% endif %}
{% if perms.dcim.add_interfacetemplate %}<li><a href="{% url 'dcim:devicetype_add_interface' pk=devicetype.pk %}">Interfaces</a></li>{% endif %}
{% if perms.dcim.add_frontporttemplate %}<li><a href="{% url 'dcim:devicetype_add_frontport' pk=devicetype.pk %}">Front Ports</a></li>{% endif %}
{% if perms.dcim.add_rearporttemplate %}<li><a href="{% url 'dcim:devicetype_add_rearport' pk=devicetype.pk %}">Rear Ports</a></li>{% endif %}
{% if perms.dcim.add_devicebaytemplate %}<li><a href="{% url 'dcim:devicetype_add_devicebay' pk=devicetype.pk %}">Device Bays</a></li>{% endif %}
</ul>
</div>
{% endif %}
{% if perms.dcim.add_devicetype %}
{% clone_button 'dcim:devicetype_add' devicetype %}
{% endif %}
{% if perms.dcim.change_devicetype %}
<a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit this device type
</a>
{% endif %}
{% if perms.dcim.delete_devicetype %}
<a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
<span class="fa fa-trash" aria-hidden="true"></span>
Delete this device type
</a>
{% endif %}
</div>
{% endif %}
</a>
{% endif %}
</div>
<h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
{% include 'inc/created_updated.html' with obj=devicetype %}
<div class="pull-right noprint">

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load static %}
{% load custom_links %}
{% load helpers %}
@ -30,6 +31,9 @@
</div>
</div>
<div class="pull-right noprint">
{% if perms.dcim.add_powerfeed %}
{% clone_button 'dcim:powerfeed_add' powerfeed %}
{% endif %}
{% if perms.dcim.change_powerfeed %}
<a href="{% url 'dcim:powerfeed_edit' pk=powerfeed.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
@ -31,6 +32,9 @@
<a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-primary">
<span class="fa fa-chevron-right" aria-hidden="true"></span> Next Rack
</a>
{% if perms.dcim.add_rack %}
{% clone_button 'dcim:rack_add' rack %}
{% endif %}
{% if perms.dcim.change_rack %}
<a href="{% url 'dcim:rack_edit' pk=rack.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span> Edit this rack

View File

@ -1,8 +1,9 @@
{% extends '_base.html' %}
{% load static %}
{% load tz %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
{% load static %}
{% load tz %}
{% block header %}
<div class="row noprint">
@ -38,6 +39,9 @@
Graphs
</button>
{% endif %}
{% if perms.dcim.add_site %}
{% clone_button 'dcim:site_add' site %}
{% endif %}
{% if perms.dcim.change_site %}
<a href="{% url 'dcim:site_edit' slug=site.slug %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
@ -25,6 +26,9 @@
</div>
</div>
<div class="pull-right noprint">
{% if perms.ipam.add_aggregate %}
{% clone_button 'ipam:aggregate_add' aggregate %}
{% endif %}
{% if perms.ipam.change_aggregate %}
<a href="{% url 'ipam:aggregate_edit' pk=aggregate.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
@ -27,6 +28,9 @@
</div>
</div>
<div class="pull-right noprint">
{% if perms.ipam.add_ipaddress %}
{% clone_button 'ipam:ipaddress_add' ipaddress %}
{% endif %}
{% if perms.ipam.change_ipaddress %}
<a href="{% url 'ipam:ipaddress_edit' pk=ipaddress.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
@ -38,6 +39,9 @@
Add an IP Address
</a>
{% endif %}
{% if perms.ipam.add_prefix %}
{% clone_button 'ipam:prefix_add' prefix %}
{% endif %}
{% if perms.ipam.change_prefix %}
<a href="{% url 'ipam:prefix_edit' pk=prefix.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
@ -30,6 +31,9 @@
</div>
</div>
<div class="pull-right noprint">
{% if perms.ipam.add_vlan %}
{% clone_button 'ipam:vlan_add' vlan %}
{% endif %}
{% if perms.ipam.change_vlan %}
<a href="{% url 'ipam:vlan_edit' pk=vlan.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
@ -24,6 +25,9 @@
</div>
</div>
<div class="pull-right noprint">
{% if perms.ipam.add_vrf %}
{% clone_button 'ipam:vrf_add' vrf %}
{% endif %}
{% if perms.ipam.change_vrf %}
<a href="{% url 'ipam:vrf_edit' pk=vrf.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
@ -27,6 +28,9 @@
</div>
</div>
<div class="pull-right noprint">
{% if perms.tenancy.add_tenant %}
{% clone_button 'tenancy:tenant_add' tenant %}
{% endif %}
{% if perms.tenancy.change_tenant %}
<a href="{% url 'tenancy:tenant_edit' slug=tenant.slug %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
@ -27,6 +28,9 @@
</div>
</div>
<div class="pull-right noprint">
{% if perms.virtualization.add_cluster %}
{% clone_button 'virtualization:cluster_add' cluster %}
{% endif %}
{% if perms.virtualization.change_cluster %}
<a href="{% url 'virtualization:cluster_edit' pk=cluster.pk %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
@ -26,6 +27,9 @@
</div>
</div>
<div class="pull-right noprint">
{% if perms.virtualization.add_virtualmachine %}
{% clone_button 'virtualization:virtualmachine_add' virtualmachine %}
{% endif %}
{% if perms.virtualization.change_virtualmachine %}
<a href="{% url 'virtualization:virtualmachine_edit' pk=virtualmachine.pk %}" class="btn btn-warning">
<span class="fa fa-pencil"></span>

View File

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

View File

@ -0,0 +1,3 @@
<a href="{{ url }}" class="btn btn-success">
<span class="fa fa-plus" aria-hidden="true"></span> Clone
</a>

View File

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

View File

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