Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
include backend/apps/ai/Makefile
include backend/apps/github/Makefile
include backend/apps/mentorship/Makefile
include backend/apps/owasp/Makefile
include backend/apps/slack/Makefile

Expand Down
1 change: 1 addition & 0 deletions backend/apps/github/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Github app admin."""

from .issue import IssueAdmin
from .issue_comment import IssueCommentAdmin
from .label import LabelAdmin
from .milestone import MilestoneAdmin
from .organization import OrganizationAdmin
Expand Down
1 change: 1 addition & 0 deletions backend/apps/github/admin/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class IssueAdmin(admin.ModelAdmin):
)
list_filter = (
"state",
"created_at",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why adding this?

"is_locked",
)
search_fields = ("title",)
Expand Down
22 changes: 22 additions & 0 deletions backend/apps/github/admin/issue_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""GitHub app Issue model admin."""

from django.contrib import admin

from apps.github.models import IssueComment


class IssueCommentAdmin(admin.ModelAdmin):
"""Admin for IssueComment model."""

list_display = (
"body",
"issue",
"author",
"created_at",
"updated_at",
)
list_filter = ("created_at", "updated_at")
search_fields = ("body", "issue__title")


admin.site.register(IssueComment, IssueCommentAdmin)
96 changes: 96 additions & 0 deletions backend/apps/github/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from github.GithubException import UnknownObjectException

from apps.github.models.issue import Issue
from apps.github.models.issue_comment import IssueComment
from apps.github.models.label import Label
from apps.github.models.milestone import Milestone
from apps.github.models.organization import Organization
Expand Down Expand Up @@ -227,3 +228,98 @@ def sync_repository(
)

return organization, repository


def sync_issue_comments(gh_client, issue: Issue):
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is inconsistent. Either use type hint for both or don't use it at all.

"""Sync new comments for a mentorship program specific issue on-demand.

Args:
gh_client (Github): GitHub client.
issue (Issue): The local database Issue object to sync comments for.

"""
logger.info("Starting comment sync for issue #%s", issue.number)

try:
repository = issue.repository
if not repository:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use :=

logger.warning("Issue #%s has no repository, skipping", issue.number)
return

if not repository.owner:
logger.warning("Repository for issue #%s has no owner, skipping", issue.number)
return

repository_full_name = f"{repository.owner.login}/{repository.name}"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why don't you use repository model existing properties like path

logger.info("Fetching repository: %s", repository_full_name)

gh_repository = gh_client.get_repo(repository_full_name)
gh_issue = gh_repository.get_issue(number=issue.number)

last_comment = issue.latest_comment
since = None

if last_comment:
since = last_comment.created_at
logger.info("Found last comment at: %s, fetching newer comments", since)
else:
logger.info("No existing comments found, fetching all comments")

existing_github_ids = set(issue.comments.values_list("github_id", flat=True))

comments_synced = 0
comments_to_save = []

gh_comments = gh_issue.get_comments(since=since) if since else gh_issue.get_comments()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does it work with just gh_comments = gh_issue.get_comments(since=since), e.g when it's None?

Copy link
Collaborator Author

@Rajgupta36 Rajgupta36 Sep 11, 2025

Choose a reason for hiding this comment

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

sorry i have checked the github type for this. when we pass null it throws error.
description
. This is the type(Argument of type "None" cannot be assigned to parameter "since" of type "Opt[datetime]" in function "get_comments"
Type "None" is not assignable to type "Opt[datetim

Copy link
Collaborator

Choose a reason for hiding this comment

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

Okay, thanks for confirming


for gh_comment in gh_comments:
if gh_comment.id in existing_github_ids:
logger.info("Skipping existing comment %s", gh_comment.id)
continue
Copy link
Collaborator

Choose a reason for hiding this comment

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

Doesn't since make sure that there will be only updated comments?

Copy link
Collaborator Author

@Rajgupta36 Rajgupta36 Sep 5, 2025

Choose a reason for hiding this comment

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

since parameter tells gitHub to return comments that were created or updated after the given timestamp.

Copy link
Collaborator

Choose a reason for hiding this comment

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

That's what I'm talking about -- if only updated comments returned you don't need the check for existing ones, you need to make sure the content is updated. Maybe someone changed their mind and is no longer interested in the task


if since and gh_comment.created_at <= since:
logger.info("Skipping comment %s - not newer than our last comment", gh_comment.id)
continue

author = User.update_data(gh_comment.user)

if author:
try:
comment = IssueComment.update_data(
gh_comment, issue=issue, author=author, save=False
)
comments_to_save.append(comment)
comments_synced += 1
logger.info(
"Prepared new comment %s for issue #%s", gh_comment.id, issue.number
)
except Exception:
logger.exception(
"Failed to prepare comment %s for issue #%s",
gh_comment.id,
issue.number,
)
else:
logger.warning("Could not sync author for comment %s", gh_comment.id)

if comments_to_save:
IssueComment.bulk_save(comments_to_save)
logger.info(
"Synced %d new comments for issue #%s",
comments_synced,
issue.number,
)
else:
logger.info("No new comments found for issue #%s", issue.number)

except UnknownObjectException as e:
logger.warning(
"Could not access issue #%s. Error: %s",
issue.number,
str(e),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you need str() here?

)
except Exception:
logger.exception(
"An unexpected error occurred during comment sync for issue #%s",
issue.number,
)
45 changes: 45 additions & 0 deletions backend/apps/github/migrations/0034_issuecomment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 5.2.4 on 2025-08-14 19:16

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("github", "0033_alter_release_published_at"),
]

