Repository: django-guardian/django-guardian

Issue:
Support `only_with_perms_in` in `shortcuts.get_groups_with_perms`
There is a filter option `only_with_perms_in` for `shortcuts.get_users_with_perms`.

It would be nice to have the same filter option also for `shortcuts.get_groups_with_perms`.




Discussion:

Patch:
diff --git a/guardian/shortcuts.py b/guardian/shortcuts.py
index 0babdfab..d224278b 100644
--- a/guardian/shortcuts.py
+++ b/guardian/shortcuts.py
@@ -373,7 +373,7 @@ def get_users_with_perms(
         return users
 
 
-def get_groups_with_perms(obj: Model, attach_perms: bool = False) -> Union[Group, dict]:
+def get_groups_with_perms(obj: Model, attach_perms: bool = False, only_with_perms_in: Optional[list[str]] = None) -> Union[Group, dict]:
     """Get all groups with *any* object permissions for the given `obj`.
 
     Parameters:
@@ -381,9 +381,10 @@ def get_groups_with_perms(obj: Model, attach_perms: bool = False) -> Union[Group
         attach_perms (bool): Whether return result as a dict of `Group` instances
             with permissions' codenames list of values.
             This would fetch groups eagerly!
+        only_with_perms_in (list[str]): Only return groups with these permissions.
 
     Returns:
-        All `Group` objects with *any* object permissions for the given `obj`.
+        All `Group` objects with the matching object permissions for the given `obj`.
 
     Example:
         ```shell
@@ -415,11 +416,17 @@ def get_groups_with_perms(obj: Model, attach_perms: bool = False) -> Union[Group
             }
         else:
             group_filters = {"%s__content_object" % group_rel_name: obj}
+        if only_with_perms_in is not None:
+            permission_ids = Permission.objects.filter(content_type=ctype, codename__in=only_with_perms_in).values_list("id", flat=True)
+            group_filters.update({
+                "%s__permission_id__in" % group_rel_name: permission_ids,
+            })
+
         group_rel_model = group_model.group.field.related_model
         return group_rel_model.objects.filter(**group_filters).distinct()
     else:
         group_perms_mapping = defaultdict(list)
-        groups_with_perms = get_groups_with_perms(obj)
+        groups_with_perms = get_groups_with_perms(obj, only_with_perms_in=only_with_perms_in)
         qs = group_model.objects.filter(group__in=groups_with_perms).prefetch_related("group", "permission")
         if group_model.objects.is_generic():
             qs = qs.filter(object_pk=obj.pk, content_type=ctype)
diff --git a/pyproject.toml b/pyproject.toml
index 3424ea0e..85d9a524 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -100,6 +100,7 @@ authors = [
   { name = "Chris Maggiuli" },
   { name = "Bona Fide IT GmbH"},
   { name = "Columbia University, SIS Development and Application Services" },
+  { name = "Cornelius Hoffmann", email = "coding@volucra.de" },
 ]
 maintainers = [
   {name = "Tom Clark", email = "django-guardian@octue.com"},


Test patch:
diff --git a/guardian/testapp/tests/test_shortcuts.py b/guardian/testapp/tests/test_shortcuts.py
index 0cb6b523..81ddbfe8 100644
--- a/guardian/testapp/tests/test_shortcuts.py
+++ b/guardian/testapp/tests/test_shortcuts.py
@@ -354,7 +354,34 @@ def test_simple(self):
             {user.username for user in (self.user1, self.user2)},
         )
 
-    def test_only_with_perms_in(self):
+    def test_only_with_perms_in_groups(self):
+        assign_perm("change_contenttype", self.group1, self.obj1)
+        assign_perm("delete_contenttype", self.group2, self.obj1)
+        assign_perm("delete_contenttype", self.group3, self.obj2)
+
+        result = get_groups_with_perms(self.obj1, only_with_perms_in=('change_contenttype',))
+        result_vals = result.values_list('name', flat=True)
+
+        self.assertEqual(
+            set(result_vals),
+            {self.group1.name},
+        )
+
+    def test_only_with_perms_in_groups_attached(self):
+        assign_perm("change_contenttype", self.group1, self.obj1)
+        assign_perm("change_contenttype", self.group2, self.obj1)
+        assign_perm("delete_contenttype", self.group2, self.obj1)
+        assign_perm("delete_contenttype", self.group3, self.obj2)
+
+        result = get_groups_with_perms(self.obj1, only_with_perms_in=('delete_contenttype',),
+            attach_perms=True)
+
+        expected = {self.group2: ('change_contenttype', 'delete_contenttype')}
+        self.assertEqual(result.keys(), expected.keys())
+        for key, perms in result.items():
+            self.assertEqual(set(perms), set(expected[key]))
+
+    def test_only_with_perms_in_users(self):
         assign_perm("change_contenttype", self.user1, self.obj1)
         assign_perm("delete_contenttype", self.user2, self.obj1)
         assign_perm("delete_contenttype", self.user3, self.obj2)
@@ -367,7 +394,7 @@ def test_only_with_perms_in(self):
             {self.user1.username},
         )
 
-    def test_only_with_perms_in_and_with_group_users(self):
+    def test_only_with_perms_in_users_with_group_users(self):
         self.user1.groups.add(self.group1)
         self.user2.groups.add(self.group2)
         self.user3.groups.add(self.group3)
@@ -387,7 +414,7 @@ def test_only_with_perms_in_and_with_group_users(self):
             {self.user1.username, self.user2.username},
         )
 
-    def test_only_with_perms_in_and_not_with_group_users(self):
+    def test_only_with_perms_in_users_without_group_users(self):
         self.user1.groups.add(self.group1)
         self.user2.groups.add(self.group2)
         self.user3.groups.add(self.group3)
@@ -410,7 +437,7 @@ def test_only_with_perms_in_and_not_with_group_users(self):
             {self.user2.username},
         )
 
-    def test_only_with_perms_in_attached(self):
+    def test_only_with_perms_in_users_attached(self):
         assign_perm("change_contenttype", self.user1, self.obj1)
         assign_perm("change_contenttype", self.user2, self.obj1)
         assign_perm("delete_contenttype", self.user2, self.obj1)