We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Staged Changes: InventoryItem Fails Sync
### NetBox version

v3.5.1

### Python version

3.10

### Steps to Reproduce

Currently when working with staged changes there is a edge-case where the staged changes failed on the merge. 

I believe this is because of the serialiser removing key parts of the data required to recreate the device when `merge()` is called. 

https://github.com/netbox-community/netbox/blob/f5a1f83f9fa9d98c945d21eb0f7ccb8cd37fbf59/netbox/netbox/staging.py#L119

And when `serialize_object` is called it removes all MPTT model fields: https://github.com/netbox-community/netbox/blob/f5a1f83f9fa9d98c945d21eb0f7ccb8cd37fbf59/netbox/utilities/utils.py#L157
This causes the merge to fail because the MPTT fields are now null. In the staged changes table. 

Error message:

```
>>> Branch.objects.all()[0].merge()
TEST
<RestrictedQuerySet [<StagedChange: Create dcim.manufacturer (182)>, <StagedChange: Create dcim.devicetype (177)>, <StagedChange: Create dcim.devicerole (120)>, <StagedChange: Update dcim.site (124)>, <StagedChange: Create dcim.device (9608)>, <StagedChange: Create dcim.device (9609)>, <StagedChange: Create dcim.inventoryitem (848)>, <StagedChange: Create dcim.inventoryitem (849)>]>
Create dcim.manufacturer (182)
Create dcim.devicetype (177)
Create dcim.devicerole (120)
Update dcim.site (124)
Create dcim.device (9608)
Create dcim.device (9609)
Create dcim.inventoryitem (848)
Traceback (most recent call last):
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/db/backends/utils.py", line 89, in _execute
    return self.cursor.execute(sql, params)
psycopg2.errors.NotNullViolation: null value in column "lft" of relation "dcim_inventoryitem" violates not-null constraint
DETAIL:  Failing row contains (2023-08-09 12:54:07.062+00, 2023-08-09 12:54:07.062+00, {}, 848, Inventory Item 1, , , , , , null, f, null, null, null, null, 9608, null, null, null, null, null).


The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/opt/netbox/netbox/extras/models/staging.py", line 52, in merge
    change.apply()
  File "/opt/netbox/netbox/extras/models/staging.py", line 107, in apply
    instance.save()
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/core/serializers/base.py", line 288, in save
    models.Model.save_base(self.object, using=using, raw=True, **kwargs)
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/db/models/base.py", line 863, in save_base
    updated = self._save_table(
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/db/models/base.py", line 1006, in _save_table
    results = self._do_insert(
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/db/models/base.py", line 1047, in _do_insert
    return manager._insert(
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/db/models/query.py", line 1791, in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 1660, in execute_sql
    cursor.execute(sql, params)
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/db/backends/utils.py", line 102, in execute
    return super().execute(sql, params)
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/db/backends/utils.py", line 84, in _execute
    with self.db.wrap_database_errors:
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/db/utils.py", line 91, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/opt/netbox/venv/lib/python3.10/site-packages/django/db/backends/utils.py", line 89, in _execute
    return self.cursor.execute(sql, params)
django.db.utils.IntegrityError: null value in column "lft" of relation "dcim_inventoryitem" violates not-null constraint
DETAIL:  Failing row contains (2023-08-09 12:54:07.062+00, 2023-08-09 12:54:07.062+00, {}, 848, Inventory Item 1, , , , , , null, f, null, null, null, null, 9608, null, null, null, null, null).

```

Script to recreate:

```python
from netbox.staging import checkout
from extras.models import Branch
from dcim.models import Device, VirtualChassis, DeviceType, Manufacturer, Site, DeviceRole, InventoryItem

Branch.objects.all().delete()
branch = Branch.objects.create(name='Branch 1')

Device.objects.filter(name="test device").delete()
Device.objects.filter(name="test device1").delete()
VirtualChassis.objects.filter(name='VC1').delete()
DeviceType.objects.filter(slug='device-type-1').delete()
Manufacturer.objects.filter(slug='manufacturer-1').delete()
DeviceRole.objects.filter(slug='device-role-1').delete()


with checkout(branch):

    manufacturer = Manufacturer.objects.create(
        name='Manufacturer', slug='manufacturer-1')
    device_type = DeviceType.objects.create(
        manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
    )
    device_role = DeviceRole.objects.create(
        name='Device Role', slug='device-role-1'
    )
    site, _ = Site.objects.update_or_create(
        defaults={'name': "test"}, slug="test")
    device, _ = Device.objects.update_or_create(defaults={
        "name": "test device", "device_role": device_role, "device_type": device_type, "site": site}, serial="123")
    device1, _ = Device.objects.update_or_create(defaults={
        "name": "test device1", "device_role": device_role, "device_type": device_type, "site": site}, serial="543")
    inv1 = InventoryItem.objects.create(device=device, name='Inventory Item 1'),
    inv2 = InventoryItem.objects.create(device=device1, name='Inventory Item 2'),



branch.merge()

print("DONE")

```