operations = [
migrations.CreateModel(
name="IssueComment",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("github_id", models.BigIntegerField(unique=True)),
("body", models.TextField()),
("created_at", models.DateTimeField()),
("updated_at", models.DateTimeField()),
(
"author",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="issue_comments",
to="github.user",
),
),
(
"issue",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="comments",
to="github.issue",
),
),
],
),
]
1 change: 1 addition & 0 deletions backend/apps/github/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Github app."""

from .issue_comment import IssueComment
from .milestone import Milestone
from .pull_request import PullRequest
from .user import User
10 changes: 10 additions & 0 deletions backend/apps/github/models/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ class Meta:
blank=True,
)

@property
def latest_comment(self):
"""Get the latest comment for this issue.

Returns:
Comment | None: The most recently created comment, or None if no comments exist.

"""
return self.comments.order_by("-created_at").first()

def from_github(self, gh_issue, *, author=None, milestone=None, repository=None):
"""Update the instance based on GitHub issue data.

Expand Down
66 changes: 66 additions & 0 deletions backend/apps/github/models/issue_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""GitHub app issue comment model."""

from django.db import models

from apps.common.models import BulkSaveModel


class IssueComment(BulkSaveModel, models.Model):
Copy link
Collaborator

Choose a reason for hiding this comment

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

This design tightly couples comment to issue. Let's have Comment model instead and add m2m on Issue (and later we may use them for PRs as well if needed)

"""Represents a comment on a GitHub issue."""

github_id = models.BigIntegerField(unique=True)
Copy link
Collaborator

Choose a reason for hiding this comment

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

What are the values here, do you have examples?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

image

issue = models.ForeignKey("github.Issue", on_delete=models.CASCADE, related_name="comments")
author = models.ForeignKey(
"github.User", on_delete=models.SET_NULL, null=True, related_name="issue_comments"
)
body = models.TextField()
created_at = models.DateTimeField()
updated_at = models.DateTimeField()

def __str__(self):
"""Return a string representation of the issue comment."""
return f"{self.issue} - {self.author} - {self.body}"

def from_github(self, gh_comment, issue=None, author=None):
"""Populate fields from a GitHub API comment object."""
field_mapping = {
"body": "body",
"created_at": "created_at",
"updated_at": "updated_at",
}

for model_field, gh_field in field_mapping.items():
value = getattr(gh_comment, gh_field, None)
if value is not None:
setattr(self, model_field, value)

self.issue = issue
self.author = author

@staticmethod
def bulk_save(comments, fields=None): # type: ignore[override]
"""Bulk save comments."""
BulkSaveModel.bulk_save(IssueComment, comments, fields=fields)

@staticmethod
def update_data(gh_comment, *, issue=None, author=None, save=True):
"""Update IssueComment data.
Args:
gh_comment (github.IssueComment.IssueComment): GitHub comment object.
issue (Issue, optional): Related issue. Defaults to None.
author (User, optional): Comment author. Defaults to None.
save (bool, optional): Whether to save the instance. Defaults to True.
"""
try:
comment = IssueComment.objects.get(github_id=gh_comment.id)
except IssueComment.DoesNotExist:
comment = IssueComment(github_id=gh_comment.id)

comment.from_github(gh_comment, issue, author)

if save:
comment.save()

return comment
3 changes: 3 additions & 0 deletions backend/apps/mentorship/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mentorship-update-comments:
@echo "Syncing Github Comments related to issues"
@CMD="python manage.py mentorship_update_comments" $(MAKE) exec-backend-command
1 change: 1 addition & 0 deletions backend/apps/mentorship/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Mentorship app admin."""

from .contributor_interest import ContributorInterestAdmin
from .mentee import MenteeAdmin
from .mentee_program import MenteeProgramAdmin
from .mentor import MentorAdmin
Expand Down
22 changes: 22 additions & 0 deletions backend/apps/mentorship/admin/contributor_interest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Mentorship app ContributorInterest admin."""

from django.contrib import admin

from apps.mentorship.models import ContributorInterest


class ContributorInterestAdmin(admin.ModelAdmin):
"""ContributorInterest admin."""

list_display = ("module", "issue", "users_count")
search_fields = ("module__name", "users__login", "issue__title")
list_filter = ("module", "issue")

def users_count(self, obj):
"""Return the count of users interested in the issue."""
return obj.users.count()

users_count.short_description = "Interested Users"


admin.site.register(ContributorInterest, ContributorInterestAdmin)
Empty file.
Empty file.
Loading