Skip to content
18 changes: 16 additions & 2 deletions backend/apps/github/api/internal/nodes/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@
"avatar_url",
"bio",
"company",
"contributions_count",
"email",
"followers_count",
"following_count",
"id",
"is_owasp_staff",
"location",
"login",
"name",
Expand All @@ -28,6 +26,14 @@
class UserNode:
"""GitHub user node."""

def _resolve_contributions_count(self) -> int:
"""Resolve contributions count."""
if hasattr(self, "owasp_profile"):
return self.owasp_profile.contributions_count
return super().__getattribute__("contributions_count")

contributions_count: int = strawberry.field(resolver=_resolve_contributions_count)

@strawberry.field
def badge_count(self) -> int:
"""Resolve badge count."""
Expand Down Expand Up @@ -83,6 +89,14 @@ def is_gsoc_mentor(self) -> bool:
return self.owasp_profile.is_gsoc_mentor
return False

def _resolve_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 super().__getattribute__("is_owasp_staff")

is_owasp_staff: bool = strawberry.field(resolver=_resolve_is_owasp_staff)

@strawberry.field
def issues_count(self) -> int:
"""Resolve issues count."""
Expand Down
9 changes: 9 additions & 0 deletions backend/apps/github/api/internal/queries/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,13 @@ def user(
User or None: The user object if found, otherwise None.

"""
user = (
User.objects.select_related("owasp_profile")
.filter(owasp_profile__has_public_member_page=True, login=login)
.first()
)

if user:
return user

return User.objects.filter(has_public_member_page=True, login=login).first()
33 changes: 29 additions & 4 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 @@ -45,15 +46,39 @@ def handle(self, *args, **options):
.values("user_id")
.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}")

user.contributions_count = user_contributions.get(user.id, 0)
profile, created = MemberProfile.objects.get_or_create(github_user=user)
if created:
profile.github_user = user
contributions = user_contributions.get(user.id, 0)
profile.contributions_count = contributions
profiles.append(profile)

user.contributions_count = contributions
users.append(user)

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",),
)
User.bulk_save(
users,
fields=("contributions_count",),
)
if profiles:
MemberProfile.bulk_save(
profiles,
fields=("contributions_count",),
)

User.bulk_save(users, fields=("contributions_count",))
if users:
User.bulk_save(
users,
fields=("contributions_count",),
)
2 changes: 2 additions & 0 deletions backend/apps/github/models/mixins/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ def idx_contributions(self):
@property
def idx_contributions_count(self) -> int:
"""Return contributions count for indexing."""
if hasattr(self, "owasp_profile") and self.owasp_profile.contributions_count:
return int(self.owasp_profile.contributions_count)
return self.contributions_count

@property
Expand Down
4 changes: 3 additions & 1 deletion backend/apps/nest/api/internal/nodes/user.py
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also here

Original file line number Diff line number Diff line change
Expand Up @@ -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 hasattr(self.github_user, "owasp_profile"):
return self.github_user.owasp_profile.is_owasp_staff
return self.github_user.is_owasp_staff
19 changes: 17 additions & 2 deletions backend/apps/nest/management/commands/nest_update_badges.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,20 @@ 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,
)
count = employees_without_badge.count()

if not count:
employees_without_badge = User.objects.filter(
is_owasp_staff=True,
).exclude(
user_badges__badge=badge,
)
count = employees_without_badge.count()

if count:
for user in employees_without_badge:
user_badge, created = UserBadge.objects.get_or_create(user=user, badge=badge)
Expand All @@ -60,11 +68,18 @@ 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()
Copy link
Collaborator

Choose a reason for hiding this comment

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

And here


if not removed_count:
non_employees = User.objects.filter(
is_owasp_staff=False,
user_badges__badge=badge,
).distinct()
removed_count = non_employees.count()

if removed_count:
UserBadge.objects.filter(
user_id__in=non_employees.values_list("id", flat=True),
Expand Down
12 changes: 11 additions & 1 deletion 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 Expand Up @@ -47,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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ class HasDashboardAccess(BasePermission):

def has_permission(self, source, info, **kwargs) -> bool:
"""Check if the user has dashboard access."""
return (
(user := info.context.request.user)
and user.is_authenticated
and user.github_user.is_owasp_staff
)
user = info.context.request.user
if not (user and user.is_authenticated and user.github_user):
return False

if hasattr(user.github_user, "owasp_profile"):
return user.github_user.owasp_profile.is_owasp_staff

return user.github_user.is_owasp_staff
10 changes: 9 additions & 1 deletion backend/apps/owasp/api/internal/views/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@

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 hasattr(user, "github_user") and user.github_user):
return False

github_user = user.github_user
if hasattr(github_user, "owasp_profile"):
return github_user.owasp_profile.is_owasp_staff

return github_user.is_owasp_staff


def dashboard_access_required(view_func):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 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,43 @@
# 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):
"""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
profiles_to_update.append(profile)

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"),
]

operations = [
migrations.RunPython(copy_user_data_to_member_profile, migrations.RunPython.noop),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 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",
),
),
]
21 changes: 20 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,25 @@ class Meta:
help_text="LinkedIn username or custom URL ID (e.g., 'john-doe-123')",
)

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",
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)
Loading