-
-
Notifications
You must be signed in to change notification settings - Fork 336
feat: Add Contribution Heatmap to Chapter and Project Pages #2674
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
Open
mrkeshav-05
wants to merge
84
commits into
OWASP:main
Choose a base branch
from
mrkeshav-05:feature/contribution-heatmap-chapter-project
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,880
−286
Open
Changes from all commits
Commits
Show all changes
84 commits
Select commit
Hold shift + click to select a range
e2c562e
feat: add contribution_data field to Chapter and Project models
mrkeshav-05 abd2373
Create migration 0066 to add fields to database
mrkeshav-05 5f4613b
add management command to aggregate contributions
mrkeshav-05 c032255
expose contribution_data in GraphQL API
mrkeshav-05 66f003c
refactor: extract contribution date aggregation into a separate metho…
mrkeshav-05 9dd093b
test: add contribution_data to the test's expected field names set.
mrkeshav-05 682de60
test: add unit tests for ChapterNode field resolutions and configurat…
mrkeshav-05 2af3339
test: add unit tests for owasp_aggregate_contributions management com…
mrkeshav-05 4b34607
feat: add contributionData to GraphQL queries
mrkeshav-05 281a28f
feat: update TypeScript types for contribution data
mrkeshav-05 80b14e6
feat: add contributionData field to both project and chapter nodes
mrkeshav-05 bc5251c
fixed linting errors
mrkeshav-05 45d6cc3
fixed failed tests in chapter_test.py
mrkeshav-05 2b2f088
fixed backend tests in owasp_aggregate_contributions_test.py
mrkeshav-05 11c2789
feat: add contribution heatmap to chapter details page
mrkeshav-05 be9dc34
feat: integrate contribution heatmap into project details page
mrkeshav-05 db17c65
feat: enhance contribution heatmap component with isCompact prop
mrkeshav-05 e6553e3
apply variant compact
mrkeshav-05 40aab52
feat: add contribution stats and heatmap to chapter and project detai…
mrkeshav-05 7f760de
make chapter page same as project page
mrkeshav-05 c0d6673
reduce the gap between the cards
mrkeshav-05 a47f4cf
optimize queryset by select_related for owasp_repository
mrkeshav-05 5f00d61
feat: optimize project queryset with select_related and prefetch_related
mrkeshav-05 1a86d98
chapter contribution aggregation tests
mrkeshav-05 3a9e23f
fix: update contribution_data test to assert field type
mrkeshav-05 71c051f
update contribution stats calculation to provide estimated values
mrkeshav-05 080818f
fix: handle null contribution stats in project and chapter details so…
mrkeshav-05 99ec8ea
fixing sonalcloud errors
mrkeshav-05 d5ff8eb
run pnpm lint
mrkeshav-05 1ed2c05
adjust layout and formatting
mrkeshav-05 6a921e1
fixedbackend testcases
mrkeshav-05 a633111
remove redundant code in project and chapter
mrkeshav-05 da65529
make another component for github stats
mrkeshav-05 3cd1725
fixing sonar issues
mrkeshav-05 b1f71f7
fixing tests
mrkeshav-05 e559ae2
remove unused variables
mrkeshav-05 cc8eaae
refactor: improve heatmap series generation and chart options
mrkeshav-05 b70a242
fixing sonar cloud issues
mrkeshav-05 ec33a79
fix make check
mrkeshav-05 7401aed
fixing sonarcloud issues
mrkeshav-05 331908f
handling null/undefined values
mrkeshav-05 c259538
include test identifiers
mrkeshav-05 c06ea65
add tests for ContributionStats component
mrkeshav-05 783d3cc
sort active chapters and projects
mrkeshav-05 76ef770
added trailing whitespace
mrkeshav-05 49d81b4
apply make check
mrkeshav-05 0b0380a
pnpm run lint -- --fix
mrkeshav-05 945bdfc
removing sonarcloud issues with contributionheatmap and contributions…
mrkeshav-05 405163a
add contribution_stats field to Chapter and Project models for github…
mrkeshav-05 c446b1f
add contribution_stats field to Chapter and Project GraphQL nodes
mrkeshav-05 57872e9
add contribution statistics calculation for chapters and projects
mrkeshav-05 f6a8561
add contribution_stats fields to Chapter and Project migrations
mrkeshav-05 a8c93b1
add contributionStats field to Project type
mrkeshav-05 e364054
add contributionStats field to Chapter type
mrkeshav-05 2e69524
run pnpm run --graphql-codegen to update the generated types
mrkeshav-05 bc6d2f7
add contributionStats field to GET_CHAPTER_DATA and GET_PROJECT_DATA …
mrkeshav-05 30244d8
create contributionDataUtils file for handling contribution stats logic
mrkeshav-05 f3cde9f
refactor: replace estimeted contribution stats with actual
mrkeshav-05 671d686
added tests for contributionStats field
mrkeshav-05 ec2519c
Run pre-commit
mrkeshav-05 29b966b
add .slice().reverse() for compatibility
mrkeshav-05 d8e8166
resolve some issues by coderabbitai
mrkeshav-05 ff4b67d
make check
mrkeshav-05 677ac8a
made the contributionheatmap responsive for both project and chapter …
mrkeshav-05 3955af7
fixed failed testcases
mrkeshav-05 939ebe6
fixing contributionheatmap for tests
mrkeshav-05 fd6f425
run pnpm run test:unit
mrkeshav-05 4254e11
make check
mrkeshav-05 c455613
fixing sonarcloud issues
mrkeshav-05 e7396c3
adjust heatmap chart dimensions for better responsiveness
mrkeshav-05 4634c03
update command in backend/Makefile to aggregate contributions
mrkeshav-05 ab0ae8d
change makefile to add owasp-aggregate-contributions target
mrkeshav-05 db546da
regenerate owasp migration 0067_chapter_contribution_stats_and_more
mrkeshav-05 9c8f362
refactor migration 0067 to improve code readability and formatting
mrkeshav-05 44a0c47
fix: update date in ProgramCard test to reflect correct end date
mrkeshav-05 2f20caa
style: update padding and heading size in CardDetailsPage test
mrkeshav-05 19278c3
fix: update ContributionHeatmap test to reflect correct class names a…
mrkeshav-05 71ee147
test: add variant rendering tests for ContributionHeatmap (coderabbitai)
mrkeshav-05 adb0258
fixing the test for programCard
mrkeshav-05 696b32a
added contribution_stats field to ProjectNode and ChapterNode
mrkeshav-05 28c226b
fix: optimize chapter and project queryset with select_related and pr…
mrkeshav-05 c5a73e6
Update code
arkid15r 77f9b1b
update contributionsStats
mrkeshav-05 9be1f3f
update frontend code
mrkeshav-05 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
Some comments aren't visible on the classic Files Changed page.
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
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
262 changes: 262 additions & 0 deletions
262
backend/apps/owasp/management/commands/owasp_aggregate_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,262 @@ | ||
| """Management command to aggregate contributions for chapters and projects.""" | ||
|
|
||
| from datetime import datetime, timedelta | ||
|
|
||
| from django.core.management.base import BaseCommand | ||
| 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.release import Release | ||
| from apps.owasp.models.chapter import Chapter | ||
| from apps.owasp.models.project import Project | ||
|
|
||
|
|
||
| class Command(BaseCommand): | ||
| """Aggregate contribution data for chapters and projects.""" | ||
|
|
||
| help = "Aggregate contributions (commits, issues, PRs, releases) for chapters and projects" | ||
|
|
||
| def add_arguments(self, parser): | ||
| """Add command arguments.""" | ||
| parser.add_argument( | ||
| "--entity-type", | ||
| choices=["chapter", "project"], | ||
| help="Entity type to aggregate: chapter, project", | ||
| required=True, | ||
| type=str, | ||
| ) | ||
| parser.add_argument( | ||
| "--days", | ||
| default=365, | ||
| help="Number of days to look back for contributions (default: 365)", | ||
| type=int, | ||
| ) | ||
| parser.add_argument( | ||
| "--key", | ||
| help="Specific chapter or project key to aggregate", | ||
| type=str, | ||
| ) | ||
| parser.add_argument( | ||
| "--offset", | ||
| default=0, | ||
| help="Skip the first N entities", | ||
| type=int, | ||
| ) | ||
|
|
||
| def _aggregate_contribution_dates( | ||
| self, | ||
| queryset, | ||
| date_field: str, | ||
| contribution_map: dict[str, int], | ||
| ) -> None: | ||
| """Aggregate contribution dates from a queryset into the contribution map. | ||
|
|
||
| Args: | ||
| queryset: Django queryset to aggregate | ||
| date_field: Name of the date field to aggregate on | ||
| contribution_map: Dictionary to update with counts | ||
|
|
||
| """ | ||
| for date_value in queryset.values_list(date_field, flat=True): | ||
| if not date_value: | ||
| continue | ||
|
|
||
| date_key = date_value.date().isoformat() | ||
| contribution_map[date_key] = contribution_map.get(date_key, 0) + 1 | ||
|
|
||
| def _get_repository_ids(self, entity): | ||
| """Extract repository IDs from chapter or project.""" | ||
| repo_ids: set[int] = set() | ||
|
|
||
| # Handle single owasp_repository | ||
| if hasattr(entity, "owasp_repository") and entity.owasp_repository: | ||
| repo_ids.add(entity.owasp_repository.id) | ||
| # Handle multiple repositories (for projects) | ||
| if hasattr(entity, "repositories"): | ||
| repo_ids.update([r.id for r in entity.repositories.all()]) | ||
|
|
||
| return list(repo_ids) | ||
|
|
||
| def aggregate_contributions(self, entity, start_date: datetime) -> dict[str, int]: | ||
| """Aggregate contributions for a chapter or project. | ||
|
|
||
| Args: | ||
| entity: Chapter or Project instance | ||
| start_date: Start date for aggregation | ||
|
|
||
| Returns: | ||
| Dictionary mapping YYYY-MM-DD to contribution count | ||
|
|
||
| """ | ||
| contribution_map: dict[str, int] = {} | ||
|
|
||
| repo_ids = self._get_repository_ids(entity) | ||
| if not repo_ids: | ||
| return contribution_map | ||
|
|
||
| # Aggregate commits | ||
| self._aggregate_contribution_dates( | ||
| Commit.objects.filter( | ||
| repository_id__in=repo_ids, | ||
| created_at__gte=start_date, | ||
| ), | ||
| "created_at", | ||
| contribution_map, | ||
| ) | ||
|
|
||
| # Aggregate issues | ||
| self._aggregate_contribution_dates( | ||
| Issue.objects.filter( | ||
| repository_id__in=repo_ids, | ||
| created_at__gte=start_date, | ||
| ), | ||
| "created_at", | ||
| contribution_map, | ||
| ) | ||
|
|
||
| # Aggregate pull requests | ||
| self._aggregate_contribution_dates( | ||
| PullRequest.objects.filter( | ||
| repository_id__in=repo_ids, | ||
| created_at__gte=start_date, | ||
| ), | ||
| "created_at", | ||
| contribution_map, | ||
| ) | ||
|
|
||
| # Aggregate releases | ||
| self._aggregate_contribution_dates( | ||
| Release.objects.filter( | ||
| repository_id__in=repo_ids, | ||
| published_at__gte=start_date, | ||
| is_draft=False, | ||
| ), | ||
| "published_at", | ||
| contribution_map, | ||
| ) | ||
|
|
||
| return contribution_map | ||
|
|
||
| def calculate_contribution_stats(self, entity, start_date: datetime) -> dict[str, int]: | ||
| """Calculate contribution statistics for a chapter or project. | ||
|
|
||
| Args: | ||
| entity: Chapter or Project instance | ||
| start_date: Start date for calculation | ||
|
|
||
| Returns: | ||
| Dictionary with commits, issues, pull requests, releases counts | ||
|
|
||
| """ | ||
| stats = { | ||
| "commits": 0, | ||
| "issues": 0, | ||
| "pullRequests": 0, | ||
| "releases": 0, | ||
| "total": 0, | ||
| } | ||
|
|
||
| repo_ids = self._get_repository_ids(entity) | ||
| if not repo_ids: | ||
| return stats | ||
|
|
||
| # Count commits | ||
| stats["commits"] = Commit.objects.filter( | ||
| repository_id__in=repo_ids, | ||
| created_at__gte=start_date, | ||
| ).count() | ||
|
|
||
| # Count issues | ||
| stats["issues"] = Issue.objects.filter( | ||
| repository_id__in=repo_ids, | ||
| created_at__gte=start_date, | ||
| ).count() | ||
|
|
||
| # Count pullRequests | ||
| stats["pullRequests"] = PullRequest.objects.filter( | ||
| repository_id__in=repo_ids, | ||
| created_at__gte=start_date, | ||
| ).count() | ||
|
|
||
| # Count releases | ||
| stats["releases"] = Release.objects.filter( | ||
| repository_id__in=repo_ids, | ||
| published_at__gte=start_date, | ||
| is_draft=False, | ||
| ).count() | ||
|
|
||
| stats["total"] = ( | ||
| stats["commits"] + stats["issues"] + stats["pullRequests"] + stats["releases"] | ||
| ) | ||
|
|
||
| return stats | ||
|
|
||
| def handle(self, *args, **options): | ||
| """Execute the command.""" | ||
| entity_type = options["entity_type"] | ||
| days = options["days"] | ||
| key = options.get("key") | ||
| offset = options["offset"] | ||
|
|
||
| start_date = timezone.now() - timedelta(days=days) | ||
|
|
||
| self.stdout.write( | ||
| self.style.SUCCESS( | ||
| f"Aggregating contributions from {start_date.date()} ({days} days back)", | ||
| ), | ||
| ) | ||
|
|
||
| if entity_type == "chapter": | ||
| self._process_chapters(start_date, key, offset) | ||
| elif entity_type == "project": | ||
| self._process_projects(start_date, key, offset) | ||
|
|
||
| self.stdout.write(self.style.SUCCESS("Done!")) | ||
|
|
||
| def _process_chapters(self, start_date, key, offset): | ||
| """Process chapters for contribution aggregation.""" | ||
| queryset = Chapter.objects.filter(is_active=True).order_by("id") | ||
|
|
||
| if key: | ||
| queryset = queryset.filter(key=key) | ||
|
|
||
| queryset = queryset.select_related("owasp_repository") | ||
|
|
||
| if offset: | ||
| queryset = queryset[offset:] | ||
|
|
||
| self._process_entities(queryset, start_date, "chapters", Chapter) | ||
|
|
||
| def _process_projects(self, start_date, key, offset): | ||
| """Process projects for contribution aggregation.""" | ||
| queryset = ( | ||
| Project.objects.filter(is_active=True) | ||
| .order_by("id") | ||
| .select_related("owasp_repository") | ||
| .prefetch_related("repositories") | ||
| ) | ||
|
|
||
| if key: | ||
| queryset = queryset.filter(key=key) | ||
|
|
||
| if offset: | ||
| queryset = queryset[offset:] | ||
|
|
||
| self._process_entities(queryset, start_date, "projects", Project) | ||
|
|
||
| def _process_entities(self, queryset, start_date, label, model_class): | ||
| """Process entities (chapters or projects) for contribution aggregation.""" | ||
| count = queryset.count() | ||
| self.stdout.write(f"Processing {count} {label}...") | ||
|
|
||
| entities = [] | ||
| for entity in queryset: | ||
| entity.contribution_data = self.aggregate_contributions(entity, start_date) | ||
| entity.contribution_stats = self.calculate_contribution_stats(entity, start_date) | ||
| entities.append(entity) | ||
|
|
||
| if entities: | ||
| model_class.bulk_save(entities, fields=("contribution_data", "contribution_stats")) | ||
| self.stdout.write(self.style.SUCCESS(f"Updated {count} {label}")) |
32 changes: 32 additions & 0 deletions
32
backend/apps/owasp/migrations/0066_chapter_contribution_data_project_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,32 @@ | ||
| # Generated by Django 5.2.8 on 2025-11-16 18:18 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("owasp", "0065_memberprofile_linkedin_page_id"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="chapter", | ||
| name="contribution_data", | ||
| field=models.JSONField( | ||
| blank=True, | ||
| default=dict, | ||
| help_text="Daily contribution counts (YYYY-MM-DD -> count mapping)", | ||
| verbose_name="Contribution Data", | ||
| ), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="project", | ||
| name="contribution_data", | ||
| field=models.JSONField( | ||
| blank=True, | ||
| default=dict, | ||
| help_text="Daily contribution counts (YYYY-MM-DD -> count mapping)", | ||
| verbose_name="Contribution Data", | ||
| ), | ||
| ), | ||
| ] |
32 changes: 32 additions & 0 deletions
32
backend/apps/owasp/migrations/0067_chapter_contribution_stats_and_more.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,32 @@ | ||
| # Generated by Django 5.2.8 on 2025-11-29 19:46 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("owasp", "0066_chapter_contribution_data_project_contribution_data"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="chapter", | ||
| name="contribution_stats", | ||
| field=models.JSONField( | ||
| blank=True, | ||
| default=dict, | ||
| help_text="Detailed contribution breakdown (commits, issues, pullRequests, releases)", | ||
| verbose_name="Contribution Statistics", | ||
| ), | ||
| ), | ||
| migrations.AddField( | ||
| model_name="project", | ||
| name="contribution_stats", | ||
| field=models.JSONField( | ||
| blank=True, | ||
| default=dict, | ||
| help_text="Detailed contribution breakdown (commits, issues, pullRequests, releases)", | ||
| verbose_name="Contribution Statistics", | ||
| ), | ||
| ), | ||
| ] |
32 changes: 32 additions & 0 deletions
32
backend/apps/owasp/migrations/0068_alter_chapter_contribution_stats_and_more.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,32 @@ | ||
| # Generated by Django 6.0 on 2025-12-10 04:11 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("owasp", "0067_chapter_contribution_stats_and_more"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AlterField( | ||
| model_name="chapter", | ||
| name="contribution_stats", | ||
| field=models.JSONField( | ||
| blank=True, | ||
| default=dict, | ||
| help_text="Detailed contribution breakdown (commits, issues, pull requests, releases)", | ||
| verbose_name="Contribution Statistics", | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="project", | ||
| name="contribution_stats", | ||
| field=models.JSONField( | ||
| blank=True, | ||
| default=dict, | ||
| help_text="Detailed contribution breakdown (commits, issues, pull requests, releases)", | ||
| verbose_name="Contribution Statistics", | ||
| ), | ||
| ), | ||
| ] |
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.
Uh oh!
There was an error while loading. Please reload this page.