Skip to content
Draft
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",
"created_at",
"updated_at",
)
list_filter = ("created_at", "updated_at")
search_fields = ("body", "author__login")


admin.site.register(Comment, CommentAdmin)
114 changes: 114 additions & 0 deletions backend/apps/github/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.utils import timezone
from github.GithubException import UnknownObjectException

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 +228,116 @@ 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:
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.last_comment_sync
if not since:
Comment on lines +253 to +254
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use := here too (I've already left this comment before)

last_comment = issue.latest_comment
since = last_comment.created_at if last_comment else None

if since:
logger.info("Found last comment at: %s, fetching newer comments", since)
else:
logger.info("No existing comments found, fetching all comments")
Comment on lines +258 to +261
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this useful?


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

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?


for gh_comment in gh_comments:
existing_comment = existing_comments.get(gh_comment.id)

if existing_comment:
Comment on lines +271 to +273
Copy link
Collaborator

Choose a reason for hiding this comment

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

:=

if since and gh_comment.updated_at <= since:
logger.info("Skipping unchanged comment %s", gh_comment.id)
continue

author = User.update_data(gh_comment.user)
if author:
Comment on lines +278 to +279
Copy link
Collaborator

Choose a reason for hiding this comment

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

:=

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)
else:
if since and gh_comment.created_at <= since:
logger.info(
"Skipping comment %s - not newer than our last sync", gh_comment.id
)
continue
Comment on lines +288 to +292
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do you skip comments instead of syncing it?


author = User.update_data(gh_comment.user)
if author:
comment = Comment.update_data(gh_comment, author=author, save=False)
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:
new_comment_github_ids = [c.github_id for c in comments_to_save]

Comment.bulk_save(comments_to_save)

newly_saved_comments = Comment.objects.filter(github_id__in=new_comment_github_ids)

issue.comments.add(*newly_saved_comments)

logger.info(
"Synced and associated %d new comments for issue #%s",
newly_saved_comments.count(),
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,
)

issue.last_comment_sync = current_time
issue.save(update_fields=["last_comment_sync"])

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,
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,
)
47 changes: 47 additions & 0 deletions backend/apps/github/migrations/0036_comment_issue_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 5.2.4 on 2025-09-05 22:55

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


class Migration(migrations.Migration):
dependencies = [
("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"
),
),
("github_id", models.BigIntegerField(unique=True)),
("body", models.TextField()),
("created_at", models.DateTimeField(db_index=True)),
("updated_at", models.DateTimeField(db_index=True)),
(
"author",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="comments",
to="github.user",
),
),
],
options={
"verbose_name": "Comment",
"verbose_name_plural": "Comments",
"ordering": ("-created_at",),
},
),
migrations.AddField(
model_name="issue",
name="comments",
field=models.ManyToManyField(blank=True, related_name="issues", to="github.comment"),
),
]
19 changes: 19 additions & 0 deletions backend/apps/github/migrations/0037_issue_last_comment_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.2.4 on 2025-09-07 17:45

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("github", "0036_comment_issue_comments"),
]

