From dd4d9cb8dd844eced9715fc6d33c8462db2d3f19 Mon Sep 17 00:00:00 2001 From: Mattia Giupponi Date: Mon, 17 Aug 2026 11:13:54 +0200 Subject: [PATCH] [Backport #14519] GroupProfile.access default is public, not a valid choice --- .../0036_fix_groupprofile_access_default.py | 38 +++++++++++++++++++ geonode/groups/models.py | 2 +- geonode/groups/tests.py | 11 ++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 geonode/groups/migrations/0036_fix_groupprofile_access_default.py diff --git a/geonode/groups/migrations/0036_fix_groupprofile_access_default.py b/geonode/groups/migrations/0036_fix_groupprofile_access_default.py new file mode 100644 index 00000000000..bb922454f11 --- /dev/null +++ b/geonode/groups/migrations/0036_fix_groupprofile_access_default.py @@ -0,0 +1,38 @@ +from django.db import migrations, models + +# The old default, with a stray apostrophe that matches none of the field's own +# GROUP_CHOICES. Every GroupProfile created without an explicit access — which is +# what geonode_ldap's group mirroring does — was stored with this value. +BAD_DEFAULT = "public'" + + +def repair_access(apps, schema_editor): + GroupProfile = apps.get_model("groups", "GroupProfile") + GroupProfile.objects.filter(access=BAD_DEFAULT).update(access="public") + + +class Migration(migrations.Migration): + dependencies = [ + ("groups", "0035_remove_modeltranslation"), + ] + + operations = [ + migrations.AlterField( + model_name="groupprofile", + name="access", + field=models.CharField( + choices=[ + ("public", "Public"), + ("public-invite", "Public (invite-only)"), + ("private", "Private"), + ], + default="public", + help_text="Public: Any registered user can view and join a public group.
Public (invite-only):Any registered user can view the group. Only invited users can join.
Private: Registered users cannot see any details about the group, including membership. Only invited users can join.", + max_length=15, + verbose_name="Access", + ), + ), + # Reversing would mean writing the invalid value back, so this only goes + # forward; the AlterField above is reversible on its own. + migrations.RunPython(repair_access, migrations.RunPython.noop), + ] diff --git a/geonode/groups/models.py b/geonode/groups/models.py index 8c66ca61bbc..b70511ef0e4 100644 --- a/geonode/groups/models.py +++ b/geonode/groups/models.py @@ -91,7 +91,7 @@ class GroupProfile(models.Model): email = models.EmailField(_("Email"), null=True, blank=True, help_text=email_help_text) keywords = TaggableManager(_("Keywords"), help_text=_("A space or comma-separated list of keywords"), blank=True) access = models.CharField( - _("Access"), max_length=15, default="public'", choices=GROUP_CHOICES, help_text=access_help_text + _("Access"), max_length=15, default="public", choices=GROUP_CHOICES, help_text=access_help_text ) categories = models.ManyToManyField(GroupCategory, verbose_name=_("Categories"), blank=True, related_name="groups") created = models.DateTimeField(auto_now_add=True, null=True, blank=True) diff --git a/geonode/groups/tests.py b/geonode/groups/tests.py index 337c188055c..b17993098e3 100644 --- a/geonode/groups/tests.py +++ b/geonode/groups/tests.py @@ -93,6 +93,17 @@ def test_registered_group_exists(self): group = Group.objects.filter(name=groups_settings.REGISTERED_MEMBERS_GROUP_NAME).first() self.assertTrue(group) + def test_default_access_is_a_valid_choice(self): + """ + Ensures a GroupProfile created without an explicit access gets one of the + field's own GROUP_CHOICES, so that code filtering on access="public" + (e.g. geonode.people.utils.get_available_users) can see it. + """ + group = GroupProfile.objects.create(slug="default_access_group", title="default_access_group") + self.assertEqual(group.access, "public") + self.assertIn(group.access, dict(GroupProfile.GROUP_CHOICES)) + self.assertIn(group, GroupProfile.objects.filter(access="public")) + def test_users_group_list_view(self): """ 1. Ensures that a superuser can see the whole group list.