Skip to content
Open
Show file tree
Hide file tree
Changes from all 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/nest/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,5 +1,6 @@
"""Github app admin."""

from .comment import CommentAdmin
from .issue import IssueAdmin
from .label import LabelAdmin
from .milestone import MilestoneAdmin
Expand Down
21 changes: 21 additions & 0 deletions backend/apps/github/admin/comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""GitHub app Comment model admin."""

from django.contrib import admin

from apps.github.models import Comment


class CommentAdmin(admin.ModelAdmin):
"""Admin for Comment model."""

list_display = (
"body",
"author",
"nest_created_at",
"nest_updated_at",
)
list_filter = ("nest_created_at", "nest_updated_at")
search_fields = ("body", "author__login")


admin.site.register(Comment, CommentAdmin)
90 changes: 90 additions & 0 deletions backend/apps/github/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@

import logging
from datetime import timedelta as td
from typing import TYPE_CHECKING

from django.utils import timezone
from github.GithubException import UnknownObjectException

if TYPE_CHECKING:
from github import Github

from apps.github.models.comment import Comment
from apps.github.models.issue import Issue
from apps.github.models.label import Label
from apps.github.models.milestone import Milestone
Expand Down Expand Up @@ -227,3 +232,88 @@ def sync_repository(
)

return organization, repository


def sync_issue_comments(gh_client: Github, issue: Issue):
"""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:
if not (repository := issue.repository):
logger.warning("Issue #%s has no repository, skipping", issue.number)
return

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

gh_repository = gh_client.get_repo(repository.path)
gh_issue = gh_repository.get_issue(number=issue.number)

since = issue.latest_comment.nest_created_at if issue.latest_comment else None

existing_comments = {c.github_id: c for c in issue.comments.select_related("author").all()}
comments_to_save = []
comments_to_update = []

# Since Used to tell GitHub to fetch comments created or updated after this time.
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 existing_comment := existing_comments.get(gh_comment.id):
Copy link
Collaborator

Choose a reason for hiding this comment

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

My other question is still relevant: why do we need to have separate logic for existing and new comments? Is it possible to have a simple loop with no branches and a single bulk save operation at the end?

if author := User.update_data(gh_comment.user):
existing_comment.from_github(gh_comment, author=author)
comments_to_update.append(existing_comment)
logger.info(
"Prepared update for comment %s on issue #%s",
gh_comment.id,
issue.number,
)
else:
logger.warning("Could not sync author for comment update %s", gh_comment.id)
elif author := User.update_data(gh_comment.user):
comment = Comment.update_data(gh_comment, author=author, save=False)
comment.content_object = issue
comments_to_save.append(comment)
logger.info(
"Prepared new 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:
Comment.bulk_save(comments_to_save)
logger.info(
"Synced and associated %d new comments for issue #%s",
len(comments_to_save),
issue.number,
)

if comments_to_update:
Comment.bulk_save(comments_to_update)
logger.info(
"Updated %d existing comments for issue #%s",
len(comments_to_update),
issue.number,
)

if not comments_to_save and not comments_to_update:
logger.info("No new or updated comments found for issue #%s", issue.number)

except UnknownObjectException as e:
logger.warning(
"Could not access issue #%s. Error: %s",
issue.number,
e,
)
except Exception:
logger.exception(
"An unexpected error occurred during comment sync for issue #%s",
issue.number,
)
51 changes: 51 additions & 0 deletions backend/apps/github/migrations/0036_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Generated by Django 5.2.4 on 2025-09-11 16:42

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


class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("github", "0035_alter_user_bio_alter_user_is_owasp_staff"),
]

operations = [
migrations.CreateModel(
name="Comment",
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)),
("github_id", models.BigIntegerField(unique=True, verbose_name="Github ID")),
("body", models.TextField(verbose_name="Body")),
("object_id", models.PositiveIntegerField()),
(
"author",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="comments",
to="github.user",
),
),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype"
),
),
],
options={
"verbose_name": "Comment",
"verbose_name_plural": "Comments",
"db_table": "github_comment",
"ordering": ("-nest_created_at",),
},
),
]
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 .comment import Comment
from .milestone import Milestone
from .pull_request import PullRequest
from .user import User
75 changes: 75 additions & 0 deletions backend/apps/github/models/comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""GitHub app comment model."""

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models

from apps.common.models import BulkSaveModel, TimestampedModel
from apps.common.utils import truncate


class Comment(BulkSaveModel, TimestampedModel):
"""Represents a comment on a GitHub Issue."""

class Meta:
db_table = "github_comment"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
db_table = "github_comment"
db_table = "github_comments"

verbose_name = "Comment"
Copy link
Collaborator

Choose a reason for hiding this comment

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

The db_table is missing here too (I already added this comment before for different model, you need to start applying the suggestions comprehensively).

verbose_name_plural = "Comments"
ordering = ("-nest_created_at",)

github_id = models.BigIntegerField(unique=True, verbose_name="Github ID")
author = models.ForeignKey(
"github.User", on_delete=models.SET_NULL, null=True, related_name="comments"
)
body = models.TextField(verbose_name="Body")

content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey("content_type", "object_id")

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

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

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.author = author

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

@staticmethod
def update_data(gh_comment, *, author=None, save: bool = True):
"""Update or create a Comment instance from a GitHub comment object.

Args:
gh_comment (github.IssueComment.IssueComment): GitHub comment object.
author (User, optional): Comment author. Defaults to None.
save (bool, optional): Whether to save the instance immediately. Defaults to True.

Returns:
Comment: The updated or newly created Comment instance.

"""
try:
comment = Comment.objects.get(github_id=gh_comment.id)
except Comment.DoesNotExist:
comment = Comment(github_id=gh_comment.id)

comment.from_github(gh_comment, author=author)

if save:
comment.save()

return comment
14 changes: 14 additions & 0 deletions backend/apps/github/models/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from functools import lru_cache

from django.contrib.contenttypes.fields import GenericRelation
from django.db import models

from apps.common.index import IndexBase
Expand Down Expand Up @@ -54,6 +55,9 @@ class Meta:
null=True,
related_name="created_issues",
)

comments = GenericRelation("github.Comment", related_query_name="issue")

milestone = models.ForeignKey(
"github.Milestone",
on_delete=models.CASCADE,
Expand Down Expand Up @@ -83,6 +87,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("-nest_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.

This doesn't look right -- the GitHub created_at/updated_at data is not the same as Nest one.


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

Expand Down
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 .issue_user_interest import IssueUserInterest
from .mentee import MenteeAdmin
from .mentee_program import MenteeProgramAdmin
from .mentor import MentorAdmin
Expand Down
16 changes: 16 additions & 0 deletions backend/apps/mentorship/admin/issue_user_interest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Mentorship app IssueUserInterest admin."""

from django.contrib import admin

from apps.mentorship.models import IssueUserInterest


class IssueUserInterestAdmin(admin.ModelAdmin):
"""IssueUserInterest admin."""

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


admin.site.register(IssueUserInterest, IssueUserInterestAdmin)
Empty file.
Empty file.
Loading