mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Closes #9414: Add clone() method to NetBoxModel for copying instance attributes
This commit is contained in:
parent
12bd3840f9
commit
f9d81fd362
@ -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.)
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
@ -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 %}
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user