Przeglądaj źródła

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

jeremystretch 3 lat temu
rodzic
commit
f9d81fd362

+ 18 - 0
docs/plugins/development/models.md

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

+ 4 - 0
docs/release-notes/version-3.3.md

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

+ 20 - 0
netbox/netbox/models/__init__.py

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

+ 3 - 3
netbox/netbox/views/generic/object_views.py

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

+ 1 - 1
netbox/templates/generic/object.html

@@ -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 %}

+ 14 - 18
netbox/utilities/utils.py

@@ -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
-    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 = []
     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 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)