### Expected Behavior

If there is a serializer or argument to pass to the serializer that does not remove key fields to recreate objects from the staged changes table. 

### Observed Behavior

The serialized object has key fields removed which are required to save the object. 

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `netbox/extras/models/staging.py`
Content:
```
1 import logging
2 
3 from django.contrib.auth import get_user_model
4 from django.contrib.contenttypes.fields import GenericForeignKey
5 from django.db import models, transaction
6 from django.utils.translation import gettext_lazy as _
7 
8 from extras.choices import ChangeActionChoices
9 from netbox.models import ChangeLoggedModel
10 from netbox.models.features import *
11 from utilities.serialization import deserialize_object
12 
13 __all__ = (
14     'Branch',
15     'StagedChange',
16 )
17 
18 logger = logging.getLogger('netbox.staging')
19 
20 
21 class Branch(ChangeLoggedModel):
22     """
23     A collection of related StagedChanges.
24     """
25     name = models.CharField(
26         verbose_name=_('name'),
27         max_length=100,
28         unique=True
29     )
30     description = models.CharField(
31         verbose_name=_('description'),
32         max_length=200,
33         blank=True
34     )
35     user = models.ForeignKey(
36         to=get_user_model(),
37         on_delete=models.SET_NULL,
38         blank=True,
39         null=True
40     )
41 
42     class Meta:
43         ordering = ('name',)
44         verbose_name = _('branch')
45         verbose_name_plural = _('branches')
46 
47     def __str__(self):
48         return f'{self.name} ({self.pk})'
49 
50     def merge(self):
51         logger.info(f'Merging changes in branch {self}')
52         with transaction.atomic():
53             for change in self.staged_changes.all():
54                 change.apply()
55         self.staged_changes.all().delete()
56 
57 
58 class StagedChange(CustomValidationMixin, EventRulesMixin, models.Model):
59     """
60     The prepared creation, modification, or deletion of an object to be applied to the active database at a
61     future point.
62     """
63     branch = models.ForeignKey(
64         to=Branch,
65         on_delete=models.CASCADE,
66         related_name='staged_changes'
67     )
68     action = models.CharField(
69         verbose_name=_('action'),
70         max_length=20,
71         choices=ChangeActionChoices
72     )
73     object_type = models.ForeignKey(
74         to='contenttypes.ContentType',
75         on_delete=models.CASCADE,
76         related_name='+'
77     )
78     object_id = models.PositiveBigIntegerField(
79         blank=True,
80         null=True
81     )
82     object = GenericForeignKey(
83         ct_field='object_type',
84         fk_field='object_id'
85     )
86     data = models.JSONField(
87         verbose_name=_('data'),
88         blank=True,
89         null=True
90     )
91 
92     class Meta:
93         ordering = ('pk',)
94         indexes = (
95             models.Index(fields=('object_type', 'object_id')),
96         )
97         verbose_name = _('staged change')
98         verbose_name_plural = _('staged changes')
99 
100     def __str__(self):
101         action = self.get_action_display()
102         app_label, model_name = self.object_type.natural_key()
103         return f"{action} {app_label}.{model_name} ({self.object_id})"
104 
105     @property
106     def model(self):
107         return self.object_type.model_class()
108 
109     def apply(self):
110         """
111         Apply the staged create/update/delete action to the database.
112         """
113         if self.action == ChangeActionChoices.ACTION_CREATE:
114             instance = deserialize_object(self.model, self.data, pk=self.object_id)
115             logger.info(f'Creating {self.model._meta.verbose_name} {instance}')
116             instance.save()
117 
118         if self.action == ChangeActionChoices.ACTION_UPDATE:
119             instance = deserialize_object(self.model, self.data, pk=self.object_id)
120             logger.info(f'Updating {self.model._meta.verbose_name} {instance}')
121             instance.save()
122 
123         if self.action == ChangeActionChoices.ACTION_DELETE:
124             instance = self.model.objects.get(pk=self.object_id)
125             logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')
126             instance.delete()
127     apply.alters_data = True
128 
129     def get_action_color(self):
130         return ChangeActionChoices.colors.get(self.action)
131
```
--- 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/netbox/extras/models/staging.py b/netbox/extras/models/staging.py
--- a/netbox/extras/models/staging.py
+++ b/netbox/extras/models/staging.py
@@ -4,6 +4,7 @@
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.db import models, transaction
 from django.utils.translation import gettext_lazy as _
