Repository: nautobot/nautobot

Issue:
Graphql query for device_types fails after upgrade to 1.2.1
### Environment
* Python version: 3.8.10
* Nautobot version: 1.2.1

### Steps to Reproduce

1. In the graphql query browser insert the following query:

```graphql
query { 
  device_types {
    id
    model
  } 
}
```

2. Run the query
3. With DEBUG enabled the following result is shown.

```graphql
{
  "errors": [
    {
      "message": "maximum recursion depth exceeded while calling a Python object"
    }
  ],
  "data": {
    "device_types": null
  }
}
```

4. With uwsgi and nxing configured ngnix logs the following:

`upstream prematurely closed connection while reading response header from upstream`

And in the webui a HTTP 502 error is shown.

### Expected Behavior

Receive all configured device types as it did before upgrading to version 1.2.1.

### Observed Behavior

See point 3 and 4.



Discussion:
Thanks for the report! Here's the stack trace I see:

```
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/graphql/execution/executor.py", line 481, in complete_value_catching_error
    exe_context, return_type, field_asts, info, path, result
  File "/usr/local/lib/python3.6/site-packages/graphql/execution/executor.py", line 557, in complete_value
    exe_context, return_type, field_asts, info, path, result
  File "/usr/local/lib/python3.6/site-packages/graphql/execution/executor.py", line 599, in complete_list_value
    for item in result:
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 287, in __iter__
    self._fetch_all()
  File "/usr/local/lib/python3.6/site-packages/cacheops/query.py", line 283, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 71, in __iter__
    obj = model_cls.from_db(db, init_list, row[model_fields_start:model_fields_end])
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 513, in from_db
    new = cls(*values)
  File "/usr/local/lib/python3.6/site-packages/nautobot/dcim/models/devices.py", line 152, in __init__
    self._original_u_height = self.u_height
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query_utils.py", line 149, in __get__
    instance.refresh_from_db(fields=[field_name])
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 635, in refresh_from_db
    db_instance = db_instance_qs.get()
  File "/usr/local/lib/python3.6/site-packages/cacheops/query.py", line 353, in get
    return qs._no_monkey.get(qs, *args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 425, in get
    num = len(clone)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 269, in __len__
    self._fetch_all()
  File "/usr/local/lib/python3.6/site-packages/cacheops/query.py", line 283, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 71, in __iter__
    obj = model_cls.from_db(db, init_list, row[model_fields_start:model_fields_end])
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 513, in from_db
    new = cls(*values)
  File "/usr/local/lib/python3.6/site-packages/nautobot/dcim/models/devices.py", line 155, in __init__
    self._original_front_image = self.front_image
  File "/usr/local/lib/python3.6/site-packages/django/db/models/fields/files.py", line 180, in __get__
    instance.refresh_from_db(fields=[self.field.name])
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 635, in refresh_from_db
    db_instance = db_instance_qs.get()
  File "/usr/local/lib/python3.6/site-packages/cacheops/query.py", line 353, in get
    return qs._no_monkey.get(qs, *args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 425, in get
    num = len(clone)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 269, in __len__
    self._fetch_all()
  File "/usr/local/lib/python3.6/site-packages/cacheops/query.py", line 283, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 71, in __iter__
    obj = model_cls.from_db(db, init_list, row[model_fields_start:model_fields_end])
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 513, in from_db
    new = cls(*values)
  File "/usr/local/lib/python3.6/site-packages/nautobot/dcim/models/devices.py", line 152, in __init__
    self._original_u_height = self.u_height
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query_utils.py", line 149, in __get__
    instance.refresh_from_db(fields=[field_name])
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 635, in refresh_from_db
    db_instance = db_instance_qs.get()
  File "/usr/local/lib/python3.6/site-packages/cacheops/query.py", line 353, in get
    return qs._no_monkey.get(qs, *args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 425, in get
    num = len(clone)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 269, in __len__
    self._fetch_all()
  File "/usr/local/lib/python3.6/site-packages/cacheops/query.py", line 283, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 71, in __iter__
    obj = model_cls.from_db(db, init_list, row[model_fields_start:model_fields_end])
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 513, in from_db
    new = cls(*values)
  File "/usr/local/lib/python3.6/site-packages/nautobot/dcim/models/devices.py", line 155, in __init__
    self._original_front_image = self.front_image
...
```

where the following seems to be the infinite recursive loop:

