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 ### 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.) 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 * [#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 * [#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 ### 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 * [#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 django.db import models
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from extras.utils import is_taggable
from utilities.mptt import TreeManager from utilities.mptt import TreeManager
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from netbox.models.features import * from netbox.models.features import *
@ -52,6 +53,25 @@ class NetBoxModel(NetBoxFeatureSet, models.Model):
class Meta: class Meta:
abstract = True 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): class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
""" """

View File

@ -394,11 +394,11 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
if '_addanother' in request.POST: if '_addanother' in request.POST:
redirect_url = request.path 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) params = prepare_cloned_fields(obj)
if 'return_url' in request.GET:
params['return_url'] = request.GET.get('return_url')
if params: if params:
if 'return_url' in request.GET:
params['return_url'] = request.GET.get('return_url')
redirect_url += f"?{params.urlencode()}" redirect_url += f"?{params.urlencode()}"
return redirect(redirect_url) return redirect(redirect_url)

View File

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

View File

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