-
-
Notifications
You must be signed in to change notification settings - Fork 532
Add reusable Contribution Heatmap to member page for design consistency #3298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
arkid15r
merged 26 commits into
OWASP:main
from
mrkeshav-05:fix/member-contribution-heatmap
Jan 28, 2026
Merged
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 7953acb
update code
mrkeshav-05 ba086d0
code-rabbit
mrkeshav-05 2c4805a
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 3b573b1
Merge remote-tracking branch 'upstream/main' into fix/member-contribu…
mrkeshav-05 e7a27ed
minor fix
mrkeshav-05 4416f14
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 0278be4
minor fix
mrkeshav-05 e34cf02
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 0cbb0b4
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 3efb649
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 867a67b
update DB volume and rename command
mrkeshav-05 dd605d2
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 0f8fbb6
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 ac7bfe7
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 45a16f4
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 c4dc08d
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 1fc8025
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 681a02b
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 65f0aa5
Merge remote-tracking branch 'upstream/main' into fix/member-contribu…
mrkeshav-05 a1d06ff
Merge branch 'main' into fix/member-contribution-heatmap
mrkeshav-05 15df789
Merge branch 'main' into pr/mrkeshav-05/3298
arkid15r a11db4a
Update code
arkid15r 9e48d05
Update backend/Makefile
arkid15r 0a83697
Update docker-compose/local/compose.yaml
arkid15r 1c93f78
Update volumes
arkid15r File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
backend/apps/github/migrations/0041_user_contribution_data.py
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, I have changed the volumes to |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
12
backend/apps/github/migrations/0042_merge_20260127_2218.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 = [] |
23 changes: 23 additions & 0 deletions
23
backend/apps/github/migrations/0043_alter_user_contribution_data.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
| ), | ||
| ), | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
140 changes: 140 additions & 0 deletions
140
backend/apps/owasp/management/commands/owasp_aggregate_member_contributions.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"] | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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"] | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return contribution_data | ||
142 changes: 142 additions & 0 deletions
142
backend/apps/owasp/management/commands/owasp_aggregate_member_contributions.py.bak
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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_statslogic into a shared Mixin (e.g., ContributionStatsMixin) located inapps/core. I will then updateUserNode,ProjectNode, andChapterNodeto inherit from it.Let me know if this sounds good.