Skip to content
Open
64 changes: 64 additions & 0 deletions website/migrations/0261_githubcomment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Generated by Django 5.2.9 on 2025-12-24 13:49

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


class Migration(migrations.Migration):
dependencies = [
("website", "0260_add_username_to_slackbotactivity"),
]

operations = [
migrations.CreateModel(
name="GitHubComment",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("comment_id", models.BigIntegerField(unique=True)),
("body", models.TextField()),
("comment_type", models.CharField(max_length=50)),
("created_at", models.DateTimeField()),
("updated_at", models.DateTimeField()),
("url", models.URLField()),
(
"contributor",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="github_comments",
to="website.contributor",
),
),
(
"github_issue",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="comments", to="website.githubissue"
),
),
(
"repo",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="github_comments",
to="website.repo",
),
),
(
"user_profile",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="github_comments",
to="website.userprofile",
),
),
],
options={
"ordering": ["-created_at"],
},
),
]
37 changes: 37 additions & 0 deletions website/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2328,6 +2328,43 @@ def add_labels(self, labels):
return False


class GitHubComment(models.Model):
github_issue = models.ForeignKey(GitHubIssue, on_delete=models.CASCADE, related_name="comments")
user_profile = models.ForeignKey(
UserProfile,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="github_comments",
)
contributor = models.ForeignKey(
Contributor,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="github_comments",
)
repo = models.ForeignKey(
Repo,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="github_comments",
)
comment_id = models.BigIntegerField(unique=True)
body = models.TextField()
comment_type = models.CharField(max_length=50)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
url = models.URLField()

class Meta:
ordering = ["-created_at"]

def __str__(self):
return f"Comment {self.comment_id} - {self.comment_type}"


