Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
59b165e
update member with contribution heatmap component
mrkeshav-05 Jan 11, 2026
7953acb
update code
mrkeshav-05 Jan 13, 2026
ba086d0
code-rabbit
mrkeshav-05 Jan 13, 2026
2c4805a
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 Jan 14, 2026
3b573b1
Merge remote-tracking branch 'upstream/main' into fix/member-contribu…
mrkeshav-05 Jan 15, 2026
e7a27ed
minor fix
mrkeshav-05 Jan 15, 2026
4416f14
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 Jan 15, 2026
0278be4
minor fix
mrkeshav-05 Jan 16, 2026
e34cf02
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 Jan 16, 2026
0cbb0b4
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 Jan 18, 2026
3efb649
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 Jan 19, 2026
867a67b
update DB volume and rename command
mrkeshav-05 Jan 19, 2026
dd605d2
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 Jan 19, 2026
0f8fbb6
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 Jan 20, 2026
ac7bfe7
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 Jan 21, 2026
45a16f4
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 Jan 21, 2026
c4dc08d
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 Jan 23, 2026
1fc8025
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 Jan 24, 2026
681a02b
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 Jan 25, 2026
65f0aa5
Merge remote-tracking branch 'upstream/main' into fix/member-contribu…
mrkeshav-05 Jan 26, 2026
a1d06ff
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 Jan 27, 2026
15df789
Merge branch 'main' into pr/mrkeshav-05/3298
arkid15r Jan 27, 2026
a11db4a
Update code
arkid15r Jan 28, 2026
9e48d05
Update backend/Makefile
arkid15r Jan 28, 2026
0a83697
Update docker-compose/local/compose.yaml
arkid15r Jan 28, 2026
1c93f78
Update volumes
arkid15r Jan 28, 2026
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
3 changes: 2 additions & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ update-data: \
github-update-related-organizations \
github-update-users \
owasp-aggregate-projects \
owasp-aggregate-contributions \
owasp-aggregate-entity-contributions \
owasp-aggregate-member-contributions \
owasp-update-events \
owasp-sync-posts \
owasp-update-sponsors \
Expand Down
1 change: 1 addition & 0 deletions backend/apps/github/api/internal/nodes/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"avatar_url",
"bio",
"company",
"contribution_data",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's be consistent and implement it the same way as for chapters/projects. I think introducing an abstract model to derive from is reasonable.

Copy link
Contributor Author

@mrkeshav-05 mrkeshav-05 Jan 19, 2026

Choose a reason for hiding this comment

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

Hi @arkid15r ,
I agree that an abstract model is the best way to keep this DRY.
My plan is to abstract the contribution_stats logic into a shared Mixin (e.g., ContributionStatsMixin) located in apps/core. I will then update UserNode, ProjectNode, and ChapterNode to inherit from it.

Let me know if this sounds good.

"contributions_count",
"email",
"followers_count",
Expand Down
22 changes: 22 additions & 0 deletions backend/apps/github/migrations/0041_user_contribution_data.py
Copy link
Collaborator

Choose a reason for hiding this comment

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

If your PR has a migration you must temporary change the docker DB volume name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I have changed the volumes to db-data-pr-3298

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 6.0.1 on 2026-01-12 19:20

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("github", "0040_merge_20251117_0136"),
]

operations = [
migrations.AddField(
model_name="user",
name="contribution_data",
field=models.JSONField(
blank=True,
default=dict,
help_text="Aggregated contribution data as date -> count mapping",
verbose_name="Contribution heatmap data",
),
),
]
12 changes: 12 additions & 0 deletions backend/apps/github/migrations/0042_merge_20260127_2218.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Generated by Django 6.0.1 on 2026-01-27 22:18

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("github", "0041_milestone_github_milestone_created_at_and_more"),
("github", "0041_user_contribution_data"),
]

operations = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 6.0.1 on 2026-01-28 01:28

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("github", "0042_merge_20260127_2218"),
]

operations = [
migrations.AlterField(
model_name="user",
name="contribution_data",
field=models.JSONField(
blank=True,
default=dict,
help_text="Aggregated contribution data as date -> count mapping",
null=True,
verbose_name="Contribution heatmap data",
),
),
]
8 changes: 8 additions & 0 deletions backend/apps/github/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ class Meta:
verbose_name="Contributions count", default=0
)

