diff --git a/backend/clubs/management/commands/sync.py b/backend/clubs/management/commands/sync.py index f2dae3ce7..57f27b306 100644 --- a/backend/clubs/management/commands/sync.py +++ b/backend/clubs/management/commands/sync.py @@ -12,6 +12,7 @@ class Command(BaseCommand): "There should be no issues with repeatedly running this script. " ) web_execute = True + dry_run = False # Initialize for usage as standalone utility def add_arguments(self, parser): parser.add_argument( @@ -95,43 +96,61 @@ def sync_club_fairs(self): else: self.stdout.write(f"Would have deleted {len(dups)} duplicate entries!") - def sync_badges(self): + def sync_badge(self, badge: Badge): """ - Synchronizes badges based on parent child relationships. - Tends to favor adding objects to fix relationships instead of removing them. + Synchronizes a badge with its parent child relationships. + If a club has one of them (badge, parent), add the other. """ - # add badges to parent child relationships - count = 0 - for badge in Badge.objects.all(): - if badge.org is not None: - self._visited = set() - count += self.recursively_add_badge(badge.org, badge) - self.stdout.write( - self.style.SUCCESS(f"Modified {count} club badge relationships.") - ) + if badge.org is None: + return 0, 0 + + # add badge to parent child relationships + badge_add_count = 0 + parent_child_add_count = 0 + self._visited = set() + badge_add_count += self.recursively_add_badge(badge.org, badge) # if badge exist on child, link it to the parent directly # unless it is already indirectly linked - count = 0 + for club in badge.club_set.all(): + if club.pk == badge.org.pk: + continue + + parent_club_codes = self.get_parent_club_codes(club) + if badge.org.code not in parent_club_codes: + if not self.dry_run: + self.stdout.write( + f"Adding {badge.org.name} as parent for {club.name}." + ) + club.parent_orgs.add(badge.org) + else: + self.stdout.write( + f"Would have added {badge.org.name} " + f"as a parent for {club.name}." + ) + parent_child_add_count += 1 + return badge_add_count, parent_child_add_count + + def sync_badges(self): + """ + Synchronizes badges based on parent child relationships. + Uses parent child relationships as source of truth. + """ + # remove badges with orgs from all clubs + for club in Club.objects.all(): + club.badges = club.badges.filter(org__isnull=False) + club.save() + + badge_add_count = 0 + parent_child_add_count = 0 for badge in Badge.objects.all(): - if badge.org is not None: - for club in badge.club_set.all(): - if club.pk == badge.org.pk: - continue - - parent_club_codes = self.get_parent_club_codes(club) - if badge.org.code not in parent_club_codes: - if not self.dry_run: - self.stdout.write( - f"Adding {badge.org.name} as parent for {club.name}." - ) - club.parent_orgs.add(badge.org) - else: - self.stdout.write( - f"Would have added {badge.org.name} " - f"as a parent for {club.name}." - ) - count += 1 + b_count, pc_count = self.sync_badge(badge) + badge_add_count += b_count + parent_child_add_count += pc_count + self.stdout.write( - self.style.SUCCESS(f"Modified {count} parent child relationships.") + self.style.SUCCESS( + f"Modified {badge_add_count} club badge relationships." + f"Modified {parent_child_add_count} parent child relationships." + ) ) diff --git a/backend/clubs/models.py b/backend/clubs/models.py index c8c2dcc61..b41e2b0d5 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1489,7 +1489,11 @@ class Badge(models.Model): # The organization that this badge represents (If this is the "SAC Funded" badge, # then this would link to SAC) - org = models.ForeignKey(Club, on_delete=models.CASCADE, blank=True, null=True) + # TODO: Editability can be loosened if correspondance betweent parent_orgs, badges + # of clubs with the badge is maintained when modifying this field + org = models.ForeignKey( + Club, on_delete=models.CASCADE, blank=True, null=True, editable=False + ) # The fair that this badge is related to fair = models.ForeignKey(ClubFair, on_delete=models.CASCADE, blank=True, null=True) diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 7b91ec941..5d0faae12 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -80,6 +80,7 @@ from tatsu.exceptions import FailedParse from clubs.filters import RandomOrderingFilter, RandomPageNumberPagination +from clubs.management.commands.sync import Command as SyncCommand from clubs.mixins import XLSXFormatterMixin from clubs.models import ( AdminNote, @@ -7539,12 +7540,15 @@ def create(self, request, *args, **kwargs): badge = get_object_or_404(Badge, pk=self.kwargs["badge_pk"]) club = get_object_or_404(Club, code=request.data["club"]) club.badges.add(badge) + SyncCommand().sync_badge(badge) return Response({"success": True}) def destroy(self, request, *args, **kwargs): club = self.get_object() badge = get_object_or_404(Badge, pk=self.kwargs["badge_pk"]) club.badges.remove(badge) + club.parent_orgs.remove(badge.org) + SyncCommand().sync_badge(badge) return Response({"success": True}) def get_queryset(self): diff --git a/backend/tests/clubs/test_views.py b/backend/tests/clubs/test_views.py index 47ec6c58d..c7cf9d2c8 100644 --- a/backend/tests/clubs/test_views.py +++ b/backend/tests/clubs/test_views.py @@ -1617,8 +1617,20 @@ def test_club_modify_child(self): self.assertIn(resp.status_code, [400, 403], resp.content) # assert that we can modify a child club - child_club.badges.add(self.wc_badge) - call_command("sync") + self.client.login(username=self.user5.username, password="test") + resp = self.client.post( + reverse("badge-clubs-list", args=(self.wc_badge.id,)), + {"club": child_club.code}, + content_type="application/json", + ) + self.assertIn(resp.status_code, [200, 201], resp.content) + child_club.refresh_from_db() + self.assertEqual(child_club.badges.count(), 1) + self.assertEqual(child_club.badges.all()[0], self.wc_badge) + self.assertEqual(child_club.parent_orgs.count(), 1) + self.assertEqual(child_club.parent_orgs.all()[0], self.wc) + + self.client.login(username=self.user4.username, password="test") resp = self.client.patch( reverse("clubs-detail", args=(child_club.code,)), {"description": "We hate Wharton"},