Repository: nautobot/nautobot

Issue:
"Check Secret" button on Secrets page is broken
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Nautobot version (Docker tag too if applicable): 1.5.23
* Python version: 3.10
* Database platform, version: postgresql, 13.10
* Middleware(s): N/A

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. Create a secret.
2. Go to that secret's page.
3. Press the "Check Secret" button in the top right.

<!-- What did you expect to happen? -->
### Expected Behavior
I expected to see a popup with a status message of either "The secret was successfully retrieved." or "Error retrieving secret: ...".

<!-- What happened instead? -->
### Observed Behavior
No popup is created and the following error messages are generated in the console:

* Upon loading the page:
`Uncaught SyntaxError: expected expression, got '<'`
* Upon pressing the button:
`Uncaught ReferenceError: checkSecret is not defined`

### Likely Cause
[PR#4048](https://github.com/nautobot/nautobot/pull/4048) added `{{ block.super }}` inside of a script tag, causing it to be nested and generate these errors. Moving this import outside of the script tag should make it work again.



Discussion:

Resolving patch:
diff --git a/examples/example_plugin/example_plugin/signals.py b/examples/example_plugin/example_plugin/signals.py
--- a/examples/example_plugin/example_plugin/signals.py
+++ b/examples/example_plugin/example_plugin/signals.py
@@ -1,4 +1,6 @@
 """Signal handlers for the example example_plugin."""
+EXAMPLE_PLUGIN_CUSTOM_FIELD_DEFAULT = "Default value"
+EXAMPLE_PLUGIN_CUSTOM_FIELD_NAME = "example_plugin_auto_custom_field"  # Note underscores rather than dashes!
 
 
 def nautobot_database_ready_callback(sender, *, apps, **kwargs):
@@ -29,20 +31,20 @@ def nautobot_database_ready_callback(sender, *, apps, **kwargs):
 
     if hasattr(CustomField, "slug"):
         field, _ = CustomField.objects.update_or_create(
-            slug="example_plugin_auto_custom_field",  # Note underscores rather than dashes!
+            slug=EXAMPLE_PLUGIN_CUSTOM_FIELD_NAME,
             defaults={
                 "type": CustomFieldTypeChoices.TYPE_TEXT,
                 "label": "Example Plugin Automatically Added Custom Field",
-                "default": "Default value",
+                "default": EXAMPLE_PLUGIN_CUSTOM_FIELD_DEFAULT,
             },
         )
     else:
         field, _ = CustomField.objects.update_or_create(
-            key="example_plugin_auto_custom_field",  # Note underscores rather than dashes!
+            key=EXAMPLE_PLUGIN_CUSTOM_FIELD_NAME,
             defaults={
                 "type": CustomFieldTypeChoices.TYPE_TEXT,
                 "label": "Example Plugin Automatically Added Custom Field",
-                "default": "Default value",
+                "default": EXAMPLE_PLUGIN_CUSTOM_FIELD_DEFAULT,
             },
         )
     field.content_types.set([ContentType.objects.get_for_model(Location)])
diff --git a/nautobot/extras/models/customfields.py b/nautobot/extras/models/customfields.py
--- a/nautobot/extras/models/customfields.py
+++ b/nautobot/extras/models/customfields.py
@@ -233,10 +233,13 @@ def clean(self):
             except ValidationError as e:
                 raise ValidationError(f"Invalid value for custom field '{field_key}': {e.message}")
 
-        # Check for missing required values
+        # Check for missing values, erroring on required ones and populating non-required ones automatically
         for cf in custom_fields.values():
-            if cf.required and cf.key not in self._custom_field_data:
-                raise ValidationError(f"Missing required custom field '{cf.key}'.")
+            if cf.key not in self._custom_field_data:
+                if cf.required:
+                    raise ValidationError(f"Missing required custom field '{cf.key}'.")
+                else:
+                    self._custom_field_data[cf.key] = cf.default
 
     # Computed Field Methods
     def has_computed_fields(self, advanced_ui=None):


Test patch:
diff --git a/nautobot/extras/tests/test_changelog.py b/nautobot/extras/tests/test_changelog.py
--- a/nautobot/extras/tests/test_changelog.py
+++ b/nautobot/extras/tests/test_changelog.py
@@ -4,6 +4,7 @@
 from django.utils.html import escape
 from rest_framework import status
 
+from example_plugin.signals import EXAMPLE_PLUGIN_CUSTOM_FIELD_DEFAULT, EXAMPLE_PLUGIN_CUSTOM_FIELD_NAME
 from nautobot.core.graphql import execute_query
 from nautobot.core.testing import APITestCase, TestCase
 from nautobot.core.testing.utils import post_data
@@ -262,6 +263,7 @@ def test_create_object(self):
             "custom_fields": {
                 "my_field": "ABC",
                 "my_field_select": "Bar",
+                EXAMPLE_PLUGIN_CUSTOM_FIELD_NAME: EXAMPLE_PLUGIN_CUSTOM_FIELD_DEFAULT,
             },
             "tags": [
                 {"name": self.tags[0].name},
@@ -299,6 +301,7 @@ def test_update_object(self):
             "custom_fields": {
                 "my_field": "DEF",
                 "my_field_select": "Foo",
+                EXAMPLE_PLUGIN_CUSTOM_FIELD_NAME: EXAMPLE_PLUGIN_CUSTOM_FIELD_DEFAULT,
             },
             "tags": [{"name": self.tags[2].name}],
         }
@@ -509,12 +512,13 @@ def setUpTestData(cls):
     def test_get_snapshots(self):
         with context_managers.web_request_context(self.user):
             location_type = LocationType.objects.get(name="Campus")
-            location = Location.objects.create(
+            location = Location(
                 name="testobjectchangelocation",
                 description="initial description",
                 status=self.location_status,
                 location_type=location_type,
             )
+            location.validated_save()
         initial_object_change = get_changes_for_model(location).first()
 
         with self.subTest("test get_snapshots ObjectChange create"):
diff --git a/nautobot/extras/tests/test_customfields.py b/nautobot/extras/tests/test_customfields.py
--- a/nautobot/extras/tests/test_customfields.py
+++ b/nautobot/extras/tests/test_customfields.py
@@ -9,6 +9,7 @@
 from rest_framework import status
 from nautobot.core.forms.widgets import MultiValueCharInput, StaticSelect2
 
+from nautobot.circuits.models import Provider
 from nautobot.core.models.fields import slugify_dashes_to_underscores
 from nautobot.core.tables import CustomFieldColumn
 from nautobot.core.testing import APITestCase, TestCase, TransactionTestCase
@@ -1185,6 +1186,84 @@ def setUpTestData(cls):
             weight=200,
         )
 
+    def test_custom_field_dict_population(self):
+        """Test that custom_field_data is properly populated when no data is passed in."""
+        label = "Custom Field"
+        custom_field = CustomField.objects.create(
+            label=label,
+            key="custom_field",
+            type=CustomFieldTypeChoices.TYPE_TEXT,
+        )
+        custom_field.validated_save()
+        custom_field.content_types.set([ContentType.objects.get_for_model(Provider)])
+
+        provider = Provider.objects.create(name="Test")
+        provider.validated_save()
+
+        self.assertIn(
+            "custom_field",
+            provider._custom_field_data.keys(),
+            "Custom fields aren't being set properly on a model on save.",
+        )
+
+    def test_custom_field_required(self):
+        """Test that omitting required custom fields raises a ValidationError."""
+        label = "Custom Field"
+        custom_field = CustomField.objects.create(
+            # 2.0 TODO: #824 remove name field
+            label=label,
+            key="custom_field",
+            type=CustomFieldTypeChoices.TYPE_TEXT,
+            required=True,
+        )
+        custom_field.validated_save()
+        custom_field.content_types.set([ContentType.objects.get_for_model(Provider)])
+
+        provider = Provider.objects.create(name="Test")
+        with self.assertRaises(ValidationError):
+            provider.validated_save()
+
+    def test_custom_field_required_on_update(self):
+        """Test that removing required custom fields and then updating an object raises a ValidationError."""
+        label = "Custom Field"
+        custom_field = CustomField.objects.create(
+            # 2.0 TODO: #824 remove name field
+            label=label,
+            key="custom_field",
+            type=CustomFieldTypeChoices.TYPE_TEXT,
+            required=True,
+        )
+        custom_field.validated_save()
+        custom_field.content_types.set([ContentType.objects.get_for_model(Provider)])
+
+        provider = Provider.objects.create(name="Test", _custom_field_data={"custom_field": "Value"})
+        provider.validated_save()
+        provider._custom_field_data.pop("custom_field")
+        with self.assertRaises(ValidationError):
+            provider.validated_save()
+
+    def test_update_removed_custom_field(self):
+        """Test that missing custom field keys are added on save."""
+        label = "Custom Field"
+        custom_field = CustomField.objects.create(
+            label=label,
+            key="custom_field",
+            type=CustomFieldTypeChoices.TYPE_TEXT,
+        )
+        custom_field.validated_save()
+        custom_field.content_types.set([ContentType.objects.get_for_model(Provider)])
+
+        # Explicitly there is no `validated_save` so the custom field is not populated
+        provider = Provider.objects.create(name="Test")
+
+        self.assertEqual(
+            {}, provider._custom_field_data, "Custom field data was not empty despite clean not being called."
+        )
+
+        provider.validated_save()
+
+        self.assertIn("custom_field", provider._custom_field_data.keys())
+
     def test_cf_data(self):
         """
         Check that custom field data is present on the instance immediately after being set and after being fetched