contribution_data = models.JSONField(
verbose_name="Contribution heatmap data",
default=dict,
blank=True,
null=True,
help_text="Aggregated contribution data as date -> count mapping",
)

def __str__(self) -> str:
"""Return a human-readable representation of the user.

Expand Down
10 changes: 7 additions & 3 deletions backend/apps/owasp/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ owasp-aggregate-projects:
@echo "Aggregating OWASP projects"
@CMD="python manage.py owasp_aggregate_projects" $(MAKE) exec-backend-command

owasp-aggregate-contributions:
owasp-aggregate-entity-contributions:
@echo "Aggregating OWASP contributions"
@CMD="python manage.py owasp_aggregate_contributions --entity-type chapter" $(MAKE) exec-backend-command
@CMD="python manage.py owasp_aggregate_contributions --entity-type project" $(MAKE) exec-backend-command
@CMD="python manage.py owasp_aggregate_entity_contributions --entity-type chapter" $(MAKE) exec-backend-command
@CMD="python manage.py owasp_aggregate_entity_contributions --entity-type project" $(MAKE) exec-backend-command

owasp-aggregate-member-contributions:
@echo "Aggregating OWASP community member contributions"
@CMD="python manage.py owasp_aggregate_member_contributions" $(MAKE) exec-backend-command

owasp-create-project-metadata-file:
@echo "Generating metadata"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Management command to aggregate user contribution data."""

from datetime import datetime, timedelta
from typing import Any

from django.core.management.base import BaseCommand
from django.db.models import Count
from django.db.models.functions import TruncDate
from django.utils import timezone

from apps.github.models.commit import Commit
from apps.github.models.issue import Issue
from apps.github.models.pull_request import PullRequest
from apps.github.models.user import User


class Command(BaseCommand):
"""Aggregate contribution data for users."""

help = "Aggregate contribution data (commits, PRs, issues) for users"

def add_arguments(self, parser):
"""Add command arguments."""
parser.add_argument(
"--user",
type=str,
help="Specific user login to process",
)
parser.add_argument(
"--days",
type=int,
default=365,
help="Number of days to look back (default: 365)",
)
parser.add_argument(
"--batch-size",
type=int,
default=100,
help="Batch size for processing users (default: 100)",
)

def handle(self, *args: Any, **options: Any) -> None:
"""Handle the command execution."""
user_login = options.get("user")
days = options.get("days", 365)
batch_size = options.get("batch_size", 100)

start_date = timezone.now() - timedelta(days=days)

self.stdout.write(
self.style.SUCCESS(
f"Aggregating contributions since {start_date.date()} ({days} days back)"
)
)

if user_login:
users = User.objects.filter(login=user_login)
if not users.exists():
self.stdout.write(self.style.ERROR(f"Member '{user_login}' not found"))
return
else:
users = User.objects.filter(contributions_count__gt=0)

total_users = users.count()
self.stdout.write(f"Processing {total_users} members...")

updated_users = []
for user in users.iterator(chunk_size=batch_size):
user.contribution_data = self._aggregate_user_contributions(user, start_date)
updated_users.append(user)

User.bulk_save(updated_users, fields=["contribution_data"])

self.stdout.write(
self.style.SUCCESS(f"Successfully aggregated contributions for {total_users} members")
)

def _aggregate_user_contributions(self, user: User, start_date: datetime) -> dict[str, int]:
"""Aggregate contributions for a user.

Args:
user: User instance
start_date: Start datetime for aggregation

Returns:
Dictionary mapping YYYY-MM-DD to contribution counts

"""
contribution_data = {}
current_date = start_date.date()
end_date = timezone.now().date()

while current_date <= end_date:
date_str = current_date.strftime("%Y-%m-%d")
contribution_data[date_str] = 0
current_date += timedelta(days=1)

commits = (
Commit.objects.filter(
author=user,
created_at__gte=start_date,
)
.annotate(date=TruncDate("created_at"))
.values("date")
.annotate(count=Count("id"))
)

for commit in commits:
date_str = commit["date"].strftime("%Y-%m-%d")
contribution_data[date_str] = contribution_data.get(date_str, 0) + commit["count"]

