Repository: nautobot/nautobot

Issue:
"AttributeError: 'NoneType' object has no attribute '_meta'" on object detail view when Relationships reference nonexistent data models
### Environment
* Python version:
* Nautobot version: 1.3.8

### Steps to Reproduce

1. Define a Relationship between a core model and a plugin model
2. Define RelationshipAssociations involving this relationship
3. Uninstall the plugin and restart Nautobot
4. Access the detail view of an instance of the core model that has at least one such RelationshipAssociation defined

### Expected Behavior

Page to load and render successfully

### Observed Behavior

Page load fails, with `AttributeError: 'NoneType' object has no attribute '_meta'`

Deleting the Relationship(s) in question appears to restore normal operation.

May be reproducible on `main.demo.nautobot.com` when the database is regenerated, as its data set includes plugin-model relationships on Device, but `main.demo` (unlike `demo.nautobot.com`) doesn't actually install any plugins.



Discussion:
Similar to the issues fixed in #1644; need to identify how this case differs and why it wasn't caught in that fix.
![image](https://user-images.githubusercontent.com/5603551/179999220-0455c018-fc05-449b-b850-c2afa0044680.png)



Resolving patch:
diff --git a/examples/example_plugin/example_plugin/forms.py b/examples/example_plugin/example_plugin/forms.py
--- a/examples/example_plugin/example_plugin/forms.py
+++ b/examples/example_plugin/example_plugin/forms.py
@@ -1,5 +1,6 @@
 from django import forms
 
+from nautobot.extras.forms import NautobotModelForm
 from nautobot.utilities.forms import (
     BootstrapMixin,
     BulkEditForm,
@@ -16,7 +17,7 @@ class ExamplePluginConfigForm(BootstrapMixin, forms.Form):
     maximum_velocity = forms.IntegerField(help_text="Meters per second")
 
 
-class ExampleModelForm(BootstrapMixin, forms.ModelForm):
+class ExampleModelForm(NautobotModelForm):
     """Generic create/update form for `ExampleModel` objects."""
 
     class Meta:
@@ -52,7 +53,7 @@ class Meta:
         nullable_fields = []
 
 
-class AnotherExampleModelForm(BootstrapMixin, forms.ModelForm):
+class AnotherExampleModelForm(NautobotModelForm):
     """Generic create/update form for `AnotherExampleModel` objects."""
 
     class Meta:
diff --git a/examples/example_plugin/example_plugin/models.py b/examples/example_plugin/example_plugin/models.py
--- a/examples/example_plugin/example_plugin/models.py
+++ b/examples/example_plugin/example_plugin/models.py
@@ -6,9 +6,13 @@
 
 
 @extras_features(
+    "custom_fields",
     "custom_links",
+    "custom_validators",
     "dynamic_groups",
+    "export_templates",
     "graphql",
+    "relationships",
     "webhooks",
 )
 class ExampleModel(OrganizationalModel):
@@ -35,7 +39,12 @@ def to_csv(self):
 
 @extras_features(
     "custom_fields",
+    "custom_validators",
     "dynamic_groups",
+    "export_templates",
+    # "graphql", Not specified here as we have a custom type for this model, see example_plugin.graphql.types
+    "relationships",
+    "webhooks",
 )
 class AnotherExampleModel(OrganizationalModel):
     name = models.CharField(max_length=20)
diff --git a/nautobot/extras/tables.py b/nautobot/extras/tables.py
--- a/nautobot/extras/tables.py
+++ b/nautobot/extras/tables.py
@@ -656,10 +656,10 @@ class RelationshipAssociationTable(BaseTable):
     actions = ButtonsColumn(RelationshipAssociation, buttons=("delete",))
 
     source_type = tables.Column()
-    source = tables.Column(linkify=True, orderable=False, accessor="get_source")
+    source = tables.Column(linkify=True, orderable=False, accessor="get_source", default="unknown")
 
     destination_type = tables.Column()
-    destination = tables.Column(linkify=True, orderable=False, accessor="get_destination")
+    destination = tables.Column(linkify=True, orderable=False, accessor="get_destination", default="unknown")
 
     class Meta(BaseTable.Meta):
         model = RelationshipAssociation


Test patch:
diff --git a/nautobot/circuits/tests/integration/test_relationships.py b/nautobot/circuits/tests/integration/test_relationships.py
--- a/nautobot/circuits/tests/integration/test_relationships.py
+++ b/nautobot/circuits/tests/integration/test_relationships.py
@@ -1,3 +1,5 @@
+import uuid
+
 from django.contrib.contenttypes.models import ContentType
 
 from nautobot.circuits.models import Circuit, CircuitTermination, CircuitType, Provider
@@ -96,6 +98,20 @@ def setUp(self):
             source=circuit_termination,
             destination=power_panel,
         )
+        # https://github.com/nautobot/nautobot/issues/2077
+        fake_ct = ContentType.objects.create(app_label="nonexistent", model="nonexistentmodel")
+        bad_relation = Relationship.objects.create(
+            name="Termination 2 Nonexistent",
+            source_type=circuit_termination_ct,
+            destination_type=fake_ct,
+            type=RelationshipTypeChoices.TYPE_MANY_TO_MANY,
+        )
+        RelationshipAssociation.objects.create(
+            relationship=bad_relation,
+            source=circuit_termination,
+            destination_type=fake_ct,
+            destination_id=uuid.uuid4(),
+        )
 
     def tearDown(self):
         self.logout()
@@ -120,3 +136,4 @@ def test_relationships_are_visible(self):
         self.assertTrue(self.browser.is_text_present("Power Panel"))
         self.assertTrue(self.browser.is_text_present("2 providers"))
         self.assertTrue(self.browser.is_text_present("1 site"))
+        self.assertTrue(self.browser.is_text_present("1 nonexistentmodel(s)"))