class BaconEarning(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
tokens_earned = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) # Tokens earned by user
Expand Down
61 changes: 52 additions & 9 deletions website/templates/leaderboard_global.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
<h1 class="page-header">Global Leaderboard</h1>
</div>
</div>
<div class="flex flex-col md:gap-10 px-4 gap-2 md:px-0">
<div class="flex flex-col md:gap-10 px-4 gap-2 md:px-0 container mx-auto pb-8">
<div class="flex justify-center gap-6 flex-wrap">
<div class="flex-1 border-2 border-[#e74c3c] pb-3 rounded-lg flex-1 min-w-[300px] max-w-[550px]">
<div class="flex-1 border-2 border-[#e74c3c] pb-3 rounded-lg min-w-[300px] max-w-[550px]">
<div class="text-center text-3xl font-bold py-4 border-b-2 border-[#e74c3c] rounded-t-lg">Points Leaderboard</div>
<div class="list-group w-full p-4">
{% if not leaderboard %}
Expand Down Expand Up @@ -77,7 +77,7 @@ <h1 class="page-header">Global Leaderboard</h1>
{% endif %}
</div>
</div>
<div class="flex-1 border-2 border-[#e74c3c] pb-3 rounded-lg flex-1 min-w-[300px] max-w-[550px]">
<div class="flex-1 border-2 border-[#e74c3c] pb-3 rounded-lg min-w-[300px] max-w-[550px]">
<div class="text-center text-3xl font-bold py-4 border-b-2 border-[#e74c3c] rounded-t-lg">
Pull Request Leaderboard
<div class="text-sm font-normal text-gray-600 dark:text-gray-400 mt-1">Last 6 months</div>
Expand Down Expand Up @@ -125,7 +125,7 @@ <h1 class="page-header">Global Leaderboard</h1>
{% endif %}
</div>
</div>
<div class="flex-1 border-2 border-[#e74c3c] pb-3 rounded-lg flex-1 min-w-[300px] max-w-[550px]">
<div class="flex-1 border-2 border-[#e74c3c] pb-3 rounded-lg min-w-[300px] max-w-[550px]">
<div class="text-center text-3xl font-bold py-4 border-b-2 border-[#e74c3c] rounded-t-lg">
Code Review Leaderboard
<div class="text-sm font-normal text-gray-600 dark:text-gray-400 mt-1">Last 6 months</div>
Expand Down Expand Up @@ -176,8 +176,8 @@ <h1 class="page-header">Global Leaderboard</h1>
</div>
</div>
</div>
<div class="flex justify-center gap-6 flex-wrap">
<div class="flex-1 border-2 border-[#e74c3c] pb-3 rounded-lg flex-1 min-w-[300px] max-w-[550px]">
<div class="justify-center gap-6 flex-wrap grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
<div class="flex-1 border-2 border-[#e74c3c] pb-3 rounded-lg min-w-[300px] max-w-[550px]">
<div class="text-center text-3xl font-bold py-4 border-b-2 border-[#e74c3c] rounded-t-lg">Top Visitors</div>
<div class="list-group w-full p-4">
{% if top_visitors %}
Expand All @@ -187,7 +187,7 @@ <h1 class="page-header">Global Leaderboard</h1>
<div class="flex gap-2 items-center truncate">
{% if profile.avatar %}
<img src="{{ profile.avatar }}"
class="size-11 select-none border border-gray-200 dark:border-gray-700 rounded-full rounded-full"
class="size-11 select-none border border-gray-200 dark:border-gray-700 rounded-full"
alt="{{ profile.user.username }}"
width="50px"
height="50px">
Expand All @@ -214,7 +214,50 @@ <h1 class="page-header">Global Leaderboard</h1>
{% endif %}
</div>
</div>
<div class="flex-1 border-2 border-[#e74c3c] pb-3 rounded-lg flex-1 min-w-[300px] max-w-[550px]">
<div class="flex-1 border-2 border-[#e74c3c] pb-3 rounded-lg min-w-[300px] max-w-[550px]">
<div class="text-center text-3xl font-bold py-4 border-b-2 border-[#e74c3c] rounded-t-lg">
GitHub Comment Leaderboard
<div class="text-sm font-normal text-gray-600 dark:text-gray-400 mt-1">Last 6 months</div>
</div>
<div class="list-group w-full p-4">
{% if comment_leaderboard %}
<div class="flex flex-col gap-2">
{% for leader in comment_leaderboard %}
<div class="flex justify-between items-center">
<div class="flex gap-2 items-center truncate">
{% if leader.contributor__avatar_url %}
<img src="{{ leader.contributor__avatar_url }}"
class="size-11 select-none border border-gray-200 dark:border-gray-700 rounded-full"
alt="{{ leader.contributor__name }}"
width="50px"
height="50px">
{% else %}
<img src="{% gravatar_url 'default@example.com' 50 %}"
class="size-11 select-none border border-gray-200 dark:border-gray-700 rounded-full"
alt="{{ leader.contributor__name|default:'Unknown User' }}"
width="50px"
height="50px">
{% endif %}
<a href="{{ leader.contributor__github_url }}"
target="_blank"
class="text-lg transition-all duration-200 hover:text-[#e74c3c]">{{ leader.contributor__name|default:'Unknown User' }}</a>
<a href="{{ leader.contributor__github_url }}"
target="_blank"
class="ml-2 text-gray-600 hover:text-[#e74c3c] transition-all duration-200">
<i class="fab fa-github text-xl"></i>
</a>
</div>
<span class="pull-right badge bg-gray-100 dark:bg-gray-800 flex-shrink-0 rounded-md py-1 px-2 select-none">{{ leader.total_comments }} Comments</span>
</div>
<div class="border-t border-gray-200 dark:border-gray-700 my-1"></div>
{% endfor %}
</div>
{% else %}
<p class="text-[#e74c3c] text-center font-medium">No comment data available!</p>
{% endif %}
</div>
</div>
<div class="flex-1 border-2 border-[#e74c3c] pb-3 rounded-lg min-w-[300px] max-w-[550px]">
<div class="text-center text-3xl font-bold py-4 border-b-2 border-[#e74c3c] rounded-t-lg">Issue Bounties</div>
<div class="list-group w-full p-4">
{% if not issue_bounties %}
Expand All @@ -223,7 +266,7 @@ <h1 class="page-header">Global Leaderboard</h1>
{% endif %}
</div>
</div>
<div class="flex-1 border-2 border-[#e74c3c] pb-3 rounded-lg flex-1 min-w-[300px] max-w-[550px]">
<div class="flex-1 border-2 border-[#e74c3c] pb-3 rounded-lg min-w-[300px] max-w-[550px]">
<div class="text-center text-3xl font-bold py-4 border-b-2 border-[#e74c3c] rounded-t-lg">Bug Bounties</div>
<div class="list-group w-full p-4">
{% if not bug_bounties %}
Expand Down
110 changes: 110 additions & 0 deletions website/tests/test_webhook_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import json