```
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 635, in refresh_from_db
    db_instance = db_instance_qs.get()
  File "/usr/local/lib/python3.6/site-packages/cacheops/query.py", line 353, in get
    return qs._no_monkey.get(qs, *args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 425, in get
    num = len(clone)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 269, in __len__
    self._fetch_all()
  File "/usr/local/lib/python3.6/site-packages/cacheops/query.py", line 283, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 71, in __iter__
    obj = model_cls.from_db(db, init_list, row[model_fields_start:model_fields_end])
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 513, in from_db
    new = cls(*values)
  File "/usr/local/lib/python3.6/site-packages/nautobot/dcim/models/devices.py", line 152, in __init__
    self._original_u_height = self.u_height
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query_utils.py", line 149, in __get__
    instance.refresh_from_db(fields=[field_name])
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 635, in refresh_from_db
    db_instance = db_instance_qs.get()
  File "/usr/local/lib/python3.6/site-packages/cacheops/query.py", line 353, in get
    return qs._no_monkey.get(qs, *args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 425, in get
    num = len(clone)
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 269, in __len__
    self._fetch_all()
  File "/usr/local/lib/python3.6/site-packages/cacheops/query.py", line 283, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/usr/local/lib/python3.6/site-packages/django/db/models/query.py", line 71, in __iter__
    obj = model_cls.from_db(db, init_list, row[model_fields_start:model_fields_end])
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 513, in from_db
    new = cls(*values)
  File "/usr/local/lib/python3.6/site-packages/nautobot/dcim/models/devices.py", line 155, in __init__
    self._original_front_image = self.front_image
  File "/usr/local/lib/python3.6/site-packages/django/db/models/fields/files.py", line 180, in __get__
    instance.refresh_from_db(fields=[self.field.name])
  File "/usr/local/lib/python3.6/site-packages/django/db/models/base.py", line 635, in refresh_from_db
    db_instance = db_instance_qs.get()
```

Oddly enough I don't think this is code we've touched recently, so I'm not sure yet why this issue is appearing.
FYI, we upgraded from 1.1.5.

Resolving patch:
diff --git a/nautobot/dcim/models/devices.py b/nautobot/dcim/models/devices.py
--- a/nautobot/dcim/models/devices.py
+++ b/nautobot/dcim/models/devices.py
@@ -149,11 +149,11 @@ def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
         # Save a copy of u_height for validation in clean()
-        self._original_u_height = self.u_height
+        self._original_u_height = self.u_height if self.present_in_database else 1
 
         # Save references to the original front/rear images
-        self._original_front_image = self.front_image
-        self._original_rear_image = self.rear_image
+        self._original_front_image = self.front_image if self.present_in_database else None
+        self._original_rear_image = self.rear_image if self.present_in_database else None
 
     def get_absolute_url(self):
         return reverse("dcim:devicetype", args=[self.pk])
@@ -296,9 +296,9 @@ def save(self, *args, **kwargs):
         ret = super().save(*args, **kwargs)
 
         # Delete any previously uploaded image files that are no longer in use
-        if self.front_image != self._original_front_image:
+        if self._original_front_image and self.front_image != self._original_front_image:
             self._original_front_image.delete(save=False)
-        if self.rear_image != self._original_rear_image:
+        if self._original_rear_image and self.rear_image != self._original_rear_image:
             self._original_rear_image.delete(save=False)
 
         return ret


Test patch:
diff --git a/nautobot/core/tests/test_graphql.py b/nautobot/core/tests/test_graphql.py
--- a/nautobot/core/tests/test_graphql.py
+++ b/nautobot/core/tests/test_graphql.py
@@ -1251,3 +1251,19 @@ def test_query_webhooks_filter(self):
                 result = self.execute_query(query)
                 self.assertIsNone(result.errors)
                 self.assertEqual(len(result.data["webhooks"]), nbr_expected_results)
+
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
+    def test_query_device_types(self):
+        """Test querying of device types, specifically checking for issue #1203."""
+        query = """
+        query {
+            device_types {
+                model
+            }
+        }
+        """
+        result = self.execute_query(query)
+        self.assertIsNone(result.errors)
+        self.assertIsInstance(result.data, dict, result)
+        self.assertIsInstance(result.data["device_types"], list, result)
+        self.assertEqual(result.data["device_types"][0]["model"], self.devicetype.model, result)