operations = [
migrations.AddField(
model_name="issue",
name="last_comment_sync",
field=models.DateTimeField(
blank=True, db_index=True, null=True, verbose_name="Last comment sync"
),
),
]
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
71 changes: 71 additions & 0 deletions backend/apps/github/models/comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""GitHub app comment model."""

from django.db import models

from apps.common.models import BulkSaveModel


class Comment(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.

Please make it a timestamped model.

"""Represents a comment on a GitHub Issue."""

class Meta:
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 = ("-created_at",)

github_id = models.BigIntegerField(unique=True)
author = models.ForeignKey(
"github.User", on_delete=models.SET_NULL, null=True, related_name="comments"
)
body = models.TextField()
created_at = models.DateTimeField(db_index=True)
updated_at = models.DateTimeField(db_index=True)

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

def from_github(self, gh_comment, 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.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
20 changes: 20 additions & 0 deletions backend/apps/github/models/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class Meta:
)

comments_count = models.PositiveIntegerField(verbose_name="Comments", default=0)
last_comment_sync = models.DateTimeField(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why can't we use latest comment date instead?

verbose_name="Last comment sync", blank=True, null=True, db_index=True
)

# FKs.
author = models.ForeignKey(
Expand All @@ -54,6 +57,13 @@ class Meta:
null=True,
related_name="created_issues",
)

comments = models.ManyToManyField(
"github.Comment",
related_name="issues",
blank=True,
)
Comment on lines +61 to +65
Copy link
Contributor

@coderabbitai coderabbitai bot Aug 23, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Prefer FK (or GFK) over M2M for Issue ↔ Comment; current schema allows invalid many-to-many linkage

A GitHub issue comment belongs to exactly one issue. Using a ManyToMany here (Issue.comments) permits linking a single comment to multiple issues, which does not match GitHub’s data model and can lead to integrity problems and more expensive queries. Recommend refactoring to either:

  • Option A (recommended now): Move to Comment.issue = ForeignKey(Issue, related_name="comments", on_delete=CASCADE) and drop Issue.comments M2M.
  • Option B (if you truly need to support comments for different “commentable” entities, e.g., PRs): use a GenericForeignKey (content_type, object_id) on Comment.

Minimal diff for this file if adopting Option A (the reverse manager still appears as issue.comments):

-    comments = models.ManyToManyField(
-        "github.Comment",
-        related_name="issues",
-        blank=True,
-    )

Run to find usages that rely on M2M semantics and would need adjustments:


🏁 Script executed:

#!/bin/bash
rg -n -C2 -g '!**/migrations/**' -e '\bcomments\.add\(' -e '\bcomments\.set\(' -e '\bcomments\.remove\('

Length of output: 453


Refactor Issue ↔ Comment to a one-to-many relationship

A GitHub issue comment belongs to exactly one issue, so the current ManyToManyField allows invalid linkages and carries unnecessary join-table overhead. To align the schema with GitHub’s data model and simplify queries, please migrate to a ForeignKey on Comment (Option A below).

• Remove the M2M on Issue:

-    # backend/apps/github/models/issue.py
-    comments = models.ManyToManyField(
-        "github.Comment",
-        related_name="issues",
-        blank=True,
-    )

• Add a one-to-many FK on Comment:

+    # backend/apps/github/models/comment.py
+    issue = models.ForeignKey(
+        "github.Issue",
+        related_name="comments",
+        on_delete=models.CASCADE,
+    )

• Migrations:
– Generate a schema migration to drop the M2M table and add the FK.
– Provide a data migration to backfill comment.issue_id from the existing join table.
• Usage impact:
– We found one call site in backend/apps/github/common.py at line 292 using issue.comments.add(*newly_saved_comments); the FK reverse manager supports add() (it sets comment.issue) so this should continue working unchanged.
– No .set() or .remove() calls were detected in non-migration code, so no further call-site updates are anticipated.

Option B (GFK) is only needed if Comments must also attach to other content types (e.g. PRs).

Please implement Option A to enforce data integrity and simplify the model.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
comments = models.ManyToManyField(
"github.Comment",
related_name="issues",
blank=True,
)
# backend/apps/github/models/issue.py
- comments = models.ManyToManyField(
- "github.Comment",
- related_name="issues",
- blank=True,
- )
# backend/apps/github/models/comment.py
issue = models.ForeignKey(
"github.Issue",
related_name="comments",
on_delete=models.CASCADE,
)

Copy link
Collaborator

Choose a reason for hiding this comment

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

The comment makes sense. The DB modeling here is not the best. Let's use GenericForeignKey to accommodate both issue/pr options and GenericRelations in Issue (and future PR model), e.g.


class Issue(models.Model):
    title = models.CharField(max_length=255)
    description = models.TextField()
    comments = GenericRelation("github.Comment", related_query_name="issue")


class PullRequest(models.Model):
    title = models.CharField(max_length=255)
    description = models.TextField()
    comments = GenericRelation("github.Comment", related_query_name="pull_request")

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

@arkid15r Great suggestion! Using GenericForeignKey is indeed the better approach for future extensibility. Based on my analysis of the current code, here's the concrete implementation:

Comment Model Changes (backend/apps/github/models/comment.py):

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

class Comment(BulkSaveModel, models.Model):
    # ... existing fields ...
    
    # Replace any existing issue-specific relationship with generic ones
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')
    
    # ... rest of the model ...

Issue Model Changes (backend/apps/github/models/issue.py):

from django.contrib.contenttypes.fields import GenericRelation

class Issue(GenericIssueModel):
    # ... existing fields ...
    
    # Replace the current M2M with GenericRelation
    comments = GenericRelation(
        "github.Comment",
        related_query_name="issue"
    )
    
    # ... rest of the model ...

Migration Considerations:

  1. Schema Migration: Drop the M2M table, add content_type and object_id fields to Comment
  2. Data Migration: Backfill the new fields from existing M2M relationships
  3. Update sync_issue_comments: Modify the issue.comments.add() call in backend/apps/github/common.py line 292 to work with the new relationship

Future PullRequest Model (when implemented):

class PullRequest(models.Model):
    # ... fields ...
    comments = GenericRelation(
        "github.Comment", 
        related_query_name="pull_request"
    )

The existing issue.comments.add(*newly_saved_comments) usage should continue to work seamlessly with GenericRelation, as it automatically handles setting the content_type and object_id fields.

Would you like me to help create the migration files for this refactor?


🏁 Script executed:

#!/bin/bash
# First, let's examine the current Comment model structure
cat backend/apps/github/models/comment.py

Length of output: 2280


🏁 Script executed:

#!/bin/bash
# Check for other usage patterns of issue.comments beyond the add() call we found
rg -n -C3 -g '!**/migrations/**' 'issue\.comments\.' --type py

Length of output: 148


🏁 Script executed:

#!/bin/bash
# Check if there are any references to the comments field in the Issue model or related code
rg -n -C2 -g '!**/migrations/**' 'comments.*issues' --type py

Length of output: 148


🏁 Script executed:

#!/bin/bash
# Look for any existing GenericForeignKey usage in the codebase for reference
rg -n -C2 'GenericForeignKey\|GenericRelation' --type py

Length of output: 0


Perfect! Now I have the complete picture. Based on the current Comment model structure and usage patterns, here's the updated implementation guidance:

@arkid15r Here's the concrete implementation for the GenericForeignKey approach:

Comment Model Changes (backend/apps/github/models/comment.py):

"""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


