We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Child locations up to 1 level down are able to have a site
<!--
    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.4.5
* Python version: 3.8.10
* Database platform, version: Postgres 12.12
* Middleware(s):

<!--
    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 parent location and attach it to a site
2. Create a child of the above parent, and attach to a site

<!-- What did you expect to happen? -->
### Expected Behavior
I expected to be unable to add a child location to a site as I am unable to do so with child locations two levels down, IE the child of a child location.

<!-- What happened instead? -->
### Observed Behavior
I was able to attach a child location to a site

![Screen Shot 2022-10-21 at 12 34 11 PM](https://user-images.githubusercontent.com/9521210/197245261-c76b8e63-5068-4887-bf2e-0d297924fa6f.png)
![Screen Shot 2022-10-21 at 12 34 58 PM](https://user-images.githubusercontent.com/9521210/197245405-2e9d44c7-3557-438d-8b44-9b1f301702a5.png)


--- 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/dcim/models/locations.py`
Content:
```
1 from django.contrib.contenttypes.fields import GenericRelation
2 from django.contrib.contenttypes.models import ContentType
3 from django.core.exceptions import ObjectDoesNotExist, ValidationError
4 from django.db import models
5 from django.urls import reverse
6 
7 from tree_queries.models import TreeNode
8 
9 from nautobot.core.fields import AutoSlugField
10 from nautobot.core.models.generics import OrganizationalModel, PrimaryModel
11 from nautobot.extras.models import StatusModel
12 from nautobot.extras.utils import extras_features, FeatureQuery
13 from nautobot.utilities.fields import NaturalOrderingField
14 from nautobot.utilities.tree_queries import TreeManager
15 
16 
17 @extras_features(
18     "custom_fields",
19     "custom_links",
20     "custom_validators",
21     "export_templates",
22     "graphql",
23     "relationships",
24     "webhooks",
25 )
26 class LocationType(TreeNode, OrganizationalModel):
27     """
28     Definition of a category of Locations, including its hierarchical relationship to other LocationTypes.
29 
30     A LocationType also specifies the content types that can be associated to a Location of this category.
31     For example a "Building" LocationType might allow Prefix and VLANGroup, but not Devices,
32     while a "Room" LocationType might allow Racks and Devices.
33     """
34 
35     name = models.CharField(max_length=100, unique=True)
36     slug = AutoSlugField(populate_from="name")
37     description = models.CharField(max_length=200, blank=True)
38     content_types = models.ManyToManyField(
39         to=ContentType,
40         related_name="location_types",
41         verbose_name="Permitted object types",
42         limit_choices_to=FeatureQuery("locations"),
43         help_text="The object type(s) that can be associated to a Location of this type.",
44     )
45 
46     objects = TreeManager()
47 
48     csv_headers = ["name", "slug", "parent", "description", "content_types"]
49 
50     class Meta:
51         ordering = ("name",)
52 
53     def __str__(self):
54         return self.name
55 
56     def get_absolute_url(self):
57         return reverse("dcim:locationtype", args=[self.slug])
58 
59     def to_csv(self):
60         return (
61             self.name,
62             self.slug,
63             self.parent.name if self.parent else None,
64             self.description,
65             ",".join(f"{ct.app_label}.{ct.model}" for ct in self.content_types.order_by("app_label", "model")),
66         )
67 
68     def clean(self):
69         """
70         Disallow LocationTypes whose name conflicts with existing location-related models, to avoid confusion.
71 
72         In the longer term we will collapse these other models into special cases of LocationType.
73         """
74         super().clean()
75 
76         if self.name.lower() in [
77             "region",
78             "regions",
79             "site",
80             "sites",
81             "rackgroup",
82             "rackgroups",
83             "rack group",
84             "rack groups",
85         ]:
86             raise ValidationError({"name": "This name is reserved for future use."})
87 
88     @property
89     def display(self):
90         """
91         Include the parent type names as well in order to provide UI clarity.
92         `self.ancestors()` returns all the preceding nodes from the top down.
93         So if we are looking at node C and its node structure is the following:
94             A
95            /
96           B
97          /
98         C
99         This method will return "A → B → C".
100         Note that `self.ancestors()` may throw an `ObjectDoesNotExist` during bulk-delete operations.
101         """
102         display_str = ""
103         try:
104             for ancestor in self.ancestors():
105                 display_str += ancestor.name + " → "
106         except ObjectDoesNotExist:
107             pass
108         finally:
109             display_str += self.name
110             return display_str  # pylint: disable=lost-exception
111 
112 
113 @extras_features(
114     "custom_fields",
115     "custom_links",
116     "custom_validators",
117     "export_templates",
118     "graphql",
119     "relationships",
120     "statuses",
121     "webhooks",
122 )
123 class Location(TreeNode, StatusModel, PrimaryModel):
124     """
125     A Location represents an arbitrarily specific geographic location, such as a campus, building, floor, room, etc.
126 
127     As presently implemented, Location is an intermediary model between Site and RackGroup - more specific than a Site,
128     less specific (and more broadly applicable) than a RackGroup:
129 
130     Region
131       Region
132         Site
133           Location (location_type="Building")
134             Location (location_type="Room")
135               RackGroup
136                 Rack
137                   Device
138               Device
139             Prefix
140             etc.
141           VLANGroup
142           Prefix
143           etc.
144 
145     As such, as presently implemented, every Location either has a parent Location or a "parent" Site.
146 
147     In the future, we plan to collapse Region and Site (and likely RackGroup as well) into the Location model.
148     """
149 
150     # A Location's name is unique within context of its parent, not globally unique.
151     name = models.CharField(max_length=100, db_index=True)
152     _name = NaturalOrderingField(target_field="name", max_length=100, blank=True, db_index=True)
153     # However a Location's slug *is* globally unique.
154     slug = AutoSlugField(populate_from=["parent__slug", "name"])
155     location_type = models.ForeignKey(
156         to="dcim.LocationType",
157         on_delete=models.PROTECT,
158         related_name="locations",
159     )
160     site = models.ForeignKey(
161         to="dcim.Site",
162         on_delete=models.CASCADE,
163         related_name="locations",
164         blank=True,
165         null=True,
166     )
167     tenant = models.ForeignKey(
168         to="tenancy.Tenant",
169         on_delete=models.PROTECT,
170         related_name="locations",
171         blank=True,
172         null=True,
173     )
174     description = models.CharField(max_length=200, blank=True)
175     images = GenericRelation(to="extras.ImageAttachment")
176 
177     objects = TreeManager()
178 
179     csv_headers = [
180         "name",
181         "slug",
182         "location_type",
183         "site",
184         "status",
185         "parent",
186         "tenant",
187         "description",
188     ]
189 
190     clone_fields = [
191         "location_type",
192         "site",
193         "status",
194         "parent",
195         "tenant",
196         "description",
197     ]
198 
199     class Meta:
200         ordering = ("_name",)
201         unique_together = [["parent", "name"]]
202 
203     def __str__(self):
204         return self.name
205 
206     def get_absolute_url(self):
207         return reverse("dcim:location", args=[self.slug])
208 
209     def to_csv(self):
210         return (
211             self.name,
212             self.slug,
213             self.location_type.name,
214             self.site.name if self.site else None,
215             self.get_status_display(),
216             self.parent.name if self.parent else None,
217             self.tenant.name if self.tenant else None,
218             self.description,
219         )
220 
221     @property
222     def base_site(self):
223         """The site that this Location belongs to, if any, or that its root ancestor belongs to, if any."""
224         return self.site or self.ancestors().first().site
225 
226     @property
227     def display(self):
228         """
229         Location name is unique per parent but not globally unique, so include parent information as context.
230         `self.ancestors()` returns all the preceding nodes from the top down.
231         So if we are looking at node C and its node structure is the following:
232             A
233            /
234           B
235          /
236         C
237         This method will return "A → B → C".
238 
239         Note that `self.ancestors()` may throw an `ObjectDoesNotExist` during bulk-delete operations.
240         """
241         display_str = ""
242         try:
243             for ancestor in self.ancestors():
244                 display_str += ancestor.name + " → "
245         except ObjectDoesNotExist:
246             pass
247         finally:
248             display_str += self.name
249             return display_str  # pylint: disable=lost-exception
250 
251     def validate_unique(self, exclude=None):
252         # Check for a duplicate name on a Location with no parent.
253         # This is necessary because Django does not consider two NULL fields to be equal.
254         if self.parent is None:
255             if Location.objects.exclude(pk=self.pk).filter(parent__isnull=True, name=self.name).exists():
256                 raise ValidationError({"name": "A root-level location with this name already exists."})
257 
258         super().validate_unique(exclude=exclude)
259 
260     def clean(self):
261         super().clean()
262 
263         # Prevent changing location type as that would require a whole bunch of cascading logic checks,
264         # e.g. what if the new type doesn't allow all of the associated objects that the old type did?
265         if self.present_in_database and self.location_type != Location.objects.get(pk=self.pk).location_type:
266             raise ValidationError({"location_type": "Changing the type of an existing Location is not permitted."})
267 
268         if self.location_type.parent is not None:
269             # We must have a parent and it must match the parent location_type.
270             if self.parent is None or self.parent.location_type != self.location_type.parent:
271                 raise ValidationError(
272                     {
273                         "parent": f"A Location of type {self.location_type} must have "
274                         f"a parent Location of type {self.location_type.parent}"
275                     }
276                 )
277             # We must *not* have a site.
278             # In a future release, Site will become a kind of Location, and the resulting data migration will be
279             # much cleaner if it doesn't have to deal with Locations that have two "parents".
280             if self.site is not None:
281                 raise ValidationError(
282                     {"site": f"A location of type {self.location_type} must not have an associated Site."}
283                 )
284 
285         # If this location_type does *not* have a parent type,
286         # this location must have an associated Site.
287         # This check will be removed in the future once Site and Region become special cases of Location;
288         # at that point a "root" LocationType will correctly have no parent (or site) associated.
289         if self.location_type.parent is None and self.site is None:
290             raise ValidationError(
291                 {"site": f"A Location of type {self.location_type} has no parent Location, but must have a Site."}
292             )
293
```
--- 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/dcim/models/locations.py b/nautobot/dcim/models/locations.py
--- a/nautobot/dcim/models/locations.py
+++ b/nautobot/dcim/models/locations.py
@@ -70,6 +70,8 @@
         Disallow LocationTypes whose name conflicts with existing location-related models, to avoid confusion.
 
         In the longer term we will collapse these other models into special cases of LocationType.
+
+        Also, disallow re-parenting a LocationType if there are Locations already using this LocationType.
         """
         super().clean()
 
@@ -85,6 +87,18 @@
         ]:
             raise ValidationError({"name": "This name is reserved for future use."})
 
+        if (
+            self.present_in_database
+            and self.parent != LocationType.objects.get(pk=self.pk).parent
+            and self.locations.exists()
+        ):
+            raise ValidationError(
+                {
+                    "parent": "This LocationType currently has Locations using it, "
+                    "therefore its parent cannot be changed at this time."
+                }
+            )
+
     @property
     def display(self):
         """

{"golden_diff": "diff --git a/nautobot/dcim/models/locations.py b/nautobot/dcim/models/locations.py\n--- a/nautobot/dcim/models/locations.py\n+++ b/nautobot/dcim/models/locations.py\n@@ -70,6 +70,8 @@\n         Disallow LocationTypes whose name conflicts with existing location-related models, to avoid confusion.\n \n         In the longer term we will collapse these other models into special cases of LocationType.\n+\n+        Also, disallow re-parenting a LocationType if there are Locations already using this LocationType.\n         \"\"\"\n         super().clean()\n \n@@ -85,6 +87,18 @@\n         ]:\n             raise ValidationError({\"name\": \"This name is reserved for future use.\"})\n \n+        if (\n+            self.present_in_database\n+            and self.parent != LocationType.objects.get(pk=self.pk).parent\n+            and self.locations.exists()\n+        ):\n+            raise ValidationError(\n+                {\n+                    \"parent\": \"This LocationType currently has Locations using it, \"\n+                    \"therefore its parent cannot be changed at this time.\"\n+                }\n+            )\n+\n     @property\n     def display(self):\n         \"\"\"\n", "issue": "Child locations up to 1 level down are able to have a site\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* Nautobot version (Docker tag too if applicable): 1.4.5\r\n* Python version: 3.8.10\r\n* Database platform, version: Postgres 12.12\r\n* Middleware(s):\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\n### Steps to Reproduce\r\n1. Create a parent location and attach it to a site\r\n2. Create a child of the above parent, and attach to a site\r\n\r\n<!-- What did you expect to happen? -->\r\n### Expected Behavior\r\nI expected to be unable to add a child location to a site as I am unable to do so with child locations two levels down, IE the child of a child location.\r\n\r\n<!-- What happened instead? -->\r\n### Observed Behavior\r\nI was able to attach a child location to a site\r\n\r\n![Screen Shot 2022-10-21 at 12 34 11 PM](https://user-images.githubusercontent.com/9521210/197245261-c76b8e63-5068-4887-bf2e-0d297924fa6f.png)\r\n![Screen Shot 2022-10-21 at 12 34 58 PM](https://user-images.githubusercontent.com/9521210/197245405-2e9d44c7-3557-438d-8b44-9b1f301702a5.png)\r\n\n", "before_files": [{"content": "from django.contrib.contenttypes.fields import GenericRelation\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.core.exceptions import ObjectDoesNotExist, ValidationError\nfrom django.db import models\nfrom django.urls import reverse\n\nfrom tree_queries.models import TreeNode\n\nfrom nautobot.core.fields import AutoSlugField\nfrom nautobot.core.models.generics import OrganizationalModel, PrimaryModel\nfrom nautobot.extras.models import StatusModel\nfrom nautobot.extras.utils import extras_features, FeatureQuery\nfrom nautobot.utilities.fields import NaturalOrderingField\nfrom nautobot.utilities.tree_queries import TreeManager\n\n\n@extras_features(\n    \"custom_fields\",\n    \"custom_links\",\n    \"custom_validators\",\n    \"export_templates\",\n    \"graphql\",\n    \"relationships\",\n    \"webhooks\",\n)\nclass LocationType(TreeNode, OrganizationalModel):\n    \"\"\"\n    Definition of a category of Locations, including its hierarchical relationship to other LocationTypes.\n\n    A LocationType also specifies the content types that can be associated to a Location of this category.\n    For example a \"Building\" LocationType might allow Prefix and VLANGroup, but not Devices,\n    while a \"Room\" LocationType might allow Racks and Devices.\n    \"\"\"\n\n    name = models.CharField(max_length=100, unique=True)\n    slug = AutoSlugField(populate_from=\"name\")\n    description = models.CharField(max_length=200, blank=True)\n    content_types = models.ManyToManyField(\n        to=ContentType,\n        related_name=\"location_types\",\n        verbose_name=\"Permitted object types\",\n        limit_choices_to=FeatureQuery(\"locations\"),\n        help_text=\"The object type(s) that can be associated to a Location of this type.\",\n    )\n\n    objects = TreeManager()\n\n    csv_headers = [\"name\", \"slug\", \"parent\", \"description\", \"content_types\"]\n\n    class Meta:\n        ordering = (\"name\",)\n\n    def __str__(self):\n        return self.name\n\n    def get_absolute_url(self):\n        return reverse(\"dcim:locationtype\", args=[self.slug])\n\n    def to_csv(self):\n        return (\n            self.name,\n            self.slug,\n            self.parent.name if self.parent else None,\n            self.description,\n            \",\".join(f\"{ct.app_label}.{ct.model}\" for ct in self.content_types.order_by(\"app_label\", \"model\")),\n        )\n\n    def clean(self):\n        \"\"\"\n        Disallow LocationTypes whose name conflicts with existing location-related models, to avoid confusion.\n\n        In the longer term we will collapse these other models into special cases of LocationType.\n        \"\"\"\n        super().clean()\n\n        if self.name.lower() in [\n            \"region\",\n            \"regions\",\n            \"site\",\n            \"sites\",\n            \"rackgroup\",\n            \"rackgroups\",\n            \"rack group\",\n            \"rack groups\",\n        ]:\n            raise ValidationError({\"name\": \"This name is reserved for future use.\"})\n\n    @property\n    def display(self):\n        \"\"\"\n        Include the parent type names as well in order to provide UI clarity.\n        `self.ancestors()` returns all the preceding nodes from the top down.\n        So if we are looking at node C and its node structure is the following:\n            A\n           /\n          B\n         /\n        C\n        This method will return \"A \u2192 B \u2192 C\".\n        Note that `self.ancestors()` may throw an `ObjectDoesNotExist` during bulk-delete operations.\n        \"\"\"\n        display_str = \"\"\n        try:\n            for ancestor in self.ancestors():\n                display_str += ancestor.name + \" \u2192 \"\n        except ObjectDoesNotExist:\n            pass\n        finally:\n            display_str += self.name\n            return display_str  # pylint: disable=lost-exception\n\n\n@extras_features(\n    \"custom_fields\",\n    \"custom_links\",\n    \"custom_validators\",\n    \"export_templates\",\n    \"graphql\",\n    \"relationships\",\n    \"statuses\",\n    \"webhooks\",\n)\nclass Location(TreeNode, StatusModel, PrimaryModel):\n    \"\"\"\n    A Location represents an arbitrarily specific geographic location, such as a campus, building, floor, room, etc.\n\n    As presently implemented, Location is an intermediary model between Site and RackGroup - more specific than a Site,\n    less specific (and more broadly applicable) than a RackGroup:\n\n    Region\n      Region\n        Site\n          Location (location_type=\"Building\")\n            Location (location_type=\"Room\")\n              RackGroup\n                Rack\n                  Device\n              Device\n            Prefix\n            etc.\n          VLANGroup\n          Prefix\n          etc.\n\n    As such, as presently implemented, every Location either has a parent Location or a \"parent\" Site.\n\n    In the future, we plan to collapse Region and Site (and likely RackGroup as well) into the Location model.\n    \"\"\"\n\n    # A Location's name is unique within context of its parent, not globally unique.\n    name = models.CharField(max_length=100, db_index=True)\n    _name = NaturalOrderingField(target_field=\"name\", max_length=100, blank=True, db_index=True)\n    # However a Location's slug *is* globally unique.\n    slug = AutoSlugField(populate_from=[\"parent__slug\", \"name\"])\n    location_type = models.ForeignKey(\n        to=\"dcim.LocationType\",\n        on_delete=models.PROTECT,\n        related_name=\"locations\",\n    )\n    site = models.ForeignKey(\n        to=\"dcim.Site\",\n        on_delete=models.CASCADE,\n        related_name=\"locations\",\n        blank=True,\n        null=True,\n    )\n    tenant = models.ForeignKey(\n        to=\"tenancy.Tenant\",\n        on_delete=models.PROTECT,\n        related_name=\"locations\",\n        blank=True,\n        null=True,\n    )\n    description = models.CharField(max_length=200, blank=True)\n    images = GenericRelation(to=\"extras.ImageAttachment\")\n\n    objects = TreeManager()\n\n    csv_headers = [\n        \"name\",\n        \"slug\",\n        \"location_type\",\n        \"site\",\n        \"status\",\n        \"parent\",\n        \"tenant\",\n        \"description\",\n    ]\n\n    clone_fields = [\n        \"location_type\",\n        \"site\",\n        \"status\",\n        \"parent\",\n        \"tenant\",\n        \"description\",\n    ]\n\n    class Meta:\n        ordering = (\"_name\",)\n        unique_together = [[\"parent\", \"name\"]]\n\n    def __str__(self):\n        return self.name\n\n    def get_absolute_url(self):\n        return reverse(\"dcim:location\", args=[self.slug])\n\n    def to_csv(self):\n        return (\n            self.name,\n            self.slug,\n            self.location_type.name,\n            self.site.name if self.site else None,\n            self.get_status_display(),\n            self.parent.name if self.parent else None,\n            self.tenant.name if self.tenant else None,\n            self.description,\n        )\n\n    @property\n    def base_site(self):\n        \"\"\"The site that this Location belongs to, if any, or that its root ancestor belongs to, if any.\"\"\"\n        return self.site or self.ancestors().first().site\n\n    @property\n    def display(self):\n        \"\"\"\n        Location name is unique per parent but not globally unique, so include parent information as context.\n        `self.ancestors()` returns all the preceding nodes from the top down.\n        So if we are looking at node C and its node structure is the following:\n            A\n           /\n          B\n         /\n        C\n        This method will return \"A \u2192 B \u2192 C\".\n\n        Note that `self.ancestors()` may throw an `ObjectDoesNotExist` during bulk-delete operations.\n        \"\"\"\n        display_str = \"\"\n        try:\n            for ancestor in self.ancestors():\n                display_str += ancestor.name + \" \u2192 \"\n        except ObjectDoesNotExist:\n            pass\n        finally:\n            display_str += self.name\n            return display_str  # pylint: disable=lost-exception\n\n    def validate_unique(self, exclude=None):\n        # Check for a duplicate name on a Location with no parent.\n        # This is necessary because Django does not consider two NULL fields to be equal.\n        if self.parent is None:\n            if Location.objects.exclude(pk=self.pk).filter(parent__isnull=True, name=self.name).exists():\n                raise ValidationError({\"name\": \"A root-level location with this name already exists.\"})\n\n        super().validate_unique(exclude=exclude)\n\n    def clean(self):\n        super().clean()\n\n        # Prevent changing location type as that would require a whole bunch of cascading logic checks,\n        # e.g. what if the new type doesn't allow all of the associated objects that the old type did?\n        if self.present_in_database and self.location_type != Location.objects.get(pk=self.pk).location_type:\n            raise ValidationError({\"location_type\": \"Changing the type of an existing Location is not permitted.\"})\n\n        if self.location_type.parent is not None:\n            # We must have a parent and it must match the parent location_type.\n            if self.parent is None or self.parent.location_type != self.location_type.parent:\n                raise ValidationError(\n                    {\n                        \"parent\": f\"A Location of type {self.location_type} must have \"\n                        f\"a parent Location of type {self.location_type.parent}\"\n                    }\n                )\n            # We must *not* have a site.\n            # In a future release, Site will become a kind of Location, and the resulting data migration will be\n            # much cleaner if it doesn't have to deal with Locations that have two \"parents\".\n            if self.site is not None:\n                raise ValidationError(\n                    {\"site\": f\"A location of type {self.location_type} must not have an associated Site.\"}\n                )\n\n        # If this location_type does *not* have a parent type,\n        # this location must have an associated Site.\n        # This check will be removed in the future once Site and Region become special cases of Location;\n        # at that point a \"root\" LocationType will correctly have no parent (or site) associated.\n        if self.location_type.parent is None and self.site is None:\n            raise ValidationError(\n                {\"site\": f\"A Location of type {self.location_type} has no parent Location, but must have a Site.\"}\n            )\n", "path": "nautobot/dcim/models/locations.py"}], "after_files": [{"content": "from django.contrib.contenttypes.fields import GenericRelation\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.core.exceptions import ObjectDoesNotExist, ValidationError\nfrom django.db import models\nfrom django.urls import reverse\n\nfrom tree_queries.models import TreeNode\n\nfrom nautobot.core.fields import AutoSlugField\nfrom nautobot.core.models.generics import OrganizationalModel, PrimaryModel\nfrom nautobot.extras.models import StatusModel\nfrom nautobot.extras.utils import extras_features, FeatureQuery\nfrom nautobot.utilities.fields import NaturalOrderingField\nfrom nautobot.utilities.tree_queries import TreeManager\n\n\n@extras_features(\n    \"custom_fields\",\n    \"custom_links\",\n    \"custom_validators\",\n    \"export_templates\",\n    \"graphql\",\n    \"relationships\",\n    \"webhooks\",\n)\nclass LocationType(TreeNode, OrganizationalModel):\n    \"\"\"\n    Definition of a category of Locations, including its hierarchical relationship to other LocationTypes.\n\n    A LocationType also specifies the content types that can be associated to a Location of this category.\n    For example a \"Building\" LocationType might allow Prefix and VLANGroup, but not Devices,\n    while a \"Room\" LocationType might allow Racks and Devices.\n    \"\"\"\n\n    name = models.CharField(max_length=100, unique=True)\n    slug = AutoSlugField(populate_from=\"name\")\n    description = models.CharField(max_length=200, blank=True)\n    content_types = models.ManyToManyField(\n        to=ContentType,\n        related_name=\"location_types\",\n        verbose_name=\"Permitted object types\",\n        limit_choices_to=FeatureQuery(\"locations\"),\n        help_text=\"The object type(s) that can be associated to a Location of this type.\",\n    )\n\n    objects = TreeManager()\n\n    csv_headers = [\"name\", \"slug\", \"parent\", \"description\", \"content_types\"]\n\n    class Meta:\n        ordering = (\"name\",)\n\n    def __str__(self):\n        return self.name\n\n    def get_absolute_url(self):\n        return reverse(\"dcim:locationtype\", args=[self.slug])\n\n    def to_csv(self):\n        return (\n            self.name,\n            self.slug,\n            self.parent.name if self.parent else None,\n            self.description,\n            \",\".join(f\"{ct.app_label}.{ct.model}\" for ct in self.content_types.order_by(\"app_label\", \"model\")),\n        )\n\n    def clean(self):\n        \"\"\"\n        Disallow LocationTypes whose name conflicts with existing location-related models, to avoid confusion.\n\n        In the longer term we will collapse these other models into special cases of LocationType.\n\n        Also, disallow re-parenting a LocationType if there are Locations already using this LocationType.\n        \"\"\"\n        super().clean()\n\n        if self.name.lower() in [\n            \"region\",\n            \"regions\",\n            \"site\",\n            \"sites\",\n            \"rackgroup\",\n            \"rackgroups\",\n            \"rack group\",\n            \"rack groups\",\n        ]:\n            raise ValidationError({\"name\": \"This name is reserved for future use.\"})\n\n        if (\n            self.present_in_database\n            and self.parent != LocationType.objects.get(pk=self.pk).parent\n            and self.locations.exists()\n        ):\n            raise ValidationError(\n                {\n                    \"parent\": \"This LocationType currently has Locations using it, \"\n                    \"therefore its parent cannot be changed at this time.\"\n                }\n            )\n\n    @property\n    def display(self):\n        \"\"\"\n        Include the parent type names as well in order to provide UI clarity.\n        `self.ancestors()` returns all the preceding nodes from the top down.\n        So if we are looking at node C and its node structure is the following:\n            A\n           /\n          B\n         /\n        C\n        This method will return \"A \u2192 B \u2192 C\".\n        Note that `self.ancestors()` may throw an `ObjectDoesNotExist` during bulk-delete operations.\n        \"\"\"\n        display_str = \"\"\n        try:\n            for ancestor in self.ancestors():\n                display_str += ancestor.name + \" \u2192 \"\n        except ObjectDoesNotExist:\n            pass\n        finally:\n            display_str += self.name\n            return display_str  # pylint: disable=lost-exception\n\n\n@extras_features(\n    \"custom_fields\",\n    \"custom_links\",\n    \"custom_validators\",\n    \"export_templates\",\n    \"graphql\",\n    \"relationships\",\n    \"statuses\",\n    \"webhooks\",\n)\nclass Location(TreeNode, StatusModel, PrimaryModel):\n    \"\"\"\n    A Location represents an arbitrarily specific geographic location, such as a campus, building, floor, room, etc.\n\n    As presently implemented, Location is an intermediary model between Site and RackGroup - more specific than a Site,\n    less specific (and more broadly applicable) than a RackGroup:\n\n    Region\n      Region\n        Site\n          Location (location_type=\"Building\")\n            Location (location_type=\"Room\")\n              RackGroup\n                Rack\n                  Device\n              Device\n            Prefix\n            etc.\n          VLANGroup\n          Prefix\n          etc.\n\n    As such, as presently implemented, every Location either has a parent Location or a \"parent\" Site.\n\n    In the future, we plan to collapse Region and Site (and likely RackGroup as well) into the Location model.\n    \"\"\"\n\n    # A Location's name is unique within context of its parent, not globally unique.\n    name = models.CharField(max_length=100, db_index=True)\n    _name = NaturalOrderingField(target_field=\"name\", max_length=100, blank=True, db_index=True)\n    # However a Location's slug *is* globally unique.\n    slug = AutoSlugField(populate_from=[\"parent__slug\", \"name\"])\n    location_type = models.ForeignKey(\n        to=\"dcim.LocationType\",\n        on_delete=models.PROTECT,\n        related_name=\"locations\",\n    )\n    site = models.ForeignKey(\n        to=\"dcim.Site\",\n        on_delete=models.CASCADE,\n        related_name=\"locations\",\n        blank=True,\n        null=True,\n    )\n    tenant = models.ForeignKey(\n        to=\"tenancy.Tenant\",\n        on_delete=models.PROTECT,\n        related_name=\"locations\",\n        blank=True,\n        null=True,\n    )\n    description = models.CharField(max_length=200, blank=True)\n    images = GenericRelation(to=\"extras.ImageAttachment\")\n\n    objects = TreeManager()\n\n    csv_headers = [\n        \"name\",\n        \"slug\",\n        \"location_type\",\n        \"site\",\n        \"status\",\n        \"parent\",\n        \"tenant\",\n        \"description\",\n    ]\n\n    clone_fields = [\n        \"location_type\",\n        \"site\",\n        \"status\",\n        \"parent\",\n        \"tenant\",\n        \"description\",\n    ]\n\n    class Meta:\n        ordering = (\"_name\",)\n        unique_together = [[\"parent\", \"name\"]]\n\n    def __str__(self):\n        return self.name\n\n    def get_absolute_url(self):\n        return reverse(\"dcim:location\", args=[self.slug])\n\n    def to_csv(self):\n        return (\n            self.name,\n            self.slug,\n            self.location_type.name,\n            self.site.name if self.site else None,\n            self.get_status_display(),\n            self.parent.name if self.parent else None,\n            self.tenant.name if self.tenant else None,\n            self.description,\n        )\n\n    @property\n    def base_site(self):\n        \"\"\"The site that this Location belongs to, if any, or that its root ancestor belongs to, if any.\"\"\"\n        return self.site or self.ancestors().first().site\n\n    @property\n    def display(self):\n        \"\"\"\n        Location name is unique per parent but not globally unique, so include parent information as context.\n        `self.ancestors()` returns all the preceding nodes from the top down.\n        So if we are looking at node C and its node structure is the following:\n            A\n           /\n          B\n         /\n        C\n        This method will return \"A \u2192 B \u2192 C\".\n\n        Note that `self.ancestors()` may throw an `ObjectDoesNotExist` during bulk-delete operations.\n        \"\"\"\n        display_str = \"\"\n        try:\n            for ancestor in self.ancestors():\n                display_str += ancestor.name + \" \u2192 \"\n        except ObjectDoesNotExist:\n            pass\n        finally:\n            display_str += self.name\n            return display_str  # pylint: disable=lost-exception\n\n    def validate_unique(self, exclude=None):\n        # Check for a duplicate name on a Location with no parent.\n        # This is necessary because Django does not consider two NULL fields to be equal.\n        if self.parent is None:\n            if Location.objects.exclude(pk=self.pk).filter(parent__isnull=True, name=self.name).exists():\n                raise ValidationError({\"name\": \"A root-level location with this name already exists.\"})\n\n        super().validate_unique(exclude=exclude)\n\n    def clean(self):\n        super().clean()\n\n        # Prevent changing location type as that would require a whole bunch of cascading logic checks,\n        # e.g. what if the new type doesn't allow all of the associated objects that the old type did?\n        if self.present_in_database and self.location_type != Location.objects.get(pk=self.pk).location_type:\n            raise ValidationError({\"location_type\": \"Changing the type of an existing Location is not permitted.\"})\n\n        if self.location_type.parent is not None:\n            # We must have a parent and it must match the parent location_type.\n            if self.parent is None or self.parent.location_type != self.location_type.parent:\n                raise ValidationError(\n                    {\n                        \"parent\": f\"A Location of type {self.location_type} must have \"\n                        f\"a parent Location of type {self.location_type.parent}\"\n                    }\n                )\n            # We must *not* have a site.\n            # In a future release, Site will become a kind of Location, and the resulting data migration will be\n            # much cleaner if it doesn't have to deal with Locations that have two \"parents\".\n            if self.site is not None:\n                raise ValidationError(\n                    {\"site\": f\"A location of type {self.location_type} must not have an associated Site.\"}\n                )\n\n        # If this location_type does *not* have a parent type,\n        # this location must have an associated Site.\n        # This check will be removed in the future once Site and Region become special cases of Location;\n        # at that point a \"root\" LocationType will correctly have no parent (or site) associated.\n        if self.location_type.parent is None and self.site is None:\n            raise ValidationError(\n                {\"site\": f\"A Location of type {self.location_type} has no parent Location, but must have a Site.\"}\n            )\n", "path": "nautobot/dcim/models/locations.py"}]}