Repository: nautobot/nautobot

Issue:
Static Groups
As a Network Automation Engineer, I want the ability to create and manage static groups that function much like dynamic groups, except their membership is statically managed.


This is to support grouping use cases in which dynamic membership is problematic or otherwise unwanted. These static groups will also be used to underpin a refactor of the way dynamic groups operate, to solve critical performance issues.


AC:
- Groups are defined per content type
- Membership is based upon being explicitly related to the group
- **Not** implemented as a TreeModel to start


Discussion:
StaticGroup model and StaticGroupAssociation model. 

Model Fields:
StaticGroup: name, contenttype, description, tags
StaticGroupAssociation: FK to StaticGroup, and generic FK to members of the Group.

Design Considerations:
1. It will be very similar to ContactAssociation. 
2. Need the ability to `FeatureQuery` for relevant content types. e.g. exclude ObjectChanges
3. Need a standardized pattern for related object tables in the UI
4. StaticGroup should only contain ONE content type
5. The UI has to mirror that of DynamicGroup (get_table_for_model lookup)
6. From any object List view, it would be nice to have a bulk assign operation button to add objects to a StaticGroup (next to bulk edit and bulk delete)
7. Add to a static group and remove from a static group in all bulk edit forms (mixin to add to the base class)
8. ConfigContext filter (Is this a requirement for 2.3?)
9. `get_groups_for_model()` API is something we should add to the BaseModel class.
10. GraphQL support for `object.static_groups`
11. `BaseFilterSet` enhancement: filter based on StaticGroup membership


Questions to explore:
Any mixins or abstract base classes that can be shared between ContactAssociation and StaticGroupAssociation? 

Stretch goal:
Adding a linked count column to the `BaseTable`.
Retrofit some of these design considerations for the Contact/Team feature.
After #5631 and #5668, remaining items are:

- ConfigContext filter (Is this a requirement for 2.3?)
- GraphQL support for object.static_groups
- (stretch) Retrofit some of these design considerations for the Contact/Team feature.

Resolving patch:
diff --git a/nautobot/extras/migrations/0107_staticgroup_staticgroupassociation.py b/nautobot/extras/migrations/0107_staticgroup_staticgroupassociation.py
--- a/nautobot/extras/migrations/0107_staticgroup_staticgroupassociation.py
+++ b/nautobot/extras/migrations/0107_staticgroup_staticgroupassociation.py
@@ -112,6 +112,16 @@ class Migration(migrations.Migration):
             options={
                 "ordering": ["static_group", "associated_object_type", "associated_object_id"],
                 "unique_together": {("static_group", "associated_object_type", "associated_object_id")},
+                "indexes": [
+                    models.Index(
+                        name="extras_sga_double",
+                        fields=["static_group", "associated_object_id"],
+                    ),
+                    models.Index(
+                        name="extras_sga_associated_object",
+                        fields=["associated_object_type_id", "associated_object_id"],
+                    ),
+                ],
             },
             managers=[
                 ("objects", nautobot.extras.models.groups.StaticGroupAssociationDefaultManager()),
diff --git a/nautobot/extras/models/groups.py b/nautobot/extras/models/groups.py
--- a/nautobot/extras/models/groups.py
+++ b/nautobot/extras/models/groups.py
@@ -247,6 +247,16 @@ class StaticGroupAssociation(OrganizationalModel):
     class Meta:
         unique_together = [["static_group", "associated_object_type", "associated_object_id"]]
         ordering = ["static_group", "associated_object_type", "associated_object_id"]
+        indexes = [
+            models.Index(
+                name="extras_sga_double",
+                fields=["static_group", "associated_object_id"],
+            ),
+            models.Index(
+                name="extras_sga_associated_object",
+                fields=["associated_object_type_id", "associated_object_id"],
+            ),
+        ]
 
     def __str__(self):
         return f"{self.associated_object} as a member of {self.static_group}"


Test patch: