From 7d33b9a7d356315f27b805d785a72ec982d29516 Mon Sep 17 00:00:00 2001 From: kart-u Date: Tue, 18 Nov 2025 22:54:03 +0000 Subject: [PATCH 01/10] 1st pr ensure backward compatibility and added field to MemberProfile --- .../commands/github_update_users.py | 22 ++++++++++++ backend/apps/owasp/admin/member_profile.py | 3 ++ ...berprofile_contributions_count_and_more.py | 28 +++++++++++++++ ...67_memberprofile_backward_compatibility.py | 24 +++++++++++++ backend/apps/owasp/models/member_profile.py | 19 ++++++++++- .../commands/github_update_users_test.py | 34 +++++++++++++++---- 6 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 backend/apps/owasp/migrations/0066_memberprofile_contributions_count_and_more.py create mode 100644 backend/apps/owasp/migrations/0067_memberprofile_backward_compatibility.py diff --git a/backend/apps/github/management/commands/github_update_users.py b/backend/apps/github/management/commands/github_update_users.py index 42a6e51e4d..8345e921ef 100644 --- a/backend/apps/github/management/commands/github_update_users.py +++ b/backend/apps/github/management/commands/github_update_users.py @@ -8,6 +8,7 @@ from apps.common.models import BATCH_SIZE from apps.github.models.repository_contributor import RepositoryContributor from apps.github.models.user import User +from apps.owasp.models.member_profile import MemberProfile logger = logging.getLogger(__name__) @@ -46,6 +47,7 @@ def handle(self, *args, **options): .annotate(total_contributions=Sum("contributions_count")) } users = [] + profiles = [] for idx, user in enumerate(active_users[offset:]): prefix = f"{idx + offset + 1} of {active_users_count - offset}" print(f"{prefix:<10} {user.title}") @@ -53,7 +55,27 @@ def handle(self, *args, **options): user.contributions_count = user_contributions.get(user.id, 0) users.append(user) + profile, created = MemberProfile.objects.get_or_create(github_user_id=user.id) + if created: + profile.github_user = user + profile.contributions_count = user.contributions_count + profile.is_owasp_staff = user.is_owasp_staff + profile.has_public_member_page = user.has_public_member_page + profiles.append(profile) + if not len(users) % BATCH_SIZE: User.bulk_save(users, fields=("contributions_count",)) + MemberProfile.bulk_save( + profiles, + fields=( + "contributions_count", + "is_owasp_staff", + "has_public_member_page", + ), + ) User.bulk_save(users, fields=("contributions_count",)) + MemberProfile.bulk_save( + profiles, + fields=("contributions_count", "is_owasp_staff", "has_public_member_page"), + ) diff --git a/backend/apps/owasp/admin/member_profile.py b/backend/apps/owasp/admin/member_profile.py index 9e5ec7ddaa..c756f16b2f 100644 --- a/backend/apps/owasp/admin/member_profile.py +++ b/backend/apps/owasp/admin/member_profile.py @@ -11,6 +11,9 @@ class MemberProfileAdmin(admin.ModelAdmin): autocomplete_fields = ("github_user",) list_display = ( "github_user", + "is_owasp_staff", + "has_public_member_page", + "contributions_count", "owasp_slack_id", "first_contribution_at", "is_owasp_board_member", diff --git a/backend/apps/owasp/migrations/0066_memberprofile_contributions_count_and_more.py b/backend/apps/owasp/migrations/0066_memberprofile_contributions_count_and_more.py new file mode 100644 index 0000000000..7b592eb760 --- /dev/null +++ b/backend/apps/owasp/migrations/0066_memberprofile_contributions_count_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2025-11-18 17:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('owasp', '0065_memberprofile_linkedin_page_id'), + ] + + operations = [ + migrations.AddField( + model_name='memberprofile', + name='contributions_count', + field=models.PositiveIntegerField(default=0, verbose_name='Contributions count'), + ), + migrations.AddField( + model_name='memberprofile', + name='has_public_member_page', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='memberprofile', + name='is_owasp_staff', + field=models.BooleanField(default=False, help_text='Indicates if the user is OWASP Foundation staff.', verbose_name='Is OWASP Staff'), + ), + ] diff --git a/backend/apps/owasp/migrations/0067_memberprofile_backward_compatibility.py b/backend/apps/owasp/migrations/0067_memberprofile_backward_compatibility.py new file mode 100644 index 0000000000..a6bf6d04ca --- /dev/null +++ b/backend/apps/owasp/migrations/0067_memberprofile_backward_compatibility.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2025-11-18 18:04 + +from django.db import migrations + +def copy_user_data_to_member_profile(apps, schema_editor): + User = apps.get_model('github', 'User') + MemberProfile = apps.get_model('owasp', 'MemberProfile') + for user in User.objects.all(): + profile, _ = MemberProfile.objects.get_or_create(github_user=user) + profile.has_public_member_page = user.has_public_member_page + profile.is_owasp_staff = user.is_owasp_staff + profile.contributions_count = user.contributions_count + profile.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('owasp', '0066_memberprofile_contributions_count_and_more'), + ('github', '0039_remove_commit_commit_repo_created_idx'), + ] + + operations = [ + migrations.RunPython(copy_user_data_to_member_profile, migrations.RunPython.noop), + ] diff --git a/backend/apps/owasp/models/member_profile.py b/backend/apps/owasp/models/member_profile.py index ba52ed5a1d..df9b732cdc 100644 --- a/backend/apps/owasp/models/member_profile.py +++ b/backend/apps/owasp/models/member_profile.py @@ -5,7 +5,7 @@ from django.core.validators import RegexValidator from django.db import models -from apps.common.models import TimestampedModel +from apps.common.models import BulkSaveModel, TimestampedModel from apps.github.models.user import User @@ -71,6 +71,23 @@ class Meta: help_text="LinkedIn username or custom URL ID (e.g., 'john-doe-123')", ) + has_public_member_page = models.BooleanField(default=True) + is_owasp_staff = models.BooleanField( + default=False, + verbose_name="Is OWASP Staff", + help_text="Indicates if the user is OWASP Foundation staff.", + ) + contributions_count = models.PositiveIntegerField( + verbose_name="Contributions count", default=0 + ) + + def __str__(self) -> str: """Return human-readable representation.""" return f"OWASP member profile for {self.github_user.login}" + + @staticmethod + def bulk_save(profiles, fields=None) -> None: + """Bulk save member profiles.""" + BulkSaveModel.bulk_save(MemberProfile, profiles, fields=fields) + diff --git a/backend/tests/apps/github/management/commands/github_update_users_test.py b/backend/tests/apps/github/management/commands/github_update_users_test.py index 2f4f7e6080..4992f184ad 100644 --- a/backend/tests/apps/github/management/commands/github_update_users_test.py +++ b/backend/tests/apps/github/management/commands/github_update_users_test.py @@ -30,11 +30,15 @@ def test_add_arguments(self): "--offset", default=0, required=False, type=int ) + @patch("apps.github.management.commands.github_update_users.MemberProfile") @patch("apps.github.management.commands.github_update_users.User") @patch("apps.github.management.commands.github_update_users.RepositoryContributor") @patch("apps.github.management.commands.github_update_users.BATCH_SIZE", 2) - def test_handle_with_default_offset(self, mock_repository_contributor, mock_user): + def test_handle_with_default_offset( + self, mock_repository_contributor, mock_user, mock_member_profile + ): """Test command execution with default offset.""" + mock_member_profile.objects.get_or_create.return_value = (MagicMock(), False) mock_user1 = MagicMock(id=1, title="User 1", contributions_count=0) mock_user2 = MagicMock(id=2, title="User 2", contributions_count=0) mock_user3 = MagicMock(id=3, title="User 3", contributions_count=0) @@ -76,11 +80,15 @@ def test_handle_with_default_offset(self, mock_repository_contributor, mock_user assert mock_user.bulk_save.call_count == 2 assert mock_user.bulk_save.call_args_list[-1][0][0] == [mock_user1, mock_user2, mock_user3] + @patch("apps.github.management.commands.github_update_users.MemberProfile") @patch("apps.github.management.commands.github_update_users.User") @patch("apps.github.management.commands.github_update_users.RepositoryContributor") @patch("apps.github.management.commands.github_update_users.BATCH_SIZE", 2) - def test_handle_with_custom_offset(self, mock_repository_contributor, mock_user): + def test_handle_with_custom_offset( + self, mock_repository_contributor, mock_user, mock_member_profile + ): """Test command execution with custom offset.""" + mock_member_profile.objects.get_or_create.return_value = (MagicMock(), False) mock_user1 = MagicMock(id=2, title="User 2", contributions_count=0) mock_user2 = MagicMock(id=3, title="User 3", contributions_count=0) @@ -115,13 +123,15 @@ def test_handle_with_custom_offset(self, mock_repository_contributor, mock_user) assert mock_user.bulk_save.call_count == 2 assert mock_user.bulk_save.call_args_list[-1][0][0] == [mock_user1, mock_user2] + @patch("apps.github.management.commands.github_update_users.MemberProfile") @patch("apps.github.management.commands.github_update_users.User") @patch("apps.github.management.commands.github_update_users.RepositoryContributor") @patch("apps.github.management.commands.github_update_users.BATCH_SIZE", 3) def test_handle_with_users_having_no_contributions( - self, mock_repository_contributor, mock_user + self, mock_repository_contributor, mock_user, mock_member_profile ): """Test command execution when users have no contributions.""" + mock_member_profile.objects.get_or_create.return_value = (MagicMock(), False) mock_user1 = MagicMock(id=1, title="User 1", contributions_count=0) mock_user2 = MagicMock(id=2, title="User 2", contributions_count=0) @@ -149,11 +159,15 @@ def test_handle_with_users_having_no_contributions( assert mock_user.bulk_save.call_count == 1 assert mock_user.bulk_save.call_args_list[-1][0][0] == [mock_user1, mock_user2] + @patch("apps.github.management.commands.github_update_users.MemberProfile") @patch("apps.github.management.commands.github_update_users.User") @patch("apps.github.management.commands.github_update_users.RepositoryContributor") @patch("apps.github.management.commands.github_update_users.BATCH_SIZE", 1) - def test_handle_with_single_user(self, mock_repository_contributor, mock_user): + def test_handle_with_single_user( + self, mock_repository_contributor, mock_user, mock_member_profile + ): """Test command execution with single user.""" + mock_member_profile.objects.get_or_create.return_value = (MagicMock(), False) mock_user1 = MagicMock(id=1, title="User 1", contributions_count=0) mock_users_queryset = MagicMock() @@ -179,11 +193,15 @@ def test_handle_with_single_user(self, mock_repository_contributor, mock_user): assert mock_user.bulk_save.call_count == 2 assert mock_user.bulk_save.call_args_list[-1][0][0] == [mock_user1] + @patch("apps.github.management.commands.github_update_users.MemberProfile") @patch("apps.github.management.commands.github_update_users.User") @patch("apps.github.management.commands.github_update_users.RepositoryContributor") @patch("apps.github.management.commands.github_update_users.BATCH_SIZE", 2) - def test_handle_with_empty_user_list(self, mock_repository_contributor, mock_user): + def test_handle_with_empty_user_list( + self, mock_repository_contributor, mock_user, mock_member_profile + ): """Test command execution with no users.""" + mock_member_profile.objects.get_or_create.return_value = (MagicMock(), False) mock_users_queryset = MagicMock() mock_users_queryset.count.return_value = 0 mock_users_queryset.__getitem__.return_value = [] @@ -203,11 +221,15 @@ def test_handle_with_empty_user_list(self, mock_repository_contributor, mock_use assert mock_user.bulk_save.call_count == 1 assert mock_user.bulk_save.call_args_list[-1][0][0] == [] + @patch("apps.github.management.commands.github_update_users.MemberProfile") @patch("apps.github.management.commands.github_update_users.User") @patch("apps.github.management.commands.github_update_users.RepositoryContributor") @patch("apps.github.management.commands.github_update_users.BATCH_SIZE", 2) - def test_handle_with_exact_batch_size(self, mock_repository_contributor, mock_user): + def test_handle_with_exact_batch_size( + self, mock_repository_contributor, mock_user, mock_member_profile + ): """Test command execution when user count equals batch size.""" + mock_member_profile.objects.get_or_create.return_value = (MagicMock(), False) mock_user1 = MagicMock(id=1, title="User 1", contributions_count=0) mock_user2 = MagicMock(id=2, title="User 2", contributions_count=0) From e78b731237dfc9c174afcce6c43595a88dd8a9fe Mon Sep 17 00:00:00 2001 From: kart-u Date: Wed, 19 Nov 2025 19:25:06 +0000 Subject: [PATCH 02/10] corrected lint/format and recommendation --- backend/apps/owasp/admin/member_profile.py | 9 ++++- ...berprofile_contributions_count_and_more.py | 23 ++++++----- ...67_memberprofile_backward_compatibility.py | 37 +++++++++++++----- backend/apps/owasp/models/member_profile.py | 2 - .../commands/github_update_users_test.py | 38 +++++++++++++++++++ .../apps/owasp/admin/member_profile_test.py | 3 ++ 6 files changed, 90 insertions(+), 22 deletions(-) diff --git a/backend/apps/owasp/admin/member_profile.py b/backend/apps/owasp/admin/member_profile.py index c756f16b2f..8851a4a9a5 100644 --- a/backend/apps/owasp/admin/member_profile.py +++ b/backend/apps/owasp/admin/member_profile.py @@ -50,7 +50,14 @@ class MemberProfileAdmin(admin.ModelAdmin): ), ( "Contribution Information", - {"fields": ("first_contribution_at",)}, + { + "fields": ( + "first_contribution_at", + "is_owasp_staff", + "has_public_member_page", + "contributions_count", + ) + }, ), ( "Membership Flags", diff --git a/backend/apps/owasp/migrations/0066_memberprofile_contributions_count_and_more.py b/backend/apps/owasp/migrations/0066_memberprofile_contributions_count_and_more.py index 7b592eb760..a03d95c3d5 100644 --- a/backend/apps/owasp/migrations/0066_memberprofile_contributions_count_and_more.py +++ b/backend/apps/owasp/migrations/0066_memberprofile_contributions_count_and_more.py @@ -4,25 +4,28 @@ class Migration(migrations.Migration): - dependencies = [ - ('owasp', '0065_memberprofile_linkedin_page_id'), + ("owasp", "0065_memberprofile_linkedin_page_id"), ] operations = [ migrations.AddField( - model_name='memberprofile', - name='contributions_count', - field=models.PositiveIntegerField(default=0, verbose_name='Contributions count'), + model_name="memberprofile", + name="contributions_count", + field=models.PositiveIntegerField(default=0, verbose_name="Contributions count"), ), migrations.AddField( - model_name='memberprofile', - name='has_public_member_page', + model_name="memberprofile", + name="has_public_member_page", field=models.BooleanField(default=True), ), migrations.AddField( - model_name='memberprofile', - name='is_owasp_staff', - field=models.BooleanField(default=False, help_text='Indicates if the user is OWASP Foundation staff.', verbose_name='Is OWASP Staff'), + model_name="memberprofile", + name="is_owasp_staff", + field=models.BooleanField( + default=False, + help_text="Indicates if the user is OWASP Foundation staff.", + verbose_name="Is OWASP Staff", + ), ), ] diff --git a/backend/apps/owasp/migrations/0067_memberprofile_backward_compatibility.py b/backend/apps/owasp/migrations/0067_memberprofile_backward_compatibility.py index a6bf6d04ca..9fd8eb1275 100644 --- a/backend/apps/owasp/migrations/0067_memberprofile_backward_compatibility.py +++ b/backend/apps/owasp/migrations/0067_memberprofile_backward_compatibility.py @@ -2,23 +2,42 @@ from django.db import migrations -def copy_user_data_to_member_profile(apps, schema_editor): - User = apps.get_model('github', 'User') - MemberProfile = apps.get_model('owasp', 'MemberProfile') - for user in User.objects.all(): + +def copy_user_data_to_member_profile(apps, _schema_editor): + """Copy user data to member profile.""" + User = apps.get_model("github", "User") + MemberProfile = apps.get_model("owasp", "MemberProfile") + profiles_to_update = [] + batch_size = 500 + update_fields = [ + "has_public_member_page", + "is_owasp_staff", + "contributions_count", + ] + + for user in User.objects.all().iterator(chunk_size=batch_size): profile, _ = MemberProfile.objects.get_or_create(github_user=user) profile.has_public_member_page = user.has_public_member_page profile.is_owasp_staff = user.is_owasp_staff profile.contributions_count = user.contributions_count - profile.save() + profiles_to_update.append(profile) -class Migration(migrations.Migration): + if len(profiles_to_update) >= batch_size: + MemberProfile.objects.bulk_update( + profiles_to_update, update_fields, batch_size=batch_size + ) + profiles_to_update = [] + if profiles_to_update: + MemberProfile.objects.bulk_update(profiles_to_update, update_fields, batch_size=batch_size) + + +class Migration(migrations.Migration): dependencies = [ - ('owasp', '0066_memberprofile_contributions_count_and_more'), - ('github', '0039_remove_commit_commit_repo_created_idx'), + ("owasp", "0066_memberprofile_contributions_count_and_more"), + ("github", "0039_remove_commit_commit_repo_created_idx"), ] operations = [ - migrations.RunPython(copy_user_data_to_member_profile, migrations.RunPython.noop), + migrations.RunPython(copy_user_data_to_member_profile, migrations.RunPython.noop), ] diff --git a/backend/apps/owasp/models/member_profile.py b/backend/apps/owasp/models/member_profile.py index df9b732cdc..7fdb626300 100644 --- a/backend/apps/owasp/models/member_profile.py +++ b/backend/apps/owasp/models/member_profile.py @@ -80,7 +80,6 @@ class Meta: contributions_count = models.PositiveIntegerField( verbose_name="Contributions count", default=0 ) - def __str__(self) -> str: """Return human-readable representation.""" @@ -90,4 +89,3 @@ def __str__(self) -> str: def bulk_save(profiles, fields=None) -> None: """Bulk save member profiles.""" BulkSaveModel.bulk_save(MemberProfile, profiles, fields=fields) - diff --git a/backend/tests/apps/github/management/commands/github_update_users_test.py b/backend/tests/apps/github/management/commands/github_update_users_test.py index 4992f184ad..bfb48353a1 100644 --- a/backend/tests/apps/github/management/commands/github_update_users_test.py +++ b/backend/tests/apps/github/management/commands/github_update_users_test.py @@ -259,3 +259,41 @@ def test_handle_with_exact_batch_size( assert mock_user.bulk_save.call_count == 2 assert mock_user.bulk_save.call_args_list[-1][0][0] == [mock_user1, mock_user2] + + @patch("apps.github.management.commands.github_update_users.MemberProfile") + @patch("apps.github.management.commands.github_update_users.User") + @patch("apps.github.management.commands.github_update_users.RepositoryContributor") + @patch("apps.github.management.commands.github_update_users.BATCH_SIZE", 2) + def test_handle_member_profile_created( + self, mock_repository_contributor, mock_user, mock_member_profile + ): + """Test command execution when MemberProfile is newly created.""" + mock_profile = MagicMock() + mock_member_profile.objects.get_or_create.return_value = (mock_profile, True) + + mock_user1 = MagicMock( + id=1, + contributions_count=0, + is_owasp_staff=True, + has_public_member_page=False, + ) + + mock_users_queryset = MagicMock() + mock_users_queryset.count.return_value = 1 + mock_users_queryset.__getitem__.return_value = [mock_user1] + mock_user.objects.order_by.return_value = mock_users_queryset + + mock_rc_objects = MagicMock() + mock_rc_objects.exclude.return_value.values.return_value.annotate.return_value = [ + {"user_id": 1, "total_contributions": 5} + ] + mock_repository_contributor.objects = mock_rc_objects + + command = Command() + command.handle(offset=0) + + assert mock_profile.github_user == mock_user1 + assert mock_profile.contributions_count == 5 + assert mock_profile.is_owasp_staff is True + assert mock_profile.has_public_member_page is False + mock_member_profile.bulk_save.assert_called_once() diff --git a/backend/tests/apps/owasp/admin/member_profile_test.py b/backend/tests/apps/owasp/admin/member_profile_test.py index f5a887a39d..8d61c87ed2 100644 --- a/backend/tests/apps/owasp/admin/member_profile_test.py +++ b/backend/tests/apps/owasp/admin/member_profile_test.py @@ -12,6 +12,9 @@ def test_list_display(self): expected_fields = ( "github_user", + "is_owasp_staff", + "has_public_member_page", + "contributions_count", "owasp_slack_id", "first_contribution_at", "is_owasp_board_member", From e6fb0e27e5e188f7b2aaaaab81a6de5542d89352 Mon Sep 17 00:00:00 2001 From: kart-u Date: Wed, 19 Nov 2025 21:11:52 +0000 Subject: [PATCH 03/10] corrected based on further recommendation --- backend/apps/owasp/models/member_profile.py | 6 +++++- .../github/management/commands/github_update_users_test.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/apps/owasp/models/member_profile.py b/backend/apps/owasp/models/member_profile.py index 7fdb626300..546e565d2c 100644 --- a/backend/apps/owasp/models/member_profile.py +++ b/backend/apps/owasp/models/member_profile.py @@ -71,7 +71,11 @@ class Meta: help_text="LinkedIn username or custom URL ID (e.g., 'john-doe-123')", ) - has_public_member_page = models.BooleanField(default=True) + has_public_member_page = models.BooleanField( + default=True, + verbose_name="Has Public Member Page", + help_text="Whether the member's profile is publicly visible on the OWASP website", + ) is_owasp_staff = models.BooleanField( default=False, verbose_name="Is OWASP Staff", diff --git a/backend/tests/apps/github/management/commands/github_update_users_test.py b/backend/tests/apps/github/management/commands/github_update_users_test.py index bfb48353a1..e27b8d71fe 100644 --- a/backend/tests/apps/github/management/commands/github_update_users_test.py +++ b/backend/tests/apps/github/management/commands/github_update_users_test.py @@ -220,6 +220,8 @@ def test_handle_with_empty_user_list( assert mock_user.bulk_save.call_count == 1 assert mock_user.bulk_save.call_args_list[-1][0][0] == [] + assert mock_member_profile.bulk_save.call_count == 1 + assert mock_member_profile.bulk_save.call_args_list[-1][0][0] == [] @patch("apps.github.management.commands.github_update_users.MemberProfile") @patch("apps.github.management.commands.github_update_users.User") From c06428451f123a9dd842cbd7918c9c2a9de84286 Mon Sep 17 00:00:00 2001 From: kart-u Date: Wed, 26 Nov 2025 17:33:02 +0000 Subject: [PATCH 04/10] contribution_count done changed backend/apps/github/api/internal/nodes/user.py changed backend/apps/github/api/internal/queries/user.py changed backend/apps/github/management/commands/github_update_users.py changed backend/apps/github/models/mixins/user.py --- .../apps/github/api/internal/nodes/user.py | 8 ++++++- .../apps/github/api/internal/queries/user.py | 6 ++++- .../commands/github_update_users.py | 24 ++++--------------- backend/apps/github/models/mixins/user.py | 2 +- 4 files changed, 18 insertions(+), 22 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index c305d7764e..b5374edbcd 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -13,7 +13,6 @@ "avatar_url", "bio", "company", - "contributions_count", "email", "followers_count", "following_count", @@ -28,6 +27,13 @@ class UserNode: """GitHub user node.""" + @strawberry.field + def contributions_count(self) -> int: + """Resolve contributions count.""" + if hasattr(self, "owasp_profile"): + return self.owasp_profile.contributions_count + return 0 + @strawberry.field def badge_count(self) -> int: """Resolve badge count.""" diff --git a/backend/apps/github/api/internal/queries/user.py b/backend/apps/github/api/internal/queries/user.py index 199c33aa90..1297428a8d 100644 --- a/backend/apps/github/api/internal/queries/user.py +++ b/backend/apps/github/api/internal/queries/user.py @@ -50,4 +50,8 @@ def user( User or None: The user object if found, otherwise None. """ - return User.objects.filter(has_public_member_page=True, login=login).first() + return ( + User.objects.select_related("owasp_profile") + .filter(owasp_profile__has_public_member_page=True, login=login) + .first() + ) diff --git a/backend/apps/github/management/commands/github_update_users.py b/backend/apps/github/management/commands/github_update_users.py index 8345e921ef..3b258f237b 100644 --- a/backend/apps/github/management/commands/github_update_users.py +++ b/backend/apps/github/management/commands/github_update_users.py @@ -46,36 +46,22 @@ def handle(self, *args, **options): .values("user_id") .annotate(total_contributions=Sum("contributions_count")) } - users = [] profiles = [] for idx, user in enumerate(active_users[offset:]): prefix = f"{idx + offset + 1} of {active_users_count - offset}" print(f"{prefix:<10} {user.title}") - user.contributions_count = user_contributions.get(user.id, 0) - users.append(user) - - profile, created = MemberProfile.objects.get_or_create(github_user_id=user.id) - if created: - profile.github_user = user - profile.contributions_count = user.contributions_count - profile.is_owasp_staff = user.is_owasp_staff - profile.has_public_member_page = user.has_public_member_page + profile, _ = MemberProfile.objects.get_or_create(github_user=user) + profile.contributions_count = user_contributions.get(user.id, 0) profiles.append(profile) - if not len(users) % BATCH_SIZE: - User.bulk_save(users, fields=("contributions_count",)) + if not len(profiles) % BATCH_SIZE: MemberProfile.bulk_save( profiles, - fields=( - "contributions_count", - "is_owasp_staff", - "has_public_member_page", - ), + fields=("contributions_count",), ) - User.bulk_save(users, fields=("contributions_count",)) MemberProfile.bulk_save( profiles, - fields=("contributions_count", "is_owasp_staff", "has_public_member_page"), + fields=("contributions_count",), ) diff --git a/backend/apps/github/models/mixins/user.py b/backend/apps/github/models/mixins/user.py index bdbb2c2f19..3ac4cef9d4 100644 --- a/backend/apps/github/models/mixins/user.py +++ b/backend/apps/github/models/mixins/user.py @@ -124,7 +124,7 @@ def idx_contributions(self): @property def idx_contributions_count(self) -> int: """Return contributions count for indexing.""" - return self.contributions_count + return self.owasp_profile.contributions_count @property def idx_issues(self) -> list[dict]: From 607f50f0970d04f50a2dddfd5a02f2a02518aece Mon Sep 17 00:00:00 2001 From: kart-u Date: Wed, 26 Nov 2025 18:10:47 +0000 Subject: [PATCH 05/10] has_public_member_page , is_owasp_staff migrated logic --- backend/apps/github/api/internal/nodes/user.py | 8 +++++++- backend/apps/nest/api/internal/nodes/user.py | 4 +++- .../apps/nest/management/commands/nest_update_badges.py | 4 ++-- .../api/internal/permissions/project_health_metrics.py | 9 ++++++--- backend/apps/owasp/api/internal/views/permissions.py | 9 ++++++++- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index b5374edbcd..2ec3a71e94 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -17,7 +17,6 @@ "followers_count", "following_count", "id", - "is_owasp_staff", "location", "login", "name", @@ -89,6 +88,13 @@ def is_gsoc_mentor(self) -> bool: return self.owasp_profile.is_gsoc_mentor return False + @strawberry.field + def is_owasp_staff(self) -> bool: + """Resolve if the user is an OWASP staff member.""" + if hasattr(self, "owasp_profile"): + return self.owasp_profile.is_owasp_staff + return False + @strawberry.field def issues_count(self) -> int: """Resolve issues count.""" diff --git a/backend/apps/nest/api/internal/nodes/user.py b/backend/apps/nest/api/internal/nodes/user.py index ab56a4f5c6..1bef655b03 100644 --- a/backend/apps/nest/api/internal/nodes/user.py +++ b/backend/apps/nest/api/internal/nodes/user.py @@ -13,4 +13,6 @@ class AuthUserNode(strawberry.relay.Node): @strawberry_django.field def is_owasp_staff(self) -> bool: """Check if the user is an OWASP staff member.""" - return self.github_user.is_owasp_staff if self.github_user else False + if self.github_user and hasattr(self.github_user, "owasp_profile"): + return self.github_user.owasp_profile.is_owasp_staff + return False diff --git a/backend/apps/nest/management/commands/nest_update_badges.py b/backend/apps/nest/management/commands/nest_update_badges.py index 209313134c..d7ae14cc10 100644 --- a/backend/apps/nest/management/commands/nest_update_badges.py +++ b/backend/apps/nest/management/commands/nest_update_badges.py @@ -42,7 +42,7 @@ def update_owasp_staff_badge(self): # Assign badge to employees who don't have it. employees_without_badge = User.objects.filter( - is_owasp_staff=True, + owasp_profile__is_owasp_staff=True, ).exclude( user_badges__badge=badge, ) @@ -60,7 +60,7 @@ def update_owasp_staff_badge(self): # Remove badge from non-OWASP employees. non_employees = User.objects.filter( - is_owasp_staff=False, + owasp_profile__is_owasp_staff=False, user_badges__badge=badge, ).distinct() removed_count = non_employees.count() diff --git a/backend/apps/owasp/api/internal/permissions/project_health_metrics.py b/backend/apps/owasp/api/internal/permissions/project_health_metrics.py index aa75f689ec..7de85652a0 100644 --- a/backend/apps/owasp/api/internal/permissions/project_health_metrics.py +++ b/backend/apps/owasp/api/internal/permissions/project_health_metrics.py @@ -10,8 +10,11 @@ class HasDashboardAccess(BasePermission): def has_permission(self, source, info, **kwargs) -> bool: """Check if the user has dashboard access.""" + user = info.context.request.user + if not (user and user.is_authenticated and user.github_user): + return False + return ( - (user := info.context.request.user) - and user.is_authenticated - and user.github_user.is_owasp_staff + hasattr(user.github_user, "owasp_profile") + and user.github_user.owasp_profile.is_owasp_staff ) diff --git a/backend/apps/owasp/api/internal/views/permissions.py b/backend/apps/owasp/api/internal/views/permissions.py index c193e0c985..3d76b8f04a 100644 --- a/backend/apps/owasp/api/internal/views/permissions.py +++ b/backend/apps/owasp/api/internal/views/permissions.py @@ -7,7 +7,14 @@ def has_dashboard_permission(request): """Check if user has dashboard access.""" - return (user := request.user) and user.is_authenticated and user.github_user.is_owasp_staff + user = request.user + if not (user and user.is_authenticated and user.github_user): + return False + + return ( + hasattr(user.github_user, "owasp_profile") + and user.github_user.owasp_profile.is_owasp_staff + ) def dashboard_access_required(view_func): From e895d6e7948e4f1398b0f277587e19598749af7d Mon Sep 17 00:00:00 2001 From: kart-u Date: Wed, 26 Nov 2025 23:46:04 +0000 Subject: [PATCH 06/10] test,lint,format check --- .../commands/github_update_users.py | 4 +- ...er_memberprofile_has_public_member_page.py | 18 +++ .../github/api/internal/queries/user_test.py | 50 ++++--- .../commands/github_update_users_test.py | 137 ++++++++++++------ .../commands/nest_update_badges_test.py | 15 +- 5 files changed, 156 insertions(+), 68 deletions(-) create mode 100644 backend/apps/owasp/migrations/0068_alter_memberprofile_has_public_member_page.py diff --git a/backend/apps/github/management/commands/github_update_users.py b/backend/apps/github/management/commands/github_update_users.py index 3b258f237b..b7cf215fe5 100644 --- a/backend/apps/github/management/commands/github_update_users.py +++ b/backend/apps/github/management/commands/github_update_users.py @@ -51,7 +51,9 @@ def handle(self, *args, **options): prefix = f"{idx + offset + 1} of {active_users_count - offset}" print(f"{prefix:<10} {user.title}") - profile, _ = MemberProfile.objects.get_or_create(github_user=user) + profile, created = MemberProfile.objects.get_or_create(github_user=user) + if created: + profile.github_user = user profile.contributions_count = user_contributions.get(user.id, 0) profiles.append(profile) diff --git a/backend/apps/owasp/migrations/0068_alter_memberprofile_has_public_member_page.py b/backend/apps/owasp/migrations/0068_alter_memberprofile_has_public_member_page.py new file mode 100644 index 0000000000..9a8376b8e9 --- /dev/null +++ b/backend/apps/owasp/migrations/0068_alter_memberprofile_has_public_member_page.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2025-11-26 14:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('owasp', '0067_memberprofile_backward_compatibility'), + ] + + operations = [ + migrations.AlterField( + model_name='memberprofile', + name='has_public_member_page', + field=models.BooleanField(default=True, help_text="Whether the member's profile is publicly visible on the OWASP website", verbose_name='Has Public Member Page'), + ), + ] diff --git a/backend/tests/apps/github/api/internal/queries/user_test.py b/backend/tests/apps/github/api/internal/queries/user_test.py index 4b430d030b..56920afc2d 100644 --- a/backend/tests/apps/github/api/internal/queries/user_test.py +++ b/backend/tests/apps/github/api/internal/queries/user_test.py @@ -16,49 +16,61 @@ def mock_user(self): def test_resolve_user_existing_with_public_member_page(self, mock_user): """Test resolving an existing user with has_public_member_page=True.""" - with patch("apps.github.models.user.User.objects.filter") as mock_filter: - mock_queryset = mock_filter.return_value - mock_queryset.first.return_value = mock_user + with patch("apps.github.models.user.User.objects.select_related") as mock_select_related: + mock_queryset = mock_select_related.return_value + mock_queryset.filter.return_value.first.return_value = mock_user result = UserQuery().user(login="test-user") assert result == mock_user - mock_filter.assert_called_once_with(has_public_member_page=True, login="test-user") - mock_queryset.first.assert_called_once() + mock_select_related.assert_called_once_with("owasp_profile") + mock_queryset.filter.assert_called_once_with( + owasp_profile__has_public_member_page=True, login="test-user" + ) + mock_queryset.filter.return_value.first.assert_called_once() def test_resolve_user_not_found_when_has_public_member_page_false(self): """Test resolving a user with has_public_member_page=False returns None.""" - with patch("apps.github.models.user.User.objects.filter") as mock_filter: - mock_queryset = mock_filter.return_value - mock_queryset.first.return_value = None + with patch("apps.github.models.user.User.objects.select_related") as mock_select_related: + mock_queryset = mock_select_related.return_value + mock_queryset.filter.return_value.first.return_value = None result = UserQuery().user(login="test-user") assert result is None - mock_filter.assert_called_once_with(has_public_member_page=True, login="test-user") - mock_queryset.first.assert_called_once() + mock_select_related.assert_called_once_with("owasp_profile") + mock_queryset.filter.assert_called_once_with( + owasp_profile__has_public_member_page=True, login="test-user" + ) + mock_queryset.filter.return_value.first.assert_called_once() def test_resolve_user_not_found(self): """Test resolving a non-existent user.""" - with patch("apps.github.models.user.User.objects.filter") as mock_filter: - mock_queryset = mock_filter.return_value - mock_queryset.first.return_value = None + with patch("apps.github.models.user.User.objects.select_related") as mock_select_related: + mock_queryset = mock_select_related.return_value + mock_queryset.filter.return_value.first.return_value = None result = UserQuery().user(login="non-existent") assert result is None - mock_filter.assert_called_once_with(has_public_member_page=True, login="non-existent") - mock_queryset.first.assert_called_once() + mock_select_related.assert_called_once_with("owasp_profile") + mock_queryset.filter.assert_called_once_with( + owasp_profile__has_public_member_page=True, login="non-existent" + ) + mock_queryset.filter.return_value.first.assert_called_once() def test_resolve_user_filters_by_public_member_page_and_login(self): """Test that user query filters by both has_public_member_page and login.""" - with patch("apps.github.models.user.User.objects.filter") as mock_filter: - mock_queryset = mock_filter.return_value - mock_queryset.first.return_value = None + with patch("apps.github.models.user.User.objects.select_related") as mock_select_related: + mock_queryset = mock_select_related.return_value + mock_queryset.filter.return_value.first.return_value = None UserQuery().user(login="test-user") - mock_filter.assert_called_once_with(has_public_member_page=True, login="test-user") + mock_select_related.assert_called_once_with("owasp_profile") + mock_queryset.filter.assert_called_once_with( + owasp_profile__has_public_member_page=True, login="test-user" + ) def test_top_contributed_repositories(self): """Test resolving top contributed repositories.""" diff --git a/backend/tests/apps/github/management/commands/github_update_users_test.py b/backend/tests/apps/github/management/commands/github_update_users_test.py index e27b8d71fe..15e0231465 100644 --- a/backend/tests/apps/github/management/commands/github_update_users_test.py +++ b/backend/tests/apps/github/management/commands/github_update_users_test.py @@ -38,10 +38,23 @@ def test_handle_with_default_offset( self, mock_repository_contributor, mock_user, mock_member_profile ): """Test command execution with default offset.""" - mock_member_profile.objects.get_or_create.return_value = (MagicMock(), False) - mock_user1 = MagicMock(id=1, title="User 1", contributions_count=0) - mock_user2 = MagicMock(id=2, title="User 2", contributions_count=0) - mock_user3 = MagicMock(id=3, title="User 3", contributions_count=0) + mock_profile1 = MagicMock(contributions_count=0) + mock_profile2 = MagicMock(contributions_count=0) + mock_profile3 = MagicMock(contributions_count=0) + + def get_or_create_side_effect(github_user): + if github_user.id == 1: + return mock_profile1, False + if github_user.id == 2: + return mock_profile2, False + if github_user.id == 3: + return mock_profile3, False + return MagicMock(), False + + mock_member_profile.objects.get_or_create.side_effect = get_or_create_side_effect + mock_user1 = MagicMock(id=1, title="User 1") + mock_user2 = MagicMock(id=2, title="User 2") + mock_user3 = MagicMock(id=3, title="User 3") mock_users_queryset = MagicMock() mock_users_queryset.count.return_value = 3 @@ -73,12 +86,16 @@ def test_handle_with_default_offset( mock_print.assert_any_call("2 of 3 User 2") mock_print.assert_any_call("3 of 3 User 3") - assert mock_user1.contributions_count == 10 - assert mock_user2.contributions_count == 20 - assert mock_user3.contributions_count == 30 + assert mock_profile1.contributions_count == 10 + assert mock_profile2.contributions_count == 20 + assert mock_profile3.contributions_count == 30 - assert mock_user.bulk_save.call_count == 2 - assert mock_user.bulk_save.call_args_list[-1][0][0] == [mock_user1, mock_user2, mock_user3] + assert mock_member_profile.bulk_save.call_count == 2 + assert mock_member_profile.bulk_save.call_args_list[-1][0][0] == [ + mock_profile1, + mock_profile2, + mock_profile3, + ] @patch("apps.github.management.commands.github_update_users.MemberProfile") @patch("apps.github.management.commands.github_update_users.User") @@ -88,9 +105,19 @@ def test_handle_with_custom_offset( self, mock_repository_contributor, mock_user, mock_member_profile ): """Test command execution with custom offset.""" - mock_member_profile.objects.get_or_create.return_value = (MagicMock(), False) - mock_user1 = MagicMock(id=2, title="User 2", contributions_count=0) - mock_user2 = MagicMock(id=3, title="User 3", contributions_count=0) + mock_profile1 = MagicMock(contributions_count=0) + mock_profile2 = MagicMock(contributions_count=0) + + def get_or_create_side_effect(github_user): + if github_user.id == 2: + return mock_profile1, False + if github_user.id == 3: + return mock_profile2, False + return MagicMock(), False + + mock_member_profile.objects.get_or_create.side_effect = get_or_create_side_effect + mock_user1 = MagicMock(id=2, title="User 2") + mock_user2 = MagicMock(id=3, title="User 3") mock_users_queryset = MagicMock() mock_users_queryset.count.return_value = 3 @@ -117,11 +144,14 @@ def test_handle_with_custom_offset( mock_print.assert_any_call("2 of 2 User 2") mock_print.assert_any_call("3 of 2 User 3") - assert mock_user1.contributions_count == 20 - assert mock_user2.contributions_count == 30 + assert mock_profile1.contributions_count == 20 + assert mock_profile2.contributions_count == 30 - assert mock_user.bulk_save.call_count == 2 - assert mock_user.bulk_save.call_args_list[-1][0][0] == [mock_user1, mock_user2] + assert mock_member_profile.bulk_save.call_count == 2 + assert mock_member_profile.bulk_save.call_args_list[-1][0][0] == [ + mock_profile1, + mock_profile2, + ] @patch("apps.github.management.commands.github_update_users.MemberProfile") @patch("apps.github.management.commands.github_update_users.User") @@ -130,10 +160,19 @@ def test_handle_with_custom_offset( def test_handle_with_users_having_no_contributions( self, mock_repository_contributor, mock_user, mock_member_profile ): - """Test command execution when users have no contributions.""" - mock_member_profile.objects.get_or_create.return_value = (MagicMock(), False) - mock_user1 = MagicMock(id=1, title="User 1", contributions_count=0) - mock_user2 = MagicMock(id=2, title="User 2", contributions_count=0) + mock_profile1 = MagicMock(contributions_count=0) + mock_profile2 = MagicMock(contributions_count=0) + + def get_or_create_side_effect(github_user): + if github_user.id == 1: + return mock_profile1, False + if github_user.id == 2: + return mock_profile2, False + return MagicMock(), False + + mock_member_profile.objects.get_or_create.side_effect = get_or_create_side_effect + mock_user1 = MagicMock(id=1, title="User 1") + mock_user2 = MagicMock(id=2, title="User 2") mock_users_queryset = MagicMock() mock_users_queryset.count.return_value = 2 @@ -153,11 +192,14 @@ def test_handle_with_users_having_no_contributions( mock_print.assert_any_call("1 of 2 User 1") mock_print.assert_any_call("2 of 2 User 2") - assert mock_user1.contributions_count == 0 - assert mock_user2.contributions_count == 0 + assert mock_profile1.contributions_count == 0 + assert mock_profile2.contributions_count == 0 - assert mock_user.bulk_save.call_count == 1 - assert mock_user.bulk_save.call_args_list[-1][0][0] == [mock_user1, mock_user2] + assert mock_member_profile.bulk_save.call_count == 1 + assert mock_member_profile.bulk_save.call_args_list[-1][0][0] == [ + mock_profile1, + mock_profile2, + ] @patch("apps.github.management.commands.github_update_users.MemberProfile") @patch("apps.github.management.commands.github_update_users.User") @@ -167,12 +209,15 @@ def test_handle_with_single_user( self, mock_repository_contributor, mock_user, mock_member_profile ): """Test command execution with single user.""" - mock_member_profile.objects.get_or_create.return_value = (MagicMock(), False) - mock_user1 = MagicMock(id=1, title="User 1", contributions_count=0) + mock_profile1 = MagicMock(contributions_count=0) + mock_member_profile.objects.get_or_create.return_value = (mock_profile1, False) + mock_user1 = MagicMock(id=1, title="User 1") mock_users_queryset = MagicMock() mock_users_queryset.count.return_value = 1 mock_users_queryset.__getitem__.return_value = [mock_user1] + mock_users_queryset.count.return_value = 1 + mock_users_queryset.__getitem__.return_value = [mock_user1] mock_user.objects.order_by.return_value = mock_users_queryset @@ -188,10 +233,10 @@ def test_handle_with_single_user( mock_print.assert_called_once_with("1 of 1 User 1") - assert mock_user1.contributions_count == 15 + assert mock_profile1.contributions_count == 15 - assert mock_user.bulk_save.call_count == 2 - assert mock_user.bulk_save.call_args_list[-1][0][0] == [mock_user1] + assert mock_member_profile.bulk_save.call_count == 2 + assert mock_member_profile.bulk_save.call_args_list[-1][0][0] == [mock_profile1] @patch("apps.github.management.commands.github_update_users.MemberProfile") @patch("apps.github.management.commands.github_update_users.User") @@ -218,8 +263,6 @@ def test_handle_with_empty_user_list( mock_print.assert_not_called() - assert mock_user.bulk_save.call_count == 1 - assert mock_user.bulk_save.call_args_list[-1][0][0] == [] assert mock_member_profile.bulk_save.call_count == 1 assert mock_member_profile.bulk_save.call_args_list[-1][0][0] == [] @@ -231,9 +274,19 @@ def test_handle_with_exact_batch_size( self, mock_repository_contributor, mock_user, mock_member_profile ): """Test command execution when user count equals batch size.""" - mock_member_profile.objects.get_or_create.return_value = (MagicMock(), False) - mock_user1 = MagicMock(id=1, title="User 1", contributions_count=0) - mock_user2 = MagicMock(id=2, title="User 2", contributions_count=0) + mock_profile1 = MagicMock(contributions_count=0) + mock_profile2 = MagicMock(contributions_count=0) + + def get_or_create_side_effect(github_user): + if github_user.id == 1: + return mock_profile1, False + if github_user.id == 2: + return mock_profile2, False + return MagicMock(), False + + mock_member_profile.objects.get_or_create.side_effect = get_or_create_side_effect + mock_user1 = MagicMock(id=1, title="User 1") + mock_user2 = MagicMock(id=2, title="User 2") mock_users_queryset = MagicMock() mock_users_queryset.count.return_value = 2 @@ -256,11 +309,14 @@ def test_handle_with_exact_batch_size( mock_print.assert_any_call("1 of 2 User 1") mock_print.assert_any_call("2 of 2 User 2") - assert mock_user1.contributions_count == 10 - assert mock_user2.contributions_count == 20 + assert mock_profile1.contributions_count == 10 + assert mock_profile2.contributions_count == 20 - assert mock_user.bulk_save.call_count == 2 - assert mock_user.bulk_save.call_args_list[-1][0][0] == [mock_user1, mock_user2] + assert mock_member_profile.bulk_save.call_count == 2 + assert mock_member_profile.bulk_save.call_args_list[-1][0][0] == [ + mock_profile1, + mock_profile2, + ] @patch("apps.github.management.commands.github_update_users.MemberProfile") @patch("apps.github.management.commands.github_update_users.User") @@ -275,9 +331,6 @@ def test_handle_member_profile_created( mock_user1 = MagicMock( id=1, - contributions_count=0, - is_owasp_staff=True, - has_public_member_page=False, ) mock_users_queryset = MagicMock() @@ -296,6 +349,4 @@ def test_handle_member_profile_created( assert mock_profile.github_user == mock_user1 assert mock_profile.contributions_count == 5 - assert mock_profile.is_owasp_staff is True - assert mock_profile.has_public_member_page is False mock_member_profile.bulk_save.assert_called_once() diff --git a/backend/tests/apps/nest/management/commands/nest_update_badges_test.py b/backend/tests/apps/nest/management/commands/nest_update_badges_test.py index 8ec63cff2d..bfc7c1d23c 100644 --- a/backend/tests/apps/nest/management/commands/nest_update_badges_test.py +++ b/backend/tests/apps/nest/management/commands/nest_update_badges_test.py @@ -31,13 +31,18 @@ def make_mock_former_employees(mock_former_employee): def extract_is_owasp_staff(arg): """Extract is_owasp_staff value from Q object, dict, or tuple.""" + key_to_check = "owasp_profile__is_owasp_staff" + legacy_key_to_check = "is_owasp_staff" if hasattr(arg, "children"): for key, value in arg.children: - if key == "is_owasp_staff": + if key in (key_to_check, legacy_key_to_check): return value - if isinstance(arg, dict) and "is_owasp_staff" in arg: - return arg["is_owasp_staff"] - if isinstance(arg, tuple) and len(arg) == 2 and arg[0] == "is_owasp_staff": + if isinstance(arg, dict): + if key_to_check in arg: + return arg[key_to_check] + if legacy_key_to_check in arg: + return arg[legacy_key_to_check] + if isinstance(arg, tuple) and len(arg) == 2 and arg[0] in (key_to_check, legacy_key_to_check): return arg[1] return None @@ -53,7 +58,7 @@ def get_mock_for_staff_value(value): return None def user_filter_side_effect(*args, **kwargs): - staff_value = kwargs.get("is_owasp_staff") + staff_value = kwargs.get("owasp_profile__is_owasp_staff", kwargs.get("is_owasp_staff")) if staff_value is not None: return get_mock_for_staff_value(staff_value) for arg in args: From 818b3987e6272557022b4ad54a7a0563e23ae7c8 Mon Sep 17 00:00:00 2001 From: kart-u Date: Thu, 27 Nov 2025 00:10:30 +0000 Subject: [PATCH 07/10] rabbit-bot recommendation implemented --- backend/apps/github/models/mixins/user.py | 4 +++- .../owasp/api/internal/views/permissions.py | 19 +++++++++++++------ ...er_memberprofile_has_public_member_page.py | 13 ++++++++----- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/backend/apps/github/models/mixins/user.py b/backend/apps/github/models/mixins/user.py index 3ac4cef9d4..255531e481 100644 --- a/backend/apps/github/models/mixins/user.py +++ b/backend/apps/github/models/mixins/user.py @@ -124,7 +124,9 @@ def idx_contributions(self): @property def idx_contributions_count(self) -> int: """Return contributions count for indexing.""" - return self.owasp_profile.contributions_count + if not hasattr(self, "owasp_profile"): + return 0 + return int(self.owasp_profile.contributions_count) @property def idx_issues(self) -> list[dict]: diff --git a/backend/apps/owasp/api/internal/views/permissions.py b/backend/apps/owasp/api/internal/views/permissions.py index 3d76b8f04a..8b99ef5b6a 100644 --- a/backend/apps/owasp/api/internal/views/permissions.py +++ b/backend/apps/owasp/api/internal/views/permissions.py @@ -2,19 +2,26 @@ from functools import wraps +from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponseForbidden def has_dashboard_permission(request): """Check if user has dashboard access.""" - user = request.user - if not (user and user.is_authenticated and user.github_user): + user = getattr(request, "user", None) + if not (user and user.is_authenticated and hasattr(user, "github_user")): + return False + try: + github_user = user.github_user + except ObjectDoesNotExist: + return False + + try: + profile = github_user.owasp_profile + except ObjectDoesNotExist: return False - return ( - hasattr(user.github_user, "owasp_profile") - and user.github_user.owasp_profile.is_owasp_staff - ) + return bool(getattr(profile, "is_owasp_staff", False)) def dashboard_access_required(view_func): diff --git a/backend/apps/owasp/migrations/0068_alter_memberprofile_has_public_member_page.py b/backend/apps/owasp/migrations/0068_alter_memberprofile_has_public_member_page.py index 9a8376b8e9..bd844150d3 100644 --- a/backend/apps/owasp/migrations/0068_alter_memberprofile_has_public_member_page.py +++ b/backend/apps/owasp/migrations/0068_alter_memberprofile_has_public_member_page.py @@ -4,15 +4,18 @@ class Migration(migrations.Migration): - dependencies = [ - ('owasp', '0067_memberprofile_backward_compatibility'), + ("owasp", "0067_memberprofile_backward_compatibility"), ] operations = [ migrations.AlterField( - model_name='memberprofile', - name='has_public_member_page', - field=models.BooleanField(default=True, help_text="Whether the member's profile is publicly visible on the OWASP website", verbose_name='Has Public Member Page'), + model_name="memberprofile", + name="has_public_member_page", + field=models.BooleanField( + default=True, + help_text="Whether the member's profile is publicly visible on the OWASP website", + verbose_name="Has Public Member Page", + ), ), ] From 2316c6d2674f4efe5fcdfde50609afd67c853e2b Mon Sep 17 00:00:00 2001 From: kart-u Date: Thu, 27 Nov 2025 11:39:42 +0000 Subject: [PATCH 08/10] code rabbit recommendation --- backend/apps/owasp/api/internal/views/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/apps/owasp/api/internal/views/permissions.py b/backend/apps/owasp/api/internal/views/permissions.py index 8b99ef5b6a..641b3a11ed 100644 --- a/backend/apps/owasp/api/internal/views/permissions.py +++ b/backend/apps/owasp/api/internal/views/permissions.py @@ -9,7 +9,7 @@ def has_dashboard_permission(request): """Check if user has dashboard access.""" user = getattr(request, "user", None) - if not (user and user.is_authenticated and hasattr(user, "github_user")): + if not (user and getattr(user, "is_authenticated", False)): return False try: github_user = user.github_user From d145a04e6baff7712c08da5cdeb6e845777e91ec Mon Sep 17 00:00:00 2001 From: kart-u Date: Tue, 2 Dec 2025 13:59:41 +0000 Subject: [PATCH 09/10] backward compatibility --- backend/apps/github/api/internal/nodes/user.py | 6 +++--- .../apps/github/api/internal/queries/user.py | 6 +----- .../management/commands/github_update_users.py | 15 ++++++++++++++- backend/apps/github/models/mixins/user.py | 6 +++--- backend/apps/nest/api/internal/nodes/user.py | 4 ++-- .../management/commands/nest_update_badges.py | 4 ++-- .../permissions/project_health_metrics.py | 8 ++++---- .../owasp/api/internal/views/permissions.py | 18 ++++++------------ 8 files changed, 35 insertions(+), 32 deletions(-) diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index 2ec3a71e94..2885602649 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -29,9 +29,9 @@ class UserNode: @strawberry.field def contributions_count(self) -> int: """Resolve contributions count.""" - if hasattr(self, "owasp_profile"): + if hasattr(self, "owasp_profile") and self.owasp_profile.contributions_count: return self.owasp_profile.contributions_count - return 0 + return self.contributions_count @strawberry.field def badge_count(self) -> int: @@ -93,7 +93,7 @@ def is_owasp_staff(self) -> bool: """Resolve if the user is an OWASP staff member.""" if hasattr(self, "owasp_profile"): return self.owasp_profile.is_owasp_staff - return False + return self.is_owasp_staff @strawberry.field def issues_count(self) -> int: diff --git a/backend/apps/github/api/internal/queries/user.py b/backend/apps/github/api/internal/queries/user.py index 1297428a8d..199c33aa90 100644 --- a/backend/apps/github/api/internal/queries/user.py +++ b/backend/apps/github/api/internal/queries/user.py @@ -50,8 +50,4 @@ def user( User or None: The user object if found, otherwise None. """ - return ( - User.objects.select_related("owasp_profile") - .filter(owasp_profile__has_public_member_page=True, login=login) - .first() - ) + return User.objects.filter(has_public_member_page=True, login=login).first() diff --git a/backend/apps/github/management/commands/github_update_users.py b/backend/apps/github/management/commands/github_update_users.py index b7cf215fe5..1774c40332 100644 --- a/backend/apps/github/management/commands/github_update_users.py +++ b/backend/apps/github/management/commands/github_update_users.py @@ -47,6 +47,7 @@ def handle(self, *args, **options): .annotate(total_contributions=Sum("contributions_count")) } profiles = [] + users = [] for idx, user in enumerate(active_users[offset:]): prefix = f"{idx + offset + 1} of {active_users_count - offset}" print(f"{prefix:<10} {user.title}") @@ -54,16 +55,28 @@ def handle(self, *args, **options): profile, created = MemberProfile.objects.get_or_create(github_user=user) if created: profile.github_user = user - profile.contributions_count = user_contributions.get(user.id, 0) + contributions = user_contributions.get(user.id, 0) + profile.contributions_count = contributions profiles.append(profile) + user.contributions_count = contributions + users.append(user) + if not len(profiles) % BATCH_SIZE: MemberProfile.bulk_save( profiles, fields=("contributions_count",), ) + User.bulk_save( + users, + fields=("contributions_count",), + ) MemberProfile.bulk_save( profiles, fields=("contributions_count",), ) + User.bulk_save( + users, + fields=("contributions_count",), + ) diff --git a/backend/apps/github/models/mixins/user.py b/backend/apps/github/models/mixins/user.py index 255531e481..b479da18eb 100644 --- a/backend/apps/github/models/mixins/user.py +++ b/backend/apps/github/models/mixins/user.py @@ -124,9 +124,9 @@ def idx_contributions(self): @property def idx_contributions_count(self) -> int: """Return contributions count for indexing.""" - if not hasattr(self, "owasp_profile"): - return 0 - return int(self.owasp_profile.contributions_count) + if hasattr(self, "owasp_profile") and self.owasp_profile.contributions_count: + return int(self.owasp_profile.contributions_count) + return self.contributions_count @property def idx_issues(self) -> list[dict]: diff --git a/backend/apps/nest/api/internal/nodes/user.py b/backend/apps/nest/api/internal/nodes/user.py index 1bef655b03..ccb6c3cda1 100644 --- a/backend/apps/nest/api/internal/nodes/user.py +++ b/backend/apps/nest/api/internal/nodes/user.py @@ -13,6 +13,6 @@ class AuthUserNode(strawberry.relay.Node): @strawberry_django.field def is_owasp_staff(self) -> bool: """Check if the user is an OWASP staff member.""" - if self.github_user and hasattr(self.github_user, "owasp_profile"): + if hasattr(self.github_user, "owasp_profile"): return self.github_user.owasp_profile.is_owasp_staff - return False + return self.github_user.is_owasp_staff diff --git a/backend/apps/nest/management/commands/nest_update_badges.py b/backend/apps/nest/management/commands/nest_update_badges.py index d7ae14cc10..209313134c 100644 --- a/backend/apps/nest/management/commands/nest_update_badges.py +++ b/backend/apps/nest/management/commands/nest_update_badges.py @@ -42,7 +42,7 @@ def update_owasp_staff_badge(self): # Assign badge to employees who don't have it. employees_without_badge = User.objects.filter( - owasp_profile__is_owasp_staff=True, + is_owasp_staff=True, ).exclude( user_badges__badge=badge, ) @@ -60,7 +60,7 @@ def update_owasp_staff_badge(self): # Remove badge from non-OWASP employees. non_employees = User.objects.filter( - owasp_profile__is_owasp_staff=False, + is_owasp_staff=False, user_badges__badge=badge, ).distinct() removed_count = non_employees.count() diff --git a/backend/apps/owasp/api/internal/permissions/project_health_metrics.py b/backend/apps/owasp/api/internal/permissions/project_health_metrics.py index 7de85652a0..fdb33555e3 100644 --- a/backend/apps/owasp/api/internal/permissions/project_health_metrics.py +++ b/backend/apps/owasp/api/internal/permissions/project_health_metrics.py @@ -14,7 +14,7 @@ def has_permission(self, source, info, **kwargs) -> bool: if not (user and user.is_authenticated and user.github_user): return False - return ( - hasattr(user.github_user, "owasp_profile") - and user.github_user.owasp_profile.is_owasp_staff - ) + if hasattr(user.github_user, "owasp_profile"): + return user.github_user.owasp_profile.is_owasp_staff + + return user.github_user.is_owasp_staff diff --git a/backend/apps/owasp/api/internal/views/permissions.py b/backend/apps/owasp/api/internal/views/permissions.py index 641b3a11ed..53053e682b 100644 --- a/backend/apps/owasp/api/internal/views/permissions.py +++ b/backend/apps/owasp/api/internal/views/permissions.py @@ -2,26 +2,20 @@ from functools import wraps -from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponseForbidden def has_dashboard_permission(request): """Check if user has dashboard access.""" - user = getattr(request, "user", None) - if not (user and getattr(user, "is_authenticated", False)): - return False - try: - github_user = user.github_user - except ObjectDoesNotExist: + user = request.user + if not (user and user.is_authenticated and hasattr(user, "github_user") and user.github_user): return False - try: - profile = github_user.owasp_profile - except ObjectDoesNotExist: - return False + github_user = user.github_user + if hasattr(github_user, "owasp_profile"): + return github_user.owasp_profile.is_owasp_staff - return bool(getattr(profile, "is_owasp_staff", False)) + return github_user.is_owasp_staff def dashboard_access_required(view_func): From e9e053576605ce8ec29a56b629fd339477c97b4d Mon Sep 17 00:00:00 2001 From: kart-u Date: Tue, 2 Dec 2025 17:49:01 +0000 Subject: [PATCH 10/10] corrected tests --- .../github/api/internal/queries/user_test.py | 46 ++++++------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/backend/tests/apps/github/api/internal/queries/user_test.py b/backend/tests/apps/github/api/internal/queries/user_test.py index 56920afc2d..1ae5b888dd 100644 --- a/backend/tests/apps/github/api/internal/queries/user_test.py +++ b/backend/tests/apps/github/api/internal/queries/user_test.py @@ -16,61 +16,45 @@ def mock_user(self): def test_resolve_user_existing_with_public_member_page(self, mock_user): """Test resolving an existing user with has_public_member_page=True.""" - with patch("apps.github.models.user.User.objects.select_related") as mock_select_related: - mock_queryset = mock_select_related.return_value - mock_queryset.filter.return_value.first.return_value = mock_user + with patch("apps.github.models.user.User.objects.filter") as mock_filter: + mock_filter.return_value.first.return_value = mock_user result = UserQuery().user(login="test-user") assert result == mock_user - mock_select_related.assert_called_once_with("owasp_profile") - mock_queryset.filter.assert_called_once_with( - owasp_profile__has_public_member_page=True, login="test-user" - ) - mock_queryset.filter.return_value.first.assert_called_once() + mock_filter.assert_called_once_with(has_public_member_page=True, login="test-user") + mock_filter.return_value.first.assert_called_once() def test_resolve_user_not_found_when_has_public_member_page_false(self): """Test resolving a user with has_public_member_page=False returns None.""" - with patch("apps.github.models.user.User.objects.select_related") as mock_select_related: - mock_queryset = mock_select_related.return_value - mock_queryset.filter.return_value.first.return_value = None + with patch("apps.github.models.user.User.objects.filter") as mock_filter: + mock_filter.return_value.first.return_value = None result = UserQuery().user(login="test-user") assert result is None - mock_select_related.assert_called_once_with("owasp_profile") - mock_queryset.filter.assert_called_once_with( - owasp_profile__has_public_member_page=True, login="test-user" - ) - mock_queryset.filter.return_value.first.assert_called_once() + mock_filter.assert_called_once_with(has_public_member_page=True, login="test-user") + mock_filter.return_value.first.assert_called_once() def test_resolve_user_not_found(self): """Test resolving a non-existent user.""" - with patch("apps.github.models.user.User.objects.select_related") as mock_select_related: - mock_queryset = mock_select_related.return_value - mock_queryset.filter.return_value.first.return_value = None + with patch("apps.github.models.user.User.objects.filter") as mock_filter: + mock_filter.return_value.first.return_value = None result = UserQuery().user(login="non-existent") assert result is None - mock_select_related.assert_called_once_with("owasp_profile") - mock_queryset.filter.assert_called_once_with( - owasp_profile__has_public_member_page=True, login="non-existent" - ) - mock_queryset.filter.return_value.first.assert_called_once() + mock_filter.assert_called_once_with(has_public_member_page=True, login="non-existent") + mock_filter.return_value.first.assert_called_once() def test_resolve_user_filters_by_public_member_page_and_login(self): """Test that user query filters by both has_public_member_page and login.""" - with patch("apps.github.models.user.User.objects.select_related") as mock_select_related: - mock_queryset = mock_select_related.return_value - mock_queryset.filter.return_value.first.return_value = None + with patch("apps.github.models.user.User.objects.filter") as mock_filter: + mock_filter.return_value.first.return_value = None UserQuery().user(login="test-user") - mock_select_related.assert_called_once_with("owasp_profile") - mock_queryset.filter.assert_called_once_with( - owasp_profile__has_public_member_page=True, login="test-user" - ) + mock_filter.assert_called_once_with(has_public_member_page=True, login="test-user") def test_top_contributed_repositories(self): """Test resolving top contributed repositories."""