Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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)
91 changes: 89 additions & 2 deletions backend/apps/github/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from __future__ import annotations

import logging
from datetime import UTC
from datetime import timedelta as td

from django.utils import timezone
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 @@ -147,8 +149,6 @@ def sync_repository(
issue.labels.add(Label.update_data(gh_issue_label))
except UnknownObjectException:
logger.exception("Couldn't get GitHub issue label %s", issue.url)
else:
logger.info("Skipping issues sync for %s", repository.name)

# GitHub repository pull requests.
kwargs = {
Expand Down Expand Up @@ -227,3 +227,90 @@ def sync_repository(
)

return organization, repository


def sync_issue_comments(gh_app, issue: Issue):
"""Sync new comments for a mentorship program specific issue on-demand.

Args:
gh_app (Github): An authenticated PyGithub instance.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are you sure it's an app here?

issue (Issue): The local database Issue object to sync comments for.

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

try:
repo_full_name = f"{issue.repository.owner.login}/{issue.repository.name}"
logger.info("Fetching repository: %s", repo_full_name)

gh_repo = gh_app.get_repo(repo_full_name)
gh_issue = gh_repo.get_issue(number=issue.number)

last_comment = issue.comments.order_by("-created_at").first()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add this as a property for Issue -- we have latest_release, latest_pull_request as examples.

since = None

if last_comment:
# Convert Django datetime to naive datetime for GitHub API
since = last_comment.created_at
if timezone.is_aware(since):
since = timezone.make_naive(since, UTC)
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

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 last_comment and gh_comment.created_at <= last_comment.created_at:
logger.info("Skipping comment %s - not newer than our last comment", gh_comment.id)
continue

Copy link
Contributor

@coderabbitai coderabbitai bot Aug 14, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Existing comments are never updated; switch to updated_at-based sync and update-if-changed

Current logic skips any comment whose GitHub ID already exists locally. That means edited comments (updated_at newer) are never refreshed, leaving stale bodies/timestamps. Also, the GitHub API’s since for comments filters by update time, not creation time.

Refactor to:

  • Compute since from the most recent local updated_at.
  • Maintain a map of existing comment updated_at values (normalized).
  • Update an existing comment if gh_comment.updated_at is newer.

Suggested diff:

-        existing_github_ids = set(issue.comments.values_list("github_id", flat=True))
+        # Map existing comments by GitHub ID -> last local updated_at (naive UTC) for change detection
+        existing_pairs = dict(issue.comments.values_list("github_id", "updated_at"))
+        existing_updated_naive = {
+            gid: (timezone.make_naive(dt, UTC) if timezone.is_aware(dt) else dt)
+            for gid, dt in existing_pairs.items()
+        }
@@
-        comments_synced = 0
+        comments_synced = 0
+        comments_updated = 0
@@
-        gh_comments = gh_issue.get_comments(since=since) if since else gh_issue.get_comments()
+        gh_comments = gh_issue.get_comments(since=since) if since else gh_issue.get_comments()
@@
-            if gh_comment.id in existing_github_ids:
-                logger.info("Skipping existing comment %s", gh_comment.id)
-                continue
-            if last_comment and gh_comment.created_at <= last_comment.created_at:
-                logger.info("Skipping comment %s - not newer than our last comment", gh_comment.id)
-                continue
+            # If already present, update if the remote comment was edited after our local copy
+            if gh_comment.id in existing_updated_naive:
+                local_updated = existing_updated_naive[gh_comment.id]
+                if gh_comment.updated_at > local_updated:
+                    author_obj = User.update_data(gh_comment.user)
+                    if author_obj:
+                        try:
+                            IssueComment.update_data(gh_comment, issue, author_obj)
+                            comments_updated += 1
+                            logger.info("Updated existing comment %s for issue #%s", gh_comment.id, issue.number)
+                        except Exception:
+                            logger.exception(
+                                "Failed to update comment %s for issue #%s",
+                                gh_comment.id,
+                                issue.number,
+                            )
+                    else:
+                        logger.warning("Could not update author for comment %s", gh_comment.id)
+                else:
+                    logger.debug("Skipping unchanged existing comment %s", gh_comment.id)
+                continue
+
+            # New comment (not seen locally)
+            if last_comment_created_naive_utc and gh_comment.created_at <= last_comment_created_naive_utc:
+                logger.debug("Skipping comment %s - not newer than our last known created_at", gh_comment.id)
+                continue
@@
-                        logger.info(
-                            "Synced new comment %s for issue #%s", gh_comment.id, issue.number
-                        )
+                        logger.info("Synced new comment %s for issue #%s", gh_comment.id, issue.number)

And adjust the computation of since to be based on updated_at:

-        last_comment = issue.comments.order_by("-created_at").first()
+        last_comment = issue.comments.order_by("-updated_at").first()

Finally, consider reflecting comments_updated in the summary log (see next comment for a return value suggestion).

Also applies to: 269-293

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We are interested only to find user who are interested to work on the issue m so maybe we can ignore update comments. Am I right @arkid15r

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

author_obj = User.update_data(gh_comment.user)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder how many names with _obj suffix you can find in the code base. This is redundant, they are all objects obviously. You don't even have author taken by some other value. Why this kind of naming 🤷‍♂️ ?


if author_obj:
try:
comment_obj = IssueComment.update_data(gh_comment, issue, author_obj)
if comment_obj:
comments_synced += 1
logger.info(
"Synced new comment %s for issue #%s", gh_comment.id, issue.number
)
except Exception:
logger.exception(
"Failed to create 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_synced > 0:
logger.info(
"Synced %d new comments for issue #%s in %s",
comments_synced,
issue.number,
issue.repository.name,
)
else:
logger.info("No new comments found for issue #%s", issue.number)

except UnknownObjectException as e:
logger.warning(
"Could not access issue #%s in %s/%s. Error: %s",
issue.number,
issue.repository.owner.login,
issue.repository.name,
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
1 change: 0 additions & 1 deletion backend/apps/github/models/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ class Meta:
related_name="issues",
)

# M2Ms.
assignees = models.ManyToManyField(
"github.User",
verbose_name="Assignees",
Expand Down
35 changes: 35 additions & 0 deletions backend/apps/github/models/issue_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""GitHub app issue comment model."""

from django.db import models


class IssueComment(models.Model):
"""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()

@classmethod
def update_data(cls, gh_comment, issue_obj, author_obj):
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 not how update_data works. Please check other model examples. This needs bulk save support and should never create an object unless save=True is passed.

"""Create or update an IssueComment instance from a GitHub API object."""
comment, _ = cls.objects.update_or_create(
github_id=gh_comment.id,
defaults={
"issue": issue_obj,
"author": author_obj,
"body": gh_comment.body,
"created_at": gh_comment.created_at,
"updated_at": gh_comment.updated_at,
},
)
return comment

def __str__(self):
"""Return a string representation of the issue comment."""
return f"{self.issue} - {self.author} - {self.body}"
3 changes: 3 additions & 0 deletions backend/apps/mentorship/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
sync_mentorship_issue_comments:
@echo "Syncing Github Comments related to issues"
@CMD="python manage.py sync_mentorship_issue_comments" $(MAKE) exec-backend-command
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please follow the naming patterns we use for management commands.

24 changes: 24 additions & 0 deletions backend/apps/mentorship/admin/interested_contributors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Mentorship app interested contributors admin."""

from django.contrib import admin

from apps.mentorship.models import ParticipantInterest


class ParticipantInterestAdmin(admin.ModelAdmin):
"""ParticipantInterest admin."""

list_display = (
"program",
"user",
"issue",
)

search_fields = (
"program__name",
"user__username",
"issue__title",
)


admin.site.register(ParticipantInterest, ParticipantInterestAdmin)
Empty file.
Empty file.
Loading