Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions backend/apps/github/management/commands/github_update_users.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This command was originally made for updating the GitHub.User contributions count. So, I suggest to move this command to Owasp app and only update the contributions count of MemberProfile.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you asked, I created a new member profile update in the owasp folder only updating contributions_count,
so far a few changes are still pending. I’ll have this PR ready for review soon. (I have exams going on, so my time is a bit limited, but I’ll do my best to finish this in the next 24 hour)

Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -46,14 +47,35 @@ 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}")

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only need to update the contribution_count of the MemberProfile. No other fields. Also, you should update the field directly from the calculated value not by accessing the contribution count field of the GitHub.User. You will remove this field from GitHub.User in another PR, right?

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"),
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, just update the contributions_count

3 changes: 3 additions & 0 deletions backend/apps/owasp/admin/member_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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'),
),
]
Original file line number Diff line number Diff line change
@@ -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),
]
19 changes: 18 additions & 1 deletion backend/apps/owasp/models/member_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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)

Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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()
Expand All @@ -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 = []
Expand All @@ -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)

Expand Down