Skip to content

Commit 98ec835

Browse files
authored
Always inherit visibility from root node (#7209)
1 parent 865257b commit 98ec835

File tree

3 files changed

+118
-0
lines changed

3 files changed

+118
-0
lines changed

kitsune/groups/admin.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,34 @@ class GroupProfileAdmin(TreeAdmin):
1212
raw_id_fields = ["leaders"]
1313
search_fields = ["slug", "group__name"]
1414

15+
def get_readonly_fields(self, request, obj=None):
16+
"""Make visibility read-only for subgroups (non-root nodes)."""
17+
readonly = list(super().get_readonly_fields(request, obj))
18+
19+
if obj and not obj.is_root():
20+
if "visibility" not in readonly:
21+
readonly.append("visibility")
22+
23+
return readonly
24+
25+
def get_form(self, request, obj=None, **kwargs):
26+
"""Add help text for visibility field based on node type."""
27+
form = super().get_form(request, obj, **kwargs)
28+
29+
if "visibility" in form.base_fields:
30+
if obj and not obj.is_root():
31+
form.base_fields["visibility"].help_text = (
32+
"Visibility is inherited from parent group and cannot be changed. "
33+
f"This group inherits '{obj.get_parent().visibility}' from its parent. "
34+
"To change visibility, update the root group."
35+
)
36+
else:
37+
form.base_fields["visibility"].help_text = (
38+
"Who can see this group. Children automatically inherit parent's visibility. "
39+
"Changing this will update all descendants in the tree."
40+
)
41+
42+
return form
43+
1544

1645
admin.site.register(GroupProfile, GroupProfileAdmin)

kitsune/groups/models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,26 @@ def save(self, *args, **kwargs):
8484
if not self.pk or update_fields is None or "information" in update_fields:
8585
self.information_html = wiki_to_html(self.information)
8686

87+
# update visibility based on parent group
88+
old_visibility = None
89+
if self.pk:
90+
try:
91+
old_instance = GroupProfile.objects.get(pk=self.pk)
92+
old_visibility = old_instance.visibility
93+
except GroupProfile.DoesNotExist:
94+
pass
95+
8796
if len(self.path) > self.steplen:
8897
parent = self.get_parent()
8998
if parent:
9099
self.visibility = parent.visibility
91100

92101
super().save(*args, **kwargs)
93102

103+
# Propagate visibility changes to all descendants
104+
if old_visibility is not None and old_visibility != self.visibility:
105+
self.get_descendants().update(visibility=self.visibility)
106+
94107
def update_visibility(self, new_visibility, propagate=True):
95108
"""
96109
Update visibility for this group and optionally all descendants.

kitsune/groups/tests/test_models.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,82 @@ def test_updating_root_propagates_to_descendants(self):
882882
self.assertEqual(child.visibility, GroupProfile.Visibility.PRIVATE)
883883
self.assertEqual(grandchild.visibility, GroupProfile.Visibility.PRIVATE)
884884

885+
def test_save_propagates_visibility_to_descendants(self):
886+
"""Changing visibility via save() (as in admin) propagates to descendants."""
887+
parent_group = Group.objects.create(name="Parent")
888+
parent = GroupProfile.add_root(
889+
group=parent_group,
890+
slug="parent",
891+
visibility=GroupProfile.Visibility.PUBLIC,
892+
)
893+
894+
child_group = Group.objects.create(name="Child")
895+
child = parent.add_child(
896+
group=child_group,
897+
slug="child",
898+
)
899+
900+
grandchild_group = Group.objects.create(name="Grandchild")
901+
grandchild = child.add_child(
902+
group=grandchild_group,
903+
slug="grandchild",
904+
)
905+
906+
# All should be PUBLIC initially
907+
self.assertEqual(parent.visibility, GroupProfile.Visibility.PUBLIC)
908+
child.refresh_from_db()
909+
grandchild.refresh_from_db()
910+
self.assertEqual(child.visibility, GroupProfile.Visibility.PUBLIC)
911+
self.assertEqual(grandchild.visibility, GroupProfile.Visibility.PUBLIC)
912+
913+
# Change root to PRIVATE via save() (simulates admin form submission)
914+
parent.visibility = GroupProfile.Visibility.PRIVATE
915+
parent.save()
916+
917+
# All descendants should now be PRIVATE
918+
parent.refresh_from_db()
919+
child.refresh_from_db()
920+
grandchild.refresh_from_db()
921+
self.assertEqual(parent.visibility, GroupProfile.Visibility.PRIVATE)
922+
self.assertEqual(child.visibility, GroupProfile.Visibility.PRIVATE)
923+
self.assertEqual(grandchild.visibility, GroupProfile.Visibility.PRIVATE)
924+
925+
def test_cannot_change_subgroup_visibility(self):
926+
"""Attempting to change subgroup visibility is overridden by parent."""
927+
parent_group = Group.objects.create(name="Parent")
928+
parent = GroupProfile.add_root(
929+
group=parent_group,
930+
slug="parent",
931+
visibility=GroupProfile.Visibility.PRIVATE,
932+
)
933+
934+
child_group = Group.objects.create(name="Child")
935+
child = parent.add_child(
936+
group=child_group,
937+
slug="child",
938+
)
939+
940+
# Verify child is PRIVATE (inherited)
941+
child.refresh_from_db()
942+
self.assertEqual(child.visibility, GroupProfile.Visibility.PRIVATE)
943+
944+
# Try to change child to PUBLIC (simulates admin form submission)
945+
child.visibility = GroupProfile.Visibility.PUBLIC
946+
child.save()
947+
948+
# Refresh and verify it's still PRIVATE (overridden by parent)
949+
child.refresh_from_db()
950+
self.assertEqual(child.visibility, GroupProfile.Visibility.PRIVATE)
951+
952+
# Verify descendants also remain PRIVATE
953+
grandchild_group = Group.objects.create(name="Grandchild")
954+
grandchild = child.add_child(
955+
group=grandchild_group,
956+
slug="grandchild",
957+
)
958+
grandchild.refresh_from_db()
959+
self.assertEqual(grandchild.visibility, GroupProfile.Visibility.PRIVATE)
960+
885961

886962
class ModeratedVisibilityTests(TestCase):
887963
"""Test MODERATED visibility with nested groups."""

0 commit comments

Comments
 (0)