From e9e22880660af3bd554bddbbb9447cfdce8103e7 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Mon, 21 Apr 2025 15:02:23 +0200 Subject: [PATCH 01/56] Add Milestone model and OpenMilestoneManager --- .../apps/github/models/managers/milestone.py | 11 ++ backend/apps/github/models/milestone.py | 107 ++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 backend/apps/github/models/managers/milestone.py create mode 100644 backend/apps/github/models/milestone.py diff --git a/backend/apps/github/models/managers/milestone.py b/backend/apps/github/models/managers/milestone.py new file mode 100644 index 0000000000..b7cf8ec50c --- /dev/null +++ b/backend/apps/github/models/managers/milestone.py @@ -0,0 +1,11 @@ +"""Github app milestone manager.""" + +from django.db import models + + +class OpenMilestoneManager(models.Manager): + """Open milestone manager.""" + + def get_queryset(self): + """Get open milestones.""" + return super().get_queryset().filter(state="open") diff --git a/backend/apps/github/models/milestone.py b/backend/apps/github/models/milestone.py new file mode 100644 index 0000000000..b1f297413d --- /dev/null +++ b/backend/apps/github/models/milestone.py @@ -0,0 +1,107 @@ +"""Gitub app Milestone model.""" + +from django.db import models + +from apps.common.models import BulkSaveModel +from apps.github.models.generic_issue_model import GenericIssueModel +from apps.github.models.managers.milestone import OpenMilestoneManager + + +class Milestone(GenericIssueModel): + """GitHub Milestone model.""" + + objects = models.Manager() + open_milestones = OpenMilestoneManager() + + class Meta: + db_table = "github_milestone" + verbose_name_plural = "Milestones" + ordering = ["-updated_at", "-state"] + + open_issues_count = models.PositiveIntegerField(default=0) + closed_issues_count = models.PositiveIntegerField(default=0) + due_on = models.DateTimeField(blank=True, null=True) + + repository = models.ForeignKey( + "Repository", + on_delete=models.CASCADE, + related_name="milestones", + blank=True, + null=True, + ) + + author = models.ForeignKey( + "User", + on_delete=models.CASCADE, + related_name="milestones", + blank=True, + null=True, + ) + + labels = models.ManyToManyField( + "Label", + related_name="milestones", + blank=True, + ) + + def __str__(self) -> str: + """Return string representation of Milestone.""" + return f"Milestone #{self.number} - {self.title}" + + def from_github(self, gh_milestone: dict, author=None, repository=None) -> None: + """Populate Milestone from GitHub API response. + + Args: + gh_milestone (dict): GitHub milestone data. + author (User, optional): Author of the milestone. Defaults to None. + repository (Repository, optional): Repository of the milestone. Defaults to None. + + """ + field_mapping = { + "url": "html_url", + "body": "description", + "number": "number", + "title": "title", + "state": "state", + "created_at": "created_at", + "updated_at": "updated_at", + "closed_at": "closed_at", + "due_on": "due_on", + "open_issues_count": "open_issues", + "closed_issues_count": "closed_issues", + } + + for model_field, gh_field in field_mapping.items(): + value = getattr(gh_milestone, gh_field) + if value is not None: + setattr(self, model_field, value) + + self.author = author + self.repository = repository + + @staticmethod + def bulk_save(milestones, fields=None): + """Bulk save milestones.""" + BulkSaveModel.bulk_save(Milestone, milestones, fields=fields) + + @staticmethod + def update_data(gh_milestone, author=None, repository=None, save=True): + """Update Milestone data. + + Args: + gh_milestone (github.Milestone.Milestone): GitHub milestone object. + author (User, optional): Author of the issue. Defaults to None. + repository (Repository, optional): Repository of the issue. Defaults to None. + save (bool, optional): Whether to save the instance after updating. Defaults to True. + + """ + milestone_node_id = Milestone.get_node_id(gh_milestone) + try: + milestone = Milestone.objects.get(node_id=milestone_node_id) + except Milestone.DoesNotExist: + milestone = Milestone(node_id=milestone_node_id) + + milestone.from_github(gh_milestone, author, repository) + if save: + milestone.save() + return milestone From bf32c6b8a3f48e6dea09a58ddf78831bce08870b Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Mon, 21 Apr 2025 15:29:22 +0200 Subject: [PATCH 02/56] Add MilestoneNode and MilestoneQuery for GitHub milestones --- .../apps/github/graphql/nodes/milestone.py | 20 +++++ .../apps/github/graphql/queries/milestone.py | 87 +++++++++++++++++++ .../apps/github/models/managers/milestone.py | 8 ++ backend/apps/github/models/milestone.py | 3 +- 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 backend/apps/github/graphql/nodes/milestone.py create mode 100644 backend/apps/github/graphql/queries/milestone.py diff --git a/backend/apps/github/graphql/nodes/milestone.py b/backend/apps/github/graphql/nodes/milestone.py new file mode 100644 index 0000000000..6a1e81a500 --- /dev/null +++ b/backend/apps/github/graphql/nodes/milestone.py @@ -0,0 +1,20 @@ +"""Github Milestone Node.""" + +from apps.common.graphql.nodes import BaseNode +from apps.github.models.milestone import Milestone + + +class MilestoneNode(BaseNode): + """Github Milestone Node.""" + + class Meta: + model = Milestone + + fields = ( + "author", + "created_at", + "state", + "title", + "open_issues_count", + "closed_issues_count", + ) diff --git a/backend/apps/github/graphql/queries/milestone.py b/backend/apps/github/graphql/queries/milestone.py new file mode 100644 index 0000000000..67a35772f4 --- /dev/null +++ b/backend/apps/github/graphql/queries/milestone.py @@ -0,0 +1,87 @@ +"""Github Milestone Queries.""" + +import graphene + +from apps.github.graphql.nodes.milestone import MilestoneNode +from apps.github.graphql.queries import BaseQuery +from apps.github.models.milestone import Milestone + + +class MilestoneQuery(BaseQuery): + """Github Milestone Queries.""" + + open_milestones = graphene.List( + MilestoneNode, + limit=graphene.Int(default_value=5), + login=graphene.String(required=False), + organization=graphene.String(required=False), + distinct=graphene.Boolean(default_value=False), + ) + + closed_milestones = graphene.List( + MilestoneNode, + limit=graphene.Int(default_value=5), + login=graphene.String(required=False), + organization=graphene.String(required=False), + distinct=graphene.Boolean(default_value=False), + ) + + def resolve_open_milestones(root, info, limit, login=None, organization=None, distinct=False): + """Resolve open milestones. + + Args: + root (object): The root object. + info (ResolveInfo): The GraphQL execution context. + limit (int): The maimum number of milestones to return. + login (str, optional): Filter milestones by author login. + organization (str, optional): Filter milestones by organization login. + distinct (bool, optional): Whether to return distinct milestones. + + Returns: + list: A list of open milestones. + + """ + open_milestones = Milestone.open_milestones.select_related( + "author", + "repository", + "repository__organization", + ) + + if login: + open_milestones = open_milestones.filter(author__login=login) + if organization: + open_milestones = open_milestones.filter(repository__organization__login=organization) + + return open_milestones[:limit] + + def resolve_closed_milestones( + root, info, limit, login=None, organization=None, distinct=False + ): + """Resolve closed milestones. + + Args: + root (object): The root object. + info (ResolveInfo): The GraphQL execution context. + limit (int): The maimum number of milestones to return. + login (str, optional): Filter milestones by author login. + organization (str, optional): Filter milestones by organization login. + distinct (bool, optional): Whether to return distinct milestones. + + Returns: + list: A list of closed milestones. + + """ + closed_milestones = Milestone.closed_milestones.select_related( + "author", + "repository", + "repository__organization", + ) + + if login: + closed_milestones = closed_milestones.filter(author__login=login) + if organization: + closed_milestones = closed_milestones.filter( + repository__organization__login=organization + ) + + return closed_milestones[:limit] diff --git a/backend/apps/github/models/managers/milestone.py b/backend/apps/github/models/managers/milestone.py index b7cf8ec50c..5b838b3dd7 100644 --- a/backend/apps/github/models/managers/milestone.py +++ b/backend/apps/github/models/managers/milestone.py @@ -9,3 +9,11 @@ class OpenMilestoneManager(models.Manager): def get_queryset(self): """Get open milestones.""" return super().get_queryset().filter(state="open") + + +class ClosedMilestoneManager(models.Manager): + """Closed milestone manager.""" + + def get_queryset(self): + """Get closed milestones.""" + return super().get_queryset().filter(state="closed") diff --git a/backend/apps/github/models/milestone.py b/backend/apps/github/models/milestone.py index b1f297413d..8a3b8d8ed6 100644 --- a/backend/apps/github/models/milestone.py +++ b/backend/apps/github/models/milestone.py @@ -4,7 +4,7 @@ from apps.common.models import BulkSaveModel from apps.github.models.generic_issue_model import GenericIssueModel -from apps.github.models.managers.milestone import OpenMilestoneManager +from apps.github.models.managers.milestone import ClosedMilestoneManager, OpenMilestoneManager class Milestone(GenericIssueModel): @@ -12,6 +12,7 @@ class Milestone(GenericIssueModel): objects = models.Manager() open_milestones = OpenMilestoneManager() + closed_milestones = ClosedMilestoneManager() class Meta: db_table = "github_milestone" From 84d3f19494c136b939bff63e16be473e8e7b8e9a Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Mon, 21 Apr 2025 16:24:27 +0200 Subject: [PATCH 03/56] Fix docstrings --- backend/apps/github/models/milestone.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/backend/apps/github/models/milestone.py b/backend/apps/github/models/milestone.py index 8a3b8d8ed6..db90458fc6 100644 --- a/backend/apps/github/models/milestone.py +++ b/backend/apps/github/models/milestone.py @@ -24,7 +24,7 @@ class Meta: due_on = models.DateTimeField(blank=True, null=True) repository = models.ForeignKey( - "Repository", + "github.Repository", on_delete=models.CASCADE, related_name="milestones", blank=True, @@ -32,7 +32,7 @@ class Meta: ) author = models.ForeignKey( - "User", + "github.User", on_delete=models.CASCADE, related_name="milestones", blank=True, @@ -40,20 +40,16 @@ class Meta: ) labels = models.ManyToManyField( - "Label", + "github.Label", related_name="milestones", blank=True, ) - def __str__(self) -> str: - """Return string representation of Milestone.""" - return f"Milestone #{self.number} - {self.title}" - - def from_github(self, gh_milestone: dict, author=None, repository=None) -> None: + def from_github(self, gh_milestone, author=None, repository=None) -> None: """Populate Milestone from GitHub API response. Args: - gh_milestone (dict): GitHub milestone data. + gh_milestone (github.Milestone.Milestone): GitHub milestone object. author (User, optional): Author of the milestone. Defaults to None. repository (Repository, optional): Repository of the milestone. Defaults to None. From fbf7375f7f4a77bdc26ddbd1e22ef707ddd3e183 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Mon, 21 Apr 2025 16:46:27 +0200 Subject: [PATCH 04/56] Add Milestone model migration and update model imports --- .../apps/github/migrations/0024_milestone.py | 84 +++++++++++++++++++ backend/apps/github/models/__init__.py | 1 + backend/apps/github/models/milestone.py | 4 + 3 files changed, 89 insertions(+) create mode 100644 backend/apps/github/migrations/0024_milestone.py diff --git a/backend/apps/github/migrations/0024_milestone.py b/backend/apps/github/migrations/0024_milestone.py new file mode 100644 index 0000000000..c07442fe34 --- /dev/null +++ b/backend/apps/github/migrations/0024_milestone.py @@ -0,0 +1,84 @@ +# Generated by Django 5.2 on 2025-04-21 14:45 + +import django.db.models.deletion +from django.db import migrations, models + +import apps.github.models.mixins.issue + + +class Migration(migrations.Migration): + dependencies = [ + ("github", "0023_alter_user_contributions_count"), + ] + + operations = [ + migrations.CreateModel( + name="Milestone", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("nest_created_at", models.DateTimeField(auto_now_add=True)), + ("nest_updated_at", models.DateTimeField(auto_now=True)), + ("node_id", models.CharField(unique=True, verbose_name="Node ID")), + ("title", models.CharField(max_length=1000, verbose_name="Title")), + ("body", models.TextField(default="", verbose_name="Body")), + ( + "state", + models.CharField( + choices=[("open", "Open"), ("closed", "Closed")], + default="open", + max_length=6, + verbose_name="State", + ), + ), + ("url", models.URLField(default="", max_length=500, verbose_name="URL")), + ("number", models.PositiveBigIntegerField(default=0, verbose_name="Number")), + ("sequence_id", models.PositiveBigIntegerField(default=0, verbose_name="ID")), + ( + "closed_at", + models.DateTimeField(blank=True, null=True, verbose_name="Closed at"), + ), + ("created_at", models.DateTimeField(verbose_name="Created at")), + ("updated_at", models.DateTimeField(db_index=True, verbose_name="Updated at")), + ("open_issues_count", models.PositiveIntegerField(default=0)), + ("closed_issues_count", models.PositiveIntegerField(default=0)), + ("due_on", models.DateTimeField(blank=True, null=True)), + ( + "author", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="milestones", + to="github.user", + ), + ), + ( + "labels", + models.ManyToManyField( + blank=True, related_name="milestones", to="github.label" + ), + ), + ( + "repository", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="milestones", + to="github.repository", + ), + ), + ], + options={ + "verbose_name_plural": "Milestones", + "db_table": "github_milestone", + "ordering": ["-updated_at", "-state"], + }, + bases=(apps.github.models.mixins.issue.IssueIndexMixin, models.Model), + ), + ] diff --git a/backend/apps/github/models/__init__.py b/backend/apps/github/models/__init__.py index 8a74e83367..7d14cf7128 100644 --- a/backend/apps/github/models/__init__.py +++ b/backend/apps/github/models/__init__.py @@ -1,3 +1,4 @@ """Github app.""" +from .milestone import Milestone from .pull_request import PullRequest diff --git a/backend/apps/github/models/milestone.py b/backend/apps/github/models/milestone.py index db90458fc6..3699a0f127 100644 --- a/backend/apps/github/models/milestone.py +++ b/backend/apps/github/models/milestone.py @@ -76,6 +76,10 @@ def from_github(self, gh_milestone, author=None, repository=None) -> None: self.author = author self.repository = repository + def save(self, *args, **kwargs): + """Save Milestone.""" + super().save(*args, **kwargs) + @staticmethod def bulk_save(milestones, fields=None): """Bulk save milestones.""" From efa926e9269203192051b0a666890af72ce7051b Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Mon, 21 Apr 2025 16:56:19 +0200 Subject: [PATCH 05/56] Fix spelling --- backend/apps/github/graphql/queries/milestone.py | 4 ++-- backend/apps/github/models/milestone.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/apps/github/graphql/queries/milestone.py b/backend/apps/github/graphql/queries/milestone.py index 67a35772f4..d7be0e92c5 100644 --- a/backend/apps/github/graphql/queries/milestone.py +++ b/backend/apps/github/graphql/queries/milestone.py @@ -32,7 +32,7 @@ def resolve_open_milestones(root, info, limit, login=None, organization=None, di Args: root (object): The root object. info (ResolveInfo): The GraphQL execution context. - limit (int): The maimum number of milestones to return. + limit (int): The maximum number of milestones to return. login (str, optional): Filter milestones by author login. organization (str, optional): Filter milestones by organization login. distinct (bool, optional): Whether to return distinct milestones. @@ -62,7 +62,7 @@ def resolve_closed_milestones( Args: root (object): The root object. info (ResolveInfo): The GraphQL execution context. - limit (int): The maimum number of milestones to return. + limit (int): The maximum number of milestones to return. login (str, optional): Filter milestones by author login. organization (str, optional): Filter milestones by organization login. distinct (bool, optional): Whether to return distinct milestones. diff --git a/backend/apps/github/models/milestone.py b/backend/apps/github/models/milestone.py index 3699a0f127..9be7e27106 100644 --- a/backend/apps/github/models/milestone.py +++ b/backend/apps/github/models/milestone.py @@ -1,4 +1,4 @@ -"""Gitub app Milestone model.""" +"""Github app Milestone model.""" from django.db import models From 817bdeaaad0f939a889b255ab799c50d031d3307 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 24 Apr 2025 14:19:08 +0200 Subject: [PATCH 06/56] Merge migrations --- .../github/migrations/0025_merge_20250424_1217.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 backend/apps/github/migrations/0025_merge_20250424_1217.py diff --git a/backend/apps/github/migrations/0025_merge_20250424_1217.py b/backend/apps/github/migrations/0025_merge_20250424_1217.py new file mode 100644 index 0000000000..3b80139867 --- /dev/null +++ b/backend/apps/github/migrations/0025_merge_20250424_1217.py @@ -0,0 +1,12 @@ +# Generated by Django 5.2 on 2025-04-24 12:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("github", "0024_alter_organization_email_alter_organization_location_and_more"), + ("github", "0024_milestone"), + ] + + operations = [] From b8c3df0a9f6c8c172fb7b4eae69b2c0d676d2525 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 24 Apr 2025 14:35:50 +0200 Subject: [PATCH 07/56] Add milestone field to Issue and PullRequest models --- ...6_issue_milestone_pullrequest_milestone.py | 35 +++++++++++++++++++ backend/apps/github/models/issue.py | 18 ++++++++-- backend/apps/github/models/pull_request.py | 20 +++++++++-- 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 backend/apps/github/migrations/0026_issue_milestone_pullrequest_milestone.py diff --git a/backend/apps/github/migrations/0026_issue_milestone_pullrequest_milestone.py b/backend/apps/github/migrations/0026_issue_milestone_pullrequest_milestone.py new file mode 100644 index 0000000000..7d8280af71 --- /dev/null +++ b/backend/apps/github/migrations/0026_issue_milestone_pullrequest_milestone.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2 on 2025-04-24 12:35 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("github", "0025_merge_20250424_1217"), + ] + + operations = [ + migrations.AddField( + model_name="issue", + name="milestone", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="issues", + to="github.milestone", + ), + ), + migrations.AddField( + model_name="pullrequest", + name="milestone", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="pull_requests", + to="github.milestone", + ), + ), + ] diff --git a/backend/apps/github/models/issue.py b/backend/apps/github/models/issue.py index 71a32fc922..3acbc1660f 100644 --- a/backend/apps/github/models/issue.py +++ b/backend/apps/github/models/issue.py @@ -59,6 +59,13 @@ class Meta: null=True, related_name="issues", ) + milestone = models.ForeignKey( + "github.Milestone", + on_delete=models.CASCADE, + blank=True, + null=True, + related_name="issues", + ) # M2Ms. assignees = models.ManyToManyField( @@ -74,13 +81,14 @@ class Meta: blank=True, ) - def from_github(self, gh_issue, author=None, repository=None): + def from_github(self, gh_issue, author=None, repository=None, milestone=None): """Update the instance based on GitHub issue data. Args: gh_issue (github.Issue.Issue): The GitHub issue object. author (User, optional): The author of the issue. repository (Repository, optional): The repository instance. + milestone (Milestone, optional): The milestone related to the issue. """ field_mapping = { @@ -111,6 +119,9 @@ def from_github(self, gh_issue, author=None, repository=None): # Repository. self.repository = repository + # Milestone. + self.milestone = milestone + def generate_hint(self, open_ai=None, max_tokens=1000): """Generate a hint for the issue using AI. @@ -172,13 +183,14 @@ def open_issues_count(): return IndexBase.get_total_count("issues") @staticmethod - def update_data(gh_issue, author=None, repository=None, save=True): + def update_data(gh_issue, author=None, repository=None, milestone=None, save=True): """Update issue data. Args: gh_issue (github.Issue.Issue): The GitHub issue object. author (User, optional): The author of the issue. repository (Repository, optional): The repository instance. + milestone (Milestone, optional): The milestone related to the issue. save (bool, optional): Whether to save the instance. Returns: @@ -191,7 +203,7 @@ def update_data(gh_issue, author=None, repository=None, save=True): except Issue.DoesNotExist: issue = Issue(node_id=issue_node_id) - issue.from_github(gh_issue, author=author, repository=repository) + issue.from_github(gh_issue, author=author, repository=repository, milestone=milestone) if save: issue.save() diff --git a/backend/apps/github/models/pull_request.py b/backend/apps/github/models/pull_request.py index 778350eced..d5c6ff7ca0 100644 --- a/backend/apps/github/models/pull_request.py +++ b/backend/apps/github/models/pull_request.py @@ -40,6 +40,13 @@ class Meta: null=True, related_name="pull_requests", ) + milestone = models.ForeignKey( + "github.Milestone", + on_delete=models.CASCADE, + blank=True, + null=True, + related_name="pull_requests", + ) # M2Ms. assignees = models.ManyToManyField( @@ -55,13 +62,14 @@ class Meta: blank=True, ) - def from_github(self, gh_pull_request, author=None, repository=None): + def from_github(self, gh_pull_request, author=None, repository=None, milestone=None): """Update the instance based on GitHub pull request data. Args: gh_pull_request (github.PullRequest.PullRequest): The GitHub pull request object. author (User, optional): The author of the pull request. repository (Repository, optional): The repository instance. + milestone (Milestone, optional): The milestone related to the pull request. """ field_mapping = { @@ -89,6 +97,9 @@ def from_github(self, gh_pull_request, author=None, repository=None): # Repository. self.repository = repository + # Milestone. + self.milestone = milestone + def save(self, *args, **kwargs): """Save Pull Request.""" super().save(*args, **kwargs) @@ -99,13 +110,14 @@ def bulk_save(pull_requests, fields=None): BulkSaveModel.bulk_save(PullRequest, pull_requests, fields=fields) @staticmethod - def update_data(gh_pull_request, author=None, repository=None, save=True): + def update_data(gh_pull_request, author=None, repository=None, milestone=None, save=True): """Update pull request data. Args: gh_pull_request (github.PullRequest.PullRequest): The GitHub pull request object. author (User, optional): The author of the pull request. repository (Repository, optional): The repository instance. + milestone (Milestone, optional): The milestone related to the pull request. save (bool, optional): Whether to save the instance. Returns: @@ -118,7 +130,9 @@ def update_data(gh_pull_request, author=None, repository=None, save=True): except PullRequest.DoesNotExist: pull_request = PullRequest(node_id=pull_request_node_id) - pull_request.from_github(gh_pull_request, author=author, repository=repository) + pull_request.from_github( + gh_pull_request, author=author, repository=repository, milestone=milestone + ) if save: pull_request.save() From 79b7ac14f11686d4ddc003d53ff60da2f6bc4fe9 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 24 Apr 2025 15:02:38 +0200 Subject: [PATCH 08/56] Sync milestone data --- backend/apps/github/common.py | 41 +++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/backend/apps/github/common.py b/backend/apps/github/common.py index dba28e96dd..e7423378fa 100644 --- a/backend/apps/github/common.py +++ b/backend/apps/github/common.py @@ -8,6 +8,7 @@ from apps.github.models.issue import Issue from apps.github.models.label import Label +from apps.github.models.milestone import Milestone from apps.github.models.organization import Organization from apps.github.models.pull_request import PullRequest from apps.github.models.release import Release @@ -59,6 +60,26 @@ def sync_repository(gh_repository, organization=None, user=None): ) if not repository.is_archived: + # GitHub repository milestones. + kwargs = { + "direction": "desc", + "sort": "updated", + "state": "all", + } + + until = ( + latest_updated_milestone.updated_at + if (latest_updated_milestone := repository.latest_updated_milestone) + else timezone.now() - td(days=30) + ) + + for gh_milestone in gh_repository.get_milestones(**kwargs): + if gh_milestone.updated_at < until: + break + + author = User.update_data(gh_milestone.user) + Milestone.update_data(gh_milestone, author=author, repository=repository) + # GitHub repository issues. project_track_issues = repository.project.track_issues if repository.project else True month_ago = timezone.now() - td(days=30) @@ -82,7 +103,16 @@ def sync_repository(gh_repository, organization=None, user=None): break author = User.update_data(gh_issue.user) - issue = Issue.update_data(gh_issue, author=author, repository=repository) + + # Milestone + milestone = None + if gh_issue.milestone: + milestone = Milestone.update_data( + gh_issue.milestone, repository=repository, author=author + ) + issue = Issue.update_data( + gh_issue, author=author, repository=repository, milestone=milestone + ) # Assignees. issue.assignees.clear() @@ -115,8 +145,15 @@ def sync_repository(gh_repository, organization=None, user=None): break author = User.update_data(gh_pull_request.user) + + # Milestone + milestone = None + if gh_pull_request.milestone: + milestone = Milestone.update_data( + gh_pull_request.milestone, repository=repository, author=author + ) pull_request = PullRequest.update_data( - gh_pull_request, author=author, repository=repository + gh_pull_request, author=author, repository=repository, milestone=milestone ) # Assignees. From ee26c79e0754dad291849d77ddd0ee55bd91d97b Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 24 Apr 2025 15:09:22 +0200 Subject: [PATCH 09/56] sync milestone labels --- backend/apps/github/common.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/apps/github/common.py b/backend/apps/github/common.py index e7423378fa..2476723e0f 100644 --- a/backend/apps/github/common.py +++ b/backend/apps/github/common.py @@ -78,7 +78,16 @@ def sync_repository(gh_repository, organization=None, user=None): break author = User.update_data(gh_milestone.user) - Milestone.update_data(gh_milestone, author=author, repository=repository) + + milestone = Milestone.update_data(gh_milestone, author=author, repository=repository) + + # Labels + milestone.labels.clear() + for gh_milestone_label in gh_milestone.get_labels(): + try: + milestone.labels.add(Label.update_data(gh_milestone_label)) + except UnknownObjectException: + logger.info("Couldn't get GitHub milestone label %s", milestone.url) # GitHub repository issues. project_track_issues = repository.project.track_issues if repository.project else True From ad94c0f5247cba21c03fadd108f151674e89b2c5 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 24 Apr 2025 15:18:31 +0200 Subject: [PATCH 10/56] Add property to retrieve the latest updated milestone in the Repository model --- backend/apps/github/models/repository.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/apps/github/models/repository.py b/backend/apps/github/models/repository.py index 7a28853beb..258c297bde 100644 --- a/backend/apps/github/models/repository.py +++ b/backend/apps/github/models/repository.py @@ -136,6 +136,11 @@ def latest_updated_pull_request(self): """Repository latest updated pull request (most recently modified).""" return self.pull_requests.order_by("-updated_at").first() + @property + def latest_updated_milestone(self): + """Repository latest updated milestone (most recently modified).""" + return self.milestones.order_by("-updated_at").first() + @property def nest_key(self): """Return repository Nest key.""" From 15b2b269d1d6eff6c813f321ac928bd93513adf0 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Thu, 24 Apr 2025 15:25:48 +0200 Subject: [PATCH 11/56] Refactor import order in milestone queries and include MilestoneQuery in GithubQuery --- backend/apps/github/graphql/queries/__init__.py | 2 ++ backend/apps/github/graphql/queries/milestone.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/apps/github/graphql/queries/__init__.py b/backend/apps/github/graphql/queries/__init__.py index 328360b0eb..2282fdbc7c 100644 --- a/backend/apps/github/graphql/queries/__init__.py +++ b/backend/apps/github/graphql/queries/__init__.py @@ -1,6 +1,7 @@ """GitHub GraphQL queries.""" from apps.github.graphql.queries.issue import IssueQuery +from apps.github.graphql.queries.milestone import MilestoneQuery from apps.github.graphql.queries.organization import OrganizationQuery from apps.github.graphql.queries.pull_request import PullRequestQuery from apps.github.graphql.queries.release import ReleaseQuery @@ -11,6 +12,7 @@ class GithubQuery( IssueQuery, + MilestoneQuery, OrganizationQuery, PullRequestQuery, ReleaseQuery, diff --git a/backend/apps/github/graphql/queries/milestone.py b/backend/apps/github/graphql/queries/milestone.py index d7be0e92c5..9069243f9d 100644 --- a/backend/apps/github/graphql/queries/milestone.py +++ b/backend/apps/github/graphql/queries/milestone.py @@ -2,8 +2,8 @@ import graphene +from apps.common.graphql.queries import BaseQuery from apps.github.graphql.nodes.milestone import MilestoneNode -from apps.github.graphql.queries import BaseQuery from apps.github.models.milestone import Milestone From c33499b5ce7059094a475b7ae99f850e4ea664a7 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda <62152210+ahmedxgouda@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:54:26 +0000 Subject: [PATCH 12/56] Update milestone author retrieval in sync_repository function --- backend/apps/github/common.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/apps/github/common.py b/backend/apps/github/common.py index 2476723e0f..df72f95f48 100644 --- a/backend/apps/github/common.py +++ b/backend/apps/github/common.py @@ -116,8 +116,9 @@ def sync_repository(gh_repository, organization=None, user=None): # Milestone milestone = None if gh_issue.milestone: + milestone_author = User.update_data(gh_issue.milestone.creator) milestone = Milestone.update_data( - gh_issue.milestone, repository=repository, author=author + gh_issue.milestone, repository=repository, author=milestone_author ) issue = Issue.update_data( gh_issue, author=author, repository=repository, milestone=milestone @@ -158,8 +159,9 @@ def sync_repository(gh_repository, organization=None, user=None): # Milestone milestone = None if gh_pull_request.milestone: + milestone_author = User.update_data(gh_pull_request.milestone.creator) milestone = Milestone.update_data( - gh_pull_request.milestone, repository=repository, author=author + gh_pull_request.milestone, repository=repository, author=milestone_author ) pull_request = PullRequest.update_data( gh_pull_request, author=author, repository=repository, milestone=milestone From 5114b354d7f8c6bd068c36935a806798b06ea019 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda <62152210+ahmedxgouda@users.noreply.github.com> Date: Thu, 24 Apr 2025 15:37:57 +0000 Subject: [PATCH 13/56] Fix milestone author retrieval in sync_repository function --- backend/apps/github/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/apps/github/common.py b/backend/apps/github/common.py index df72f95f48..2c55b8da65 100644 --- a/backend/apps/github/common.py +++ b/backend/apps/github/common.py @@ -77,7 +77,7 @@ def sync_repository(gh_repository, organization=None, user=None): if gh_milestone.updated_at < until: break - author = User.update_data(gh_milestone.user) + author = User.update_data(gh_milestone.creator) milestone = Milestone.update_data(gh_milestone, author=author, repository=repository) From e5e6f405e8e7b940ea33541fe9b7c1bc0fff5707 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda <62152210+ahmedxgouda@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:06:39 +0000 Subject: [PATCH 14/56] Enhance MilestoneNode and MilestoneQuery with project and repository filters, and improve related data fetching --- .../apps/github/graphql/nodes/milestone.py | 22 ++++++++ .../apps/github/graphql/queries/milestone.py | 50 ++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/backend/apps/github/graphql/nodes/milestone.py b/backend/apps/github/graphql/nodes/milestone.py index 6a1e81a500..0164cd1539 100644 --- a/backend/apps/github/graphql/nodes/milestone.py +++ b/backend/apps/github/graphql/nodes/milestone.py @@ -1,12 +1,21 @@ """Github Milestone Node.""" +import graphene + from apps.common.graphql.nodes import BaseNode from apps.github.models.milestone import Milestone +from apps.github.graphql.nodes.issue import IssueNode +from apps.github.graphql.nodes.pull_request import PullRequestNode + + class MilestoneNode(BaseNode): """Github Milestone Node.""" + issues = graphene.List(IssueNode) + pull_requests = graphene.List(PullRequestNode) + class Meta: model = Milestone @@ -17,4 +26,17 @@ class Meta: "title", "open_issues_count", "closed_issues_count", + "due_on", + "repository", + "issues", + "pull_requests", ) + + + def resolve_issues(self, info): + """Resolve issues.""" + return self.issues.all() + + def resolve_pull_requests(self, info): + """Resolve pull requests.""" + return self.pull_requests.all() diff --git a/backend/apps/github/graphql/queries/milestone.py b/backend/apps/github/graphql/queries/milestone.py index 9069243f9d..bb8144e187 100644 --- a/backend/apps/github/graphql/queries/milestone.py +++ b/backend/apps/github/graphql/queries/milestone.py @@ -15,6 +15,8 @@ class MilestoneQuery(BaseQuery): limit=graphene.Int(default_value=5), login=graphene.String(required=False), organization=graphene.String(required=False), + project=graphene.String(required=False), + repository=graphene.String(required=False), distinct=graphene.Boolean(default_value=False), ) @@ -22,11 +24,22 @@ class MilestoneQuery(BaseQuery): MilestoneNode, limit=graphene.Int(default_value=5), login=graphene.String(required=False), + project=graphene.String(required=False), + repository=graphene.String(required=False), organization=graphene.String(required=False), distinct=graphene.Boolean(default_value=False), ) - def resolve_open_milestones(root, info, limit, login=None, organization=None, distinct=False): + def resolve_open_milestones( + root, + info, + limit, + login=None, + organization=None, + project=None, + repository=None, + distinct=False, + ): """Resolve open milestones. Args: @@ -35,6 +48,8 @@ def resolve_open_milestones(root, info, limit, login=None, organization=None, di limit (int): The maximum number of milestones to return. login (str, optional): Filter milestones by author login. organization (str, optional): Filter milestones by organization login. + project (str, optional): Filter milestones by project name. + repository (str, optional): Filter milestones by repository name. distinct (bool, optional): Whether to return distinct milestones. Returns: @@ -45,17 +60,34 @@ def resolve_open_milestones(root, info, limit, login=None, organization=None, di "author", "repository", "repository__organization", + ).prefetch_related( + "issues", + "pull_requests", + "labels", ) if login: open_milestones = open_milestones.filter(author__login=login) + if repository: + open_milestones = open_milestones.filter(repository__name=repository) if organization: open_milestones = open_milestones.filter(repository__organization__login=organization) + # if project: + # open_milestones = open_milestones.filter(repository__project__name=project) + if distinct: + open_milestones = open_milestones.distinct() return open_milestones[:limit] def resolve_closed_milestones( - root, info, limit, login=None, organization=None, distinct=False + root, + info, + limit, + login=None, + organization=None, + project=None, + repository=None, + distinct=False, ): """Resolve closed milestones. @@ -65,6 +97,8 @@ def resolve_closed_milestones( limit (int): The maximum number of milestones to return. login (str, optional): Filter milestones by author login. organization (str, optional): Filter milestones by organization login. + project (str, optional): Filter milestones by project name. + repository (str, optional): Filter milestones by repository name. distinct (bool, optional): Whether to return distinct milestones. Returns: @@ -75,13 +109,25 @@ def resolve_closed_milestones( "author", "repository", "repository__organization", + ).prefetch_related( + "issues", + "pull_requests", + "labels", ) if login: closed_milestones = closed_milestones.filter(author__login=login) + if repository: + closed_milestones = closed_milestones.filter(repository__name=repository) + # if project: + # closed_milestones = closed_milestones.filter(repository__project__name=project) + if organization: closed_milestones = closed_milestones.filter( repository__organization__login=organization ) + if distinct: + closed_milestones = closed_milestones.distinct() + return closed_milestones[:limit] From 2d0bbcbecc4e9fa27bf3729f3afa46271f19dee2 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Fri, 25 Apr 2025 07:07:25 +0300 Subject: [PATCH 15/56] Implement the project milestone queries --- .../apps/github/graphql/nodes/milestone.py | 9 ++---- .../apps/github/graphql/queries/milestone.py | 30 +++++++++++-------- backend/apps/owasp/graphql/nodes/project.py | 11 +++++++ backend/apps/owasp/models/project.py | 19 ++++++++++++ 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/backend/apps/github/graphql/nodes/milestone.py b/backend/apps/github/graphql/nodes/milestone.py index 0164cd1539..ce2761018a 100644 --- a/backend/apps/github/graphql/nodes/milestone.py +++ b/backend/apps/github/graphql/nodes/milestone.py @@ -3,11 +3,9 @@ import graphene from apps.common.graphql.nodes import BaseNode -from apps.github.models.milestone import Milestone from apps.github.graphql.nodes.issue import IssueNode from apps.github.graphql.nodes.pull_request import PullRequestNode - - +from apps.github.models.milestone import Milestone class MilestoneNode(BaseNode): @@ -15,7 +13,7 @@ class MilestoneNode(BaseNode): issues = graphene.List(IssueNode) pull_requests = graphene.List(PullRequestNode) - + class Meta: model = Milestone @@ -32,11 +30,10 @@ class Meta: "pull_requests", ) - def resolve_issues(self, info): """Resolve issues.""" return self.issues.all() - + def resolve_pull_requests(self, info): """Resolve pull requests.""" return self.pull_requests.all() diff --git a/backend/apps/github/graphql/queries/milestone.py b/backend/apps/github/graphql/queries/milestone.py index bb8144e187..fe500c24f3 100644 --- a/backend/apps/github/graphql/queries/milestone.py +++ b/backend/apps/github/graphql/queries/milestone.py @@ -1,6 +1,7 @@ """Github Milestone Queries.""" import graphene +from django.db.models import OuterRef, Subquery from apps.common.graphql.queries import BaseQuery from apps.github.graphql.nodes.milestone import MilestoneNode @@ -15,7 +16,6 @@ class MilestoneQuery(BaseQuery): limit=graphene.Int(default_value=5), login=graphene.String(required=False), organization=graphene.String(required=False), - project=graphene.String(required=False), repository=graphene.String(required=False), distinct=graphene.Boolean(default_value=False), ) @@ -24,7 +24,6 @@ class MilestoneQuery(BaseQuery): MilestoneNode, limit=graphene.Int(default_value=5), login=graphene.String(required=False), - project=graphene.String(required=False), repository=graphene.String(required=False), organization=graphene.String(required=False), distinct=graphene.Boolean(default_value=False), @@ -36,7 +35,6 @@ def resolve_open_milestones( limit, login=None, organization=None, - project=None, repository=None, distinct=False, ): @@ -48,7 +46,6 @@ def resolve_open_milestones( limit (int): The maximum number of milestones to return. login (str, optional): Filter milestones by author login. organization (str, optional): Filter milestones by organization login. - project (str, optional): Filter milestones by project name. repository (str, optional): Filter milestones by repository name. distinct (bool, optional): Whether to return distinct milestones. @@ -72,10 +69,16 @@ def resolve_open_milestones( open_milestones = open_milestones.filter(repository__name=repository) if organization: open_milestones = open_milestones.filter(repository__organization__login=organization) - # if project: - # open_milestones = open_milestones.filter(repository__project__name=project) + if distinct: - open_milestones = open_milestones.distinct() + latest_milestone_per_author = ( + open_milestones.filter(author_id=OuterRef("author_id")) + .order_by("-created_at") + .values("id")[:1] + ) + open_milestones = open_milestones.filter( + id__in=Subquery(latest_milestone_per_author), + ).order_by("-created_at") return open_milestones[:limit] @@ -85,7 +88,6 @@ def resolve_closed_milestones( limit, login=None, organization=None, - project=None, repository=None, distinct=False, ): @@ -97,7 +99,6 @@ def resolve_closed_milestones( limit (int): The maximum number of milestones to return. login (str, optional): Filter milestones by author login. organization (str, optional): Filter milestones by organization login. - project (str, optional): Filter milestones by project name. repository (str, optional): Filter milestones by repository name. distinct (bool, optional): Whether to return distinct milestones. @@ -119,8 +120,6 @@ def resolve_closed_milestones( closed_milestones = closed_milestones.filter(author__login=login) if repository: closed_milestones = closed_milestones.filter(repository__name=repository) - # if project: - # closed_milestones = closed_milestones.filter(repository__project__name=project) if organization: closed_milestones = closed_milestones.filter( @@ -128,6 +127,13 @@ def resolve_closed_milestones( ) if distinct: - closed_milestones = closed_milestones.distinct() + latest_milestone_per_author = ( + closed_milestones.filter(author_id=OuterRef("author_id")) + .order_by("-created_at") + .values("id")[:1] + ) + closed_milestones = closed_milestones.filter( + id__in=Subquery(latest_milestone_per_author), + ).order_by("-created_at") return closed_milestones[:limit] diff --git a/backend/apps/owasp/graphql/nodes/project.py b/backend/apps/owasp/graphql/nodes/project.py index db118028e4..f6b5d2c075 100644 --- a/backend/apps/owasp/graphql/nodes/project.py +++ b/backend/apps/owasp/graphql/nodes/project.py @@ -3,6 +3,7 @@ import graphene from apps.github.graphql.nodes.issue import IssueNode +from apps.github.graphql.nodes.milestone import MilestoneNode from apps.github.graphql.nodes.release import ReleaseNode from apps.github.graphql.nodes.repository import RepositoryNode from apps.owasp.graphql.nodes.common import GenericEntityNode @@ -21,6 +22,8 @@ class ProjectNode(GenericEntityNode): level = graphene.String() recent_issues = graphene.List(IssueNode) recent_releases = graphene.List(ReleaseNode) + open_milestones = graphene.List(MilestoneNode) + closed_milestones = graphene.List(MilestoneNode) repositories = graphene.List(RepositoryNode) repositories_count = graphene.Int() topics = graphene.List(graphene.String) @@ -61,6 +64,14 @@ def resolve_recent_releases(self, info): """Resolve recent releases.""" return self.published_releases.order_by("-published_at")[:RECENT_RELEASES_LIMIT] + def resolve_open_milestones(self, info): + """Resolve open milestones.""" + return self.open_milestones.select_related("author").order_by("-created_at") + + def resolve_closed_milestones(self, info): + """Resolve closed milestones.""" + return self.closed_milestones.select_related("author").order_by("-created_at") + def resolve_repositories(self, info): """Resolve repositories.""" return self.repositories.order_by("-pushed_at", "-updated_at") diff --git a/backend/apps/owasp/models/project.py b/backend/apps/owasp/models/project.py index 7da4cedd29..8816c0ede5 100644 --- a/backend/apps/owasp/models/project.py +++ b/backend/apps/owasp/models/project.py @@ -9,6 +9,7 @@ from apps.common.utils import get_absolute_url from apps.core.models.prompt import Prompt from apps.github.models.issue import Issue +from apps.github.models.milestone import Milestone from apps.github.models.release import Release from apps.owasp.models.common import RepositoryBasedEntityModel from apps.owasp.models.managers.project import ActiveProjectManager @@ -150,6 +151,24 @@ def issues(self): "repository", ) + @property + def open_milestones(self): + """Return milestones.""" + return Milestone.open_milestones.filter( + repository__in=self.repositories.all(), + ).select_related( + "repository", + ) + + @property + def closed_milestones(self): + """Return milestones.""" + return Milestone.closed_milestones.filter( + repository__in=self.repositories.all(), + ).select_related( + "repository", + ) + @property def nest_key(self): """Get Nest key.""" From bad79f3b28fbeef0c101c715d5b7502543a72457 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Fri, 25 Apr 2025 07:17:42 +0300 Subject: [PATCH 16/56] Add open and closed milestones properties to Repository model and update RepositoryNode to resolve them --- .../apps/github/graphql/nodes/repository.py | 19 +++++++++++++++++++ backend/apps/github/models/repository.py | 10 ++++++++++ 2 files changed, 29 insertions(+) diff --git a/backend/apps/github/graphql/nodes/repository.py b/backend/apps/github/graphql/nodes/repository.py index 88b31b1ad7..675f0a5018 100644 --- a/backend/apps/github/graphql/nodes/repository.py +++ b/backend/apps/github/graphql/nodes/repository.py @@ -4,6 +4,7 @@ from apps.common.graphql.nodes import BaseNode from apps.github.graphql.nodes.issue import IssueNode +from apps.github.graphql.nodes.milestone import MilestoneNode from apps.github.graphql.nodes.release import ReleaseNode from apps.github.graphql.nodes.repository_contributor import RepositoryContributorNode from apps.github.models.repository import Repository @@ -16,6 +17,8 @@ class RepositoryNode(BaseNode): """Repository node.""" issues = graphene.List(IssueNode) + open_milestones = graphene.List(MilestoneNode) + closed_milestones = graphene.List(MilestoneNode) languages = graphene.List(graphene.String) latest_release = graphene.String() owner_key = graphene.String() @@ -80,3 +83,19 @@ def resolve_topics(self, info): def resolve_url(self, info): """Resolve URL.""" return self.url + + def resolve_open_milestones(self, info): + """Resolve open milestones.""" + return self.open_milestones.select_related( + "repository", + ).order_by( + "-created_at", + ) + + def resolve_closed_milestones(self, info): + """Resolve closed milestones.""" + return self.closed_milestones.select_related( + "repository", + ).order_by( + "-created_at", + ) diff --git a/backend/apps/github/models/repository.py b/backend/apps/github/models/repository.py index 258c297bde..a61ba077e8 100644 --- a/backend/apps/github/models/repository.py +++ b/backend/apps/github/models/repository.py @@ -179,6 +179,16 @@ def url(self): """Return repository URL.""" return f"https://github.com/{self.path}" + @property + def open_milestones(self): + """Repository open milestones.""" + return self.milestones.filter(state="OPEN").order_by("-due_on") + + @property + def closed_milestones(self): + """Repository closed milestones.""" + return self.milestones.filter(state="CLOSED").order_by("-due_on") + def from_github( self, gh_repository, From 07b1bb32819d0b48a4f859d44d0478ec7347f7ba Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Fri, 25 Apr 2025 07:27:50 +0300 Subject: [PATCH 17/56] Add open and closed milestones fields to repository and project test cases --- backend/tests/apps/github/graphql/nodes/repository_test.py | 2 ++ backend/tests/apps/owasp/graphql/nodes/project_test.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/backend/tests/apps/github/graphql/nodes/repository_test.py b/backend/tests/apps/github/graphql/nodes/repository_test.py index f2e86fec11..2fecc8fffa 100644 --- a/backend/tests/apps/github/graphql/nodes/repository_test.py +++ b/backend/tests/apps/github/graphql/nodes/repository_test.py @@ -27,6 +27,8 @@ def test_meta_configuration(self): "license", "name", "open_issues_count", + "open_milestones", + "closed_milestones", "organization", "owner_key", "latest_release", diff --git a/backend/tests/apps/owasp/graphql/nodes/project_test.py b/backend/tests/apps/owasp/graphql/nodes/project_test.py index b2b3f34ae4..5e37dd6733 100644 --- a/backend/tests/apps/owasp/graphql/nodes/project_test.py +++ b/backend/tests/apps/owasp/graphql/nodes/project_test.py @@ -101,6 +101,8 @@ def test_all_fields_exist_in_model(self): "leaders", "recent_issues", "recent_releases", + "open_milestones", + "closed_milestones", "repositories_count", "repositories", "top_contributors", From 362757a791001c543c9d7a556411707fd14a967e Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Fri, 25 Apr 2025 07:53:05 +0300 Subject: [PATCH 18/56] Refactor MilestoneNode to remove issues and pull_requests fields, and add repository_name and organization_name resolvers --- .../apps/github/graphql/nodes/milestone.py | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/backend/apps/github/graphql/nodes/milestone.py b/backend/apps/github/graphql/nodes/milestone.py index ce2761018a..f4b5cb25e1 100644 --- a/backend/apps/github/graphql/nodes/milestone.py +++ b/backend/apps/github/graphql/nodes/milestone.py @@ -3,16 +3,14 @@ import graphene from apps.common.graphql.nodes import BaseNode -from apps.github.graphql.nodes.issue import IssueNode -from apps.github.graphql.nodes.pull_request import PullRequestNode from apps.github.models.milestone import Milestone class MilestoneNode(BaseNode): """Github Milestone Node.""" - issues = graphene.List(IssueNode) - pull_requests = graphene.List(PullRequestNode) + repository_name = graphene.String() + organization_name = graphene.String() class Meta: model = Milestone @@ -20,20 +18,17 @@ class Meta: fields = ( "author", "created_at", - "state", "title", "open_issues_count", "closed_issues_count", "due_on", - "repository", - "issues", - "pull_requests", + "body", ) - def resolve_issues(self, info): - """Resolve issues.""" - return self.issues.all() + def resolve_repository_name(self, info): + """Resolve repository name.""" + return self.repository.name - def resolve_pull_requests(self, info): - """Resolve pull requests.""" - return self.pull_requests.all() + def resolve_organization_name(self, info): + """Return organization name.""" + return self.repository.organization.login if self.repository.organization else None From 96e1a41aff2e5ed7590eabb2d5d1f640aa0f66fc Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Fri, 25 Apr 2025 07:55:11 +0300 Subject: [PATCH 19/56] Refactor open and closed milestones properties in Repository model to use Milestone queries --- backend/apps/github/models/repository.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/apps/github/models/repository.py b/backend/apps/github/models/repository.py index a61ba077e8..8096933d0f 100644 --- a/backend/apps/github/models/repository.py +++ b/backend/apps/github/models/repository.py @@ -9,6 +9,7 @@ from apps.common.models import TimestampedModel from apps.github.constants import OWASP_LOGIN from apps.github.models.common import NodeModel +from apps.github.models.milestone import Milestone from apps.github.models.mixins import RepositoryIndexMixin from apps.github.utils import ( check_funding_policy_compliance, @@ -182,12 +183,16 @@ def url(self): @property def open_milestones(self): """Repository open milestones.""" - return self.milestones.filter(state="OPEN").order_by("-due_on") + return Milestone.open_milestones.filter( + repository=self, + ).order_by("-created_at") @property def closed_milestones(self): """Repository closed milestones.""" - return self.milestones.filter(state="CLOSED").order_by("-due_on") + return Milestone.closed_milestones.filter( + repository=self, + ).order_by("-created_at") def from_github( self, From 70c241763e0db2ab98d7e4f94a16cae62b1934c5 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Fri, 25 Apr 2025 11:55:48 +0300 Subject: [PATCH 20/56] Add milestone queries to the frontend --- frontend/src/server/queries/homeQueries.ts | 12 ++++++++++++ frontend/src/server/queries/projectQueries.ts | 12 ++++++++++++ frontend/src/server/queries/repositoryQueries.ts | 12 ++++++++++++ 3 files changed, 36 insertions(+) diff --git a/frontend/src/server/queries/homeQueries.ts b/frontend/src/server/queries/homeQueries.ts index 57fc2fd5e6..f4a61a2db5 100644 --- a/frontend/src/server/queries/homeQueries.ts +++ b/frontend/src/server/queries/homeQueries.ts @@ -91,5 +91,17 @@ export const GET_MAIN_PAGE_DATA = gql` suggestedLocation url } + openMilestones(limit: 5) { + title + body + openIssuesCount + closedIssuesCount + } + closedMilestones(limit: 5) { + title + body + openIssuesCount + closedIssuesCount + } } ` diff --git a/frontend/src/server/queries/projectQueries.ts b/frontend/src/server/queries/projectQueries.ts index bc0de9fc4e..278dbd0348 100644 --- a/frontend/src/server/queries/projectQueries.ts +++ b/frontend/src/server/queries/projectQueries.ts @@ -64,6 +64,18 @@ export const GET_PROJECT_DATA = gql` type updatedAt url + openMilestones { + title + body + openIssuesCount + closedIssuesCount + } + closedMilestones { + title + body + openIssuesCount + closedIssuesCount + } } recentPullRequests(project: $key) { author { diff --git a/frontend/src/server/queries/repositoryQueries.ts b/frontend/src/server/queries/repositoryQueries.ts index ce2f9323c8..89aa32b833 100644 --- a/frontend/src/server/queries/repositoryQueries.ts +++ b/frontend/src/server/queries/repositoryQueries.ts @@ -51,6 +51,18 @@ export const GET_REPOSITORY_DATA = gql` topics updatedAt url + openMilestones { + title + body + openIssuesCount + closedIssuesCount + } + closedMilestones { + title + body + openIssuesCount + closedIssuesCount + } } recentPullRequests(limit: 5, organization: $organizationKey, repository: $repositoryKey) { author { From a69f324015cf83c66dc7851ade83587d4bfccce9 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 26 Apr 2025 09:25:55 +0300 Subject: [PATCH 21/56] Add Milestones component and integrate into Home page; update types and queries --- frontend/src/app/page.tsx | 5 ++ frontend/src/components/ItemCardList.tsx | 4 +- frontend/src/components/Milestones.tsx | 65 ++++++++++++++++++++++ frontend/src/server/queries/homeQueries.ts | 16 ++++++ frontend/src/types/home.ts | 4 +- frontend/src/types/project.ts | 15 +++++ 6 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/Milestones.tsx diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 8a0b3e16b2..9406819a39 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -33,6 +33,7 @@ import ChapterMapWrapper from 'components/ChapterMapWrapper' import LeadersList from 'components/LeadersList' import LoadingSpinner from 'components/LoadingSpinner' import MovingLogos from 'components/LogoCarousel' +import Milestones from 'components/Milestones' import DialogComp from 'components/Modal' import MultiSearchBar from 'components/MultiSearch' import RecentIssues from 'components/RecentIssues' @@ -325,6 +326,10 @@ export default function Home() { +
+ + +
= ({ + data, + showAvatar = true, + openMilestones, +}) => { + const router = useRouter() + + return ( + + + + } + data={data} + showAvatar={showAvatar} + icon={faFire} + renderDetails={(item) => ( +
+
+ + {formatDate(item.createdAt)} +
+ {item?.repositoryName && ( +
+ + +
+ )} +
+ )} + /> + ) +} + +export default Milestones diff --git a/frontend/src/server/queries/homeQueries.ts b/frontend/src/server/queries/homeQueries.ts index f4a61a2db5..b451f1e25e 100644 --- a/frontend/src/server/queries/homeQueries.ts +++ b/frontend/src/server/queries/homeQueries.ts @@ -92,16 +92,32 @@ export const GET_MAIN_PAGE_DATA = gql` url } openMilestones(limit: 5) { + author { + avatarUrl + login + name + } title body openIssuesCount closedIssuesCount + repositoryName + organizationName + createdAt } closedMilestones(limit: 5) { + author { + avatarUrl + login + name + } title body openIssuesCount closedIssuesCount + repositoryName + organizationName + createdAt } } ` diff --git a/frontend/src/types/home.ts b/frontend/src/types/home.ts index 0429163e91..b64e4e84b3 100644 --- a/frontend/src/types/home.ts +++ b/frontend/src/types/home.ts @@ -1,6 +1,6 @@ import { TopContributorsTypeGraphql } from './contributor' import { EventType } from './event' -import { ProjectIssuesType, ProjectReleaseType } from './project' +import { ProjectIssuesType, ProjectReleaseType, ProjectMilestonesType } from './project' export type MainPageData = { topContributors: TopContributorsTypeGraphql[] @@ -8,6 +8,8 @@ export type MainPageData = { recentReleases: ProjectReleaseType[] upcomingEvents: EventType[] recentPullRequests: PullRequestsType[] + openMilestones: ProjectMilestonesType[] + closedMilestones: ProjectMilestonesType[] recentChapters: { createdAt: string key: string diff --git a/frontend/src/types/project.ts b/frontend/src/types/project.ts index cb17114f1f..23da007eb6 100644 --- a/frontend/src/types/project.ts +++ b/frontend/src/types/project.ts @@ -29,6 +29,21 @@ export interface ProjectPullRequestsType { url: string } +export interface ProjectMilestonesType { + author: { + avatarUrl: string + key: string + name: string + login: string + } + title: string + body: string + openIssuesCount: number + closedIssuesCount: number + repositoryName: string + organizationName?: string +} + export interface ProjectStatsType { contributors: number forks: number From ae326d26cb7e478f090f0d65c7028ba7b3c47a15 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 26 Apr 2025 09:50:37 +0300 Subject: [PATCH 22/56] Enhance Milestones component: add closed and open issues count display; update icon imports --- frontend/src/components/ItemCardList.tsx | 2 ++ frontend/src/components/Milestones.tsx | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ItemCardList.tsx b/frontend/src/components/ItemCardList.tsx index f4e22b989d..d1cccc06df 100644 --- a/frontend/src/components/ItemCardList.tsx +++ b/frontend/src/components/ItemCardList.tsx @@ -26,6 +26,8 @@ const ItemCardList = ({ publishedAt: string repositoryName: string tagName: string + openIssuesCount: number + closedIssuesCount: number author: { avatarUrl: string login: string diff --git a/frontend/src/components/Milestones.tsx b/frontend/src/components/Milestones.tsx index ae1fe14813..47ba85dfaa 100644 --- a/frontend/src/components/Milestones.tsx +++ b/frontend/src/components/Milestones.tsx @@ -1,4 +1,4 @@ -import { faCalendar, faFolderOpen, faFire } from '@fortawesome/free-solid-svg-icons' +import { faCalendar, faFolderOpen, faFire, faCircleCheck, faCircleExclamation } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { useRouter } from 'next/navigation' import React from 'react' @@ -41,6 +41,14 @@ const Milestones: React.FC = ({ {formatDate(item.createdAt)} +
+ + {item.closedIssuesCount} closed +
+
+ + {item.openIssuesCount} open +
{item?.repositoryName && (
From bfb016ce55935e3aa1de266bfd58c767b6b88747 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 26 Apr 2025 09:55:54 +0300 Subject: [PATCH 23/56] Refactor Milestone queries and components: remove body field, add url and author details --- .../apps/github/graphql/nodes/milestone.py | 3 +-- frontend/src/components/Milestones.tsx | 8 ++++++- frontend/src/server/queries/homeQueries.ts | 4 ++-- frontend/src/server/queries/projectQueries.ts | 24 +++++++++++++++---- .../src/server/queries/repositoryQueries.ts | 20 ++++++++++++++-- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/backend/apps/github/graphql/nodes/milestone.py b/backend/apps/github/graphql/nodes/milestone.py index f4b5cb25e1..2fa5bac59b 100644 --- a/backend/apps/github/graphql/nodes/milestone.py +++ b/backend/apps/github/graphql/nodes/milestone.py @@ -21,8 +21,7 @@ class Meta: "title", "open_issues_count", "closed_issues_count", - "due_on", - "body", + "url", ) def resolve_repository_name(self, info): diff --git a/frontend/src/components/Milestones.tsx b/frontend/src/components/Milestones.tsx index 47ba85dfaa..3f60a6dc38 100644 --- a/frontend/src/components/Milestones.tsx +++ b/frontend/src/components/Milestones.tsx @@ -1,4 +1,10 @@ -import { faCalendar, faFolderOpen, faFire, faCircleCheck, faCircleExclamation } from '@fortawesome/free-solid-svg-icons' +import { + faCalendar, + faFolderOpen, + faFire, + faCircleCheck, + faCircleExclamation, +} from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { useRouter } from 'next/navigation' import React from 'react' diff --git a/frontend/src/server/queries/homeQueries.ts b/frontend/src/server/queries/homeQueries.ts index b451f1e25e..c6e81b792e 100644 --- a/frontend/src/server/queries/homeQueries.ts +++ b/frontend/src/server/queries/homeQueries.ts @@ -98,12 +98,12 @@ export const GET_MAIN_PAGE_DATA = gql` name } title - body openIssuesCount closedIssuesCount repositoryName organizationName createdAt + url } closedMilestones(limit: 5) { author { @@ -112,12 +112,12 @@ export const GET_MAIN_PAGE_DATA = gql` name } title - body openIssuesCount closedIssuesCount repositoryName organizationName createdAt + url } } ` diff --git a/frontend/src/server/queries/projectQueries.ts b/frontend/src/server/queries/projectQueries.ts index 278dbd0348..489ce86874 100644 --- a/frontend/src/server/queries/projectQueries.ts +++ b/frontend/src/server/queries/projectQueries.ts @@ -64,17 +64,33 @@ export const GET_PROJECT_DATA = gql` type updatedAt url - openMilestones { + openMilestones(limit: 5) { + author { + avatarUrl + login + name + } title - body openIssuesCount closedIssuesCount + repositoryName + organizationName + createdAt + url } - closedMilestones { + closedMilestones(limit: 5) { + author { + avatarUrl + login + name + } title - body openIssuesCount closedIssuesCount + repositoryName + organizationName + createdAt + url } } recentPullRequests(project: $key) { diff --git a/frontend/src/server/queries/repositoryQueries.ts b/frontend/src/server/queries/repositoryQueries.ts index 89aa32b833..381ed53187 100644 --- a/frontend/src/server/queries/repositoryQueries.ts +++ b/frontend/src/server/queries/repositoryQueries.ts @@ -52,16 +52,32 @@ export const GET_REPOSITORY_DATA = gql` updatedAt url openMilestones { + author { + avatarUrl + login + name + } title - body openIssuesCount closedIssuesCount + repositoryName + organizationName + createdAt + url } closedMilestones { + author { + avatarUrl + login + name + } title - body openIssuesCount closedIssuesCount + repositoryName + organizationName + createdAt + url } } recentPullRequests(limit: 5, organization: $organizationKey, repository: $repositoryKey) { From 8568e73f65e16e36985d1a60e73cfafff8d6cf3f Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 26 Apr 2025 10:26:10 +0300 Subject: [PATCH 24/56] Add open and closed milestones to Project and Repository details; update types and components --- backend/apps/owasp/graphql/nodes/project.py | 12 ++++++------ .../repositories/[repositoryKey]/page.tsx | 2 ++ frontend/src/app/projects/[projectKey]/page.tsx | 2 ++ frontend/src/components/CardDetailsPage.tsx | 9 +++++++++ frontend/src/types/card.ts | 9 ++++++++- frontend/src/types/project.ts | 2 ++ 6 files changed, 29 insertions(+), 7 deletions(-) diff --git a/backend/apps/owasp/graphql/nodes/project.py b/backend/apps/owasp/graphql/nodes/project.py index f6b5d2c075..8a9ffc9561 100644 --- a/backend/apps/owasp/graphql/nodes/project.py +++ b/backend/apps/owasp/graphql/nodes/project.py @@ -22,8 +22,8 @@ class ProjectNode(GenericEntityNode): level = graphene.String() recent_issues = graphene.List(IssueNode) recent_releases = graphene.List(ReleaseNode) - open_milestones = graphene.List(MilestoneNode) - closed_milestones = graphene.List(MilestoneNode) + open_milestones = graphene.List(MilestoneNode, limit=graphene.Int(default_value=5)) + closed_milestones = graphene.List(MilestoneNode, limit=graphene.Int(default_value=5)) repositories = graphene.List(RepositoryNode) repositories_count = graphene.Int() topics = graphene.List(graphene.String) @@ -64,13 +64,13 @@ def resolve_recent_releases(self, info): """Resolve recent releases.""" return self.published_releases.order_by("-published_at")[:RECENT_RELEASES_LIMIT] - def resolve_open_milestones(self, info): + def resolve_open_milestones(self, info, limit): """Resolve open milestones.""" - return self.open_milestones.select_related("author").order_by("-created_at") + return self.open_milestones.select_related("author").order_by("-created_at")[:limit] - def resolve_closed_milestones(self, info): + def resolve_closed_milestones(self, info, limit): """Resolve closed milestones.""" - return self.closed_milestones.select_related("author").order_by("-created_at") + return self.closed_milestones.select_related("author").order_by("-created_at")[:limit] def resolve_repositories(self, info): """Resolve repositories.""" diff --git a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx index 872d2738b8..a67763b9c4 100644 --- a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx +++ b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx @@ -114,6 +114,8 @@ const RepositoryDetailsPage = () => { title={repository.name} topContributors={repository.topContributors} topics={repository.topics} + openMilestones={repository.openMilestones} + closedMilestones={repository.closedMilestones} type="repository" /> ) diff --git a/frontend/src/app/projects/[projectKey]/page.tsx b/frontend/src/app/projects/[projectKey]/page.tsx index d3bb5de9dc..e097da7272 100644 --- a/frontend/src/app/projects/[projectKey]/page.tsx +++ b/frontend/src/app/projects/[projectKey]/page.tsx @@ -101,6 +101,8 @@ const ProjectDetailsPage = () => { summary={project.summary} title={project.name} topContributors={project.topContributors} + openMilestones={project.openMilestones} + closedMilestones={project.closedMilestones} topics={project.topics} type="project" /> diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index 2621c44a5a..8fb2b7e7f2 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -14,6 +14,7 @@ import { capitalize } from 'utils/capitalize' import { getSocialIcon } from 'utils/urlIconMappings' import AnchorTitle from 'components/AnchorTitle' import InfoBlock from 'components/InfoBlock' +import Milestones from 'components/Milestones' import RecentIssues from 'components/RecentIssues' import RecentPullRequests from 'components/RecentPullRequests' import RecentReleases from 'components/RecentReleases' @@ -40,6 +41,8 @@ const DetailsCard = ({ topics, recentIssues, recentReleases, + openMilestones, + closedMilestones, showAvatar = true, userSummary, geolocationData = null, @@ -192,6 +195,12 @@ const DetailsCard = ({ type === 'organization' || type === 'repository' || type === 'project') && } + {(type === 'project' || type === 'repository') && ( +
+ + +
+ )} {(type === 'project' || type === 'user' || type === 'organization') && repositories.length > 0 && ( Date: Sat, 26 Apr 2025 10:34:03 +0300 Subject: [PATCH 25/56] Add createdAt field to ProjectMilestonesType interface --- frontend/src/types/project.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/types/project.ts b/frontend/src/types/project.ts index 1eefbe5d2e..d1fdf79c5f 100644 --- a/frontend/src/types/project.ts +++ b/frontend/src/types/project.ts @@ -42,6 +42,7 @@ export interface ProjectMilestonesType { closedIssuesCount: number repositoryName: string organizationName?: string + createdAt: string } export interface ProjectStatsType { From fe592fa8096b3cc46ceefbac3c87728bab1c25d7 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 26 Apr 2025 19:07:59 +0300 Subject: [PATCH 26/56] Apply DRY to milestones queries --- .../apps/github/graphql/queries/milestone.py | 91 +++---------------- frontend/src/server/queries/homeQueries.ts | 4 +- 2 files changed, 17 insertions(+), 78 deletions(-) diff --git a/backend/apps/github/graphql/queries/milestone.py b/backend/apps/github/graphql/queries/milestone.py index fe500c24f3..2f7d5ea93a 100644 --- a/backend/apps/github/graphql/queries/milestone.py +++ b/backend/apps/github/graphql/queries/milestone.py @@ -11,25 +11,17 @@ class MilestoneQuery(BaseQuery): """Github Milestone Queries.""" - open_milestones = graphene.List( + milestones = graphene.List( MilestoneNode, limit=graphene.Int(default_value=5), login=graphene.String(required=False), organization=graphene.String(required=False), repository=graphene.String(required=False), distinct=graphene.Boolean(default_value=False), + close=graphene.Boolean(default_value=True), ) - closed_milestones = graphene.List( - MilestoneNode, - limit=graphene.Int(default_value=5), - login=graphene.String(required=False), - repository=graphene.String(required=False), - organization=graphene.String(required=False), - distinct=graphene.Boolean(default_value=False), - ) - - def resolve_open_milestones( + def resolve_milestones( root, info, limit, @@ -37,8 +29,9 @@ def resolve_open_milestones( organization=None, repository=None, distinct=False, + close=True, ): - """Resolve open milestones. + """Resolve milestones. Args: root (object): The root object. @@ -48,12 +41,14 @@ def resolve_open_milestones( organization (str, optional): Filter milestones by organization login. repository (str, optional): Filter milestones by repository name. distinct (bool, optional): Whether to return distinct milestones. + close (bool, optional): Whether to return open or closed milestones. Returns: - list: A list of open milestones. + list: A list of milestones. """ - open_milestones = Milestone.open_milestones.select_related( + milestones = Milestone.closed_milestones if close else Milestone.open_milestones + milestones = milestones.select_related( "author", "repository", "repository__organization", @@ -64,76 +59,20 @@ def resolve_open_milestones( ) if login: - open_milestones = open_milestones.filter(author__login=login) + milestones = milestones.filter(author__login=login) if repository: - open_milestones = open_milestones.filter(repository__name=repository) + milestones = milestones.filter(repository__name=repository) if organization: - open_milestones = open_milestones.filter(repository__organization__login=organization) - - if distinct: - latest_milestone_per_author = ( - open_milestones.filter(author_id=OuterRef("author_id")) - .order_by("-created_at") - .values("id")[:1] - ) - open_milestones = open_milestones.filter( - id__in=Subquery(latest_milestone_per_author), - ).order_by("-created_at") - - return open_milestones[:limit] - - def resolve_closed_milestones( - root, - info, - limit, - login=None, - organization=None, - repository=None, - distinct=False, - ): - """Resolve closed milestones. - - Args: - root (object): The root object. - info (ResolveInfo): The GraphQL execution context. - limit (int): The maximum number of milestones to return. - login (str, optional): Filter milestones by author login. - organization (str, optional): Filter milestones by organization login. - repository (str, optional): Filter milestones by repository name. - distinct (bool, optional): Whether to return distinct milestones. - - Returns: - list: A list of closed milestones. - - """ - closed_milestones = Milestone.closed_milestones.select_related( - "author", - "repository", - "repository__organization", - ).prefetch_related( - "issues", - "pull_requests", - "labels", - ) - - if login: - closed_milestones = closed_milestones.filter(author__login=login) - if repository: - closed_milestones = closed_milestones.filter(repository__name=repository) - - if organization: - closed_milestones = closed_milestones.filter( - repository__organization__login=organization - ) + milestones = milestones.filter(repository__organization__login=organization) if distinct: latest_milestone_per_author = ( - closed_milestones.filter(author_id=OuterRef("author_id")) + milestones.filter(author_id=OuterRef("author_id")) .order_by("-created_at") .values("id")[:1] ) - closed_milestones = closed_milestones.filter( + milestones = milestones.filter( id__in=Subquery(latest_milestone_per_author), ).order_by("-created_at") - return closed_milestones[:limit] + return milestones[:limit] diff --git a/frontend/src/server/queries/homeQueries.ts b/frontend/src/server/queries/homeQueries.ts index c6e81b792e..89ee799eb6 100644 --- a/frontend/src/server/queries/homeQueries.ts +++ b/frontend/src/server/queries/homeQueries.ts @@ -91,7 +91,7 @@ export const GET_MAIN_PAGE_DATA = gql` suggestedLocation url } - openMilestones(limit: 5) { + openMilestones: milestones(limit: 5, close: false) { author { avatarUrl login @@ -105,7 +105,7 @@ export const GET_MAIN_PAGE_DATA = gql` createdAt url } - closedMilestones(limit: 5) { + closedMilestones: milestones(limit: 5, close: true) { author { avatarUrl login From 917b9e2e172542bcd1aac31e9b9dd31167d12275 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 26 Apr 2025 19:26:00 +0300 Subject: [PATCH 27/56] Implement/Update unit tests for milestones. --- frontend/__tests__/unit/data/mockHomeData.ts | 60 +++++++++++++++++++ .../unit/data/mockProjectDetailsData.ts | 60 +++++++++++++++++++ .../__tests__/unit/data/mockRepositoryData.ts | 60 +++++++++++++++++++ frontend/__tests__/unit/pages/Home.test.tsx | 38 +++++++++--- .../unit/pages/ProjectDetails.test.tsx | 40 ++++++++++--- .../unit/pages/RepositoryDetails.test.tsx | 35 +++++++++-- 6 files changed, 270 insertions(+), 23 deletions(-) diff --git a/frontend/__tests__/unit/data/mockHomeData.ts b/frontend/__tests__/unit/data/mockHomeData.ts index be4369f7a3..2b7b86d79e 100644 --- a/frontend/__tests__/unit/data/mockHomeData.ts +++ b/frontend/__tests__/unit/data/mockHomeData.ts @@ -100,6 +100,66 @@ export const mockGraphQLData = { url: 'https://github.com/owasp/owasp-nest/releases/tag/v0.9.2', }, ], + openMilestones: [ + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/33333?v=4', + login: 'milestone-author1', + name: 'Milestone Author 1' + }, + title: 'v2.0.0 Release', + openIssuesCount: 5, + closedIssuesCount: 15, + repositoryName: 'Repo One', + organizationName: 'OWASP', + createdAt: '2025-03-01T10:00:00Z', + url: 'https://github.com/OWASP/repo-one/milestone/1' + }, + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/44444?v=4', + login: 'milestone-author2', + name: 'Milestone Author 2' + }, + title: 'Documentation Update', + openIssuesCount: 3, + closedIssuesCount: 7, + repositoryName: 'Repo Two', + organizationName: 'OWASP', + createdAt: '2025-02-15T14:30:00Z', + url: 'https://github.com/OWASP/repo-two/milestone/2' + } + ], + closedMilestones: [ + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/55555?v=4', + login: 'milestone-author3', + name: 'Milestone Author 3' + }, + title: 'v1.0.0 Release', + openIssuesCount: 0, + closedIssuesCount: 25, + repositoryName: 'Repo One', + organizationName: 'OWASP', + createdAt: '2024-12-01T09:00:00Z', + url: 'https://github.com/OWASP/repo-one/milestone/3' + }, + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/66666?v=4', + login: 'milestone-author4', + name: 'Milestone Author 4' + }, + title: 'Security Updates', + openIssuesCount: 0, + closedIssuesCount: 12, + repositoryName: 'Repo Two', + organizationName: 'OWASP', + createdAt: '2024-11-15T16:45:00Z', + url: 'https://github.com/OWASP/repo-two/milestone/4' + } + ], statsOverview: { activeChaptersStats: 540, activeProjectsStats: 95, diff --git a/frontend/__tests__/unit/data/mockProjectDetailsData.ts b/frontend/__tests__/unit/data/mockProjectDetailsData.ts index 55b11bf495..c17bf01e41 100644 --- a/frontend/__tests__/unit/data/mockProjectDetailsData.ts +++ b/frontend/__tests__/unit/data/mockProjectDetailsData.ts @@ -76,6 +76,66 @@ export const mockProjectDetailsData = { type: 'Tool', updatedAt: '2025-02-07T12:34:56Z', url: 'https://github.com/example-project', + openMilestones: [ + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/33333?v=4', + login: 'milestone-author1', + name: 'Milestone Author 1' + }, + title: 'v2.0.0 Release', + openIssuesCount: 5, + closedIssuesCount: 15, + repositoryName: 'Repo One', + organizationName: 'OWASP', + createdAt: '2025-03-01T10:00:00Z', + url: 'https://github.com/OWASP/repo-one/milestone/1' + }, + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/44444?v=4', + login: 'milestone-author2', + name: 'Milestone Author 2' + }, + title: 'Documentation Update', + openIssuesCount: 3, + closedIssuesCount: 7, + repositoryName: 'Repo Two', + organizationName: 'OWASP', + createdAt: '2025-02-15T14:30:00Z', + url: 'https://github.com/OWASP/repo-two/milestone/2' + } + ], + closedMilestones: [ + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/55555?v=4', + login: 'milestone-author3', + name: 'Milestone Author 3' + }, + title: 'v1.0.0 Release', + openIssuesCount: 0, + closedIssuesCount: 25, + repositoryName: 'Repo One', + organizationName: 'OWASP', + createdAt: '2024-12-01T09:00:00Z', + url: 'https://github.com/OWASP/repo-one/milestone/3' + }, + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/66666?v=4', + login: 'milestone-author4', + name: 'Milestone Author 4' + }, + title: 'Security Updates', + openIssuesCount: 0, + closedIssuesCount: 12, + repositoryName: 'Repo Two', + organizationName: 'OWASP', + createdAt: '2024-11-15T16:45:00Z', + url: 'https://github.com/OWASP/repo-two/milestone/4' + } + ] }, recentPullRequests: [ { diff --git a/frontend/__tests__/unit/data/mockRepositoryData.ts b/frontend/__tests__/unit/data/mockRepositoryData.ts index 2c653ba8f7..e42a7ad799 100644 --- a/frontend/__tests__/unit/data/mockRepositoryData.ts +++ b/frontend/__tests__/unit/data/mockRepositoryData.ts @@ -45,6 +45,66 @@ export const mockRepositoryData = { }, }, ], + openMilestones: [ + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/33333?v=4', + login: 'milestone-author1', + name: 'Milestone Author 1' + }, + title: 'v2.0.0 Release', + openIssuesCount: 5, + closedIssuesCount: 15, + repositoryName: 'Repo One', + organizationName: 'OWASP', + createdAt: '2025-03-01T10:00:00Z', + url: 'https://github.com/OWASP/repo-one/milestone/1' + }, + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/44444?v=4', + login: 'milestone-author2', + name: 'Milestone Author 2' + }, + title: 'Documentation Update', + openIssuesCount: 3, + closedIssuesCount: 7, + repositoryName: 'Repo Two', + organizationName: 'OWASP', + createdAt: '2025-02-15T14:30:00Z', + url: 'https://github.com/OWASP/repo-two/milestone/2' + } + ], + closedMilestones: [ + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/55555?v=4', + login: 'milestone-author3', + name: 'Milestone Author 3' + }, + title: 'v1.0.0 Release', + openIssuesCount: 0, + closedIssuesCount: 25, + repositoryName: 'Repo One', + organizationName: 'OWASP', + createdAt: '2024-12-01T09:00:00Z', + url: 'https://github.com/OWASP/repo-one/milestone/3' + }, + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/66666?v=4', + login: 'milestone-author4', + name: 'Milestone Author 4' + }, + title: 'Security Updates', + openIssuesCount: 0, + closedIssuesCount: 12, + repositoryName: 'Repo Two', + organizationName: 'OWASP', + createdAt: '2024-11-15T16:45:00Z', + url: 'https://github.com/OWASP/repo-two/milestone/4' + } + ] }, recentPullRequests: [ { diff --git a/frontend/__tests__/unit/pages/Home.test.tsx b/frontend/__tests__/unit/pages/Home.test.tsx index 385b3872e0..1a85edfe21 100644 --- a/frontend/__tests__/unit/pages/Home.test.tsx +++ b/frontend/__tests__/unit/pages/Home.test.tsx @@ -69,14 +69,14 @@ describe('Home', () => { let mockRouter: { push: jest.Mock } beforeEach(() => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockGraphQLData, loading: false, error: null, }) - ;(fetchAlgoliaData as jest.Mock).mockResolvedValue(mockAlgoliaData) + ; (fetchAlgoliaData as jest.Mock).mockResolvedValue(mockAlgoliaData) mockRouter = { push: jest.fn() } - ;(useRouter as jest.Mock).mockReturnValue(mockRouter) + ; (useRouter as jest.Mock).mockReturnValue(mockRouter) }) afterEach(() => { @@ -84,7 +84,7 @@ describe('Home', () => { }) test('renders loading state', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: null, loading: true, error: null, @@ -109,7 +109,7 @@ describe('Home', () => { }) test('renders error message when GraphQL request fails', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: null, error: { message: 'GraphQL error' }, }) @@ -192,11 +192,11 @@ describe('Home', () => { }) test('handles missing data gracefully', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockGraphQLData, error: null, }) - ;(fetchAlgoliaData as jest.Mock).mockResolvedValue({ hits: [] }) + ; (fetchAlgoliaData as jest.Mock).mockResolvedValue({ hits: [] }) render() @@ -230,8 +230,30 @@ describe('Home', () => { }) }) + test('renders milestones section correctly', async () => { + render() + await waitFor(() => { + const openMilestones = mockGraphQLData.openMilestones + const closedMilestones = mockGraphQLData.closedMilestones + + openMilestones.forEach((milestone) => { + expect(screen.getByText(milestone.title)).toBeInTheDocument() + expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() + expect(screen.getByText(milestone.openIssuesCount)).toBeInTheDocument() + expect(screen.getByText(milestone.closedIssuesCount)).toBeInTheDocument() + expect(screen.getByText(milestone.createdAt)).toBeInTheDocument() + }) + closedMilestones.forEach((milestone) => { + expect(screen.getByText(milestone.title)).toBeInTheDocument() + expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() + expect(screen.getByText(milestone.openIssuesCount)).toBeInTheDocument() + expect(screen.getByText(milestone.closedIssuesCount)).toBeInTheDocument() + expect(screen.getByText(milestone.createdAt)).toBeInTheDocument() + }) + }) + }) test('renders when no recent releases', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: { ...mockGraphQLData, recentReleases: [], diff --git a/frontend/__tests__/unit/pages/ProjectDetails.test.tsx b/frontend/__tests__/unit/pages/ProjectDetails.test.tsx index 4485b889ac..b0aa79918d 100644 --- a/frontend/__tests__/unit/pages/ProjectDetails.test.tsx +++ b/frontend/__tests__/unit/pages/ProjectDetails.test.tsx @@ -34,7 +34,7 @@ const mockError = { describe('ProjectDetailsPage', () => { beforeEach(() => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockProjectDetailsData, loading: false, error: null, @@ -46,7 +46,7 @@ describe('ProjectDetailsPage', () => { }) test('renders loading state', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: null, error: null, }) @@ -60,7 +60,7 @@ describe('ProjectDetailsPage', () => { }) test('renders project details when data is available', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockProjectDetailsData, error: null, }) @@ -77,7 +77,7 @@ describe('ProjectDetailsPage', () => { }) test('renders error message when GraphQL request fails', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: { repository: null }, error: mockError, }) @@ -143,7 +143,7 @@ describe('ProjectDetailsPage', () => { }) test('Handles case when no data is available', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: { repository: null }, error: null, }) @@ -169,7 +169,7 @@ describe('ProjectDetailsPage', () => { }) test('renders project details with correct capitalization', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockProjectDetailsData, error: null, }) @@ -190,7 +190,7 @@ describe('ProjectDetailsPage', () => { }) test('handles missing project stats gracefully', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: { project: { ...mockProjectDetailsData.project, @@ -222,8 +222,30 @@ describe('ProjectDetailsPage', () => { }) }) + test('renders milestones section correctly', async () => { + render() + await waitFor(() => { + const openMilestones = mockProjectDetailsData.project.openMilestones + const closedMilestones = mockProjectDetailsData.project.closedMilestones + + openMilestones.forEach((milestone) => { + expect(screen.getByText(milestone.title)).toBeInTheDocument() + expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() + expect(screen.getByText(milestone.openIssuesCount)).toBeInTheDocument() + expect(screen.getByText(milestone.closedIssuesCount)).toBeInTheDocument() + expect(screen.getByText(milestone.createdAt)).toBeInTheDocument() + }) + closedMilestones.forEach((milestone) => { + expect(screen.getByText(milestone.title)).toBeInTheDocument() + expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() + expect(screen.getByText(milestone.openIssuesCount)).toBeInTheDocument() + expect(screen.getByText(milestone.closedIssuesCount)).toBeInTheDocument() + expect(screen.getByText(milestone.createdAt)).toBeInTheDocument() + }) + }) + }) test('renders project stats correctly', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockProjectDetailsData, error: null, }) @@ -238,4 +260,4 @@ describe('ProjectDetailsPage', () => { expect(screen.getByText(`10 Issues`)).toBeInTheDocument() }) }) -}) +}) \ No newline at end of file diff --git a/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx b/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx index 783cb62dd0..8c24f63406 100644 --- a/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx +++ b/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx @@ -34,7 +34,7 @@ const mockError = { describe('RepositoryDetailsPage', () => { beforeEach(() => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockRepositoryData, loading: false, error: null, @@ -46,7 +46,7 @@ describe('RepositoryDetailsPage', () => { }) test('renders loading state', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: null, error: null, }) @@ -60,7 +60,7 @@ describe('RepositoryDetailsPage', () => { }) test('renders repository details when data is available', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: mockRepositoryData, error: null, }) @@ -79,7 +79,7 @@ describe('RepositoryDetailsPage', () => { }) test('renders error message when GraphQL request fails', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: { repository: null }, error: mockError, }) @@ -145,7 +145,7 @@ describe('RepositoryDetailsPage', () => { }) test('Handles case when no data is available', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: { repository: null }, error: null, }) @@ -179,8 +179,31 @@ describe('RepositoryDetailsPage', () => { }) }) + test('renders milestones section correctly', async () => { + render() + await waitFor(() => { + const openMilestones = mockRepositoryData.repository.openMilestones + const closedMilestones = mockRepositoryData.repository.closedMilestones + + openMilestones.forEach((milestone) => { + expect(screen.getByText(milestone.title)).toBeInTheDocument() + expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() + expect(screen.getByText(milestone.openIssuesCount)).toBeInTheDocument() + expect(screen.getByText(milestone.closedIssuesCount)).toBeInTheDocument() + expect(screen.getByText(milestone.createdAt)).toBeInTheDocument() + }) + closedMilestones.forEach((milestone) => { + expect(screen.getByText(milestone.title)).toBeInTheDocument() + expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() + expect(screen.getByText(milestone.openIssuesCount)).toBeInTheDocument() + expect(screen.getByText(milestone.closedIssuesCount)).toBeInTheDocument() + expect(screen.getByText(milestone.createdAt)).toBeInTheDocument() + }) + }) + }) + test('handles missing repository stats gracefully', async () => { - ;(useQuery as jest.Mock).mockReturnValue({ + ; (useQuery as jest.Mock).mockReturnValue({ data: { repository: { ...mockRepositoryData.repository, From 3c22b57acec9cc57816bfb5fba48525fda0f4262 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 26 Apr 2025 19:26:48 +0300 Subject: [PATCH 28/56] Fix formatting --- frontend/__tests__/unit/data/mockHomeData.ts | 20 ++++++++--------- .../unit/data/mockProjectDetailsData.ts | 22 +++++++++---------- .../__tests__/unit/data/mockRepositoryData.ts | 22 +++++++++---------- frontend/__tests__/unit/pages/Home.test.tsx | 16 +++++++------- .../unit/pages/ProjectDetails.test.tsx | 18 +++++++-------- .../unit/pages/RepositoryDetails.test.tsx | 12 +++++----- 6 files changed, 55 insertions(+), 55 deletions(-) diff --git a/frontend/__tests__/unit/data/mockHomeData.ts b/frontend/__tests__/unit/data/mockHomeData.ts index 2b7b86d79e..a7e0ed176c 100644 --- a/frontend/__tests__/unit/data/mockHomeData.ts +++ b/frontend/__tests__/unit/data/mockHomeData.ts @@ -105,7 +105,7 @@ export const mockGraphQLData = { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/33333?v=4', login: 'milestone-author1', - name: 'Milestone Author 1' + name: 'Milestone Author 1', }, title: 'v2.0.0 Release', openIssuesCount: 5, @@ -113,13 +113,13 @@ export const mockGraphQLData = { repositoryName: 'Repo One', organizationName: 'OWASP', createdAt: '2025-03-01T10:00:00Z', - url: 'https://github.com/OWASP/repo-one/milestone/1' + url: 'https://github.com/OWASP/repo-one/milestone/1', }, { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/44444?v=4', login: 'milestone-author2', - name: 'Milestone Author 2' + name: 'Milestone Author 2', }, title: 'Documentation Update', openIssuesCount: 3, @@ -127,15 +127,15 @@ export const mockGraphQLData = { repositoryName: 'Repo Two', organizationName: 'OWASP', createdAt: '2025-02-15T14:30:00Z', - url: 'https://github.com/OWASP/repo-two/milestone/2' - } + url: 'https://github.com/OWASP/repo-two/milestone/2', + }, ], closedMilestones: [ { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/55555?v=4', login: 'milestone-author3', - name: 'Milestone Author 3' + name: 'Milestone Author 3', }, title: 'v1.0.0 Release', openIssuesCount: 0, @@ -143,13 +143,13 @@ export const mockGraphQLData = { repositoryName: 'Repo One', organizationName: 'OWASP', createdAt: '2024-12-01T09:00:00Z', - url: 'https://github.com/OWASP/repo-one/milestone/3' + url: 'https://github.com/OWASP/repo-one/milestone/3', }, { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/66666?v=4', login: 'milestone-author4', - name: 'Milestone Author 4' + name: 'Milestone Author 4', }, title: 'Security Updates', openIssuesCount: 0, @@ -157,8 +157,8 @@ export const mockGraphQLData = { repositoryName: 'Repo Two', organizationName: 'OWASP', createdAt: '2024-11-15T16:45:00Z', - url: 'https://github.com/OWASP/repo-two/milestone/4' - } + url: 'https://github.com/OWASP/repo-two/milestone/4', + }, ], statsOverview: { activeChaptersStats: 540, diff --git a/frontend/__tests__/unit/data/mockProjectDetailsData.ts b/frontend/__tests__/unit/data/mockProjectDetailsData.ts index c17bf01e41..5bdfc95780 100644 --- a/frontend/__tests__/unit/data/mockProjectDetailsData.ts +++ b/frontend/__tests__/unit/data/mockProjectDetailsData.ts @@ -81,7 +81,7 @@ export const mockProjectDetailsData = { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/33333?v=4', login: 'milestone-author1', - name: 'Milestone Author 1' + name: 'Milestone Author 1', }, title: 'v2.0.0 Release', openIssuesCount: 5, @@ -89,13 +89,13 @@ export const mockProjectDetailsData = { repositoryName: 'Repo One', organizationName: 'OWASP', createdAt: '2025-03-01T10:00:00Z', - url: 'https://github.com/OWASP/repo-one/milestone/1' + url: 'https://github.com/OWASP/repo-one/milestone/1', }, { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/44444?v=4', login: 'milestone-author2', - name: 'Milestone Author 2' + name: 'Milestone Author 2', }, title: 'Documentation Update', openIssuesCount: 3, @@ -103,15 +103,15 @@ export const mockProjectDetailsData = { repositoryName: 'Repo Two', organizationName: 'OWASP', createdAt: '2025-02-15T14:30:00Z', - url: 'https://github.com/OWASP/repo-two/milestone/2' - } + url: 'https://github.com/OWASP/repo-two/milestone/2', + }, ], closedMilestones: [ { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/55555?v=4', login: 'milestone-author3', - name: 'Milestone Author 3' + name: 'Milestone Author 3', }, title: 'v1.0.0 Release', openIssuesCount: 0, @@ -119,13 +119,13 @@ export const mockProjectDetailsData = { repositoryName: 'Repo One', organizationName: 'OWASP', createdAt: '2024-12-01T09:00:00Z', - url: 'https://github.com/OWASP/repo-one/milestone/3' + url: 'https://github.com/OWASP/repo-one/milestone/3', }, { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/66666?v=4', login: 'milestone-author4', - name: 'Milestone Author 4' + name: 'Milestone Author 4', }, title: 'Security Updates', openIssuesCount: 0, @@ -133,9 +133,9 @@ export const mockProjectDetailsData = { repositoryName: 'Repo Two', organizationName: 'OWASP', createdAt: '2024-11-15T16:45:00Z', - url: 'https://github.com/OWASP/repo-two/milestone/4' - } - ] + url: 'https://github.com/OWASP/repo-two/milestone/4', + }, + ], }, recentPullRequests: [ { diff --git a/frontend/__tests__/unit/data/mockRepositoryData.ts b/frontend/__tests__/unit/data/mockRepositoryData.ts index e42a7ad799..9e424a1be3 100644 --- a/frontend/__tests__/unit/data/mockRepositoryData.ts +++ b/frontend/__tests__/unit/data/mockRepositoryData.ts @@ -50,7 +50,7 @@ export const mockRepositoryData = { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/33333?v=4', login: 'milestone-author1', - name: 'Milestone Author 1' + name: 'Milestone Author 1', }, title: 'v2.0.0 Release', openIssuesCount: 5, @@ -58,13 +58,13 @@ export const mockRepositoryData = { repositoryName: 'Repo One', organizationName: 'OWASP', createdAt: '2025-03-01T10:00:00Z', - url: 'https://github.com/OWASP/repo-one/milestone/1' + url: 'https://github.com/OWASP/repo-one/milestone/1', }, { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/44444?v=4', login: 'milestone-author2', - name: 'Milestone Author 2' + name: 'Milestone Author 2', }, title: 'Documentation Update', openIssuesCount: 3, @@ -72,15 +72,15 @@ export const mockRepositoryData = { repositoryName: 'Repo Two', organizationName: 'OWASP', createdAt: '2025-02-15T14:30:00Z', - url: 'https://github.com/OWASP/repo-two/milestone/2' - } + url: 'https://github.com/OWASP/repo-two/milestone/2', + }, ], closedMilestones: [ { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/55555?v=4', login: 'milestone-author3', - name: 'Milestone Author 3' + name: 'Milestone Author 3', }, title: 'v1.0.0 Release', openIssuesCount: 0, @@ -88,13 +88,13 @@ export const mockRepositoryData = { repositoryName: 'Repo One', organizationName: 'OWASP', createdAt: '2024-12-01T09:00:00Z', - url: 'https://github.com/OWASP/repo-one/milestone/3' + url: 'https://github.com/OWASP/repo-one/milestone/3', }, { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/66666?v=4', login: 'milestone-author4', - name: 'Milestone Author 4' + name: 'Milestone Author 4', }, title: 'Security Updates', openIssuesCount: 0, @@ -102,9 +102,9 @@ export const mockRepositoryData = { repositoryName: 'Repo Two', organizationName: 'OWASP', createdAt: '2024-11-15T16:45:00Z', - url: 'https://github.com/OWASP/repo-two/milestone/4' - } - ] + url: 'https://github.com/OWASP/repo-two/milestone/4', + }, + ], }, recentPullRequests: [ { diff --git a/frontend/__tests__/unit/pages/Home.test.tsx b/frontend/__tests__/unit/pages/Home.test.tsx index 1a85edfe21..9e1a497f40 100644 --- a/frontend/__tests__/unit/pages/Home.test.tsx +++ b/frontend/__tests__/unit/pages/Home.test.tsx @@ -69,14 +69,14 @@ describe('Home', () => { let mockRouter: { push: jest.Mock } beforeEach(() => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockGraphQLData, loading: false, error: null, }) - ; (fetchAlgoliaData as jest.Mock).mockResolvedValue(mockAlgoliaData) + ;(fetchAlgoliaData as jest.Mock).mockResolvedValue(mockAlgoliaData) mockRouter = { push: jest.fn() } - ; (useRouter as jest.Mock).mockReturnValue(mockRouter) + ;(useRouter as jest.Mock).mockReturnValue(mockRouter) }) afterEach(() => { @@ -84,7 +84,7 @@ describe('Home', () => { }) test('renders loading state', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: null, loading: true, error: null, @@ -109,7 +109,7 @@ describe('Home', () => { }) test('renders error message when GraphQL request fails', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: null, error: { message: 'GraphQL error' }, }) @@ -192,11 +192,11 @@ describe('Home', () => { }) test('handles missing data gracefully', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockGraphQLData, error: null, }) - ; (fetchAlgoliaData as jest.Mock).mockResolvedValue({ hits: [] }) + ;(fetchAlgoliaData as jest.Mock).mockResolvedValue({ hits: [] }) render() @@ -253,7 +253,7 @@ describe('Home', () => { }) }) test('renders when no recent releases', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: { ...mockGraphQLData, recentReleases: [], diff --git a/frontend/__tests__/unit/pages/ProjectDetails.test.tsx b/frontend/__tests__/unit/pages/ProjectDetails.test.tsx index b0aa79918d..9b9b7236a0 100644 --- a/frontend/__tests__/unit/pages/ProjectDetails.test.tsx +++ b/frontend/__tests__/unit/pages/ProjectDetails.test.tsx @@ -34,7 +34,7 @@ const mockError = { describe('ProjectDetailsPage', () => { beforeEach(() => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockProjectDetailsData, loading: false, error: null, @@ -46,7 +46,7 @@ describe('ProjectDetailsPage', () => { }) test('renders loading state', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: null, error: null, }) @@ -60,7 +60,7 @@ describe('ProjectDetailsPage', () => { }) test('renders project details when data is available', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockProjectDetailsData, error: null, }) @@ -77,7 +77,7 @@ describe('ProjectDetailsPage', () => { }) test('renders error message when GraphQL request fails', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: { repository: null }, error: mockError, }) @@ -143,7 +143,7 @@ describe('ProjectDetailsPage', () => { }) test('Handles case when no data is available', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: { repository: null }, error: null, }) @@ -169,7 +169,7 @@ describe('ProjectDetailsPage', () => { }) test('renders project details with correct capitalization', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockProjectDetailsData, error: null, }) @@ -190,7 +190,7 @@ describe('ProjectDetailsPage', () => { }) test('handles missing project stats gracefully', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: { project: { ...mockProjectDetailsData.project, @@ -245,7 +245,7 @@ describe('ProjectDetailsPage', () => { }) }) test('renders project stats correctly', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockProjectDetailsData, error: null, }) @@ -260,4 +260,4 @@ describe('ProjectDetailsPage', () => { expect(screen.getByText(`10 Issues`)).toBeInTheDocument() }) }) -}) \ No newline at end of file +}) diff --git a/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx b/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx index 8c24f63406..f202378bb2 100644 --- a/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx +++ b/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx @@ -34,7 +34,7 @@ const mockError = { describe('RepositoryDetailsPage', () => { beforeEach(() => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockRepositoryData, loading: false, error: null, @@ -46,7 +46,7 @@ describe('RepositoryDetailsPage', () => { }) test('renders loading state', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: null, error: null, }) @@ -60,7 +60,7 @@ describe('RepositoryDetailsPage', () => { }) test('renders repository details when data is available', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: mockRepositoryData, error: null, }) @@ -79,7 +79,7 @@ describe('RepositoryDetailsPage', () => { }) test('renders error message when GraphQL request fails', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: { repository: null }, error: mockError, }) @@ -145,7 +145,7 @@ describe('RepositoryDetailsPage', () => { }) test('Handles case when no data is available', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: { repository: null }, error: null, }) @@ -203,7 +203,7 @@ describe('RepositoryDetailsPage', () => { }) test('handles missing repository stats gracefully', async () => { - ; (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ data: { repository: { ...mockRepositoryData.repository, From 967dc922f4b3b44d33a054665f939667cb41808f Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 26 Apr 2025 20:09:41 +0300 Subject: [PATCH 29/56] Fix unit tests --- frontend/__tests__/unit/data/mockHomeData.ts | 32 ++----------------- .../unit/data/mockProjectDetailsData.ts | 32 ++----------------- .../__tests__/unit/data/mockRepositoryData.ts | 28 ---------------- frontend/__tests__/unit/pages/Home.test.tsx | 10 +++--- .../unit/pages/ProjectDetails.test.tsx | 10 +++--- .../unit/pages/RepositoryDetails.test.tsx | 10 +++--- 6 files changed, 16 insertions(+), 106 deletions(-) diff --git a/frontend/__tests__/unit/data/mockHomeData.ts b/frontend/__tests__/unit/data/mockHomeData.ts index a7e0ed176c..3d5eac0370 100644 --- a/frontend/__tests__/unit/data/mockHomeData.ts +++ b/frontend/__tests__/unit/data/mockHomeData.ts @@ -110,41 +110,13 @@ export const mockGraphQLData = { title: 'v2.0.0 Release', openIssuesCount: 5, closedIssuesCount: 15, - repositoryName: 'Repo One', + repositoryName: 'Home Repo One', organizationName: 'OWASP', createdAt: '2025-03-01T10:00:00Z', url: 'https://github.com/OWASP/repo-one/milestone/1', }, - { - author: { - avatarUrl: 'https://avatars.githubusercontent.com/u/44444?v=4', - login: 'milestone-author2', - name: 'Milestone Author 2', - }, - title: 'Documentation Update', - openIssuesCount: 3, - closedIssuesCount: 7, - repositoryName: 'Repo Two', - organizationName: 'OWASP', - createdAt: '2025-02-15T14:30:00Z', - url: 'https://github.com/OWASP/repo-two/milestone/2', - }, ], closedMilestones: [ - { - author: { - avatarUrl: 'https://avatars.githubusercontent.com/u/55555?v=4', - login: 'milestone-author3', - name: 'Milestone Author 3', - }, - title: 'v1.0.0 Release', - openIssuesCount: 0, - closedIssuesCount: 25, - repositoryName: 'Repo One', - organizationName: 'OWASP', - createdAt: '2024-12-01T09:00:00Z', - url: 'https://github.com/OWASP/repo-one/milestone/3', - }, { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/66666?v=4', @@ -154,7 +126,7 @@ export const mockGraphQLData = { title: 'Security Updates', openIssuesCount: 0, closedIssuesCount: 12, - repositoryName: 'Repo Two', + repositoryName: 'Home Repo Two', organizationName: 'OWASP', createdAt: '2024-11-15T16:45:00Z', url: 'https://github.com/OWASP/repo-two/milestone/4', diff --git a/frontend/__tests__/unit/data/mockProjectDetailsData.ts b/frontend/__tests__/unit/data/mockProjectDetailsData.ts index 5bdfc95780..f48f689b2e 100644 --- a/frontend/__tests__/unit/data/mockProjectDetailsData.ts +++ b/frontend/__tests__/unit/data/mockProjectDetailsData.ts @@ -86,41 +86,13 @@ export const mockProjectDetailsData = { title: 'v2.0.0 Release', openIssuesCount: 5, closedIssuesCount: 15, - repositoryName: 'Repo One', + repositoryName: 'Project Repo One', organizationName: 'OWASP', createdAt: '2025-03-01T10:00:00Z', url: 'https://github.com/OWASP/repo-one/milestone/1', }, - { - author: { - avatarUrl: 'https://avatars.githubusercontent.com/u/44444?v=4', - login: 'milestone-author2', - name: 'Milestone Author 2', - }, - title: 'Documentation Update', - openIssuesCount: 3, - closedIssuesCount: 7, - repositoryName: 'Repo Two', - organizationName: 'OWASP', - createdAt: '2025-02-15T14:30:00Z', - url: 'https://github.com/OWASP/repo-two/milestone/2', - }, ], closedMilestones: [ - { - author: { - avatarUrl: 'https://avatars.githubusercontent.com/u/55555?v=4', - login: 'milestone-author3', - name: 'Milestone Author 3', - }, - title: 'v1.0.0 Release', - openIssuesCount: 0, - closedIssuesCount: 25, - repositoryName: 'Repo One', - organizationName: 'OWASP', - createdAt: '2024-12-01T09:00:00Z', - url: 'https://github.com/OWASP/repo-one/milestone/3', - }, { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/66666?v=4', @@ -130,7 +102,7 @@ export const mockProjectDetailsData = { title: 'Security Updates', openIssuesCount: 0, closedIssuesCount: 12, - repositoryName: 'Repo Two', + repositoryName: 'Project Repo Two', organizationName: 'OWASP', createdAt: '2024-11-15T16:45:00Z', url: 'https://github.com/OWASP/repo-two/milestone/4', diff --git a/frontend/__tests__/unit/data/mockRepositoryData.ts b/frontend/__tests__/unit/data/mockRepositoryData.ts index 9e424a1be3..0c7c0299d6 100644 --- a/frontend/__tests__/unit/data/mockRepositoryData.ts +++ b/frontend/__tests__/unit/data/mockRepositoryData.ts @@ -60,36 +60,8 @@ export const mockRepositoryData = { createdAt: '2025-03-01T10:00:00Z', url: 'https://github.com/OWASP/repo-one/milestone/1', }, - { - author: { - avatarUrl: 'https://avatars.githubusercontent.com/u/44444?v=4', - login: 'milestone-author2', - name: 'Milestone Author 2', - }, - title: 'Documentation Update', - openIssuesCount: 3, - closedIssuesCount: 7, - repositoryName: 'Repo Two', - organizationName: 'OWASP', - createdAt: '2025-02-15T14:30:00Z', - url: 'https://github.com/OWASP/repo-two/milestone/2', - }, ], closedMilestones: [ - { - author: { - avatarUrl: 'https://avatars.githubusercontent.com/u/55555?v=4', - login: 'milestone-author3', - name: 'Milestone Author 3', - }, - title: 'v1.0.0 Release', - openIssuesCount: 0, - closedIssuesCount: 25, - repositoryName: 'Repo One', - organizationName: 'OWASP', - createdAt: '2024-12-01T09:00:00Z', - url: 'https://github.com/OWASP/repo-one/milestone/3', - }, { author: { avatarUrl: 'https://avatars.githubusercontent.com/u/66666?v=4', diff --git a/frontend/__tests__/unit/pages/Home.test.tsx b/frontend/__tests__/unit/pages/Home.test.tsx index 9e1a497f40..46c97f6359 100644 --- a/frontend/__tests__/unit/pages/Home.test.tsx +++ b/frontend/__tests__/unit/pages/Home.test.tsx @@ -239,16 +239,14 @@ describe('Home', () => { openMilestones.forEach((milestone) => { expect(screen.getByText(milestone.title)).toBeInTheDocument() expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() - expect(screen.getByText(milestone.openIssuesCount)).toBeInTheDocument() - expect(screen.getByText(milestone.closedIssuesCount)).toBeInTheDocument() - expect(screen.getByText(milestone.createdAt)).toBeInTheDocument() + expect(screen.getByText(`${milestone.openIssuesCount} open`)).toBeInTheDocument() + expect(screen.getByText(`${milestone.closedIssuesCount} closed`)).toBeInTheDocument() }) closedMilestones.forEach((milestone) => { expect(screen.getByText(milestone.title)).toBeInTheDocument() expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() - expect(screen.getByText(milestone.openIssuesCount)).toBeInTheDocument() - expect(screen.getByText(milestone.closedIssuesCount)).toBeInTheDocument() - expect(screen.getByText(milestone.createdAt)).toBeInTheDocument() + expect(screen.getByText(`${milestone.openIssuesCount} open`)).toBeInTheDocument() + expect(screen.getByText(`${milestone.closedIssuesCount} closed`)).toBeInTheDocument() }) }) }) diff --git a/frontend/__tests__/unit/pages/ProjectDetails.test.tsx b/frontend/__tests__/unit/pages/ProjectDetails.test.tsx index 9b9b7236a0..fcb3994f97 100644 --- a/frontend/__tests__/unit/pages/ProjectDetails.test.tsx +++ b/frontend/__tests__/unit/pages/ProjectDetails.test.tsx @@ -231,16 +231,14 @@ describe('ProjectDetailsPage', () => { openMilestones.forEach((milestone) => { expect(screen.getByText(milestone.title)).toBeInTheDocument() expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() - expect(screen.getByText(milestone.openIssuesCount)).toBeInTheDocument() - expect(screen.getByText(milestone.closedIssuesCount)).toBeInTheDocument() - expect(screen.getByText(milestone.createdAt)).toBeInTheDocument() + expect(screen.getByText(`${milestone.openIssuesCount} open`)).toBeInTheDocument() + expect(screen.getByText(`${milestone.closedIssuesCount} closed`)).toBeInTheDocument() }) closedMilestones.forEach((milestone) => { expect(screen.getByText(milestone.title)).toBeInTheDocument() expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() - expect(screen.getByText(milestone.openIssuesCount)).toBeInTheDocument() - expect(screen.getByText(milestone.closedIssuesCount)).toBeInTheDocument() - expect(screen.getByText(milestone.createdAt)).toBeInTheDocument() + expect(screen.getByText(`${milestone.openIssuesCount} open`)).toBeInTheDocument() + expect(screen.getByText(`${milestone.closedIssuesCount} closed`)).toBeInTheDocument() }) }) }) diff --git a/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx b/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx index f202378bb2..8a29d82e92 100644 --- a/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx +++ b/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx @@ -188,16 +188,14 @@ describe('RepositoryDetailsPage', () => { openMilestones.forEach((milestone) => { expect(screen.getByText(milestone.title)).toBeInTheDocument() expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() - expect(screen.getByText(milestone.openIssuesCount)).toBeInTheDocument() - expect(screen.getByText(milestone.closedIssuesCount)).toBeInTheDocument() - expect(screen.getByText(milestone.createdAt)).toBeInTheDocument() + expect(screen.getByText(`${milestone.openIssuesCount} open`)).toBeInTheDocument() + expect(screen.getByText(`${milestone.closedIssuesCount} closed`)).toBeInTheDocument() }) closedMilestones.forEach((milestone) => { expect(screen.getByText(milestone.title)).toBeInTheDocument() expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() - expect(screen.getByText(milestone.openIssuesCount)).toBeInTheDocument() - expect(screen.getByText(milestone.closedIssuesCount)).toBeInTheDocument() - expect(screen.getByText(milestone.createdAt)).toBeInTheDocument() + expect(screen.getByText(`${milestone.openIssuesCount} open`)).toBeInTheDocument() + expect(screen.getByText(`${milestone.closedIssuesCount} closed`)).toBeInTheDocument() }) }) }) From 95efc0233a56c5724e451322648e117789448a2a Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 26 Apr 2025 22:30:36 +0300 Subject: [PATCH 30/56] Implement e2e tests --- frontend/__tests__/e2e/data/mockHomeData.ts | 32 +++++++++++++++++++ frontend/__tests__/e2e/pages/Home.spec.ts | 14 ++++++++ .../e2e/pages/ProjectDetails.spec.ts | 14 ++++++++ .../e2e/pages/RepositoryDetails.spec.ts | 14 ++++++++ 4 files changed, 74 insertions(+) diff --git a/frontend/__tests__/e2e/data/mockHomeData.ts b/frontend/__tests__/e2e/data/mockHomeData.ts index 50adad4fe8..5c7447f620 100644 --- a/frontend/__tests__/e2e/data/mockHomeData.ts +++ b/frontend/__tests__/e2e/data/mockHomeData.ts @@ -195,6 +195,38 @@ export const mockHomeData = { __typename: 'ReleaseNode', }, ], + openMilestones: [ + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/33333?v=4', + login: 'milestone-author1', + name: 'Milestone Author 1', + }, + title: 'v2.0.0 Release', + openIssuesCount: 5, + closedIssuesCount: 15, + repositoryName: 'Home Repo One', + organizationName: 'OWASP', + createdAt: '2025-03-01T10:00:00Z', + url: 'https://github.com/OWASP/repo-one/milestone/1', + }, + ], + closedMilestones: [ + { + author: { + avatarUrl: 'https://avatars.githubusercontent.com/u/66666?v=4', + login: 'milestone-author4', + name: 'Milestone Author 4', + }, + title: 'Security Updates', + openIssuesCount: 0, + closedIssuesCount: 12, + repositoryName: 'Home Repo Two', + organizationName: 'OWASP', + createdAt: '2024-11-15T16:45:00Z', + url: 'https://github.com/OWASP/repo-two/milestone/4', + }, + ], sponsors: [ { imageUrl: diff --git a/frontend/__tests__/e2e/pages/Home.spec.ts b/frontend/__tests__/e2e/pages/Home.spec.ts index cf74b543ab..e0cf1d2fb1 100644 --- a/frontend/__tests__/e2e/pages/Home.spec.ts +++ b/frontend/__tests__/e2e/pages/Home.spec.ts @@ -76,6 +76,20 @@ test.describe('Home Page', () => { await expect(page.getByText('repo-1')).toBeVisible() }) + test('should have open milestones', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'Open Milestones' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'v2.0.0 Release' })).toBeVisible() + await expect(page.getByText('Mar 1, 2025')).toBeVisible() + await expect(page.getByText('Home Repo One')).toBeVisible() + }) + + test('should have closed milestones', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'Closed Milestones' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'Security Updates' })).toBeVisible() + await expect(page.getByText('Nov 15, 2024')).toBeVisible() + await expect(page.getByText('Home Repo Two')).toBeVisible() + }) + test('should be able to join OWASP', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Ready to Make a Difference?' })).toBeVisible() await expect(page.getByText('Join OWASP and be part of the')).toBeVisible() diff --git a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts index 27c63eaa54..93620a4193 100644 --- a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts @@ -88,6 +88,20 @@ test.describe('Project Details Page', () => { await expect(page.getByText('Jan 20, 2025')).toBeVisible() }) + test('should have project open milestones', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'Open Milestones' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'v2.0.0 Release' })).toBeVisible() + await expect(page.getByText('Mar 1, 2025')).toBeVisible() + await expect(page.getByText('Home Repo One')).toBeVisible() + }) + + test('should have closed milestones', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'Closed Milestones' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'Security Updates' })).toBeVisible() + await expect(page.getByText('Nov 15, 2024')).toBeVisible() + await expect(page.getByText('Home Repo Two')).toBeVisible() + }) + test('should display recent pull requests section', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Recent Pull Requests' })).toBeVisible() await expect(page.getByText('Test Pull Request 1')).toBeVisible() diff --git a/frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts b/frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts index 0a648d56f3..688a875689 100644 --- a/frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts @@ -84,6 +84,20 @@ test.describe('Repository Details Page', () => { await expect(page.getByText('Jan 1, 2024', { exact: true })).toBeVisible() }) + test('should have project open milestones', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'Open Milestones' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'v2.0.0 Release' })).toBeVisible() + await expect(page.getByText('Mar 1, 2025')).toBeVisible() + await expect(page.getByText('Home Repo One')).toBeVisible() + }) + + test('should have closed milestones', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'Closed Milestones' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'Security Updates' })).toBeVisible() + await expect(page.getByText('Nov 15, 2024')).toBeVisible() + await expect(page.getByText('Home Repo Two')).toBeVisible() + }) + test('should display recent pull requests section', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Recent Pull Requests' })).toBeVisible() await expect(page.getByText('Test Pull Request 1')).toBeVisible() From b1103b1970ec3fb772a9bb9e48fa8615d1e3604f Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 26 Apr 2025 22:52:54 +0300 Subject: [PATCH 31/56] Fix e2e milestone tests --- frontend/__tests__/e2e/pages/ProjectDetails.spec.ts | 4 ++-- frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts index 93620a4193..d7aee275b0 100644 --- a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts @@ -92,14 +92,14 @@ test.describe('Project Details Page', () => { await expect(page.getByRole('heading', { name: 'Open Milestones' })).toBeVisible() await expect(page.getByRole('heading', { name: 'v2.0.0 Release' })).toBeVisible() await expect(page.getByText('Mar 1, 2025')).toBeVisible() - await expect(page.getByText('Home Repo One')).toBeVisible() + await expect(page.getByText('Project Repo One')).toBeVisible() }) test('should have closed milestones', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Closed Milestones' })).toBeVisible() await expect(page.getByRole('heading', { name: 'Security Updates' })).toBeVisible() await expect(page.getByText('Nov 15, 2024')).toBeVisible() - await expect(page.getByText('Home Repo Two')).toBeVisible() + await expect(page.getByText('Project Repo Two')).toBeVisible() }) test('should display recent pull requests section', async ({ page }) => { diff --git a/frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts b/frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts index 688a875689..d7da82a902 100644 --- a/frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts @@ -88,14 +88,14 @@ test.describe('Repository Details Page', () => { await expect(page.getByRole('heading', { name: 'Open Milestones' })).toBeVisible() await expect(page.getByRole('heading', { name: 'v2.0.0 Release' })).toBeVisible() await expect(page.getByText('Mar 1, 2025')).toBeVisible() - await expect(page.getByText('Home Repo One')).toBeVisible() + await expect(page.getByText('Repo One')).toBeVisible() }) test('should have closed milestones', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Closed Milestones' })).toBeVisible() await expect(page.getByRole('heading', { name: 'Security Updates' })).toBeVisible() await expect(page.getByText('Nov 15, 2024')).toBeVisible() - await expect(page.getByText('Home Repo Two')).toBeVisible() + await expect(page.getByText('Repo Two')).toBeVisible() }) test('should display recent pull requests section', async ({ page }) => { From e39f13a42ce4486e9a076c5bd3a1b873eac69509 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 26 Apr 2025 23:15:59 +0300 Subject: [PATCH 32/56] Add test cases for MilestoneNode class --- .../github/graphql/nodes/milestone_test.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 backend/tests/apps/github/graphql/nodes/milestone_test.py diff --git a/backend/tests/apps/github/graphql/nodes/milestone_test.py b/backend/tests/apps/github/graphql/nodes/milestone_test.py new file mode 100644 index 0000000000..522ea888fe --- /dev/null +++ b/backend/tests/apps/github/graphql/nodes/milestone_test.py @@ -0,0 +1,28 @@ +"""Test cases for MilestoneNode.""" + +from apps.common.graphql.nodes import BaseNode +from apps.github.graphql.nodes.milestone import MilestoneNode +from apps.github.models.milestone import Milestone + + +class TestMilestoneNode: + """Test cases for MilestoneNode class.""" + + def test_milestone_node_inheritance(self): + """Test if IssueNode inherits from BaseNode.""" + assert issubclass(MilestoneNode, BaseNode) + + def test_meta_configuration(self): + """Test if Meta is properly configured.""" + assert MilestoneNode._meta.model == Milestone + expected_fields = { + "author", + "created_at", + "title", + "open_issues_count", + "closed_issues_count", + "url", + "repository_name", + "organization_name", + } + assert set(MilestoneNode._meta.fields) == expected_fields From 787e45d34ceeaa62ad6135102e4dc9d0a2f77eeb Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 26 Apr 2025 23:18:31 +0300 Subject: [PATCH 33/56] Rename test for closed milestones to improve clarity --- frontend/__tests__/e2e/pages/ProjectDetails.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts index d7aee275b0..4bfe051027 100644 --- a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts @@ -95,7 +95,7 @@ test.describe('Project Details Page', () => { await expect(page.getByText('Project Repo One')).toBeVisible() }) - test('should have closed milestones', async ({ page }) => { + test('should have project closed milestones', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Closed Milestones' })).toBeVisible() await expect(page.getByRole('heading', { name: 'Security Updates' })).toBeVisible() await expect(page.getByText('Nov 15, 2024')).toBeVisible() From 9444bf8659bd8b27528680d782b42f16be5c5d68 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sun, 27 Apr 2025 07:58:43 +0300 Subject: [PATCH 34/56] Fix e2e bug. --- frontend/__tests__/e2e/pages/ProjectDetails.spec.ts | 4 ++-- frontend/__tests__/unit/data/mockProjectDetailsData.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts index 4bfe051027..64d5e4072f 100644 --- a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts @@ -92,14 +92,14 @@ test.describe('Project Details Page', () => { await expect(page.getByRole('heading', { name: 'Open Milestones' })).toBeVisible() await expect(page.getByRole('heading', { name: 'v2.0.0 Release' })).toBeVisible() await expect(page.getByText('Mar 1, 2025')).toBeVisible() - await expect(page.getByText('Project Repo One')).toBeVisible() + await expect(page.getByText('Project Repo 1')).toBeVisible() }) test('should have project closed milestones', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Closed Milestones' })).toBeVisible() await expect(page.getByRole('heading', { name: 'Security Updates' })).toBeVisible() await expect(page.getByText('Nov 15, 2024')).toBeVisible() - await expect(page.getByText('Project Repo Two')).toBeVisible() + await expect(page.getByText('Project Repo 2')).toBeVisible() }) test('should display recent pull requests section', async ({ page }) => { diff --git a/frontend/__tests__/unit/data/mockProjectDetailsData.ts b/frontend/__tests__/unit/data/mockProjectDetailsData.ts index f48f689b2e..42853c9576 100644 --- a/frontend/__tests__/unit/data/mockProjectDetailsData.ts +++ b/frontend/__tests__/unit/data/mockProjectDetailsData.ts @@ -86,7 +86,7 @@ export const mockProjectDetailsData = { title: 'v2.0.0 Release', openIssuesCount: 5, closedIssuesCount: 15, - repositoryName: 'Project Repo One', + repositoryName: 'Project Repo 1', organizationName: 'OWASP', createdAt: '2025-03-01T10:00:00Z', url: 'https://github.com/OWASP/repo-one/milestone/1', @@ -102,7 +102,7 @@ export const mockProjectDetailsData = { title: 'Security Updates', openIssuesCount: 0, closedIssuesCount: 12, - repositoryName: 'Project Repo Two', + repositoryName: 'Project Repo 2', organizationName: 'OWASP', createdAt: '2024-11-15T16:45:00Z', url: 'https://github.com/OWASP/repo-two/milestone/4', From b37bdbf6a538ca1b903904ac1f6f68a6b74b1cee Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sun, 27 Apr 2025 08:11:24 +0300 Subject: [PATCH 35/56] Refactor Milestones component to remove unnecessary href prop from AnchorTitle --- frontend/src/components/Milestones.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/components/Milestones.tsx b/frontend/src/components/Milestones.tsx index 3f60a6dc38..e51a03ac95 100644 --- a/frontend/src/components/Milestones.tsx +++ b/frontend/src/components/Milestones.tsx @@ -31,9 +31,7 @@ const Milestones: React.FC = ({ -
From caf03e200ce3d7e40e2c91cad7dc7120c0ffdbb4 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sun, 27 Apr 2025 08:24:04 +0300 Subject: [PATCH 36/56] Refactor for the checks --- frontend/src/components/Milestones.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Milestones.tsx b/frontend/src/components/Milestones.tsx index e51a03ac95..5586b3cef8 100644 --- a/frontend/src/components/Milestones.tsx +++ b/frontend/src/components/Milestones.tsx @@ -31,7 +31,8 @@ const Milestones: React.FC = ({ - From 0a701cec848c6b49ee2f9feb662b259186021838 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 3 May 2025 14:04:42 +0300 Subject: [PATCH 37/56] Fix formatting in update_data method and remove unnecessary blank line in nest_key method --- backend/apps/github/models/issue.py | 2 +- backend/apps/github/models/repository.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/apps/github/models/issue.py b/backend/apps/github/models/issue.py index 49b099c815..554bbfbc39 100644 --- a/backend/apps/github/models/issue.py +++ b/backend/apps/github/models/issue.py @@ -185,7 +185,7 @@ def open_issues_count(): return IndexBase.get_total_count("issues") @staticmethod - def update_data(gh_issue, *, author=None, repository=None, milestone=None, save: bool=True): + def update_data(gh_issue, *, author=None, repository=None, milestone=None, save: bool = True): """Update issue data. Args: diff --git a/backend/apps/github/models/repository.py b/backend/apps/github/models/repository.py index 83ef269c6c..7617045925 100644 --- a/backend/apps/github/models/repository.py +++ b/backend/apps/github/models/repository.py @@ -145,7 +145,6 @@ def latest_updated_milestone(self): return self.milestones.order_by("-updated_at").first() def nest_key(self) -> str: - """Return repository Nest key.""" return f"{self.owner.login}-{self.name}" From bd3f08d35f2e66844e5223e3b7260556ddd1fed2 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Mon, 5 May 2025 07:56:56 +0300 Subject: [PATCH 38/56] Refactor MilestoneQuery to support filtering by milestone state and update Milestone model table name --- .../apps/github/graphql/queries/milestone.py | 33 ++++++++----------- .../migrations/0027_alter_milestone_table.py | 16 +++++++++ backend/apps/github/models/issue.py | 2 +- backend/apps/github/models/milestone.py | 6 +--- 4 files changed, 32 insertions(+), 25 deletions(-) create mode 100644 backend/apps/github/migrations/0027_alter_milestone_table.py diff --git a/backend/apps/github/graphql/queries/milestone.py b/backend/apps/github/graphql/queries/milestone.py index 2f7d5ea93a..5cbc4dedee 100644 --- a/backend/apps/github/graphql/queries/milestone.py +++ b/backend/apps/github/graphql/queries/milestone.py @@ -1,7 +1,7 @@ """Github Milestone Queries.""" import graphene -from django.db.models import OuterRef, Subquery +from django.core.exceptions import ValidationError from apps.common.graphql.queries import BaseQuery from apps.github.graphql.nodes.milestone import MilestoneNode @@ -17,8 +17,7 @@ class MilestoneQuery(BaseQuery): login=graphene.String(required=False), organization=graphene.String(required=False), repository=graphene.String(required=False), - distinct=graphene.Boolean(default_value=False), - close=graphene.Boolean(default_value=True), + state=graphene.String(default_value="open"), ) def resolve_milestones( @@ -28,8 +27,7 @@ def resolve_milestones( login=None, organization=None, repository=None, - distinct=False, - close=True, + state="open", ): """Resolve milestones. @@ -40,14 +38,22 @@ def resolve_milestones( login (str, optional): Filter milestones by author login. organization (str, optional): Filter milestones by organization login. repository (str, optional): Filter milestones by repository name. - distinct (bool, optional): Whether to return distinct milestones. - close (bool, optional): Whether to return open or closed milestones. + state (str, optional): The state of the milestones to return. Returns: list: A list of milestones. """ - milestones = Milestone.closed_milestones if close else Milestone.open_milestones + lower_state = state.lower() + if lower_state == "open": + milestones = Milestone.open_milestones.all() + elif lower_state == "closed": + milestones = Milestone.closed_milestones.all() + elif lower_state == "all": + milestones = Milestone.objects.all() + else: + message = f"Invalid state: {state}. Valid states are 'open', 'closed', or 'all'." + raise ValidationError(message) milestones = milestones.select_related( "author", "repository", @@ -64,15 +70,4 @@ def resolve_milestones( milestones = milestones.filter(repository__name=repository) if organization: milestones = milestones.filter(repository__organization__login=organization) - - if distinct: - latest_milestone_per_author = ( - milestones.filter(author_id=OuterRef("author_id")) - .order_by("-created_at") - .values("id")[:1] - ) - milestones = milestones.filter( - id__in=Subquery(latest_milestone_per_author), - ).order_by("-created_at") - return milestones[:limit] diff --git a/backend/apps/github/migrations/0027_alter_milestone_table.py b/backend/apps/github/migrations/0027_alter_milestone_table.py new file mode 100644 index 0000000000..0345f4f30d --- /dev/null +++ b/backend/apps/github/migrations/0027_alter_milestone_table.py @@ -0,0 +1,16 @@ +# Generated by Django 5.2 on 2025-05-05 04:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("github", "0026_issue_milestone_pullrequest_milestone"), + ] + + operations = [ + migrations.AlterModelTable( + name="milestone", + table="github_milestones", + ), + ] diff --git a/backend/apps/github/models/issue.py b/backend/apps/github/models/issue.py index 554bbfbc39..57ecb4e69a 100644 --- a/backend/apps/github/models/issue.py +++ b/backend/apps/github/models/issue.py @@ -185,7 +185,7 @@ def open_issues_count(): return IndexBase.get_total_count("issues") @staticmethod - def update_data(gh_issue, *, author=None, repository=None, milestone=None, save: bool = True): + def update_data(gh_issue, *, author=None, milestone=None, repository=None, save: bool = True): """Update issue data. Args: diff --git a/backend/apps/github/models/milestone.py b/backend/apps/github/models/milestone.py index 9be7e27106..c4b6ece6b1 100644 --- a/backend/apps/github/models/milestone.py +++ b/backend/apps/github/models/milestone.py @@ -15,7 +15,7 @@ class Milestone(GenericIssueModel): closed_milestones = ClosedMilestoneManager() class Meta: - db_table = "github_milestone" + db_table = "github_milestones" verbose_name_plural = "Milestones" ordering = ["-updated_at", "-state"] @@ -76,10 +76,6 @@ def from_github(self, gh_milestone, author=None, repository=None) -> None: self.author = author self.repository = repository - def save(self, *args, **kwargs): - """Save Milestone.""" - super().save(*args, **kwargs) - @staticmethod def bulk_save(milestones, fields=None): """Bulk save milestones.""" From 57761eca8dadbc0d7a4e9b26f34086e396fd7ac5 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Mon, 5 May 2025 08:02:30 +0300 Subject: [PATCH 39/56] Fix milestone query parameters to use state instead of close flag --- frontend/src/server/queries/homeQueries.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/server/queries/homeQueries.ts b/frontend/src/server/queries/homeQueries.ts index 9199e95b3d..ba219eaa57 100644 --- a/frontend/src/server/queries/homeQueries.ts +++ b/frontend/src/server/queries/homeQueries.ts @@ -91,7 +91,7 @@ export const GET_MAIN_PAGE_DATA = gql` suggestedLocation url } - openMilestones: milestones(limit: 5, close: false) { + openMilestones: milestones(limit: 5, state: "open") { author { avatarUrl login @@ -105,7 +105,7 @@ export const GET_MAIN_PAGE_DATA = gql` createdAt url } - closedMilestones: milestones(limit: 5, close: true) { + closedMilestones: milestones(limit: 5, state: "closed") { author { avatarUrl login From f71b8a9b241d2042f78d10d2f4cf840828779860 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Mon, 5 May 2025 08:05:29 +0300 Subject: [PATCH 40/56] Refactor from_github method signatures in Issue and PullRequest models to reorder parameters for consistency --- backend/apps/github/models/issue.py | 4 ++-- backend/apps/github/models/pull_request.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/apps/github/models/issue.py b/backend/apps/github/models/issue.py index 57ecb4e69a..4ab8d9246d 100644 --- a/backend/apps/github/models/issue.py +++ b/backend/apps/github/models/issue.py @@ -83,14 +83,14 @@ class Meta: blank=True, ) - def from_github(self, gh_issue, *, author=None, repository=None, milestone=None): + def from_github(self, gh_issue, *, author=None, milestone=None, repository=None): """Update the instance based on GitHub issue data. Args: gh_issue (github.Issue.Issue): The GitHub issue object. author (User, optional): The author of the issue. - repository (Repository, optional): The repository instance. milestone (Milestone, optional): The milestone related to the issue. + repository (Repository, optional): The repository instance. """ field_mapping = { diff --git a/backend/apps/github/models/pull_request.py b/backend/apps/github/models/pull_request.py index 3355af665e..fa8d1b4919 100644 --- a/backend/apps/github/models/pull_request.py +++ b/backend/apps/github/models/pull_request.py @@ -114,8 +114,8 @@ def update_data( gh_pull_request, *, author=None, - repository=None, milestone=None, + repository=None, save: bool = True, ) -> "PullRequest": """Update pull request data. @@ -123,8 +123,8 @@ def update_data( Args: gh_pull_request (github.PullRequest.PullRequest): The GitHub pull request object. author (User, optional): The author of the pull request. - repository (Repository, optional): The repository instance. milestone (Milestone, optional): The milestone related to the pull request. + repository (Repository, optional): The repository instance. save (bool, optional): Whether to save the instance. Returns: From 6007d77d3cf113b99ab0eeca7d02b5e9bc5142b7 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Sat, 10 May 2025 18:44:49 +0300 Subject: [PATCH 41/56] Merge migrations --- .../github/migrations/0028_merge_20250510_1542.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 backend/apps/github/migrations/0028_merge_20250510_1542.py diff --git a/backend/apps/github/migrations/0028_merge_20250510_1542.py b/backend/apps/github/migrations/0028_merge_20250510_1542.py new file mode 100644 index 0000000000..4a6991bd85 --- /dev/null +++ b/backend/apps/github/migrations/0028_merge_20250510_1542.py @@ -0,0 +1,12 @@ +# Generated by Django 5.2 on 2025-05-10 15:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("github", "0026_alter_organization_company_and_more"), + ("github", "0027_alter_milestone_table"), + ] + + operations = [] From b759bae6d2192ae57875313c43696ea520a702fb Mon Sep 17 00:00:00 2001 From: Kate Date: Sun, 11 May 2025 20:36:59 -0700 Subject: [PATCH 42/56] Update GitHub app admin --- backend/apps/github/admin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/apps/github/admin.py b/backend/apps/github/admin.py index cb5560e851..f2589e7fc6 100644 --- a/backend/apps/github/admin.py +++ b/backend/apps/github/admin.py @@ -11,11 +11,14 @@ from apps.github.models.repository import Repository from apps.github.models.repository_contributor import RepositoryContributor from apps.github.models.user import User +from apps.github.models.milestone import Milestone class LabelAdmin(admin.ModelAdmin): search_fields = ("name", "description") +class MilestoneAdmin(admin.ModelAdmin): + search_fields = ("title", "body") class PullRequestAdmin(admin.ModelAdmin): autocomplete_fields = ( @@ -192,6 +195,7 @@ class UserAdmin(admin.ModelAdmin): admin.site.register(Issue, IssueAdmin) admin.site.register(Label, LabelAdmin) +admin.site.register(Milestone, MilestoneAdmin) admin.site.register(Organization, OrganizationAdmin) admin.site.register(PullRequest, PullRequestAdmin) admin.site.register(Release, ReleaseAdmin) From 5d7845d86f9ef859b56cf5a9e2f77eca9d82f96c Mon Sep 17 00:00:00 2001 From: Kate Date: Sun, 11 May 2025 20:38:53 -0700 Subject: [PATCH 43/56] Add make-check changes --- backend/apps/github/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/apps/github/admin.py b/backend/apps/github/admin.py index f2589e7fc6..41218a3eaf 100644 --- a/backend/apps/github/admin.py +++ b/backend/apps/github/admin.py @@ -5,21 +5,23 @@ from apps.github.models.issue import Issue from apps.github.models.label import Label +from apps.github.models.milestone import Milestone from apps.github.models.organization import Organization from apps.github.models.pull_request import PullRequest from apps.github.models.release import Release from apps.github.models.repository import Repository from apps.github.models.repository_contributor import RepositoryContributor from apps.github.models.user import User -from apps.github.models.milestone import Milestone class LabelAdmin(admin.ModelAdmin): search_fields = ("name", "description") + class MilestoneAdmin(admin.ModelAdmin): search_fields = ("title", "body") + class PullRequestAdmin(admin.ModelAdmin): autocomplete_fields = ( "assignees", From 225df601c3d7127edb9a6a7d41ec22702fdb679a Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Tue, 13 May 2025 04:31:36 +0300 Subject: [PATCH 44/56] Refactor milestone handling in Repository and Project nodes to use recent milestones with a limit parameter --- .../apps/github/graphql/nodes/repository.py | 20 +++++++------------ backend/apps/github/models/repository.py | 13 +++--------- backend/apps/owasp/graphql/nodes/project.py | 13 ++++-------- backend/apps/owasp/models/project.py | 15 +++----------- 4 files changed, 17 insertions(+), 44 deletions(-) diff --git a/backend/apps/github/graphql/nodes/repository.py b/backend/apps/github/graphql/nodes/repository.py index 675f0a5018..4d6b32015e 100644 --- a/backend/apps/github/graphql/nodes/repository.py +++ b/backend/apps/github/graphql/nodes/repository.py @@ -17,8 +17,10 @@ class RepositoryNode(BaseNode): """Repository node.""" issues = graphene.List(IssueNode) - open_milestones = graphene.List(MilestoneNode) - closed_milestones = graphene.List(MilestoneNode) + recent_milestones = graphene.List( + MilestoneNode, + limit=graphene.Int(default_value=5), + ) languages = graphene.List(graphene.String) latest_release = graphene.String() owner_key = graphene.String() @@ -84,17 +86,9 @@ def resolve_url(self, info): """Resolve URL.""" return self.url - def resolve_open_milestones(self, info): - """Resolve open milestones.""" - return self.open_milestones.select_related( - "repository", - ).order_by( - "-created_at", - ) - - def resolve_closed_milestones(self, info): - """Resolve closed milestones.""" - return self.closed_milestones.select_related( + def resolve_recent_milestones(self, info, limit): + """Resolve recent milestones.""" + return self.recent_milestones.select_related( "repository", ).order_by( "-created_at", diff --git a/backend/apps/github/models/repository.py b/backend/apps/github/models/repository.py index 7617045925..cb79581fd1 100644 --- a/backend/apps/github/models/repository.py +++ b/backend/apps/github/models/repository.py @@ -182,16 +182,9 @@ def url(self) -> str: return f"https://github.com/{self.path}" @property - def open_milestones(self): - """Repository open milestones.""" - return Milestone.open_milestones.filter( - repository=self, - ).order_by("-created_at") - - @property - def closed_milestones(self): - """Repository closed milestones.""" - return Milestone.closed_milestones.filter( + def recent_milestones(self): + """Repository recent milestones.""" + return Milestone.objects.filter( repository=self, ).order_by("-created_at") diff --git a/backend/apps/owasp/graphql/nodes/project.py b/backend/apps/owasp/graphql/nodes/project.py index 8a9ffc9561..17b6be9994 100644 --- a/backend/apps/owasp/graphql/nodes/project.py +++ b/backend/apps/owasp/graphql/nodes/project.py @@ -22,8 +22,7 @@ class ProjectNode(GenericEntityNode): level = graphene.String() recent_issues = graphene.List(IssueNode) recent_releases = graphene.List(ReleaseNode) - open_milestones = graphene.List(MilestoneNode, limit=graphene.Int(default_value=5)) - closed_milestones = graphene.List(MilestoneNode, limit=graphene.Int(default_value=5)) + recent_milestones = graphene.List(MilestoneNode, limit=graphene.Int(default_value=5)) repositories = graphene.List(RepositoryNode) repositories_count = graphene.Int() topics = graphene.List(graphene.String) @@ -64,13 +63,9 @@ def resolve_recent_releases(self, info): """Resolve recent releases.""" return self.published_releases.order_by("-published_at")[:RECENT_RELEASES_LIMIT] - def resolve_open_milestones(self, info, limit): - """Resolve open milestones.""" - return self.open_milestones.select_related("author").order_by("-created_at")[:limit] - - def resolve_closed_milestones(self, info, limit): - """Resolve closed milestones.""" - return self.closed_milestones.select_related("author").order_by("-created_at")[:limit] + def resolve_recent_milestones(self, info, limit): + """Resolve recent milestones.""" + return self.recent_milestones.select_related("author").order_by("-created_at")[:limit] def resolve_repositories(self, info): """Resolve repositories.""" diff --git a/backend/apps/owasp/models/project.py b/backend/apps/owasp/models/project.py index b887eb5533..ed6e380bb8 100644 --- a/backend/apps/owasp/models/project.py +++ b/backend/apps/owasp/models/project.py @@ -154,18 +154,9 @@ def issues(self): ) @property - def open_milestones(self): - """Return milestones.""" - return Milestone.open_milestones.filter( - repository__in=self.repositories.all(), - ).select_related( - "repository", - ) - - @property - def closed_milestones(self): - """Return milestones.""" - return Milestone.closed_milestones.filter( + def recent_milestones(self): + """Return recent milestones.""" + return Milestone.objects.filter( repository__in=self.repositories.all(), ).select_related( "repository", From 7572bc3406b879701d8a5744da1d0bec3188c5eb Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Tue, 13 May 2025 06:13:33 +0300 Subject: [PATCH 45/56] Update frontend --- .../repositories/[repositoryKey]/page.tsx | 3 +-- frontend/src/app/page.tsx | 5 ++--- .../src/app/projects/[projectKey]/page.tsx | 3 +-- frontend/src/components/CardDetailsPage.tsx | 14 ++++++------- frontend/src/components/ItemCardList.tsx | 6 +++++- frontend/src/components/Milestones.tsx | 20 +++++++++---------- frontend/src/server/queries/homeQueries.ts | 18 ++--------------- frontend/src/server/queries/projectQueries.ts | 16 +-------------- .../src/server/queries/repositoryQueries.ts | 16 +-------------- frontend/src/types/card.ts | 3 +-- frontend/src/types/home.ts | 3 +-- frontend/src/types/project.ts | 3 +-- 12 files changed, 31 insertions(+), 79 deletions(-) diff --git a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx index a67763b9c4..227937f05c 100644 --- a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx +++ b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx @@ -114,8 +114,7 @@ const RepositoryDetailsPage = () => { title={repository.name} topContributors={repository.topContributors} topics={repository.topics} - openMilestones={repository.openMilestones} - closedMilestones={repository.closedMilestones} + recentMilestones={repository.recentMilestones} type="repository" /> ) diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index cc4efab57f..3db393411f 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -309,10 +309,9 @@ export default function Home() { -
- - + +
{ summary={project.summary} title={project.name} topContributors={project.topContributors} - openMilestones={project.openMilestones} - closedMilestones={project.closedMilestones} + recentMilestones={project.recentMilestones} topics={project.topics} type="project" /> diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index 2f98b5426f..bb26b17b1e 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -41,8 +41,7 @@ const DetailsCard = ({ topics, recentIssues, recentReleases, - openMilestones, - closedMilestones, + recentMilestones, showAvatar = true, userSummary, geolocationData = null, @@ -181,14 +180,13 @@ const DetailsCard = ({ )} )} - {(type === 'user' || - type === 'organization' || - type === 'repository' || - type === 'project') && } + {(type === 'user' || type === 'organization') && ( + + )} {(type === 'project' || type === 'repository') && (
- - + +
)} {(type === 'project' || type === 'user' || type === 'organization') && diff --git a/frontend/src/components/ItemCardList.tsx b/frontend/src/components/ItemCardList.tsx index d1cccc06df..e4b23c43a5 100644 --- a/frontend/src/components/ItemCardList.tsx +++ b/frontend/src/components/ItemCardList.tsx @@ -14,11 +14,13 @@ const ItemCardList = ({ icon, renderDetails, showAvatar = true, + showSingleColumn = true, }: { title: React.ReactNode data: ProjectReleaseType[] | ProjectIssuesType[] | PullRequestsType[] | ProjectMilestonesType[] icon?: IconProp showAvatar?: boolean + showSingleColumn?: boolean renderDetails: (item: { createdAt: string commentsCount: number @@ -37,7 +39,9 @@ const ItemCardList = ({ }) => ( {data && data.length > 0 ? ( -
+
{data.map((item, index) => (
diff --git a/frontend/src/components/Milestones.tsx b/frontend/src/components/Milestones.tsx index 5586b3cef8..5d7dced2e0 100644 --- a/frontend/src/components/Milestones.tsx +++ b/frontend/src/components/Milestones.tsx @@ -1,7 +1,7 @@ import { faCalendar, faFolderOpen, - faFire, + faSignsPost, faCircleCheck, faCircleExclamation, } from '@fortawesome/free-solid-svg-icons' @@ -17,13 +17,13 @@ import { TruncatedText } from './TruncatedText' interface ProjectMilestonesProps { data: ProjectMilestonesType[] showAvatar?: boolean - openMilestones?: boolean + showSingleColumn?: boolean } const Milestones: React.FC = ({ data, showAvatar = true, - openMilestones, + showSingleColumn = true, }) => { const router = useRouter() @@ -31,15 +31,13 @@ const Milestones: React.FC = ({ - +
} data={data} showAvatar={showAvatar} - icon={faFire} + icon={faSignsPost} + showSingleColumn={showSingleColumn} renderDetails={(item) => (
@@ -47,15 +45,15 @@ const Milestones: React.FC = ({ {formatDate(item.createdAt)}
- + {item.closedIssuesCount} closed
- + {item.openIssuesCount} open
{item?.repositoryName && ( -
+
)} - {(type === 'user' || type === 'organization') && ( + {(type === 'user') && ( )} - {(type === 'project' || type === 'repository') && ( + {(type === 'project' || type === 'repository' || type === 'organization') && (
diff --git a/frontend/src/server/queries/organizationQueries.ts b/frontend/src/server/queries/organizationQueries.ts index fefccf2c21..5a5657c37c 100644 --- a/frontend/src/server/queries/organizationQueries.ts +++ b/frontend/src/server/queries/organizationQueries.ts @@ -41,7 +41,7 @@ export const GET_ORGANIZATION_DATA = gql` title url } - recentReleases(limit: 9, organization: $login, distinct: true) { + recentReleases(limit: 5, organization: $login, distinct: true) { author { avatarUrl login @@ -54,6 +54,20 @@ export const GET_ORGANIZATION_DATA = gql` tagName url } + recentMilestones(limit: 5, organization: $login, distinct: true) { + author { + avatarUrl + login + name + } + title + openIssuesCount + closedIssuesCount + repositoryName + organizationName + createdAt + url + } repositories(organization: $login, limit: 12) { contributorsCount forksCount From 069b0ec320bc5b8fae85575ac4db4a781391a86f Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Fri, 16 May 2025 10:44:28 +0300 Subject: [PATCH 53/56] Add milestones to user --- frontend/src/app/members/[memberKey]/page.tsx | 30 ++++++++++++++++++- frontend/src/components/CardDetailsPage.tsx | 8 ++--- frontend/src/server/queries/userQueries.ts | 11 ++++++- frontend/src/types/project.ts | 2 +- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index f0dac9d2f2..e8d0d85f3d 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -11,7 +11,12 @@ import Link from 'next/link' import { useParams } from 'next/navigation' import React, { useState, useEffect, useRef, useMemo } from 'react' import { GET_USER_DATA } from 'server/queries/userQueries' -import type { ProjectIssuesType, ProjectReleaseType, RepositoryCardProps } from 'types/project' +import type { + ProjectIssuesType, + ProjectMilestonesType, + ProjectReleaseType, + RepositoryCardProps, +} from 'types/project' import type { ItemCardPullRequests, PullRequestsType, UserDetailsProps } from 'types/user' import { formatDate } from 'utils/dateFormatter' import { drawContributions, fetchHeatmapData, HeatmapData } from 'utils/helpers/githubHeatmap' @@ -24,6 +29,7 @@ const UserDetailsPage: React.FC = () => { const [user, setUser] = useState() const [issues, setIssues] = useState([]) const [topRepositories, setTopRepositories] = useState([]) + const [milestones, setMilestones] = useState([]) const [pullRequests, setPullRequests] = useState([]) const [releases, setReleases] = useState([]) const [data, setData] = useState({} as HeatmapData) @@ -42,6 +48,7 @@ const UserDetailsPage: React.FC = () => { if (graphQLData) { setUser(graphQLData?.user) setIssues(graphQLData?.recentIssues) + setMilestones(graphQLData?.recentMilestones) setPullRequests(graphQLData?.recentPullRequests) setReleases(graphQLData?.recentReleases) setTopRepositories(graphQLData?.topContributedRepositories) @@ -157,6 +164,26 @@ const UserDetailsPage: React.FC = () => { ) }, [releases, user]) + const formattedMilestones: ProjectMilestonesType[] = useMemo(() => { + return ( + milestones?.map((milestone) => ({ + author: { + avatarUrl: user?.avatarUrl || '', + key: user?.login || '', + login: user?.login || '', + name: user?.name || user?.login || '', + }, + createdAt: milestone.createdAt, + openIssuesCount: milestone.openIssuesCount, + closedIssuesCount: milestone.closedIssuesCount, + organizationName: milestone.organizationName, + repositoryName: milestone.repositoryName, + title: milestone.title, + url: milestone.url, + })) || [] + ) + }, [milestones, user]) + if (isLoading) { return } @@ -246,6 +273,7 @@ const UserDetailsPage: React.FC = () => { title={user?.name || user?.login || 'User'} heatmap={privateContributor ? undefined : } details={userDetails} + recentMilestones={formattedMilestones} pullRequests={formattedPullRequest} stats={userStats} type="user" diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index 19bc036fcc..08486280b2 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -180,10 +180,10 @@ const DetailsCard = ({ )}
)} - {(type === 'user') && ( - - )} - {(type === 'project' || type === 'repository' || type === 'organization') && ( + {(type === 'project' || + type === 'repository' || + type === 'organization' || + type === 'user') && (
diff --git a/frontend/src/server/queries/userQueries.ts b/frontend/src/server/queries/userQueries.ts index c7e694a32c..7782a51c14 100644 --- a/frontend/src/server/queries/userQueries.ts +++ b/frontend/src/server/queries/userQueries.ts @@ -19,6 +19,15 @@ export const GET_USER_DATA = gql` title url } + recentMilestones(limit: 5, login: $key) { + title + openIssuesCount + closedIssuesCount + repositoryName + organizationName + createdAt + url + } recentPullRequests(limit: 5, login: $key) { createdAt organizationName @@ -26,7 +35,7 @@ export const GET_USER_DATA = gql` title url } - recentReleases(limit: 6, login: $key) { + recentReleases(limit: 5, login: $key) { isPreRelease name publishedAt diff --git a/frontend/src/types/project.ts b/frontend/src/types/project.ts index 0a89713a78..d50d83a5d8 100644 --- a/frontend/src/types/project.ts +++ b/frontend/src/types/project.ts @@ -37,12 +37,12 @@ export interface ProjectMilestonesType { login: string } title: string - body: string openIssuesCount: number closedIssuesCount: number repositoryName: string organizationName?: string createdAt: string + url: string } export interface ProjectStatsType { From 255b2bc0db6b67e547c18a923ba9edac93d049af Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Fri, 16 May 2025 11:20:37 +0300 Subject: [PATCH 54/56] Add recent milestones to user details tests and mock data --- .../__tests__/e2e/pages/UserDetails.spec.ts | 7 ++++ .../__tests__/unit/data/mockUserDetails.ts | 11 +++++ .../__tests__/unit/pages/UserDetails.test.tsx | 40 +++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/frontend/__tests__/e2e/pages/UserDetails.spec.ts b/frontend/__tests__/e2e/pages/UserDetails.spec.ts index 71b3c6b753..738fca7cff 100644 --- a/frontend/__tests__/e2e/pages/UserDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/UserDetails.spec.ts @@ -49,6 +49,13 @@ test.describe('User Details Page', () => { await expect(page.getByText('v1.0.0')).toBeVisible() }) + test('should have user recent milestones', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'Recent Milestones' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'v2.0.0 Release' })).toBeVisible() + await expect(page.getByText('Mar 1, 2025')).toBeVisible() + await expect(page.getByText('Project Repo 1')).toBeVisible() + }) + test('should have user pull requests', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Pull Requests' })).toBeVisible() await expect(page.getByText('Test Pull Request')).toBeVisible() diff --git a/frontend/__tests__/unit/data/mockUserDetails.ts b/frontend/__tests__/unit/data/mockUserDetails.ts index a673a50ada..a082f47170 100644 --- a/frontend/__tests__/unit/data/mockUserDetails.ts +++ b/frontend/__tests__/unit/data/mockUserDetails.ts @@ -22,6 +22,17 @@ export const mockUserDetailsData = { url: 'https://github.com/OWASP/Nest/issues/798', }, ], + recentMilestones: [ + { + title: 'v2.0.0 Release', + openIssuesCount: 5, + closedIssuesCount: 15, + repositoryName: 'Project Repo 1', + organizationName: 'OWASP', + createdAt: '2025-03-01T10:00:00Z', + url: 'https://github.com/OWASP/repo-one/milestone/1', + }, + ], recentReleases: [ { isPreRelease: false, diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx index 8c47669d50..c6eff61522 100644 --- a/frontend/__tests__/unit/pages/UserDetails.test.tsx +++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx @@ -134,6 +134,46 @@ describe('UserDetailsPage', () => { }) }) + test('renders recent releases correctly', async () => { + ;(useQuery as jest.Mock).mockReturnValue({ + data: mockUserDetailsData, + error: null, + loading: false, + }) + render() + await waitFor(() => { + const releasesTitle = screen.getByText('Recent Releases') + expect(releasesTitle).toBeInTheDocument() + const releases = mockUserDetailsData.recentReleases + releases.forEach((release) => { + expect(screen.getByText(release.name)).toBeInTheDocument() + expect(screen.getByText(release.repositoryName)).toBeInTheDocument() + }) + }) + }) + + test('renders recent milestones correctly', async () => { + ;(useQuery as jest.Mock).mockReturnValue({ + data: mockUserDetailsData, + error: null, + loading: false, + }) + + render() + + await waitFor(() => { + const milestonesTitle = screen.getByText('Recent Milestones') + expect(milestonesTitle).toBeInTheDocument() + const milestones = mockUserDetailsData.recentMilestones + milestones.forEach((milestone) => { + expect(screen.getByText(milestone.title)).toBeInTheDocument() + expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() + expect(screen.getByText(`${milestone.openIssuesCount} open`)).toBeInTheDocument() + expect(screen.getByText(`${milestone.closedIssuesCount} closed`)).toBeInTheDocument() + }) + }) + }) + test('renders repositories section correctly', async () => { ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, From 9889cf1f590dc2ccce404bd59c58d91df1ea73b0 Mon Sep 17 00:00:00 2001 From: Ahmed Gouda Date: Fri, 16 May 2025 23:40:08 +0300 Subject: [PATCH 55/56] Add more tests --- .../unit/pages/OrganizationDetails.test.tsx | 36 ++++++++++++++++++ .../unit/pages/RepositoryDetails.test.tsx | 1 + .../__tests__/unit/pages/UserDetails.test.tsx | 38 +++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx index 94b82ecc19..f6f2072e07 100644 --- a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx +++ b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx @@ -125,6 +125,42 @@ describe('OrganizationDetailsPage', () => { }) }) + test('handles no recent releases gracefully', async () => { + const noReleasesData = { + ...mockOrganizationDetailsData, + recentReleases: [], + } + ;(useQuery as jest.Mock).mockReturnValue({ + data: noReleasesData, + loading: false, + error: null, + }) + render() + await waitFor(() => { + expect(screen.getByText('Recent Releases')).toBeInTheDocument() + expect(screen.queryByText('Test v1.0.0')).not.toBeInTheDocument() + }) + }) + + test('renders no milestones correctly', async () => { + const noMilestones = { + ...mockOrganizationDetailsData, + recentMilestones: [], + } + + ;(useQuery as jest.Mock).mockReturnValue({ + data: noMilestones, + loading: false, + error: null, + }) + + render() + await waitFor(() => { + expect(screen.getByText('Recent Milestones')).toBeInTheDocument() + expect(screen.queryByText('v2.0.0 Release')).not.toBeInTheDocument() + }) + }) + test('renders pull requests section correctly', async () => { render() diff --git a/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx b/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx index 2fffb1a03b..4ab52dab27 100644 --- a/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx +++ b/frontend/__tests__/unit/pages/RepositoryDetails.test.tsx @@ -184,6 +184,7 @@ describe('RepositoryDetailsPage', () => { await waitFor(() => { const recentMilestones = mockRepositoryData.repository.recentMilestones + expect(screen.getByText('Recent Milestones')).toBeInTheDocument() recentMilestones.forEach((milestone) => { expect(screen.getByText(milestone.title)).toBeInTheDocument() expect(screen.getByText(milestone.repositoryName)).toBeInTheDocument() diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx index c6eff61522..070333faa2 100644 --- a/frontend/__tests__/unit/pages/UserDetails.test.tsx +++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx @@ -93,6 +93,10 @@ describe('UserDetailsPage', () => { expect(screen.getByText('Contribution Heatmap')).toBeInTheDocument() expect(screen.getByText('Test Company')).toBeInTheDocument() expect(screen.getByText('Test Location')).toBeInTheDocument() + expect(screen.getByText('10 Followers')).toBeInTheDocument() + expect(screen.getByText('5 Followings')).toBeInTheDocument() + expect(screen.getByText('3 Repositories')).toBeInTheDocument() + expect(screen.getByText('100 Contributions')).toBeInTheDocument() }) test('renders recent issues correctly', async () => { @@ -368,6 +372,40 @@ describe('UserDetailsPage', () => { }) }) + test('handles no recent releases gracefully', async () => { + const noReleasesData = { + ...mockUserDetailsData, + recentReleases: [], + } + ;(useQuery as jest.Mock).mockReturnValue({ + data: noReleasesData, + loading: false, + error: null, + }) + render() + await waitFor(() => { + expect(screen.getByText('Recent Releases')).toBeInTheDocument() + expect(screen.queryByText('Test v1.0.0')).not.toBeInTheDocument() + }) + }) + + test('handles no recent milestones gracefully', async () => { + const noMilestonesData = { + ...mockUserDetailsData, + recentMilestones: [], + } + ;(useQuery as jest.Mock).mockReturnValue({ + data: noMilestonesData, + loading: false, + error: null, + }) + render() + await waitFor(() => { + expect(screen.getByText('Recent Milestones')).toBeInTheDocument() + expect(screen.queryByText('v2.0.0 Release')).not.toBeInTheDocument() + }) + }) + test('renders statistics with zero values correctly', async () => { const zeroStatsData = { ...mockUserDetailsData, From f692a0688461f455d5060fb3e43d81137d9b6c4f Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Fri, 16 May 2025 19:32:04 -0700 Subject: [PATCH 56/56] Update code --- frontend/__tests__/e2e/pages/About.spec.ts | 10 ++------ .../unit/{pages => }/global-error.test.tsx | 0 frontend/__tests__/unit/pages/About.test.tsx | 12 +++++++++ .../__tests__/unit/pages/UserDetails.test.tsx | 15 +++++++++++ frontend/src/app/about/page.tsx | 11 ++++++-- .../src/app/chapters/[chapterKey]/page.tsx | 4 +-- .../app/committees/[committeeKey]/page.tsx | 2 +- frontend/src/app/members/[memberKey]/page.tsx | 25 +++++++++---------- frontend/src/app/members/page.tsx | 2 +- .../organizations/[organizationKey]/page.tsx | 18 ++++++------- .../repositories/[repositoryKey]/page.tsx | 6 ++--- frontend/src/app/page.tsx | 16 ++++++------ .../src/app/projects/[projectKey]/page.tsx | 4 +-- frontend/src/app/snapshots/[id]/page.tsx | 18 ++++++------- frontend/src/app/snapshots/page.tsx | 2 +- frontend/src/utils/dateFormatter.ts | 4 +++ frontend/src/utils/helpers/githubHeatmap.ts | 12 ++++++--- 17 files changed, 98 insertions(+), 63 deletions(-) rename frontend/__tests__/unit/{pages => }/global-error.test.tsx (100%) diff --git a/frontend/__tests__/e2e/pages/About.spec.ts b/frontend/__tests__/e2e/pages/About.spec.ts index 39e9497aab..ee265f8651 100644 --- a/frontend/__tests__/e2e/pages/About.spec.ts +++ b/frontend/__tests__/e2e/pages/About.spec.ts @@ -75,15 +75,9 @@ test.describe('About Page', () => { await expect(page.getByText('890+Stars')).toBeVisible() }) - test('opens user profile in new window when leader button is clicked', async ({ - page, - context, - }) => { - const pagePromise = context.waitForEvent('page') + test('opens user profile in new window when leader button is clicked', async ({ page }) => { await page.getByRole('button', { name: 'View Profile' }).first().click() - const newPage = await pagePromise - await newPage.waitForLoadState() - expect(newPage.url()).toContain('/members/') + await expect(page).toHaveURL('/members/arkid15r') }) test('breadcrumb renders correct segments on /about', async ({ page }) => { diff --git a/frontend/__tests__/unit/pages/global-error.test.tsx b/frontend/__tests__/unit/global-error.test.tsx similarity index 100% rename from frontend/__tests__/unit/pages/global-error.test.tsx rename to frontend/__tests__/unit/global-error.test.tsx diff --git a/frontend/__tests__/unit/pages/About.test.tsx b/frontend/__tests__/unit/pages/About.test.tsx index 1cca14ebf6..2c29c54268 100644 --- a/frontend/__tests__/unit/pages/About.test.tsx +++ b/frontend/__tests__/unit/pages/About.test.tsx @@ -400,6 +400,18 @@ describe('About Component', () => { }) }) + test('navigates to user details on View Profile button click', async () => { + render() + + await waitFor(() => { + const viewDetailsButtons = screen.getAllByText('View Profile') + expect(viewDetailsButtons[0]).toBeInTheDocument() + fireEvent.click(viewDetailsButtons[0]) + }) + + expect(mockRouter.push).toHaveBeenCalledWith('/members/arkid15r') + }) + test('handles partial user data in leader response', async () => { const partialUserData = { data: { diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx index 070333faa2..894dc25af7 100644 --- a/frontend/__tests__/unit/pages/UserDetails.test.tsx +++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx @@ -243,6 +243,21 @@ describe('UserDetailsPage', () => { }) }) + test('handles contribution heatmap loading error correctly', async () => { + ;(useQuery as jest.Mock).mockReturnValue({ + data: mockUserDetailsData, + error: null, + }) + ;(fetchHeatmapData as jest.Mock).mockResolvedValue(null) + + render() + + await waitFor(() => { + const heatmapTitle = screen.queryByText('Contribution Heatmap') + expect(heatmapTitle).not.toBeInTheDocument() + }) + }) + test('renders user summary section correctly', async () => { ;(useQuery as jest.Mock).mockReturnValue({ data: mockUserDetailsData, diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx index c2ead04947..43546009c5 100644 --- a/frontend/src/app/about/page.tsx +++ b/frontend/src/app/about/page.tsx @@ -10,10 +10,12 @@ import { import { addToast } from '@heroui/toast' import Image from 'next/image' import Link from 'next/link' +import { useRouter } from 'next/navigation' import { useEffect, useState } from 'react' import { GET_PROJECT_DATA } from 'server/queries/projectQueries' import { GET_LEADER_DATA } from 'server/queries/userQueries' import { ProjectTypeGraphql } from 'types/project' +import { User } from 'types/user' import { aboutText, roadmap, technologies } from 'utils/aboutData' import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper' import AnchorTitle from 'components/AnchorTitle' @@ -42,7 +44,7 @@ const About = () => { useEffect(() => { if (data) { - setProject(data?.project) + setProject(data.project) setIsLoading(false) } if (graphQLRequestError) { @@ -181,6 +183,7 @@ const LeaderData = ({ username }: { username: string }) => { const { data, loading, error } = useQuery(GET_LEADER_DATA, { variables: { key: username }, }) + const router = useRouter() if (loading) return

Loading {username}...

if (error) return

Error loading {username}'s data

@@ -191,13 +194,17 @@ const LeaderData = ({ username }: { username: string }) => { return

No data available for {username}

} + const handleButtonClick = (user: User) => { + router.push(`/members/${user.login}`) + } + return ( , label: 'View Profile', - onclick: () => window.open(`/members/${username}`, '_blank', 'noopener,noreferrer'), + onclick: () => handleButtonClick(user), }} className="h-64 w-40 bg-inherit" company={user.company} diff --git a/frontend/src/app/chapters/[chapterKey]/page.tsx b/frontend/src/app/chapters/[chapterKey]/page.tsx index 8e0ea0c386..65d5cb6e94 100644 --- a/frontend/src/app/chapters/[chapterKey]/page.tsx +++ b/frontend/src/app/chapters/[chapterKey]/page.tsx @@ -23,8 +23,8 @@ export default function ChapterDetailsPage() { useEffect(() => { if (data) { - setChapter(data?.chapter) - setTopContributors(data?.topContributors) + setChapter(data.chapter) + setTopContributors(data.topContributors) setIsLoading(false) } if (graphQLRequestError) { diff --git a/frontend/src/app/committees/[committeeKey]/page.tsx b/frontend/src/app/committees/[committeeKey]/page.tsx index b5ceaf600e..72098bef3e 100644 --- a/frontend/src/app/committees/[committeeKey]/page.tsx +++ b/frontend/src/app/committees/[committeeKey]/page.tsx @@ -30,7 +30,7 @@ export default function CommitteeDetailsPage() { useEffect(() => { if (data?.committee) { setCommittee(data.committee) - setTopContributors(data?.topContributors) + setTopContributors(data.topContributors) setIsLoading(false) } if (graphQLRequestError) { diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx index e8d0d85f3d..565590b8ba 100644 --- a/frontend/src/app/members/[memberKey]/page.tsx +++ b/frontend/src/app/members/[memberKey]/page.tsx @@ -36,7 +36,7 @@ const UserDetailsPage: React.FC = () => { const [isLoading, setIsLoading] = useState(true) const [username, setUsername] = useState('') const [imageLink, setImageLink] = useState('') - const [privateContributor, setPrivateContributor] = useState(false) + const [isPrivateContributor, setIsPrivateContributor] = useState(false) const canvasRef = useRef(null) const theme = 'blue' @@ -46,12 +46,12 @@ const UserDetailsPage: React.FC = () => { useEffect(() => { if (graphQLData) { - setUser(graphQLData?.user) - setIssues(graphQLData?.recentIssues) - setMilestones(graphQLData?.recentMilestones) - setPullRequests(graphQLData?.recentPullRequests) - setReleases(graphQLData?.recentReleases) - setTopRepositories(graphQLData?.topContributedRepositories) + setUser(graphQLData.user) + setIssues(graphQLData.recentIssues) + setMilestones(graphQLData.recentMilestones) + setPullRequests(graphQLData.recentPullRequests) + setReleases(graphQLData.recentReleases) + setTopRepositories(graphQLData.topContributedRepositories) setIsLoading(false) } if (graphQLRequestError) { @@ -62,15 +62,14 @@ const UserDetailsPage: React.FC = () => { useEffect(() => { const fetchData = async () => { - if (!memberKey) { + const result = await fetchHeatmapData(memberKey as string) + if (!result) { + setIsPrivateContributor(true) return } - const result = await fetchHeatmapData(memberKey as string) - if (typeof result !== 'string' && result.contributions) { + if (result?.contributions) { setUsername(memberKey as string) setData(result as HeatmapData) - } else { - setPrivateContributor(true) } } fetchData() @@ -271,7 +270,7 @@ const UserDetailsPage: React.FC = () => { } + heatmap={isPrivateContributor ? undefined : } details={userDetails} recentMilestones={formattedMilestones} pullRequests={formattedPullRequest} diff --git a/frontend/src/app/members/page.tsx b/frontend/src/app/members/page.tsx index 8b443b329e..751669e288 100644 --- a/frontend/src/app/members/page.tsx +++ b/frontend/src/app/members/page.tsx @@ -38,7 +38,7 @@ const UsersPage = () => { { useEffect(() => { if (graphQLData) { - setMilestones(graphQLData?.recentMilestones) - setOrganization(graphQLData?.organization) - setIssues(graphQLData?.recentIssues) - setPullRequests(graphQLData?.recentPullRequests) - setReleases(graphQLData?.recentReleases) - setRepositories(graphQLData?.repositories) - setTopContributors(graphQLData?.topContributors) + setMilestones(graphQLData.recentMilestones) + setOrganization(graphQLData.organization) + setIssues(graphQLData.recentIssues) + setPullRequests(graphQLData.recentPullRequests) + setReleases(graphQLData.recentReleases) + setRepositories(graphQLData.repositories) + setTopContributors(graphQLData.topContributors) setIsLoading(false) } if (graphQLRequestError) { @@ -70,7 +70,7 @@ const OrganizationDetailsPage = () => { ), }, { - label: 'Joined', + label: 'Created', value: formatDate(organization.createdAt), }, { @@ -79,7 +79,7 @@ const OrganizationDetailsPage = () => { }, { label: 'Location', - value: organization.location || 'Not provided', + value: organization.location, }, ] diff --git a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx index 2d1ae39930..8e7c509446 100644 --- a/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx +++ b/frontend/src/app/organizations/[organizationKey]/repositories/[repositoryKey]/page.tsx @@ -29,9 +29,9 @@ const RepositoryDetailsPage = () => { }) useEffect(() => { if (data) { - setRepository(data?.repository) - setTopContributors(data?.topContributors) - setRecentPullRequests(data?.recentPullRequests) + setRepository(data.repository) + setTopContributors(data.topContributors) + setRecentPullRequests(data.recentPullRequests) setIsLoading(false) } if (graphQLRequestError) { diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 3db393411f..a21da167fd 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -112,19 +112,19 @@ export default function Home() { const counterData = [ { label: 'Active Projects', - value: data?.statsOverview.activeProjectsStats.toString().concat('+'), + value: data.statsOverview.activeProjectsStats.toString().concat('+'), }, { label: 'Contributors', - value: data?.statsOverview.contributorsStats.toString().concat('+'), + value: data.statsOverview.contributorsStats.toString().concat('+'), }, { label: 'Local Chapters', - value: data?.statsOverview.activeChaptersStats.toString().concat('+'), + value: data.statsOverview.activeChaptersStats.toString().concat('+'), }, { label: 'Countries', - value: data?.statsOverview.countriesStats.toString().concat('+'), + value: data.statsOverview.countriesStats.toString().concat('+'), }, ] @@ -142,7 +142,7 @@ export default function Home() {
- {data?.upcomingEvents?.map((event: EventType, index: number) => ( + {data.upcomingEvents.map((event: EventType, index: number) => (