Repository: netbox-community/netbox

Issue:
Replication fields broken in custom validation
### NetBox version

v3.3.8

### Python version

3.9

### Steps to Reproduce

After updating NetBox 3.2.x to 3.3.8 the custom validation logic that used to work is now broken.

1. Create a custom validator that tries to validate a replication field, for example for `VMInterface` model try to validate field `name`.
2. Try to add a new interface to a VM.

### Expected Behavior

The custom validator should have access to `name` field.

### Observed Behavior

The `name` field is always empty when creating a new object and the custom validation logic fails.

As per https://github.com/netbox-community/netbox/discussions/11031 this is apparently due to "replication fields (think interface name expansion) was refactored in that PR and seems to have caused the name field to be blank at the time of validation", possibly due to https://github.com/netbox-community/netbox/pull/10312.


Discussion:
Another side-effect of that change is that creating a duplicate interface (device or vm) fails silently instead of showing the form validation error. Before v3.3.3 it showed the also sub-optimal "This field is required" form validation error.

I'm guessing there's some order issues with how validation is executed.

Complete replication steps for posterity:

* Create a site `testsite`
* Create a virtual machine `testvm`
* Register a custom validator in configuration.py (or wherever):

```
from extras.validators import CustomValidator

class MyValidator(CustomValidator):
    def validate(self, instance):
        self.fail(f"Name is: {instance.name}")

CUSTOM_VALIDATORS = {
    'virtualization.vminterface': (
        MyValidator(),
    )
}
```

* Create an interface `testinterface` on vm `testvm`.
* Notice that validation fails with error "Name is: " - indicating the the vminterface name is blank
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. NetBox is governed by a small group of core maintainers which means not all opened issues may receive direct feedback. **Do not** attempt to circumvent this process by "bumping" the issue; doing so will result in its immediate closure and you may be barred from participating in any future discussions. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).

Resolving patch:
diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py
--- a/netbox/netbox/models/features.py
+++ b/netbox/netbox/models/features.py
@@ -257,6 +257,10 @@ class Meta:
     def clean(self):
         super().clean()
 
+        # If the instance is a base for replications, skip custom validation
+        if getattr(self, '_replicated_base', False):
+            return
+
         # Send the post_clean signal
         post_clean.send(sender=self.__class__, instance=self)
 
diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py
--- a/netbox/netbox/views/generic/object_views.py
+++ b/netbox/netbox/views/generic/object_views.py
@@ -436,6 +436,10 @@ def post(self, request):
         form = self.initialize_form(request)
         instance = self.alter_object(self.queryset.model(), request)
 
+        # Note that the form instance is a replicated field base
+        # This is needed to avoid running custom validators multiple times
+        form.instance._replicated_base = hasattr(self.form, "replication_fields")
+
         if form.is_valid():
             new_components = []
             data = deepcopy(request.POST)


Test patch: