Skip to content

Commit 00d3b4f

Browse files
committed
Enhance Hackathon Leaderboard with Detailed Pull Request Information
- Improve leaderboard design with more detailed contributor information - Add individual pull request details for each contributor - Include repository name, merge date, and link to pull request - Enhance visual styling with better spacing and hover effects
1 parent 6574d68 commit 00d3b4f

File tree

2 files changed

+364
-7
lines changed

2 files changed

+364
-7
lines changed

website/templates/hackathons/detail.html

+34-7
Original file line numberDiff line numberDiff line change
@@ -169,15 +169,42 @@ <h3 class="text-xl font-semibold">{{ prize.title }}</h3>
169169
<div class="bg-white rounded-lg shadow p-6 mb-8">
170170
<h2 class="text-2xl font-bold mb-4 text-gray-800">Leaderboard</h2>
171171
{% if leaderboard %}
172-
<div class="space-y-4">
172+
<div class="space-y-6">
173173
{% for entry in leaderboard %}
174-
<div class="flex items-center p-3 {% if forloop.counter <= 3 %}bg-gray-50{% endif %} rounded-lg">
175-
<div class="w-8 h-8 flex items-center justify-center mr-3 {% if forloop.counter == 1 %}bg-yellow-100 text-yellow-600{% elif forloop.counter == 2 %}bg-gray-100 text-gray-600{% elif forloop.counter == 3 %}bg-amber-100 text-amber-600{% else %}bg-gray-100 text-gray-500{% endif %} rounded-full font-bold">
176-
{{ forloop.counter }}
174+
<div class="p-4 {% if forloop.counter <= 3 %}bg-gray-50{% endif %} rounded-lg border border-gray-200">
175+
<div class="flex items-center mb-3">
176+
<div class="w-8 h-8 flex items-center justify-center mr-3 {% if forloop.counter == 1 %}bg-yellow-100 text-yellow-600{% elif forloop.counter == 2 %}bg-gray-100 text-gray-600{% elif forloop.counter == 3 %}bg-amber-100 text-amber-600{% else %}bg-gray-100 text-gray-500{% endif %} rounded-full font-bold">
177+
{{ forloop.counter }}
178+
</div>
179+
<div class="flex-grow">
180+
<div class="font-medium">{{ entry.user.username }}</div>
181+
<div class="text-sm text-gray-500">{{ entry.count }} pull request{{ entry.count|pluralize }}</div>
182+
</div>
177183
</div>
178-
<div class="flex-grow">
179-
<div class="font-medium">{{ entry.user.username }}</div>
180-
<div class="text-sm text-gray-500">{{ entry.count }} pull request{{ entry.count|pluralize }}</div>
184+
<!-- Pull Requests List -->
185+
<div class="mt-2 pl-11">
186+
<div class="text-sm font-medium text-gray-700 mb-2">Contributions:</div>
187+
<div class="space-y-2 max-h-48 overflow-y-auto pr-2">
188+
{% for pr in entry.prs %}
189+
<a href="{{ pr.url }}"
190+
target="_blank"
191+
rel="noopener noreferrer"
192+
class="block p-2 bg-gray-50 hover:bg-gray-100 rounded border-l-4 border-[#e74c3c] transition">
193+
<div class="font-medium text-sm truncate">{{ pr.title }}</div>
194+
<div class="flex items-center text-xs text-gray-500 mt-1">
195+
<span class="flex items-center">
196+
<i class="fas fa-code-branch mr-1"></i>
197+
{{ pr.repo.name }}
198+
</span>
199+
<span class="mx-2"></span>
200+
<span>
201+
<i class="far fa-calendar-alt mr-1"></i>
202+
{{ pr.merged_at|date:"M d, Y" }}
203+
</span>
204+
</div>
205+
</a>
206+
{% endfor %}
207+
</div>
181208
</div>
182209
</div>
183210
{% endfor %}

website/test_hackathon_leaderboard.py

