Skip to content

Commit 9aad9fd

Browse files
Rajgupta36kasya
andauthored
Added participants who have expressed interest in a specific issue in issue model . (#1995)
* add field in modela and interested-user logic * update code * added models , commands * update suggestion * update sync_comment * update code * clean * update models * cleanup * implemented suggestions * update code * update migration * update code * update models and code * simplification * fix comment update logic * update code * update code * update code --------- Co-authored-by: Kate Golovanova <[email protected]>
1 parent e49e117 commit 9aad9fd

File tree

20 files changed

+531
-0
lines changed

20 files changed

+531
-0
lines changed

backend/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
include backend/apps/ai/Makefile
22
include backend/apps/github/Makefile
3+
include backend/apps/mentorship/Makefile
34
include backend/apps/nest/Makefile
45
include backend/apps/owasp/Makefile
56
include backend/apps/slack/Makefile

backend/apps/github/admin/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Github app admin."""
22

3+
from .comment import CommentAdmin
34
from .issue import IssueAdmin
45
from .label import LabelAdmin
56
from .milestone import MilestoneAdmin
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""GitHub app Comment model admin."""
2+
3+
from django.contrib import admin
4+
5+
from apps.github.models import Comment
6+
7+
8+
class CommentAdmin(admin.ModelAdmin):
9+
"""Admin for Comment model."""
10+
11+
list_display = (
12+
"body",
13+
"author",
14+
"nest_created_at",
15+
"nest_updated_at",
16+
)
17+
list_filter = ("nest_created_at", "nest_updated_at")
18+
search_fields = ("body", "author__login")
19+
20+
21+
admin.site.register(Comment, CommentAdmin)

backend/apps/github/common.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@
44

55
import logging
66
from datetime import timedelta as td
7+
from typing import TYPE_CHECKING
78

89
from django.utils import timezone
910
from github.GithubException import UnknownObjectException
1011

12+
if TYPE_CHECKING:
13+
from github import Github
14+
15+
from apps.github.models.comment import Comment
1116
from apps.github.models.issue import Issue
1217
from apps.github.models.label import Label
1318
from apps.github.models.milestone import Milestone
@@ -227,3 +232,68 @@ def sync_repository(
227232
)
228233

229234
return organization, repository
235+
236+
237+
def sync_issue_comments(gh_client: Github, issue: Issue):
238+
"""Sync new comments for a mentorship program specific issue on-demand.
239+
240+
Args:
241+
gh_client (Github): GitHub client.
242+
issue (Issue): The local database Issue object to sync comments for.
243+
244+
"""
245+
logger.info("Starting comment sync for issue #%s", issue.number)
246+
247+
try:
248+
if not (repository := issue.repository):
249+
logger.warning("Issue #%s has no repository, skipping", issue.number)
250+
return
251+
252+
logger.info("Fetching repository: %s", repository.path)
253+
254+
gh_repository = gh_client.get_repo(repository.path)
255+
gh_issue = gh_repository.get_issue(number=issue.number)
256+
257+
since = (
258+
(issue.latest_comment.updated_at or issue.latest_comment.created_at)
259+
if issue.latest_comment
260+
else getattr(issue, "updated_at", None)
261+
)
262+
263+
comments = []
264+
265+
gh_comments = gh_issue.get_comments(since=since) if since else gh_issue.get_comments()
266+
267+
for gh_comment in gh_comments:
268+
author = User.update_data(gh_comment.user)
269+
if not author:
270+
logger.warning("Could not sync author for comment %s", gh_comment.id)
271+
continue
272+
273+
comment = Comment.update_data(
274+
gh_comment,
275+
author=author,
276+
content_object=issue,
277+
save=False,
278+
)
279+
comments.append(comment)
280+
281+
if comments:
282+
Comment.bulk_save(comments)
283+
logger.info(
284+
"%d comments synced for issue #%s",
285+
len(comments),
286+
issue.number,
287+
)
288+
289+
except UnknownObjectException as e:
290+
logger.warning(
291+
"Could not access issue #%s. Error: %s",
292+
issue.number,
293+
e,
294+
)
295+
except Exception:
296+
logger.exception(
297+
"An unexpected error occurred during comment sync for issue #%s",
298+
issue.number,
299+
)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Generated by Django 5.2.5 on 2025-09-24 13:14
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
("contenttypes", "0002_remove_content_type_name"),
10+
("github", "0035_alter_user_bio_alter_user_is_owasp_staff"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="Comment",
16+
fields=[
17+
(
18+
"id",
19+
models.BigAutoField(
20+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
21+
),
22+
),
23+
("nest_created_at", models.DateTimeField(auto_now_add=True)),
24+
("nest_updated_at", models.DateTimeField(auto_now=True)),
25+
("github_id", models.BigIntegerField(unique=True, verbose_name="Github ID")),
26+
(
27+
"created_at",
28+
models.DateTimeField(blank=True, null=True, verbose_name="Created at"),
29+
),
30+
(
31+
"updated_at",
32+
models.DateTimeField(
33+
blank=True, db_index=True, null=True, verbose_name="Updated at"
34+
),
35+
),
36+
("body", models.TextField(verbose_name="Body")),
37+
("object_id", models.PositiveIntegerField()),
38+
(
39+
"author",
40+
models.ForeignKey(
41+
null=True,
42+
on_delete=django.db.models.deletion.SET_NULL,
43+
related_name="comments",
44+
to="github.user",
45+
),
46+
),
47+
(
48+
"content_type",
49+
models.ForeignKey(
50+
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype"
51+
),
52+
),
53+
],
54+
options={
55+
"verbose_name": "Comment",
56+
"verbose_name_plural": "Comments",
57+
"db_table": "github_comments",
58+
"ordering": ("-nest_created_at",),
59+
},
60+
),
61+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Github app."""
22

3+
from .comment import Comment
34
from .milestone import Milestone
45
from .pull_request import PullRequest
56
from .user import User
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""GitHub app comment model."""
2+
3+
from django.contrib.contenttypes.fields import GenericForeignKey
4+
from django.contrib.contenttypes.models import ContentType
5+
from django.db import models
6+
7+
from apps.common.models import BulkSaveModel, TimestampedModel
8+
from apps.common.utils import truncate
9+
10+
11+
class Comment(BulkSaveModel, TimestampedModel):
12+
"""Represents a comment on a GitHub Issue."""
13+
14+
class Meta:
15+
db_table = "github_comments"
16+
verbose_name = "Comment"
17+
verbose_name_plural = "Comments"
18+
ordering = ("-nest_created_at",)
19+
20+
github_id = models.BigIntegerField(unique=True, verbose_name="Github ID")
21+
created_at = models.DateTimeField(verbose_name="Created at", null=True, blank=True)
22+
updated_at = models.DateTimeField(
23+
verbose_name="Updated at", null=True, blank=True, db_index=True
24+
)
25+
author = models.ForeignKey(
26+
"github.User", on_delete=models.SET_NULL, null=True, related_name="comments"
27+
)
28+
body = models.TextField(verbose_name="Body")
29+
30+
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
31+
object_id = models.PositiveIntegerField()
32+
content_object = GenericForeignKey("content_type", "object_id")
33+
34+
def __str__(self):
35+
"""Return a string representation of the comment."""
36+
return f"{self.author} - {truncate(self.body, 50)}"
37+
38+
def from_github(self, gh_comment, author=None):
39+
"""Populate fields from a GitHub API comment object."""
40+
field_mapping = {
41+
"body": "body",
42+
"created_at": "created_at",
43+
"updated_at": "updated_at",
44+
}
45+
46+
for model_field, gh_field in field_mapping.items():
47+
value = getattr(gh_comment, gh_field, None)
48+
if value is not None:
49+
setattr(self, model_field, value)
50+
51+
self.author = author
52+
53+
@staticmethod
54+
def bulk_save(comments, fields=None): # type: ignore[override]
55+
"""Bulk save comments."""
56+
BulkSaveModel.bulk_save(Comment, comments, fields=fields)
57+
58+
@staticmethod
59+
def update_data(gh_comment, *, author=None, content_object=None, save: bool = True):
60+
"""Update or create a Comment instance from a GitHub comment object.
61+
62+
Args:
63+
gh_comment (github.IssueComment.IssueComment): GitHub comment object.
64+
author (User, optional): Comment author. Defaults to None.
65+
content_object (GenericForeignKey, optional): Content object. Defaults to None.
66+
save (bool, optional): Whether to save the instance immediately. Defaults to True.
67+
68+
Returns:
69+
Comment: The updated or newly created Comment instance.
70+
71+
"""
72+
try:
73+
comment = Comment.objects.get(github_id=gh_comment.id)
74+
except Comment.DoesNotExist:
75+
comment = Comment(github_id=gh_comment.id)
76+
77+
comment.from_github(gh_comment, author=author)
78+
79+
if content_object is not None:
80+
comment.content_object = content_object
81+
82+
if save:
83+
comment.save()
84+
85+
return comment

backend/apps/github/models/issue.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from functools import lru_cache
66

7+
from django.contrib.contenttypes.fields import GenericRelation
78
from django.db import models
89

910
from apps.common.index import IndexBase
@@ -54,6 +55,9 @@ class Meta:
5455
null=True,
5556
related_name="created_issues",
5657
)
58+
59+
comments = GenericRelation("github.Comment", related_query_name="issue")
60+
5761
milestone = models.ForeignKey(
5862
"github.Milestone",
5963
on_delete=models.CASCADE,
@@ -83,6 +87,16 @@ class Meta:
8387
blank=True,
8488
)
8589

90+
@property
91+
def latest_comment(self):
92+
"""Get the latest comment for this issue.
93+
94+
Returns:
95+
Comment | None: The most recently created comment, or None if no comments exist.
96+
97+
"""
98+
return self.comments.order_by("-nest_created_at").first()
99+
86100
def from_github(self, gh_issue, *, author=None, milestone=None, repository=None):
87101
"""Update the instance based on GitHub issue data.
88102

backend/apps/mentorship/Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mentorship-update-comments:
2+
@echo "Syncing Github Comments related to issues"
3+
@CMD="python manage.py mentorship_update_comments" $(MAKE) exec-backend-command

backend/apps/mentorship/admin/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Mentorship app admin."""
22

3+
from .issue_user_interest import IssueUserInterest
34
from .mentee import MenteeAdmin
45
from .mentee_program import MenteeProgramAdmin
56
from .mentor import MentorAdmin

0 commit comments

Comments
 (0)