Skip to content

Commit ffa2625

Browse files
committed
Improve tier change
1 parent 86a6965 commit ffa2625

File tree

3 files changed

+341
-6
lines changed

3 files changed

+341
-6
lines changed

astra_app/core/tests/test_organization_user_views.py

Lines changed: 286 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,81 @@ def test_org_detail_shows_change_tier_button_for_multi_type_category(self) -> No
11761176
# Keep deterministic with --keepdb: only same-category sponsorship tiers.
11771177
MembershipType.objects.update(enabled=False)
11781178

1179+
MembershipType.objects.update_or_create(
1180+
code="platinum",
1181+
defaults={
1182+
"name": "Platinum Sponsor Member",
1183+
"category_id": "sponsorship",
1184+
"sort_order": 10,
1185+
"enabled": True,
1186+
"group_cn": "almalinux-platinum",
1187+
},
1188+
)
1189+
MembershipType.objects.update_or_create(
1190+
code="gold",
1191+
defaults={
1192+
"name": "Gold Sponsor Member",
1193+
"category_id": "sponsorship",
1194+
"sort_order": 20,
1195+
"enabled": True,
1196+
"group_cn": "almalinux-gold",
1197+
},
1198+
)
1199+
1200+
MembershipType.objects.update_or_create(
1201+
code="silver",
1202+
defaults={
1203+
"name": "Silver Sponsor Member",
1204+
"category_id": "sponsorship",
1205+
"sort_order": 30,
1206+
"enabled": True,
1207+
"group_cn": "almalinux-silver",
1208+
},
1209+
)
1210+
MembershipType.objects.update_or_create(
1211+
code="ruby",
1212+
defaults={
1213+
"name": "Ruby Sponsor Member",
1214+
"category_id": "sponsorship",
1215+
"sort_order": 40,
1216+
"enabled": True,
1217+
"group_cn": "almalinux-ruby",
1218+
},
1219+
)
1220+
1221+
org = Organization.objects.create(name="Tiered Org", representative="bob")
1222+
Membership.objects.create(target_organization=org, membership_type_id="silver")
1223+
1224+
bob = FreeIPAUser("bob", {"uid": ["bob"], "memberof_group": [], "c": ["US"]})
1225+
self._login_as_freeipa_user("bob")
1226+
1227+
with patch("core.backends.FreeIPAUser.get", return_value=bob):
1228+
resp = self.client.get(reverse("organization-detail", args=[org.pk]))
1229+
1230+
self.assertEqual(resp.status_code, 200)
1231+
self.assertContains(resp, "Change tier")
1232+
tier_change_url = reverse("organization-membership-request", args=[org.pk]) + "?membership_type=gold"
1233+
self.assertContains(resp, tier_change_url)
1234+
self.assertNotContains(
1235+
resp,
1236+
reverse("organization-membership-request", args=[org.pk]) + "?membership_type=ruby",
1237+
)
1238+
1239+
with (
1240+
patch("core.backends.FreeIPAUser.get", return_value=bob),
1241+
patch("core.views_membership.block_action_without_coc", return_value=None),
1242+
):
1243+
tier_change_resp = self.client.get(tier_change_url)
1244+
1245+
self.assertEqual(tier_change_resp.status_code, 200)
1246+
self.assertEqual(tier_change_resp.context["form"].initial.get("membership_type"), "gold")
1247+
self.assertNotContains(resp, "Request membership")
1248+
1249+
def test_org_detail_change_tier_uses_previous_tier_when_current_is_highest(self) -> None:
1250+
from core.models import MembershipType, Organization
1251+
1252+
MembershipType.objects.update(enabled=False)
1253+
11791254
MembershipType.objects.update_or_create(
11801255
code="silver",
11811256
defaults={
@@ -1197,8 +1272,116 @@ def test_org_detail_shows_change_tier_button_for_multi_type_category(self) -> No
11971272
},
11981273
)
11991274