+from mptt.models import MPTTModel
 
 from extras.choices import ChangeActionChoices
 from netbox.models import ChangeLoggedModel
@@ -124,6 +125,11 @@
             instance = self.model.objects.get(pk=self.object_id)
             logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')
             instance.delete()
+
+        # Rebuild the MPTT tree where applicable
+        if issubclass(self.model, MPTTModel):
+            self.model.objects.rebuild()
+
     apply.alters_data = True
 
     def get_action_color(self):

{"golden_diff": "diff --git a/netbox/extras/models/staging.py b/netbox/extras/models/staging.py\n--- a/netbox/extras/models/staging.py\n+++ b/netbox/extras/models/staging.py\n@@ -4,6 +4,7 @@\n from django.contrib.contenttypes.fields import GenericForeignKey\n from django.db import models, transaction\n from django.utils.translation import gettext_lazy as _\n+from mptt.models import MPTTModel\n \n from extras.choices import ChangeActionChoices\n from netbox.models import ChangeLoggedModel\n@@ -124,6 +125,11 @@\n             instance = self.model.objects.get(pk=self.object_id)\n             logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')\n             instance.delete()\n+\n+        # Rebuild the MPTT tree where applicable\n+        if issubclass(self.model, MPTTModel):\n+            self.model.objects.rebuild()\n+\n     apply.alters_data = True\n \n     def get_action_color(self):\n", "issue": "Staged Changes: InventoryItem Fails Sync\n### NetBox version\n\nv3.5.1\n\n### Python version\n\n3.10\n\n### Steps to Reproduce\n\nCurrently when working with staged changes there is a edge-case where the staged changes failed on the merge. \r\n\r\nI believe this is because of the serialiser removing key parts of the data required to recreate the device when `merge()` is called. \r\n\r\nhttps://github.com/netbox-community/netbox/blob/f5a1f83f9fa9d98c945d21eb0f7ccb8cd37fbf59/netbox/netbox/staging.py#L119\r\n\r\nAnd when `serialize_object` is called it removes all MPTT model fields: https://github.com/netbox-community/netbox/blob/f5a1f83f9fa9d98c945d21eb0f7ccb8cd37fbf59/netbox/utilities/utils.py#L157\r\nThis causes the merge to fail because the MPTT fields are now null. In the staged changes table. \r\n\r\nError message:\r\n\r\n```\r\n>>> Branch.objects.all()[0].merge()\r\nTEST\r\n<RestrictedQuerySet [<StagedChange: Create dcim.manufacturer (182)>, <StagedChange: Create dcim.devicetype (177)>, <StagedChange: Create dcim.devicerole (120)>, <StagedChange: Update dcim.site (124)>, <StagedChange: Create dcim.device (9608)>, <StagedChange: Create dcim.device (9609)>, <StagedChange: Create dcim.inventoryitem (848)>, <StagedChange: Create dcim.inventoryitem (849)>]>\r\nCreate dcim.manufacturer (182)\r\nCreate dcim.devicetype (177)\r\nCreate dcim.devicerole (120)\r\nUpdate dcim.site (124)\r\nCreate dcim.device (9608)\r\nCreate dcim.device (9609)\r\nCreate dcim.inventoryitem (848)\r\nTraceback (most recent call last):\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/db/backends/utils.py\", line 89, in _execute\r\n    return self.cursor.execute(sql, params)\r\npsycopg2.errors.NotNullViolation: null value in column \"lft\" of relation \"dcim_inventoryitem\" violates not-null constraint\r\nDETAIL:  Failing row contains (2023-08-09 12:54:07.062+00, 2023-08-09 12:54:07.062+00, {}, 848, Inventory Item 1, , , , , , null, f, null, null, null, null, 9608, null, null, null, null, null).\r\n\r\n\r\nThe above exception was the direct cause of the following exception:\r\n\r\nTraceback (most recent call last):\r\n  File \"<console>\", line 1, in <module>\r\n  File \"/opt/netbox/netbox/extras/models/staging.py\", line 52, in merge\r\n    change.apply()\r\n  File \"/opt/netbox/netbox/extras/models/staging.py\", line 107, in apply\r\n    instance.save()\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/core/serializers/base.py\", line 288, in save\r\n    models.Model.save_base(self.object, using=using, raw=True, **kwargs)\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/db/models/base.py\", line 863, in save_base\r\n    updated = self._save_table(\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/db/models/base.py\", line 1006, in _save_table\r\n    results = self._do_insert(\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/db/models/base.py\", line 1047, in _do_insert\r\n    return manager._insert(\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/db/models/manager.py\", line 85, in manager_method\r\n    return getattr(self.get_queryset(), name)(*args, **kwargs)\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/db/models/query.py\", line 1791, in _insert\r\n    return query.get_compiler(using=using).execute_sql(returning_fields)\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/db/models/sql/compiler.py\", line 1660, in execute_sql\r\n    cursor.execute(sql, params)\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/db/backends/utils.py\", line 102, in execute\r\n    return super().execute(sql, params)\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/db/backends/utils.py\", line 67, in execute\r\n    return self._execute_with_wrappers(\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/db/backends/utils.py\", line 80, in _execute_with_wrappers\r\n    return executor(sql, params, many, context)\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/db/backends/utils.py\", line 84, in _execute\r\n    with self.db.wrap_database_errors:\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/db/utils.py\", line 91, in __exit__\r\n    raise dj_exc_value.with_traceback(traceback) from exc_value\r\n  File \"/opt/netbox/venv/lib/python3.10/site-packages/django/db/backends/utils.py\", line 89, in _execute\r\n    return self.cursor.execute(sql, params)\r\ndjango.db.utils.IntegrityError: null value in column \"lft\" of relation \"dcim_inventoryitem\" violates not-null constraint\r\nDETAIL:  Failing row contains (2023-08-09 12:54:07.062+00, 2023-08-09 12:54:07.062+00, {}, 848, Inventory Item 1, , , , , , null, f, null, null, null, null, 9608, null, null, null, null, null).\r\n\r\n```\r\n\r\nScript to recreate:\r\n\r\n```python\r\nfrom netbox.staging import checkout\r\nfrom extras.models import Branch\r\nfrom dcim.models import Device, VirtualChassis, DeviceType, Manufacturer, Site, DeviceRole, InventoryItem\r\n\r\nBranch.objects.all().delete()\r\nbranch = Branch.objects.create(name='Branch 1')\r\n\r\nDevice.objects.filter(name=\"test device\").delete()\r\nDevice.objects.filter(name=\"test device1\").delete()\r\nVirtualChassis.objects.filter(name='VC1').delete()\r\nDeviceType.objects.filter(slug='device-type-1').delete()\r\nManufacturer.objects.filter(slug='manufacturer-1').delete()\r\nDeviceRole.objects.filter(slug='device-role-1').delete()\r\n\r\n\r\nwith checkout(branch):\r\n\r\n    manufacturer = Manufacturer.objects.create(\r\n        name='Manufacturer', slug='manufacturer-1')\r\n    device_type = DeviceType.objects.create(\r\n        manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'\r\n    )\r\n    device_role = DeviceRole.objects.create(\r\n        name='Device Role', slug='device-role-1'\r\n    )\r\n    site, _ = Site.objects.update_or_create(\r\n        defaults={'name': \"test\"}, slug=\"test\")\r\n    device, _ = Device.objects.update_or_create(defaults={\r\n        \"name\": \"test device\", \"device_role\": device_role, \"device_type\": device_type, \"site\": site}, serial=\"123\")\r\n    device1, _ = Device.objects.update_or_create(defaults={\r\n        \"name\": \"test device1\", \"device_role\": device_role, \"device_type\": device_type, \"site\": site}, serial=\"543\")\r\n    inv1 = InventoryItem.objects.create(device=device, name='Inventory Item 1'),\r\n    inv2 = InventoryItem.objects.create(device=device1, name='Inventory Item 2'),\r\n\r\n\r\n\r\nbranch.merge()\r\n\r\nprint(\"DONE\")\r\n\r\n```\r\n\n\n### Expected Behavior\n\nIf there is a serializer or argument to pass to the serializer that does not remove key fields to recreate objects from the staged changes table. \n\n### Observed Behavior\n\nThe serialized object has key fields removed which are required to save the object. \n", "before_files": [{"content": "import logging\n\nfrom django.contrib.auth import get_user_model\nfrom django.contrib.contenttypes.fields import GenericForeignKey\nfrom django.db import models, transaction\nfrom django.utils.translation import gettext_lazy as _\n\nfrom extras.choices import ChangeActionChoices\nfrom netbox.models import ChangeLoggedModel\nfrom netbox.models.features import *\nfrom utilities.serialization import deserialize_object\n\n__all__ = (\n    'Branch',\n    'StagedChange',\n)\n\nlogger = logging.getLogger('netbox.staging')\n\n\nclass Branch(ChangeLoggedModel):\n    \"\"\"\n    A collection of related StagedChanges.\n    \"\"\"\n    name = models.CharField(\n        verbose_name=_('name'),\n        max_length=100,\n        unique=True\n    )\n    description = models.CharField(\n        verbose_name=_('description'),\n        max_length=200,\n        blank=True\n    )\n    user = models.ForeignKey(\n        to=get_user_model(),\n        on_delete=models.SET_NULL,\n        blank=True,\n        null=True\n    )\n\n    class Meta:\n        ordering = ('name',)\n        verbose_name = _('branch')\n        verbose_name_plural = _('branches')\n\n    def __str__(self):\n        return f'{self.name} ({self.pk})'\n\n    def merge(self):\n        logger.info(f'Merging changes in branch {self}')\n        with transaction.atomic():\n            for change in self.staged_changes.all():\n                change.apply()\n        self.staged_changes.all().delete()\n\n\nclass StagedChange(CustomValidationMixin, EventRulesMixin, models.Model):\n    \"\"\"\n    The prepared creation, modification, or deletion of an object to be applied to the active database at a\n    future point.\n    \"\"\"\n    branch = models.ForeignKey(\n        to=Branch,\n        on_delete=models.CASCADE,\n        related_name='staged_changes'\n    )\n    action = models.CharField(\n        verbose_name=_('action'),\n        max_length=20,\n        choices=ChangeActionChoices\n    )\n    object_type = models.ForeignKey(\n        to='contenttypes.ContentType',\n        on_delete=models.CASCADE,\n        related_name='+'\n    )\n    object_id = models.PositiveBigIntegerField(\n        blank=True,\n        null=True\n    )\n    object = GenericForeignKey(\n        ct_field='object_type',\n        fk_field='object_id'\n    )\n    data = models.JSONField(\n        verbose_name=_('data'),\n        blank=True,\n        null=True\n    )\n\n    class Meta:\n        ordering = ('pk',)\n        indexes = (\n            models.Index(fields=('object_type', 'object_id')),\n        )\n        verbose_name = _('staged change')\n        verbose_name_plural = _('staged changes')\n\n    def __str__(self):\n        action = self.get_action_display()\n        app_label, model_name = self.object_type.natural_key()\n        return f\"{action} {app_label}.{model_name} ({self.object_id})\"\n\n    @property\n    def model(self):\n        return self.object_type.model_class()\n\n    def apply(self):\n        \"\"\"\n        Apply the staged create/update/delete action to the database.\n        \"\"\"\n        if self.action == ChangeActionChoices.ACTION_CREATE:\n            instance = deserialize_object(self.model, self.data, pk=self.object_id)\n            logger.info(f'Creating {self.model._meta.verbose_name} {instance}')\n            instance.save()\n\n        if self.action == ChangeActionChoices.ACTION_UPDATE:\n            instance = deserialize_object(self.model, self.data, pk=self.object_id)\n            logger.info(f'Updating {self.model._meta.verbose_name} {instance}')\n            instance.save()\n\n        if self.action == ChangeActionChoices.ACTION_DELETE:\n            instance = self.model.objects.get(pk=self.object_id)\n            logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')\n            instance.delete()\n    apply.alters_data = True\n\n    def get_action_color(self):\n        return ChangeActionChoices.colors.get(self.action)\n", "path": "netbox/extras/models/staging.py"}], "after_files": [{"content": "import logging\n\nfrom django.contrib.auth import get_user_model\nfrom django.contrib.contenttypes.fields import GenericForeignKey\nfrom django.db import models, transaction\nfrom django.utils.translation import gettext_lazy as _\nfrom mptt.models import MPTTModel\n\nfrom extras.choices import ChangeActionChoices\nfrom netbox.models import ChangeLoggedModel\nfrom netbox.models.features import *\nfrom utilities.serialization import deserialize_object\n\n__all__ = (\n    'Branch',\n    'StagedChange',\n)\n\nlogger = logging.getLogger('netbox.staging')\n\n\nclass Branch(ChangeLoggedModel):\n    \"\"\"\n    A collection of related StagedChanges.\n    \"\"\"\n    name = models.CharField(\n        verbose_name=_('name'),\n        max_length=100,\n        unique=True\n    )\n    description = models.CharField(\n        verbose_name=_('description'),\n        max_length=200,\n        blank=True\n    )\n    user = models.ForeignKey(\n        to=get_user_model(),\n        on_delete=models.SET_NULL,\n        blank=True,\n        null=True\n    )\n\n    class Meta:\n        ordering = ('name',)\n        verbose_name = _('branch')\n        verbose_name_plural = _('branches')\n\n    def __str__(self):\n        return f'{self.name} ({self.pk})'\n\n    def merge(self):\n        logger.info(f'Merging changes in branch {self}')\n        with transaction.atomic():\n            for change in self.staged_changes.all():\n                change.apply()\n        self.staged_changes.all().delete()\n\n\nclass StagedChange(CustomValidationMixin, EventRulesMixin, models.Model):\n    \"\"\"\n    The prepared creation, modification, or deletion of an object to be applied to the active database at a\n    future point.\n    \"\"\"\n    branch = models.ForeignKey(\n        to=Branch,\n        on_delete=models.CASCADE,\n        related_name='staged_changes'\n    )\n    action = models.CharField(\n        verbose_name=_('action'),\n        max_length=20,\n        choices=ChangeActionChoices\n    )\n    object_type = models.ForeignKey(\n        to='contenttypes.ContentType',\n        on_delete=models.CASCADE,\n        related_name='+'\n    )\n    object_id = models.PositiveBigIntegerField(\n        blank=True,\n        null=True\n    )\n    object = GenericForeignKey(\n        ct_field='object_type',\n        fk_field='object_id'\n    )\n    data = models.JSONField(\n        verbose_name=_('data'),\n        blank=True,\n        null=True\n    )\n\n    class Meta:\n        ordering = ('pk',)\n        indexes = (\n            models.Index(fields=('object_type', 'object_id')),\n        )\n        verbose_name = _('staged change')\n        verbose_name_plural = _('staged changes')\n\n    def __str__(self):\n        action = self.get_action_display()\n        app_label, model_name = self.object_type.natural_key()\n        return f\"{action} {app_label}.{model_name} ({self.object_id})\"\n\n    @property\n    def model(self):\n        return self.object_type.model_class()\n\n    def apply(self):\n        \"\"\"\n        Apply the staged create/update/delete action to the database.\n        \"\"\"\n        if self.action == ChangeActionChoices.ACTION_CREATE:\n            instance = deserialize_object(self.model, self.data, pk=self.object_id)\n            logger.info(f'Creating {self.model._meta.verbose_name} {instance}')\n            instance.save()\n\n        if self.action == ChangeActionChoices.ACTION_UPDATE:\n            instance = deserialize_object(self.model, self.data, pk=self.object_id)\n            logger.info(f'Updating {self.model._meta.verbose_name} {instance}')\n            instance.save()\n\n        if self.action == ChangeActionChoices.ACTION_DELETE:\n            instance = self.model.objects.get(pk=self.object_id)\n            logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')\n            instance.delete()\n\n        # Rebuild the MPTT tree where applicable\n        if issubclass(self.model, MPTTModel):\n            self.model.objects.rebuild()\n\n    apply.alters_data = True\n\n    def get_action_color(self):\n        return ChangeActionChoices.colors.get(self.action)\n", "path": "netbox/extras/models/staging.py"}]}