Repository: netbox-community/netbox
Issue: Cloning Custom Fields does not clone anything

### NetBox version

v.3.3.2

### Python version

3.9

### Steps to Reproduce

1. Navigate to "Other -> Custom Fields"
2. Click any Custom Field in order to show its details
3. Click the green "Clone" button

### Expected Behavior

Some values of the cloned Custom Field should be adopted, at least Model(s), Type, Object Type, Required, Filter logic, UI visibility.

### Observed Behavior

There's no difference whether you press Clone or Add (in Custom Fields overview). Cloning behaves just like adding a new Custom Field.

Changed files:
netbox/extras/models/customfields.py, netbox/netbox/models/features.py, netbox/netbox/models/__init__.py, netbox/extras/models/models.py

diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py
index 426565231..43c4f9671 100644
--- a/netbox/extras/models/customfields.py
+++ b/netbox/extras/models/customfields.py
@@ -14,7 +14,7 @@ from django.utils.safestring import mark_safe
 from extras.choices import *
 from extras.utils import FeatureQuery
 from netbox.models import ChangeLoggedModel
-from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
+from netbox.models.features import CloningMixin, ExportTemplatesMixin, WebhooksMixin
 from utilities import filters
 from utilities.forms import (
     CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
@@ -41,7 +41,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
         return self.get_queryset().filter(content_types=content_type)
 
 
-class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
+class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
     content_types = models.ManyToManyField(
         to=ContentType,
         related_name='custom_fields',
@@ -143,8 +143,14 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
         verbose_name='UI visibility',
         help_text='Specifies the visibility of custom field in the UI'
     )
+
     objects = CustomFieldManager()
 
+    clone_fields = (
+        'content_types', 'type', 'object_type', 'group_name', 'description', 'required', 'filter_logic', 'default',
+        'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'ui_visibility',
+    )
+
     class Meta:
         ordering = ['group_name', 'weight', 'name']
 
diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py
index 4873a1f9e..0df34c146 100644
--- a/netbox/extras/models/models.py
+++ b/netbox/extras/models/models.py
@@ -21,7 +21,7 @@ from extras.conditions import ConditionSet
 from extras.utils import FeatureQuery, image_upload
 from netbox.models import ChangeLoggedModel
 from netbox.models.features import (
-    CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, TagsMixin, WebhooksMixin,
+    CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, JobResultsMixin, TagsMixin, WebhooksMixin,
 )
 from utilities.querysets import RestrictedQuerySet
 from utilities.utils import render_jinja2
@@ -187,7 +187,7 @@ class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
         return render_jinja2(self.payload_url, context)
 
 
-class CustomLink(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
+class CustomLink(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
     """
     A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
     code to be rendered with an object as context.
@@ -230,6 +230,10 @@ class CustomLink(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
         help_text="Force link to open in a new window"
     )
 
+    clone_fields = (
+        'content_type', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
+    )
+
     class Meta:
         ordering = ['group_name', 'weight', 'name']
 
diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py
index 4c65094ca..aefb733b4 100644
--- a/netbox/netbox/models/__init__.py
+++ b/netbox/netbox/models/__init__.py
@@ -2,7 +2,6 @@ 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 *
@@ -32,7 +31,7 @@ class NetBoxFeatureSet(
     def get_prerequisite_models(cls):
         """
         Return a list of model types that are required to create this model or empty list if none.  This is used for
-        showing prequisite warnings in the UI on the list and detail views.
+        showing prerequisite warnings in the UI on the list and detail views.
         """
         return []
 
@@ -52,7 +51,7 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, models.Model)
         abstract = True
 
 
-class NetBoxModel(NetBoxFeatureSet, models.Model):
+class NetBoxModel(CloningMixin, NetBoxFeatureSet, models.Model):
     """
     Primary models represent real objects within the infrastructure being modeled.
     """
@@ -61,25 +60,6 @@ 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):
     """
diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py
index 6b2ee1f94..7f30248b4 100644
--- a/netbox/netbox/models/features.py
+++ b/netbox/netbox/models/features.py
@@ -10,12 +10,13 @@ from django.db import models
 from taggit.managers import TaggableManager
 
 from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices
-from extras.utils import register_features
+from extras.utils import is_taggable, register_features
 from netbox.signals import post_clean
 from utilities.utils import serialize_object
 
 __all__ = (
     'ChangeLoggingMixin',
+    'CloningMixin',
     'CustomFieldsMixin',
     'CustomLinksMixin',
     'CustomValidationMixin',
@@ -82,6 +83,33 @@ class ChangeLoggingMixin(models.Model):
         return objectchange
 
 
+class CloningMixin(models.Model):
+    """
+    Provides the clone() method used to prepare a copy of existing objects.
+    """
+    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 CustomFieldsMixin(models.Model):
     """
     Enables support for custom fields.