1200-
org = Organization.objects.create(name="Tiered Org", representative="bob")
1201-
Membership.objects.create(target_organization=org, membership_type_id="silver")
1275+
org = Organization.objects.create(name="Top Tier Org", representative="bob")
1276+
Membership.objects.create(target_organization=org, membership_type_id="gold")
1277+
1278+
bob = FreeIPAUser("bob", {"uid": ["bob"], "memberof_group": [], "c": ["US"]})
1279+
self._login_as_freeipa_user("bob")
1280+
1281+
with patch("core.backends.FreeIPAUser.get", return_value=bob):
1282+
resp = self.client.get(reverse("organization-detail", args=[org.pk]))
1283+
1284+
self.assertEqual(resp.status_code, 200)
1285+
self.assertContains(resp, "Change tier")
1286+
self.assertContains(
1287+
resp,
1288+
reverse("organization-membership-request", args=[org.pk]) + "?membership_type=silver",
1289+
)
1290+
1291+
def test_org_detail_change_tier_prefers_higher_ranked_tier_for_gold(self) -> None:
1292+
from core.models import MembershipType, Organization
1293+
1294+
MembershipType.objects.update(enabled=False)
1295+
1296+
MembershipType.objects.update_or_create(
1297+
code="platinum",
1298+
defaults={
1299+
"name": "Platinum Sponsor Member",
1300+
"category_id": "sponsorship",
1301+
"sort_order": 10,
1302+
"enabled": True,
1303+
"group_cn": "almalinux-platinum",
1304+
},
1305+
)
1306+
MembershipType.objects.update_or_create(
1307+
code="gold",
1308+
defaults={
1309+
"name": "Gold Sponsor Member",
1310+
"category_id": "sponsorship",
1311+
"sort_order": 20,
1312+
"enabled": True,
1313+
"group_cn": "almalinux-gold",
1314+
},
1315+
)
1316+
MembershipType.objects.update_or_create(
1317+
code="silver",
1318+
defaults={
1319+
"name": "Silver Sponsor Member",
1320+
"category_id": "sponsorship",
1321+
"sort_order": 30,
1322+
"enabled": True,
1323+
"group_cn": "almalinux-silver",
1324+
},
1325+
)
1326+
1327+
org = Organization.objects.create(name="Gold Tier Org", representative="bob")
1328+
Membership.objects.create(target_organization=org, membership_type_id="gold")
1329+
1330+
bob = FreeIPAUser("bob", {"uid": ["bob"], "memberof_group": [], "c": ["US"]})
1331+
self._login_as_freeipa_user("bob")
1332+
1333+
with patch("core.backends.FreeIPAUser.get", return_value=bob):
1334+
resp = self.client.get(reverse("organization-detail", args=[org.pk]))
1335+
1336+
self.assertEqual(resp.status_code, 200)
1337+
self.assertContains(resp, "Change tier")
1338+
self.assertContains(
1339+
resp,
1340+
reverse("organization-membership-request", args=[org.pk]) + "?membership_type=platinum",
1341+
)
1342+
self.assertNotContains(
1343+
resp,
1344+
reverse("organization-membership-request", args=[org.pk]) + "?membership_type=silver",
1345+
)
1346+
1347+
def test_org_detail_change_tier_for_ruby_suggests_silver(self) -> None:
1348+
from core.models import MembershipType, Organization
1349+
1350+
MembershipType.objects.update(enabled=False)
1351+
1352+
MembershipType.objects.update_or_create(
1353+
code="gold",
1354+
defaults={
1355+
"name": "Gold Sponsor Member",
1356+
"category_id": "sponsorship",
1357+
"sort_order": 20,
1358+
"enabled": True,
1359+
"group_cn": "almalinux-gold",
1360+
},
1361+
)
1362+
MembershipType.objects.update_or_create(
1363+
code="silver",
1364+
defaults={
1365+
"name": "Silver Sponsor Member",
1366+
"category_id": "sponsorship",
1367+
"sort_order": 30,
1368+
"enabled": True,
1369+
"group_cn": "almalinux-silver",
1370+
},
1371+
)
1372+
MembershipType.objects.update_or_create(
1373+
code="ruby",
1374+
defaults={
1375+
"name": "Ruby Sponsor Member",
1376+
"category_id": "sponsorship",
1377+
"sort_order": 40,
1378+
"enabled": True,
1379+
"group_cn": "almalinux-ruby",
1380+
},
1381+
)
1382+
1383+
org = Organization.objects.create(name="Ruby Tier Org", representative="bob")
1384+
Membership.objects.create(target_organization=org, membership_type_id="ruby")
12021385

12031386
bob = FreeIPAUser("bob", {"uid": ["bob"], "memberof_group": [], "c": ["US"]})
12041387
self._login_as_freeipa_user("bob")
@@ -1212,7 +1395,6 @@ def test_org_detail_shows_change_tier_button_for_multi_type_category(self) -> No
12121395
resp,
12131396
reverse("organization-membership-request", args=[org.pk]) + "?membership_type=silver",
12141397
)
1215-
self.assertNotContains(resp, "Request membership")
12161398

12171399
def test_org_detail_hides_request_membership_button_when_no_more_categories_available(self) -> None:
12181400
from core.models import MembershipType, Organization
@@ -1574,6 +1756,107 @@ def test_org_membership_request_requires_representative(self) -> None:
15741756
resp = self.client.get(reverse("organization-membership-request", args=[org.pk]))
15751757
self.assertEqual(resp.status_code, 200)
15761758

