We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Prefix with tags is created in 2 steps
<!--
    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
* Python version: 3.6.8
* Nautobot version: v1.0.0b2

<!--
    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.
-->

When creating a new prefix while specifying any tags, it is not created with it right away but first without tags and than it is updated with them. The result is correct, having prefix with tags created. But the process is wrong, because it triggers 2 webhooks (`type create` and `type update`), but there should only be single for `type update`, as this is creation.

### Steps to Reproduce
1. create new prefix with tags
![image](https://user-images.githubusercontent.com/26114847/113024418-02e35f00-9187-11eb-9b79-47fb9f545e39.png)


2. check changelog
![image](https://user-images.githubusercontent.com/26114847/113024187-c1eb4a80-9186-11eb-826e-ca945805b0e0.png)
![image](https://user-images.githubusercontent.com/26114847/113024374-f52dd980-9186-11eb-97d6-89aef6e63a71.png)

<!-- What did you expect to happen? -->
### Expected Behavior
Single change logging creation of the prefix

<!-- What happened instead? -->
### Observed Behavior
Change logging creation of the prefix and another one logging update (adding tags)


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `nautobot/extras/signals.py`
Content:
```
1 import os
2 import random
3 import shutil
4 import uuid
5 import logging
6 from datetime import timedelta
7 
8 from cacheops.signals import cache_invalidated, cache_read
9 from django.conf import settings
10 from django.contrib.contenttypes.models import ContentType
11 from django.db import transaction
12 from django.db.models.signals import m2m_changed, pre_delete
13 from django.dispatch import receiver
14 from django.utils import timezone
15 from django_prometheus.models import model_deletes, model_inserts, model_updates
16 from prometheus_client import Counter
17 
18 from nautobot.extras.tasks import delete_custom_field_data, provision_field
19 from .choices import JobResultStatusChoices, ObjectChangeActionChoices
20 from .models import CustomField, GitRepository, JobResult, ObjectChange
21 from .webhooks import enqueue_webhooks
22 
23 logger = logging.getLogger("nautobot.extras.signals")
24 
25 
26 #
27 # Change logging/webhooks
28 #
29 
30 
31 def _get_user_if_authenticated(request, objectchange):
32     """Return the user object associated with the request if the user is defined.
33 
34     If the user is not defined, log a warning to indicate that the user couldn't be retrived from the request
35     This is a workaround to fix a recurring issue where the user shouldn't be present in the request object randomly.
36     A similar issue was reported in NetBox https://github.com/netbox-community/netbox/issues/5142
37     """
38     if request.user.is_authenticated:
39         return request.user
40     else:
41         logger.warning(f"Unable to retrieve the user while creating the changelog for {objectchange.changed_object}")
42 
43 
44 def _handle_changed_object(request, sender, instance, **kwargs):
45     """
46     Fires when an object is created or updated.
47     """
48     # Queue the object for processing once the request completes
49     if kwargs.get("created"):
50         action = ObjectChangeActionChoices.ACTION_CREATE
51     elif "created" in kwargs:
52         action = ObjectChangeActionChoices.ACTION_UPDATE
53     elif kwargs.get("action") in ["post_add", "post_remove"] and kwargs["pk_set"]:
54         # m2m_changed with objects added or removed
55         action = ObjectChangeActionChoices.ACTION_UPDATE
56     else:
57         return
58 
59     # Record an ObjectChange if applicable
60     if hasattr(instance, "to_objectchange"):
61         objectchange = instance.to_objectchange(action)
62         objectchange.user = _get_user_if_authenticated(request, objectchange)
63         objectchange.request_id = request.id
64         objectchange.save()
65 
66     # Enqueue webhooks
67     enqueue_webhooks(instance, request.user, request.id, action)
68 
69     # Increment metric counters
70     if action == ObjectChangeActionChoices.ACTION_CREATE:
71         model_inserts.labels(instance._meta.model_name).inc()
72     elif action == ObjectChangeActionChoices.ACTION_UPDATE:
73         model_updates.labels(instance._meta.model_name).inc()
74 
75     # Housekeeping: 0.1% chance of clearing out expired ObjectChanges
76     if settings.CHANGELOG_RETENTION and random.randint(1, 1000) == 1:
77         cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)
78         ObjectChange.objects.filter(time__lt=cutoff).delete()
79 
80 
81 def _handle_deleted_object(request, sender, instance, **kwargs):
82     """
83     Fires when an object is deleted.
84     """
85     # Record an ObjectChange if applicable
86     if hasattr(instance, "to_objectchange"):
87         objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)
88         objectchange.user = _get_user_if_authenticated(request, objectchange)
89         objectchange.request_id = request.id
90         objectchange.save()
91 
92     # Enqueue webhooks
93     enqueue_webhooks(instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)
94 
95     # Increment metric counters
96     model_deletes.labels(instance._meta.model_name).inc()
97 
98 
99 #
100 # Custom fields
101 #
102 
103 
104 def handle_cf_removed_obj_types(instance, action, pk_set, **kwargs):
105     """
106     Handle the cleanup of old custom field data when a CustomField is removed from one or more ContentTypes.
107     """
108     if action == "post_remove":
109         # Existing content types have been removed from the custom field, delete their data
110         transaction.on_commit(lambda: delete_custom_field_data.delay(instance.name, pk_set))
111 
112     elif action == "post_add":
113         # New content types have been added to the custom field, provision them
114         transaction.on_commit(lambda: provision_field.delay(instance.pk, pk_set))
115 
116 
117 m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through)
118 
119 
120 #
121 # Caching
122 #
123 
124 cacheops_cache_hit = Counter("cacheops_cache_hit", "Number of cache hits")
125 cacheops_cache_miss = Counter("cacheops_cache_miss", "Number of cache misses")
126 cacheops_cache_invalidated = Counter("cacheops_cache_invalidated", "Number of cache invalidations")
127 
128 
129 def cache_read_collector(sender, func, hit, **kwargs):
130     if hit:
131         cacheops_cache_hit.inc()
132     else:
133         cacheops_cache_miss.inc()
134 
135 
136 def cache_invalidated_collector(sender, obj_dict, **kwargs):
137     cacheops_cache_invalidated.inc()
138 
139 
140 cache_read.connect(cache_read_collector)
141 cache_invalidated.connect(cache_invalidated_collector)
142 
143 
144 #
145 # Datasources
146 #
147 
148 
149 @receiver(pre_delete, sender=GitRepository)
150 def git_repository_pre_delete(instance, **kwargs):
151     """
152     When a GitRepository is deleted, invoke all registered callbacks, then remove it from the local filesystem.
153 
154     Note that GitRepository create/update operations enqueue a background job to handle the sync/resync;
155     this operation, by contrast, happens in the foreground as it needs to complete before we allow the
156     GitRepository itself to be deleted.
157     """
158     from nautobot.extras.datasources import refresh_datasource_content
159 
160     job_result = JobResult.objects.create(
161         name=instance.name,
162         obj_type=ContentType.objects.get_for_model(instance),
163         user=None,
164         job_id=uuid.uuid4(),
165         status=JobResultStatusChoices.STATUS_RUNNING,
166     )
167 
168     refresh_datasource_content("extras.gitrepository", instance, None, job_result, delete=True)
169 
170     if job_result.status not in JobResultStatusChoices.TERMINAL_STATE_CHOICES:
171         job_result.set_status(JobResultStatusChoices.STATUS_COMPLETED)
172     job_result.save()
173 
174     # TODO: In a distributed Nautobot deployment, each Django instance and/or RQ worker instance may have its own clone
175     # of this repository; we need some way to ensure that all such clones are deleted.
176     # For now we just delete the one that we have locally and rely on other methods (notably get_jobs())
177     # to clean up other clones as they're encountered.
178     if os.path.isdir(instance.filesystem_path):
179         shutil.rmtree(instance.filesystem_path)
180
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/nautobot/extras/signals.py b/nautobot/extras/signals.py
--- a/nautobot/extras/signals.py
+++ b/nautobot/extras/signals.py
@@ -45,23 +45,33 @@
     """
     Fires when an object is created or updated.
     """
-    # Queue the object for processing once the request completes
+    m2m_changed = False
+
+    # Determine the type of change being made
     if kwargs.get("created"):
         action = ObjectChangeActionChoices.ACTION_CREATE
     elif "created" in kwargs:
         action = ObjectChangeActionChoices.ACTION_UPDATE
     elif kwargs.get("action") in ["post_add", "post_remove"] and kwargs["pk_set"]:
         # m2m_changed with objects added or removed
+        m2m_changed = True
         action = ObjectChangeActionChoices.ACTION_UPDATE
     else:
         return
 
     # Record an ObjectChange if applicable
     if hasattr(instance, "to_objectchange"):
-        objectchange = instance.to_objectchange(action)
-        objectchange.user = _get_user_if_authenticated(request, objectchange)
-        objectchange.request_id = request.id
-        objectchange.save()
+        if m2m_changed:
+            ObjectChange.objects.filter(
+                changed_object_type=ContentType.objects.get_for_model(instance),
+                changed_object_id=instance.pk,
+                request_id=request.id,
+            ).update(object_data=instance.to_objectchange(action).object_data)
+        else:
+            objectchange = instance.to_objectchange(action)
+            objectchange.user = _get_user_if_authenticated(request, objectchange)
+            objectchange.request_id = request.id
+            objectchange.save()
 
     # Enqueue webhooks
     enqueue_webhooks(instance, request.user, request.id, action)

{"golden_diff": "diff --git a/nautobot/extras/signals.py b/nautobot/extras/signals.py\n--- a/nautobot/extras/signals.py\n+++ b/nautobot/extras/signals.py\n@@ -45,23 +45,33 @@\n     \"\"\"\n     Fires when an object is created or updated.\n     \"\"\"\n-    # Queue the object for processing once the request completes\n+    m2m_changed = False\n+\n+    # Determine the type of change being made\n     if kwargs.get(\"created\"):\n         action = ObjectChangeActionChoices.ACTION_CREATE\n     elif \"created\" in kwargs:\n         action = ObjectChangeActionChoices.ACTION_UPDATE\n     elif kwargs.get(\"action\") in [\"post_add\", \"post_remove\"] and kwargs[\"pk_set\"]:\n         # m2m_changed with objects added or removed\n+        m2m_changed = True\n         action = ObjectChangeActionChoices.ACTION_UPDATE\n     else:\n         return\n \n     # Record an ObjectChange if applicable\n     if hasattr(instance, \"to_objectchange\"):\n-        objectchange = instance.to_objectchange(action)\n-        objectchange.user = _get_user_if_authenticated(request, objectchange)\n-        objectchange.request_id = request.id\n-        objectchange.save()\n+        if m2m_changed:\n+            ObjectChange.objects.filter(\n+                changed_object_type=ContentType.objects.get_for_model(instance),\n+                changed_object_id=instance.pk,\n+                request_id=request.id,\n+            ).update(object_data=instance.to_objectchange(action).object_data)\n+        else:\n+            objectchange = instance.to_objectchange(action)\n+            objectchange.user = _get_user_if_authenticated(request, objectchange)\n+            objectchange.request_id = request.id\n+            objectchange.save()\n \n     # Enqueue webhooks\n     enqueue_webhooks(instance, request.user, request.id, action)\n", "issue": "Prefix with tags is created in 2 steps\n<!--\r\n    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.\r\n\r\n    This form is only for reporting reproducible bugs. If you need assistance\r\n    with Nautobot installation, or if you have a general question, please start a\r\n    discussion instead: https://github.com/nautobot/nautobot/discussions\r\n\r\n    Please describe the environment in which you are running Nautobot. Be sure\r\n    that you are running an unmodified instance of the latest stable release\r\n    before submitting a bug report, and that any plugins have been disabled.\r\n-->\r\n### Environment\r\n* Python version: 3.6.8\r\n* Nautobot version: v1.0.0b2\r\n\r\n<!--\r\n    Describe in detail the exact steps that someone else can take to reproduce\r\n    this bug using the current stable release of Nautobot. Begin with the\r\n    creation of any necessary database objects and call out every operation\r\n    being performed explicitly. If reporting a bug in the REST API, be sure to\r\n    reconstruct the raw HTTP request(s) being made: Don't rely on a client\r\n    library such as pynautobot.\r\n-->\r\n\r\nWhen creating a new prefix while specifying any tags, it is not created with it right away but first without tags and than it is updated with them. The result is correct, having prefix with tags created. But the process is wrong, because it triggers 2 webhooks (`type create` and `type update`), but there should only be single for `type update`, as this is creation.\r\n\r\n### Steps to Reproduce\r\n1. create new prefix with tags\r\n![image](https://user-images.githubusercontent.com/26114847/113024418-02e35f00-9187-11eb-9b79-47fb9f545e39.png)\r\n\r\n\r\n2. check changelog\r\n![image](https://user-images.githubusercontent.com/26114847/113024187-c1eb4a80-9186-11eb-826e-ca945805b0e0.png)\r\n![image](https://user-images.githubusercontent.com/26114847/113024374-f52dd980-9186-11eb-97d6-89aef6e63a71.png)\r\n\r\n<!-- What did you expect to happen? -->\r\n### Expected Behavior\r\nSingle change logging creation of the prefix\r\n\r\n<!-- What happened instead? -->\r\n### Observed Behavior\r\nChange logging creation of the prefix and another one logging update (adding tags)\r\n\n", "before_files": [{"content": "import os\nimport random\nimport shutil\nimport uuid\nimport logging\nfrom datetime import timedelta\n\nfrom cacheops.signals import cache_invalidated, cache_read\nfrom django.conf import settings\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.db import transaction\nfrom django.db.models.signals import m2m_changed, pre_delete\nfrom django.dispatch import receiver\nfrom django.utils import timezone\nfrom django_prometheus.models import model_deletes, model_inserts, model_updates\nfrom prometheus_client import Counter\n\nfrom nautobot.extras.tasks import delete_custom_field_data, provision_field\nfrom .choices import JobResultStatusChoices, ObjectChangeActionChoices\nfrom .models import CustomField, GitRepository, JobResult, ObjectChange\nfrom .webhooks import enqueue_webhooks\n\nlogger = logging.getLogger(\"nautobot.extras.signals\")\n\n\n#\n# Change logging/webhooks\n#\n\n\ndef _get_user_if_authenticated(request, objectchange):\n    \"\"\"Return the user object associated with the request if the user is defined.\n\n    If the user is not defined, log a warning to indicate that the user couldn't be retrived from the request\n    This is a workaround to fix a recurring issue where the user shouldn't be present in the request object randomly.\n    A similar issue was reported in NetBox https://github.com/netbox-community/netbox/issues/5142\n    \"\"\"\n    if request.user.is_authenticated:\n        return request.user\n    else:\n        logger.warning(f\"Unable to retrieve the user while creating the changelog for {objectchange.changed_object}\")\n\n\ndef _handle_changed_object(request, sender, instance, **kwargs):\n    \"\"\"\n    Fires when an object is created or updated.\n    \"\"\"\n    # Queue the object for processing once the request completes\n    if kwargs.get(\"created\"):\n        action = ObjectChangeActionChoices.ACTION_CREATE\n    elif \"created\" in kwargs:\n        action = ObjectChangeActionChoices.ACTION_UPDATE\n    elif kwargs.get(\"action\") in [\"post_add\", \"post_remove\"] and kwargs[\"pk_set\"]:\n        # m2m_changed with objects added or removed\n        action = ObjectChangeActionChoices.ACTION_UPDATE\n    else:\n        return\n\n    # Record an ObjectChange if applicable\n    if hasattr(instance, \"to_objectchange\"):\n        objectchange = instance.to_objectchange(action)\n        objectchange.user = _get_user_if_authenticated(request, objectchange)\n        objectchange.request_id = request.id\n        objectchange.save()\n\n    # Enqueue webhooks\n    enqueue_webhooks(instance, request.user, request.id, action)\n\n    # Increment metric counters\n    if action == ObjectChangeActionChoices.ACTION_CREATE:\n        model_inserts.labels(instance._meta.model_name).inc()\n    elif action == ObjectChangeActionChoices.ACTION_UPDATE:\n        model_updates.labels(instance._meta.model_name).inc()\n\n    # Housekeeping: 0.1% chance of clearing out expired ObjectChanges\n    if settings.CHANGELOG_RETENTION and random.randint(1, 1000) == 1:\n        cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)\n        ObjectChange.objects.filter(time__lt=cutoff).delete()\n\n\ndef _handle_deleted_object(request, sender, instance, **kwargs):\n    \"\"\"\n    Fires when an object is deleted.\n    \"\"\"\n    # Record an ObjectChange if applicable\n    if hasattr(instance, \"to_objectchange\"):\n        objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)\n        objectchange.user = _get_user_if_authenticated(request, objectchange)\n        objectchange.request_id = request.id\n        objectchange.save()\n\n    # Enqueue webhooks\n    enqueue_webhooks(instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)\n\n    # Increment metric counters\n    model_deletes.labels(instance._meta.model_name).inc()\n\n\n#\n# Custom fields\n#\n\n\ndef handle_cf_removed_obj_types(instance, action, pk_set, **kwargs):\n    \"\"\"\n    Handle the cleanup of old custom field data when a CustomField is removed from one or more ContentTypes.\n    \"\"\"\n    if action == \"post_remove\":\n        # Existing content types have been removed from the custom field, delete their data\n        transaction.on_commit(lambda: delete_custom_field_data.delay(instance.name, pk_set))\n\n    elif action == \"post_add\":\n        # New content types have been added to the custom field, provision them\n        transaction.on_commit(lambda: provision_field.delay(instance.pk, pk_set))\n\n\nm2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through)\n\n\n#\n# Caching\n#\n\ncacheops_cache_hit = Counter(\"cacheops_cache_hit\", \"Number of cache hits\")\ncacheops_cache_miss = Counter(\"cacheops_cache_miss\", \"Number of cache misses\")\ncacheops_cache_invalidated = Counter(\"cacheops_cache_invalidated\", \"Number of cache invalidations\")\n\n\ndef cache_read_collector(sender, func, hit, **kwargs):\n    if hit:\n        cacheops_cache_hit.inc()\n    else:\n        cacheops_cache_miss.inc()\n\n\ndef cache_invalidated_collector(sender, obj_dict, **kwargs):\n    cacheops_cache_invalidated.inc()\n\n\ncache_read.connect(cache_read_collector)\ncache_invalidated.connect(cache_invalidated_collector)\n\n\n#\n# Datasources\n#\n\n\n@receiver(pre_delete, sender=GitRepository)\ndef git_repository_pre_delete(instance, **kwargs):\n    \"\"\"\n    When a GitRepository is deleted, invoke all registered callbacks, then remove it from the local filesystem.\n\n    Note that GitRepository create/update operations enqueue a background job to handle the sync/resync;\n    this operation, by contrast, happens in the foreground as it needs to complete before we allow the\n    GitRepository itself to be deleted.\n    \"\"\"\n    from nautobot.extras.datasources import refresh_datasource_content\n\n    job_result = JobResult.objects.create(\n        name=instance.name,\n        obj_type=ContentType.objects.get_for_model(instance),\n        user=None,\n        job_id=uuid.uuid4(),\n        status=JobResultStatusChoices.STATUS_RUNNING,\n    )\n\n    refresh_datasource_content(\"extras.gitrepository\", instance, None, job_result, delete=True)\n\n    if job_result.status not in JobResultStatusChoices.TERMINAL_STATE_CHOICES:\n        job_result.set_status(JobResultStatusChoices.STATUS_COMPLETED)\n    job_result.save()\n\n    # TODO: In a distributed Nautobot deployment, each Django instance and/or RQ worker instance may have its own clone\n    # of this repository; we need some way to ensure that all such clones are deleted.\n    # For now we just delete the one that we have locally and rely on other methods (notably get_jobs())\n    # to clean up other clones as they're encountered.\n    if os.path.isdir(instance.filesystem_path):\n        shutil.rmtree(instance.filesystem_path)\n", "path": "nautobot/extras/signals.py"}], "after_files": [{"content": "import os\nimport random\nimport shutil\nimport uuid\nimport logging\nfrom datetime import timedelta\n\nfrom cacheops.signals import cache_invalidated, cache_read\nfrom django.conf import settings\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.db import transaction\nfrom django.db.models.signals import m2m_changed, pre_delete\nfrom django.dispatch import receiver\nfrom django.utils import timezone\nfrom django_prometheus.models import model_deletes, model_inserts, model_updates\nfrom prometheus_client import Counter\n\nfrom nautobot.extras.tasks import delete_custom_field_data, provision_field\nfrom .choices import JobResultStatusChoices, ObjectChangeActionChoices\nfrom .models import CustomField, GitRepository, JobResult, ObjectChange\nfrom .webhooks import enqueue_webhooks\n\nlogger = logging.getLogger(\"nautobot.extras.signals\")\n\n\n#\n# Change logging/webhooks\n#\n\n\ndef _get_user_if_authenticated(request, objectchange):\n    \"\"\"Return the user object associated with the request if the user is defined.\n\n    If the user is not defined, log a warning to indicate that the user couldn't be retrived from the request\n    This is a workaround to fix a recurring issue where the user shouldn't be present in the request object randomly.\n    A similar issue was reported in NetBox https://github.com/netbox-community/netbox/issues/5142\n    \"\"\"\n    if request.user.is_authenticated:\n        return request.user\n    else:\n        logger.warning(f\"Unable to retrieve the user while creating the changelog for {objectchange.changed_object}\")\n\n\ndef _handle_changed_object(request, sender, instance, **kwargs):\n    \"\"\"\n    Fires when an object is created or updated.\n    \"\"\"\n    m2m_changed = False\n\n    # Determine the type of change being made\n    if kwargs.get(\"created\"):\n        action = ObjectChangeActionChoices.ACTION_CREATE\n    elif \"created\" in kwargs:\n        action = ObjectChangeActionChoices.ACTION_UPDATE\n    elif kwargs.get(\"action\") in [\"post_add\", \"post_remove\"] and kwargs[\"pk_set\"]:\n        # m2m_changed with objects added or removed\n        m2m_changed = True\n        action = ObjectChangeActionChoices.ACTION_UPDATE\n    else:\n        return\n\n    # Record an ObjectChange if applicable\n    if hasattr(instance, \"to_objectchange\"):\n        if m2m_changed:\n            ObjectChange.objects.filter(\n                changed_object_type=ContentType.objects.get_for_model(instance),\n                changed_object_id=instance.pk,\n                request_id=request.id,\n            ).update(object_data=instance.to_objectchange(action).object_data)\n        else:\n            objectchange = instance.to_objectchange(action)\n            objectchange.user = _get_user_if_authenticated(request, objectchange)\n            objectchange.request_id = request.id\n            objectchange.save()\n\n    # Enqueue webhooks\n    enqueue_webhooks(instance, request.user, request.id, action)\n\n    # Increment metric counters\n    if action == ObjectChangeActionChoices.ACTION_CREATE:\n        model_inserts.labels(instance._meta.model_name).inc()\n    elif action == ObjectChangeActionChoices.ACTION_UPDATE:\n        model_updates.labels(instance._meta.model_name).inc()\n\n    # Housekeeping: 0.1% chance of clearing out expired ObjectChanges\n    if settings.CHANGELOG_RETENTION and random.randint(1, 1000) == 1:\n        cutoff = timezone.now() - timedelta(days=settings.CHANGELOG_RETENTION)\n        ObjectChange.objects.filter(time__lt=cutoff).delete()\n\n\ndef _handle_deleted_object(request, sender, instance, **kwargs):\n    \"\"\"\n    Fires when an object is deleted.\n    \"\"\"\n    # Record an ObjectChange if applicable\n    if hasattr(instance, \"to_objectchange\"):\n        objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)\n        objectchange.user = _get_user_if_authenticated(request, objectchange)\n        objectchange.request_id = request.id\n        objectchange.save()\n\n    # Enqueue webhooks\n    enqueue_webhooks(instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)\n\n    # Increment metric counters\n    model_deletes.labels(instance._meta.model_name).inc()\n\n\n#\n# Custom fields\n#\n\n\ndef handle_cf_removed_obj_types(instance, action, pk_set, **kwargs):\n    \"\"\"\n    Handle the cleanup of old custom field data when a CustomField is removed from one or more ContentTypes.\n    \"\"\"\n    if action == \"post_remove\":\n        # Existing content types have been removed from the custom field, delete their data\n        transaction.on_commit(lambda: delete_custom_field_data.delay(instance.name, pk_set))\n\n    elif action == \"post_add\":\n        # New content types have been added to the custom field, provision them\n        transaction.on_commit(lambda: provision_field.delay(instance.pk, pk_set))\n\n\nm2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through)\n\n\n#\n# Caching\n#\n\ncacheops_cache_hit = Counter(\"cacheops_cache_hit\", \"Number of cache hits\")\ncacheops_cache_miss = Counter(\"cacheops_cache_miss\", \"Number of cache misses\")\ncacheops_cache_invalidated = Counter(\"cacheops_cache_invalidated\", \"Number of cache invalidations\")\n\n\ndef cache_read_collector(sender, func, hit, **kwargs):\n    if hit:\n        cacheops_cache_hit.inc()\n    else:\n        cacheops_cache_miss.inc()\n\n\ndef cache_invalidated_collector(sender, obj_dict, **kwargs):\n    cacheops_cache_invalidated.inc()\n\n\ncache_read.connect(cache_read_collector)\ncache_invalidated.connect(cache_invalidated_collector)\n\n\n#\n# Datasources\n#\n\n\n@receiver(pre_delete, sender=GitRepository)\ndef git_repository_pre_delete(instance, **kwargs):\n    \"\"\"\n    When a GitRepository is deleted, invoke all registered callbacks, then remove it from the local filesystem.\n\n    Note that GitRepository create/update operations enqueue a background job to handle the sync/resync;\n    this operation, by contrast, happens in the foreground as it needs to complete before we allow the\n    GitRepository itself to be deleted.\n    \"\"\"\n    from nautobot.extras.datasources import refresh_datasource_content\n\n    job_result = JobResult.objects.create(\n        name=instance.name,\n        obj_type=ContentType.objects.get_for_model(instance),\n        user=None,\n        job_id=uuid.uuid4(),\n        status=JobResultStatusChoices.STATUS_RUNNING,\n    )\n\n    refresh_datasource_content(\"extras.gitrepository\", instance, None, job_result, delete=True)\n\n    if job_result.status not in JobResultStatusChoices.TERMINAL_STATE_CHOICES:\n        job_result.set_status(JobResultStatusChoices.STATUS_COMPLETED)\n    job_result.save()\n\n    # TODO: In a distributed Nautobot deployment, each Django instance and/or RQ worker instance may have its own clone\n    # of this repository; we need some way to ensure that all such clones are deleted.\n    # For now we just delete the one that we have locally and rely on other methods (notably get_jobs())\n    # to clean up other clones as they're encountered.\n    if os.path.isdir(instance.filesystem_path):\n        shutil.rmtree(instance.filesystem_path)\n", "path": "nautobot/extras/signals.py"}]}