prs = (
PullRequest.objects.filter(
author=user,
created_at__gte=start_date,
)
.annotate(date=TruncDate("created_at"))
.values("date")
.annotate(count=Count("id"))
)

for pr in prs:
date_str = pr["date"].strftime("%Y-%m-%d")
contribution_data[date_str] = contribution_data.get(date_str, 0) + pr["count"]

issues = (
Issue.objects.filter(
author=user,
created_at__gte=start_date,
)
.annotate(date=TruncDate("created_at"))
.values("date")
.annotate(count=Count("id"))
)

for issue in issues:
date_str = issue["date"].strftime("%Y-%m-%d")
contribution_data[date_str] = contribution_data.get(date_str, 0) + issue["count"]

return contribution_data
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Management command to aggregate user contribution data."""

from datetime import datetime, timedelta
from typing import Any

from django.core.management.base import BaseCommand
from django.db.models import Count
from django.db.models.functions import TruncDate
from django.utils import timezone

from apps.github.models.commit import Commit
from apps.github.models.issue import Issue
from apps.github.models.pull_request import PullRequest
from apps.github.models.user import User


class Command(BaseCommand):
"""Aggregate contribution data for users."""

help = "Aggregate contribution data (commits, PRs, issues) for users"

def add_arguments(self, parser):
"""Add command arguments."""
parser.add_argument(
"--user",
type=str,
help="Specific user login to process",
)
parser.add_argument(
"--days",
type=int,
default=365,
help="Number of days to look back (default: 365)",
)
parser.add_argument(
"--batch-size",
type=int,
default=100,
help="Batch size for processing users (default: 100)",
)

def handle(self, *args: Any, **options: Any) -> None:
"""Handle the command execution."""
user_login = options.get("user")
days = options.get("days", 365)
batch_size = options.get("batch_size", 100)

start_date = timezone.now() - timedelta(days=days)

self.stdout.write(
self.style.SUCCESS(
f"Aggregating contributions since {start_date.date()} ({days} days back)"
)
)

if user_login:
users = User.objects.filter(login=user_login)
if not users.exists():
self.stdout.write(self.style.ERROR(f"Member '{user_login}' not found"))
return
else:
users = User.objects.filter(contributions_count__gt=0)

total_users = users.count()
self.stdout.write(f"Processing {total_users} members...")

updated_users = []
for user in users.iterator(chunk_size=batch_size):
if not (contribution_data := self._aggregate_user_contributions(user, start_date)):
continue
user.contribution_data = contribution_data
updated_users.append(user)

User.bulk_save(updated_users, fields=["contribution_data"])

self.stdout.write(
self.style.SUCCESS(f"Successfully aggregated contributions for {total_users} members")
)

def _aggregate_user_contributions(self, user: User, start_date: datetime) -> dict[str, int]:
"""Aggregate contributions for a user.

Args:
user: User instance
start_date: Start datetime for aggregation

Returns:
Dictionary mapping YYYY-MM-DD to contribution counts

"""
contribution_data = {}
current_date = start_date.date()
end_date = timezone.now().date()

while current_date <= end_date:
date_str = current_date.strftime("%Y-%m-%d")
contribution_data[date_str] = 0
current_date += timedelta(days=1)

commits = (
Commit.objects.filter(
author=user,
created_at__gte=start_date,
)
.annotate(date=TruncDate("created_at"))
.values("date")
.annotate(count=Count("id"))
)

for commit in commits:
date_str = commit["date"].strftime("%Y-%m-%d")
contribution_data[date_str] = contribution_data.get(date_str, 0) + commit["count"]

prs = (
PullRequest.objects.filter(
author=user,
created_at__gte=start_date,
)
.annotate(date=TruncDate("created_at"))
.values("date")
.annotate(count=Count("id"))
)

for pr in prs:
date_str = pr["date"].strftime("%Y-%m-%d")
contribution_data[date_str] = contribution_data.get(date_str, 0) + pr["count"]

issues = (
Issue.objects.filter(
author=user,
created_at__gte=start_date,
)
.annotate(date=TruncDate("created_at"))
.values("date")
.annotate(count=Count("id"))
)

for issue in issues:
date_str = issue["date"].strftime("%Y-%m-%d")
contribution_data[date_str] = contribution_data.get(date_str, 0) + issue["count"]

return contribution_data
Loading