Closes #9414: Add clone() method to NetBoxModel for copying instance attributes

This commit is contained in:
jeremystretch 2022-06-23 15:21:10 -04:00
parent 12bd3840f9
commit f9d81fd362
6 changed files with 60 additions and 22 deletions

View File

@ -49,6 +49,24 @@ class MyModel(NetBoxModel):
...
```
### The `clone()` Method
!!! info
This method was introduced in NetBox v3.3.
The `NetBoxModel` class includes a `clone()` method to be used for gathering attriubtes which can be used to create a "cloned" instance. This is used primarily for form initialization, e.g. when using the "clone" button in the NetBox UI. By default, this method will replicate any fields listed in the model's `clone_fields` list, if defined.
Plugin models can leverage this method by defining `clone_fields` as a list of field names to be replicated, or override this method to replace or extend its content:
```python
class MyModel(NetBoxModel):
def clone(self):
attrs = super().clone()
attrs['extra-value'] = 123
return attrs
```
### Enabling Features Individually
If you prefer instead to enable only a subset of these features for a plugin model, NetBox provides a discrete "mix-in" class for each feature. You can subclass each of these individually when defining your model. (Your model will also need to inherit from Django's built-in `Model` class.)

View File

@ -30,6 +30,10 @@
* [#9536](https://github.com/netbox-community/netbox/issues/9536) - Track API token usage times
* [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location
### Plugins API
* [#9414](https://github.com/netbox-community/netbox/issues/9414) - Add `clone()` method to NetBoxModel for copying instance attributes
### Other Changes
* [#9261](https://github.com/netbox-community/netbox/issues/9261) - `NetBoxTable` no longer automatically clears pre-existing calls to `prefetch_related()` on its queryset

View File

@ -2,6 +2,7 @@ from django.core.validators import ValidationError
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
from extras.utils import is_taggable
from utilities.mptt import TreeManager
from utilities.querysets import RestrictedQuerySet
from netbox.models.features import *
@ -52,6 +53,25 @@ class NetBoxModel(NetBoxFeatureSet, models.Model):
class Meta:
abstract = True
def clone(self):
"""
Return a dictionary of attributes suitable for creating a copy of the current instance. This is used for pre-
populating an object creation form in the UI.
"""
attrs = {}
for field_name in getattr(self, 'clone_fields', []):
field = self._meta.get_field(field_name)
field_value = field.value_from_object(self)
if field_value not in (None, ''):
attrs[field_name] = field_value
# Include tags (if applicable)
if is_taggable(self):
attrs['tags'] = [tag.pk for tag in self.tags.all()]
return attrs
class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
"""

View File

@ -394,11 +394,11 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
if '_addanother' in request.POST:
redirect_url = request.path
# If the object has clone_fields, pre-populate a new instance of the form
# If cloning is supported, pre-populate a new instance of the form
params = prepare_cloned_fields(obj)
if 'return_url' in request.GET:
params['return_url'] = request.GET.get('return_url')
if params:
if 'return_url' in request.GET:
params['return_url'] = request.GET.get('return_url')
redirect_url += f"?{params.urlencode()}"
return redirect(redirect_url)

View File

@ -59,7 +59,7 @@ Context:
{# Extra buttons #}
{% block extra_controls %}{% endblock %}
{% if object.clone_fields and request.user|can_add:object %}
{% if request.user|can_add:object %}
{% clone_button object %}
{% endif %}
{% if request.user|can_change:object %}

View File

@ -282,26 +282,22 @@ def render_jinja2(template_code, context):
def prepare_cloned_fields(instance):
"""
Compile an object's `clone_fields` list into a string of URL query parameters. Tags are automatically cloned where
applicable.
Generate a QueryDict comprising attributes from an object's clone() method.
"""
# Generate the clone attributes from the instance
if not hasattr(instance, 'clone'):
return None
attrs = instance.clone()
# Prepare querydict parameters
params = []
for field_name in getattr(instance, 'clone_fields', []):
field = instance._meta.get_field(field_name)
field_value = field.value_from_object(instance)
# Pass False as null for boolean fields
if field_value is False:
params.append((field_name, ''))
# Omit empty values
elif field_value not in (None, ''):
params.append((field_name, field_value))
# Copy tags
if is_taggable(instance):
for tag in instance.tags.all():
params.append(('tags', tag.pk))
for key, value in attrs.items():
if type(value) in (list, tuple):
params.extend([(key, v) for v in value])
elif value not in (False, None):
params.append((key, value))
else:
params.append((key, ''))
# Return a QueryDict with the parameters
return QueryDict('&'.join([f'{k}={v}' for k, v in params]), mutable=True)