class Comment(BulkSaveModel, models.Model):
    """Represents a comment on a GitHub Issue or Pull Request."""

    class Meta:
        verbose_name = "Comment"
        verbose_name_plural = "Comments"
        ordering = ("-created_at",)

    github_id = models.BigIntegerField(unique=True)
    author = models.ForeignKey(
        "github.User", on_delete=models.SET_NULL, null=True, related_name="comments"
    )
    body = models.TextField()
    created_at = models.DateTimeField(db_index=True)
    updated_at = models.DateTimeField(db_index=True)
    
    # Generic foreign key fields
    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} - {self.body[:40]}"

    def from_github(self, gh_comment, 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.author = author

    @staticmethod
    def bulk_save(comments, fields=None):
        """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."""
        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

Issue Model Changes (backend/apps/github/models/issue.py):

# Add this import at the top
from django.contrib.contenttypes.fields import GenericRelation

# Replace the current ManyToManyField with:
comments = GenericRelation(
    "github.Comment",
    related_query_name="issue"
)

# Also update the latest_comment property to work with GenericRelation:
@property
def latest_comment(self):
    """Get the latest comment for this issue."""
    return self.comments.order_by("-created_at").first()

Migration Strategy:

  1. Create the schema migration to add content_type and object_id to Comment and drop the M2M table
  2. Create a data migration to populate the new fields from existing M2M relationships
  3. Update sync_issue_comments function in common.py - the issue.comments.add() call should work unchanged with GenericRelation

The existing usage in backend/apps/github/common.py line 292 (issue.comments.add(*newly_saved_comments)) will continue to work seamlessly, as GenericRelation automatically handles setting the content_type and object_id fields when adding related objects.

This approach provides the flexibility for future PR comments while maintaining clean, efficient queries and proper data integrity.


🏁 Script executed:

#!/bin/bash
# Let's check what files exist and search more broadly
find . -name "*.py" -type f | head -20

Length of output: 840


🏁 Script executed:

#!/bin/bash
# Search for usage of issue.comments with broader pattern
rg -n "issue\.comments" . --type py

Length of output: 404


🏁 Script executed:

#!/bin/bash
# Check for any comments-related usage in the codebase
rg -n "\.comments\." . --type py

Length of output: 507


🏁 Script executed:

#!/bin/bash
# Look at the common.py file where we know there's usage
cat backend/apps/github/common.py | grep -n -A5 -B5 "comments"

Length of output: 3879


Perfect! Now I can see all the usage patterns. The good news is that all existing code will work unchanged with GenericRelation. Here's the complete implementation:

@arkid15r Based on the usage analysis, here's the complete implementation that will maintain backward compatibility:

Files to Change

1. Comment Model (backend/apps/github/models/comment.py):

"""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


class Comment(BulkSaveModel, models.Model):
    """Represents a comment on a GitHub Issue or Pull Request."""

    class Meta:
        verbose_name = "Comment"
        verbose_name_plural = "Comments"
        ordering = ("-created_at",)

    github_id = models.BigIntegerField(unique=True)
    author = models.ForeignKey(
        "github.User", on_delete=models.SET_NULL, null=True, related_name="comments"
    )
    body = models.TextField()
    created_at = models.DateTimeField(db_index=True)
    updated_at = models.DateTimeField(db_index=True)
    
    # Generic foreign key to support both Issues and future PRs
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    # Rest of the methods remain unchanged...

2. Issue Model (backend/apps/github/models/issue.py):

# Add this import at the top:
from django.contrib.contenttypes.fields import GenericRelation

# Replace the current ManyToManyField with:
comments = GenericRelation(
    "github.Comment",
    related_query_name="issue"
)

# The latest_comment property remains the same - it will work unchanged

Migration Strategy

Schema Migration:

# New migration file
operations = [
    # Add GenericForeignKey fields to Comment
    migrations.AddField(
        model_name='comment',
        name='content_type',
        field=models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE),
    ),
    migrations.AddField(
        model_name='comment',
        name='object_id',
        field=models.PositiveIntegerField(),
    ),
    # Remove the M2M relationship
    migrations.RemoveField(
        model_name='issue',
        name='comments',
    ),
]

Data Migration:

# Separate data migration to populate the new fields
def migrate_comment_relationships(apps, schema_editor):
    Comment = apps.get_model('github', 'Comment')
    Issue = apps.get_model('github', 'Issue')
    ContentType = apps.get_model('contenttypes', 'ContentType')
    
    issue_content_type = ContentType.objects.get_for_model(Issue)
    
    # Get the M2M through table and populate GenericFK fields
    # This requires accessing the M2M through table before it's dropped

Compatibility Verification

All existing usage patterns will work unchanged:

Line 263: issue.comments.select_related("author").all() - works with GenericRelation
Line 311: issue.comments.add(*newly_saved_comments) - GenericRelation automatically sets content_type/object_id
Line 104: self.comments.order_by("-created_at").first() - works identically
Line 96: issue.comments.select_related("author").order_by(...) - works with GenericRelation

The refactor maintains full API compatibility while enabling future PR comment support. No changes needed in common.py or the management command!


✏️ Learnings added
Learnt from: arkid15r
PR: OWASP/Nest#1995
File: backend/apps/github/models/issue.py:61-65
Timestamp: 2025-09-10T03:14:06.485Z
Learning: When refactoring from ManyToManyField to GenericForeignKey + GenericRelation in Django, all standard relationship operations (.all(), .add(), .select_related(), .order_by()) continue to work unchanged. GenericRelation automatically handles setting content_type and object_id when using .add().

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


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