1759+
def test_org_membership_request_country_check_uses_committee_requester_profile(self) -> None:
1760+
from core.models import FreeIPAPermissionGrant, MembershipType, Organization
1761+
1762+
MembershipType.objects.update_or_create(
1763+
code="gold",
1764+
defaults={
1765+
"name": "Gold Sponsor Member",
1766+
"category_id": "sponsorship",
1767+
"sort_order": 2,
1768+
"enabled": True,
1769+
},
1770+
)
1771+
1772+
org = Organization.objects.create(name="Committee Country Check Org", representative="bob")
1773+
1774+
FreeIPAPermissionGrant.objects.create(
1775+
permission=ASTRA_ADD_MEMBERSHIP,
1776+
principal_type=FreeIPAPermissionGrant.PrincipalType.user,
1777+
principal_name="reviewer",
1778+
)
1779+
1780+
reviewer = FreeIPAUser("reviewer", {"uid": ["reviewer"], "memberof_group": [], "c": ["US"]})
1781+
representative = FreeIPAUser("bob", {"uid": ["bob"], "memberof_group": [], "c": ["DE"]})
1782+
1783+
def _get_user(username: str) -> FreeIPAUser | None:
1784+
if username == "reviewer":
1785+
return reviewer
1786+
if username == "bob":
1787+
return representative
1788+
return None
1789+
1790+
self._login_as_freeipa_user("reviewer")
1791+
1792+
with (
1793+
patch("core.backends.FreeIPAUser.get", side_effect=_get_user),
1794+
patch("core.views_membership.block_action_without_coc", return_value=None),
1795+
patch("core.views_membership.block_action_without_country_code", return_value=None) as country_mock,
1796+
):
1797+
resp = self.client.get(reverse("organization-membership-request", args=[org.pk]))
1798+
1799+
self.assertEqual(resp.status_code, 200)
1800+
self.assertEqual(country_mock.call_count, 1)
1801+
self.assertIs(country_mock.call_args.kwargs["user_data"], reviewer._user_data)
1802+
1803+
def test_org_membership_request_country_check_uses_representative_profile(self) -> None:
1804+
from core.models import MembershipType, Organization
1805+
1806+
MembershipType.objects.update_or_create(
1807+
code="gold",
1808+
defaults={
1809+
"name": "Gold Sponsor Member",
1810+
"category_id": "sponsorship",
1811+
"sort_order": 2,
1812+
"enabled": True,
1813+
},
1814+
)
1815+
1816+
org = Organization.objects.create(name="Representative Country Check Org", representative="bob")
1817+
1818+
representative = FreeIPAUser("bob", {"uid": ["bob"], "memberof_group": [], "c": ["DE"]})
1819+
self._login_as_freeipa_user("bob")
1820+
1821+
with (
1822+
patch("core.backends.FreeIPAUser.get", return_value=representative),
1823+
patch("core.views_membership.block_action_without_coc", return_value=None),
1824+
patch("core.views_membership.block_action_without_country_code", return_value=None) as country_mock,
1825+
):
1826+
resp = self.client.get(reverse("organization-membership-request", args=[org.pk]))
1827+
1828+
self.assertEqual(resp.status_code, 200)
1829+
self.assertEqual(country_mock.call_count, 1)
1830+
self.assertIs(country_mock.call_args.kwargs["user_data"], representative._user_data)
1831+
1832+
def test_user_membership_request_country_check_uses_requester_profile(self) -> None:
1833+
from core.models import MembershipType
1834+
1835+
MembershipType.objects.update_or_create(
1836+
code="individual",
1837+
defaults={
1838+
"name": "Individual",
1839+
"group_cn": "almalinux-individual",
1840+
"category_id": "individual",
1841+
"sort_order": 0,
1842+
"enabled": True,
1843+
},
1844+
)
1845+
1846+
alice = FreeIPAUser("alice", {"uid": ["alice"], "memberof_group": [], "c": ["US"]})
1847+
self._login_as_freeipa_user("alice")
1848+
1849+
with (
1850+
patch("core.backends.FreeIPAUser.get", return_value=alice),
1851+
patch("core.views_membership.block_action_without_coc", return_value=None),
1852+
patch("core.views_membership.block_action_without_country_code", return_value=None) as country_mock,
1853+
):
1854+
resp = self.client.get(reverse("membership-request"))
1855+
1856+
self.assertEqual(resp.status_code, 200)
1857+
self.assertEqual(country_mock.call_count, 1)
1858+
self.assertIs(country_mock.call_args.kwargs["user_data"], alice._user_data)
1859+
15771860
def test_org_detail_renewal_cta_uses_canonical_membership_request_link(self) -> None:
15781861
from core.models import Membership, MembershipType, Organization
15791862

astra_app/core/views_membership.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,9 +573,17 @@ def membership_request(request: HttpRequest, organization_id: int | None = None)
573573
return blocked
574574

575575
representative_user_data = representative_user._user_data if representative_user is not None else None
576+
requester_user_data = fu._user_data
577+
user_data_for_country_check = requester_user_data
578+
if is_org_request:
579+
is_requester_representative = organization is not None and username == organization.representative
580+
# Committee users can request on behalf of an organization; in that case,
581+
# validate the actor's own country code because they are initiating the action.
582+
user_data_for_country_check = representative_user_data if is_requester_representative else requester_user_data
583+
576584
blocked = block_action_without_country_code(
577585
request,
578-
user_data=representative_user_data,
586+
user_data=user_data_for_country_check,
579587
action_label=action_label,
580588
)
581589
if blocked is not None:

0 commit comments

Comments
 (0)