from django.test import Client, TestCase
from django.urls import reverse

from website.models import GitHubComment, GitHubIssue, Repo, User, UserProfile


class GitHubCommentWebhookTest(TestCase):
def setUp(self):
self.client = Client()
self.webhook_url = reverse("github-webhook")
self.repo = Repo.objects.create(repo_url="https://github.com/owner/repo")
self.user = User.objects.create(username="testuser")
self.user_profile, created = UserProfile.objects.get_or_create(
user=self.user, defaults={"github_url": "https://github.com/testuser"}
)
if not created:
self.user_profile.github_url = "https://github.com/testuser"
self.user_profile.save()
self.issue = GitHubIssue.objects.create(
issue_id=1,
repo=self.repo,
title="Test PR",
type="pull_request",
state="open",
created_at="2023-01-01T00:00:00Z",
updated_at="2023-01-01T00:00:00Z",
)

def test_issue_comment_on_pr(self):
payload = {
"action": "created",
"issue": {
"number": 1,
"pull_request": {}, # Indicates it is a PR
},
"comment": {
"id": 1001,
"body": "This is a comment",
"created_at": "2023-01-01T12:00:00Z",
"updated_at": "2023-01-01T12:00:00Z",
"html_url": "https://github.com/owner/repo/issues/1#issuecomment-1001",
},
"repository": {"html_url": "https://github.com/owner/repo", "full_name": "owner/repo"},
"sender": {"login": "testuser", "html_url": "https://github.com/testuser", "type": "User"},
}
response = self.client.post(
self.webhook_url,
data=json.dumps(payload),
content_type="application/json",
HTTP_X_GITHUB_EVENT="issue_comment",
)
self.assertEqual(response.status_code, 200)
self.assertTrue(GitHubComment.objects.filter(comment_id=1001).exists())
comment = GitHubComment.objects.get(comment_id=1001)
self.assertEqual(comment.github_issue, self.issue)
self.assertEqual(comment.user_profile, self.user_profile)

def test_issue_comment_not_on_pr(self):
payload = {
"action": "created",
"issue": {
"number": 1,
# No pull_request key
},
"comment": {
"id": 1002,
"body": "This is a comment on an issue",
"created_at": "2023-01-01T12:00:00Z",
"updated_at": "2023-01-01T12:00:00Z",
"html_url": "https://github.com/owner/repo/issues/1#issuecomment-1002",
},
"repository": {"html_url": "https://github.com/owner/repo", "full_name": "owner/repo"},
"sender": {"login": "testuser", "type": "User"},
}
response = self.client.post(
self.webhook_url,
data=json.dumps(payload),
content_type="application/json",
HTTP_X_GITHUB_EVENT="issue_comment",
)
self.assertEqual(response.status_code, 200)
self.assertFalse(GitHubComment.objects.filter(comment_id=1002).exists())

def test_bot_comment_ignored(self):
payload = {
"action": "created",
"issue": {
"number": 1,
"pull_request": {},
},
"comment": {
"id": 1003,
"body": "I am a bot",
"created_at": "2023-01-01T12:00:00Z",
"updated_at": "2023-01-01T12:00:00Z",
"html_url": "https://github.com/owner/repo/issues/1#issuecomment-1003",
},
"repository": {"html_url": "https://github.com/owner/repo", "full_name": "owner/repo"},
"sender": {"login": "copilot", "type": "Bot", "id": 12345},
}
response = self.client.post(
self.webhook_url,
data=json.dumps(payload),
content_type="application/json",
HTTP_X_GITHUB_EVENT="issue_comment",
)
self.assertEqual(response.status_code, 200)
self.assertFalse(GitHubComment.objects.filter(comment_id=1003).exists())
Loading
Loading