+330
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
import datetime
2+
3+
from django.contrib.auth.models import User
4+
from django.test import Client, TestCase
5+
from django.urls import reverse
6+
from django.utils import timezone
7+
8+
from website.models import GitHubIssue, Hackathon, Organization, Repo
9+
10+
11+
class HackathonLeaderboardTestCase(TestCase):
12+
"""Test case for the hackathon leaderboard functionality."""
13+
14+
def setUp(self):
15+
"""Set up test data for the hackathon leaderboard tests."""
16+
# Create test users
17+
self.user1 = User.objects.create_user(username="testuser1", email="test1@example.com", password="testpass123")
18+
self.user2 = User.objects.create_user(username="testuser2", email="test2@example.com", password="testpass123")
19+
self.user3 = User.objects.create_user(username="testuser3", email="test3@example.com", password="testpass123")
20+
21+
# Create organization
22+
self.organization = Organization.objects.create(
23+
name="Test Organization",
24+
slug="test-organization",
25+
url="https://example.com",
26+
)
27+
28+
# Create repositories
29+
self.repo1 = Repo.objects.create(
30+
name="Test Repo 1",
31+
slug="test-repo-1",
32+
repo_url="https://github.com/test/repo1",
33+
organization=self.organization,
34+
)
35+
self.repo2 = Repo.objects.create(
36+
name="Test Repo 2",
37+
slug="test-repo-2",
38+
repo_url="https://github.com/test/repo2",
39+
organization=self.organization,
40+
)
41+
42+
# Create hackathon
43+
now = timezone.now()
44+
self.hackathon = Hackathon.objects.create(
45+
name="Test Hackathon",
46+
slug="test-hackathon",
47+
description="A test hackathon for unit testing",
48+
organization=self.organization,
49+
start_time=now - datetime.timedelta(days=5),
50+
end_time=now + datetime.timedelta(days=5),
51+
is_active=True,
52+
rules="# Test Rules\n1. Submit PRs\n2. Be awesome",
53+
)
54+
self.hackathon.repositories.add(self.repo1, self.repo2)
55+
56+
# Create pull requests for the hackathon
57+
# User 1 has 3 PRs (2 in repo1, 1 in repo2)
58+
for i in range(1, 4):
59+
repo = self.repo1 if i <= 2 else self.repo2
60+
GitHubIssue.objects.create(
61+
issue_id=1000 + i,
62+
title=f"Test PR {i} by User 1",
63+
body="Test PR body",
64+
state="closed",
65+
type="pull_request",
66+
created_at=now - datetime.timedelta(days=3),
67+
updated_at=now - datetime.timedelta(days=2),
68+
merged_at=now - datetime.timedelta(days=1),
69+
is_merged=True,
70+
url=f"https://github.com/test/repo/pull/{1000 + i}",
71+
repo=repo,
72+
user_profile=self.user1.userprofile,
73+
)
74+
75+
# User 2 has 2 PRs (both in repo2)
76+
for i in range(1, 3):
77+
GitHubIssue.objects.create(
78+
issue_id=2000 + i,
79+
title=f"Test PR {i} by User 2",
80+
body="Test PR body",
81+
state="closed",
82+
type="pull_request",
83+
created_at=now - datetime.timedelta(days=3),
84+
updated_at=now - datetime.timedelta(days=2),
85+
merged_at=now - datetime.timedelta(days=1),
86+
is_merged=True,
87+
url=f"https://github.com/test/repo/pull/{2000 + i}",
88+
repo=self.repo2,
89+
user_profile=self.user2.userprofile,
90+
)
91+
92+
# User 3 has 1 PR (in repo1)
93+
GitHubIssue.objects.create(
94+
issue_id=3001,
95+
title="Test PR 1 by User 3",
96+
body="Test PR body",
97+
state="closed",
98+
type="pull_request",
99+
created_at=now - datetime.timedelta(days=3),
100+
updated_at=now - datetime.timedelta(days=2),
101+
merged_at=now - datetime.timedelta(days=1),
102+
is_merged=True,
103+
url="https://github.com/test/repo/pull/3001",
104+
repo=self.repo1,
105+
user_profile=self.user3.userprofile,
106+
)
107+
108+
# Create a PR that's outside the hackathon timeframe (should not be counted)
109+
GitHubIssue.objects.create(
110+
issue_id=4001,
111+
title="PR outside timeframe",
112+
body="This PR is outside the hackathon timeframe",
113+
state="closed",
114+
type="pull_request",
115+
created_at=now - datetime.timedelta(days=10),
116+
updated_at=now - datetime.timedelta(days=9),
117+
merged_at=now - datetime.timedelta(days=8),
118+
is_merged=True,
119+
url="https://github.com/test/repo/pull/4001",
120+
repo=self.repo1,
121+
user_profile=self.user1.userprofile,
122+
)
123+
124+
# Create a PR that's not merged (should not be counted)
125+
GitHubIssue.objects.create(
126+
issue_id=5001,
127+
title="Unmerged PR",
128+
body="This PR is not merged",
129+
state="open",
130+
type="pull_request",
131+
created_at=now - datetime.timedelta(days=3),
132+
updated_at=now - datetime.timedelta(days=2),
133+
merged_at=None,
134+
is_merged=False,
135+
url="https://github.com/test/repo/pull/5001",
136+
repo=self.repo1,
137+
user_profile=self.user1.userprofile,
138+
)
139+
140+
self.client = Client()
141+
142+
def test_hackathon_leaderboard(self):
143+
"""Test that the hackathon leaderboard correctly shows contributors and their PRs."""
144+
# Get the leaderboard from the model method
145+
leaderboard = self.hackathon.get_leaderboard()
146+
147+
# Check the leaderboard order and PR counts
148+
self.assertEqual(len(leaderboard), 3, "Leaderboard should have 3 contributors")
149+
150+
# User 1 should be first with 3 PRs
151+
self.assertEqual(leaderboard[0]["user"].id, self.user1.id)
152+
self.assertEqual(leaderboard[0]["count"], 3)
153+
self.assertEqual(len(leaderboard[0]["prs"]), 3)
154+
155+
# User 2 should be second with 2 PRs
156+
self.assertEqual(leaderboard[1]["user"].id, self.user2.id)
157+
self.assertEqual(leaderboard[1]["count"], 2)
158+
self.assertEqual(len(leaderboard[1]["prs"]), 2)
159+
160+
# User 3 should be third with 1 PR
161+
self.assertEqual(leaderboard[2]["user"].id, self.user3.id)
162+
self.assertEqual(leaderboard[2]["count"], 1)
163+
self.assertEqual(len(leaderboard[2]["prs"]), 1)
164+
165+
# Test the hackathon detail view
166+
response = self.client.get(reverse("hackathon_detail", kwargs={"slug": self.hackathon.slug}))
167+
self.assertEqual(response.status_code, 200)
168+
169+
# Check that the leaderboard is in the context
170+
self.assertIn("leaderboard", response.context)
171+
view_leaderboard = response.context["leaderboard"]
172+
173+
# Verify the same data is in the view context
174+
self.assertEqual(len(view_leaderboard), 3)
175+
self.assertEqual(view_leaderboard[0]["count"], 3)
176+
self.assertEqual(view_leaderboard[1]["count"], 2)
177+
self.assertEqual(view_leaderboard[2]["count"], 1)
178+
179+
# Check that the template contains the PR titles
180+
content = response.content.decode("utf-8")
181+
self.assertIn("Test PR 1 by User 1", content)
182+
self.assertIn("Test PR 2 by User 1", content)
183+
self.assertIn("Test PR 3 by User 1", content)
184+
self.assertIn("Test PR 1 by User 2", content)
185+
self.assertIn("Test PR 2 by User 2", content)
186+
self.assertIn("Test PR 1 by User 3", content)
187+
188+
# Ensure PRs outside the timeframe or unmerged are not included
189+
self.assertNotIn("PR outside timeframe", content)
190+
self.assertNotIn("Unmerged PR", content)
191+
192+
def test_hackathon_leaderboard_empty(self):
193+
"""Test that the hackathon leaderboard handles empty data correctly."""
194+
# Create a new hackathon with no PRs
195+
empty_hackathon = Hackathon.objects.create(
196+
name="Empty Hackathon",
197+
slug="empty-hackathon",
198+
description="A hackathon with no PRs",
199+
organization=self.organization,
200+
start_time=timezone.now() - datetime.timedelta(days=5),
201+
end_time=timezone.now() + datetime.timedelta(days=5),
202+
is_active=True,
203+
)
204+
205+
# Create a new repository that has no PRs
206+
empty_repo = Repo.objects.create(
207+
name="Empty Repo",
208+
slug="empty-repo",
209+
repo_url="https://github.com/test/empty",
210+
organization=self.organization,
211+
)
212+
213+
empty_hackathon.repositories.add(empty_repo)
214+
215+
# Get the leaderboard
216+
leaderboard = empty_hackathon.get_leaderboard()
217+
218+
# Check that the leaderboard is empty
219+
self.assertEqual(len(leaderboard), 0, "Leaderboard should be empty")
220+
221+
# Test the hackathon detail view
222+
response = self.client.get(reverse("hackathon_detail", kwargs={"slug": empty_hackathon.slug}))
223+
self.assertEqual(response.status_code, 200)
224+
225+
# Check that the leaderboard is in the context and empty
226+
self.assertIn("leaderboard", response.context)
227+
self.assertEqual(len(response.context["leaderboard"]), 0)
228+
229+
# Check that the template shows the "no contributions" message
230+
content = response.content.decode("utf-8")
231+
self.assertIn("No contributions yet", content)
232+
233+
def test_hackathon_leaderboard_sorting(self):
234+
"""Test that the hackathon leaderboard is sorted correctly by PR count."""
235+
# Create a new hackathon for sorting test
236+
now = timezone.now()
237+
sort_hackathon = Hackathon.objects.create(
238+
name="Sort Test Hackathon",
239+
slug="sort-test-hackathon",
240+
description="A hackathon for testing sorting",
241+
organization=self.organization,
242+
start_time=now - datetime.timedelta(days=5),
243+
end_time=now + datetime.timedelta(days=5),
244+
is_active=True,
245+
)
246+
247+
# Create new repositories for this test to avoid conflicts
248+
sort_repo1 = Repo.objects.create(
249+
name="Sort Repo 1",
250+
slug="sort-repo-1",
251+
repo_url="https://github.com/test/sort1",
252+
organization=self.organization,
253+
)
254+
sort_repo2 = Repo.objects.create(
255+
name="Sort Repo 2",
256+
slug="sort-repo-2",
257+
repo_url="https://github.com/test/sort2",
258+
organization=self.organization,
259+
)
260+
261+
sort_hackathon.repositories.add(sort_repo1, sort_repo2)
262+
263+
# Create PRs with different counts to test sorting
264+
# User 3 has 5 PRs (should be first)
265+
for i in range(1, 6):
266+
GitHubIssue.objects.create(
267+
issue_id=6000 + i,
268+
title=f"Sort Test PR {i} by User 3",
269+
body="Test PR body",
270+
state="closed",
271+
type="pull_request",
272+
created_at=now - datetime.timedelta(days=3),
273+
updated_at=now - datetime.timedelta(days=2),
274+
merged_at=now - datetime.timedelta(days=1),
275+
is_merged=True,
276+
url=f"https://github.com/test/repo/pull/{6000 + i}",
277+
repo=sort_repo1,
278+
user_profile=self.user3.userprofile,
279+
)
280+
281+
# User 2 has 3 PRs (should be second)
282+
for i in range(1, 4):
283+
GitHubIssue.objects.create(
284+
issue_id=7000 + i,
285+
title=f"Sort Test PR {i} by User 2",
286+
body="Test PR body",
287+
state="closed",
288+
type="pull_request",
289+
created_at=now - datetime.timedelta(days=3),
290+
updated_at=now - datetime.timedelta(days=2),
291+
merged_at=now - datetime.timedelta(days=1),
292+
is_merged=True,
293+
url=f"https://github.com/test/repo/pull/{7000 + i}",
294+
repo=sort_repo2,
295+
user_profile=self.user2.userprofile,
296+
)
297+
298+
# User 1 has 1 PR (should be third)
299+
GitHubIssue.objects.create(
300+
issue_id=8001,
301+
title="Sort Test PR 1 by User 1",
302+
body="Test PR body",
303+
state="closed",
304+
type="pull_request",
305+
created_at=now - datetime.timedelta(days=3),
306+
updated_at=now - datetime.timedelta(days=2),
307+
merged_at=now - datetime.timedelta(days=1),
308+
is_merged=True,
309+
url="https://github.com/test/repo/pull/8001",
310+
repo=sort_repo1,
311+
user_profile=self.user1.userprofile,
312+
)
313+
314+
# Get the leaderboard
315+
leaderboard = sort_hackathon.get_leaderboard()
316+
317+
# Check the leaderboard order and PR counts
318+
self.assertEqual(len(leaderboard), 3, "Leaderboard should have 3 contributors")
319+
320+
# User 3 should be first with 5 PRs
321+
self.assertEqual(leaderboard[0]["user"].id, self.user3.id)
322+
self.assertEqual(leaderboard[0]["count"], 5)
323+
324+
# User 2 should be second with 3 PRs
325+
self.assertEqual(leaderboard[1]["user"].id, self.user2.id)
326+
self.assertEqual(leaderboard[1]["count"], 3)
327+
328+
# User 1 should be third with 1 PR
329+
self.assertEqual(leaderboard[2]["user"].id, self.user1.id)
330+
self.assertEqual(leaderboard[2]["count"], 1)

0 commit comments

Comments
 (0)