diff --git a/accounts/tests.py b/accounts/tests.py index 2805ae166..876d1494b 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -1,20 +1,130 @@ -from unittest import mock - from django.contrib.auth.models import User -from django.test import TestCase +from django.test import TestCase, override_settings from django_hosts.resolvers import reverse +from tracdb.models import Revision, Ticket, TicketChange +from tracdb.testutils import TracDBCreateDatabaseMixin + + +class UserProfileTests(TracDBCreateDatabaseMixin, TestCase): + databases = {"default", "trac"} + + @classmethod + def setUpTestData(cls): + cls.credentials = {"username": "a-user", "password": "password"} + cls.user = User.objects.create_user(**cls.credentials) + ticket_1 = Ticket.objects.create( + status="closed", reporter="a-user", owner="a-user", resolution="fixed" + ) + ticket_2 = Ticket.objects.create( + status="closed", reporter="b-user", owner="a-user", resolution="fixed" + ) + Ticket.objects.create( + status="closed", reporter="b-user", owner="b-user", resolution="fixed" + ) + Ticket.objects.create( + status="closed", reporter="b-user", owner="a-user", resolution="wontfix" + ) + Ticket.objects.create(status="new", reporter="a-user") + Ticket.objects.create(status="new", reporter="b-user") + Revision.objects.create( + author="a-user", + rev="91c879eda595c12477bbfa6f51115e88b75ddf88", + _time=1731669560, + ) + Revision.objects.create( + author="a-user", + rev="da2432cccae841f0d7629f17a5d79ec47ed7b7cb", + _time=1731669560, + ) + Revision.objects.create( + author="b-user", + rev="63dbe30d3363715deaf280214d75b03f6d65a571", + _time=1731669560, + ) + TicketChange.objects.create( + author="a-user", + field="stage", + oldvalue="Unreviewed", + newvalue="Accepted", + ticket=ticket_1, + _time=1731669560, + ) + TicketChange.objects.create( + author="b-user", + field="stage", + oldvalue="Unreviewed", + newvalue="Accepted", + ticket=ticket_2, + _time=1731669560, + ) + TicketChange.objects.create( + author="a-user", + field="owner", + oldvalue="b-user", + newvalue="a-user", + ticket=ticket_1, + _time=1731669560, + ) + + @override_settings(TRAC_URL="https://code.djangoproject.com/") + def test_user_profile(self): + a_user_response = self.client.get( + reverse("user_profile", host="www", args=["a-user"]) + ) + b_user_response = self.client.get( + reverse("user_profile", host="www", args=["b-user"]) + ) + self.assertContains(a_user_response, "a-user") + self.assertContains(b_user_response, "b-user") + self.assertContains( + a_user_response, + 'Commits: 2', + html=True, + ) + self.assertContains( + b_user_response, + 'Commits: 1', + html=True, + ) + self.assertContains( + a_user_response, + '' + "Tickets fixed: 2", + html=True, + ) + self.assertContains( + a_user_response, + '' + "Tickets fixed: 1", + html=True, + ) + self.assertContains( + a_user_response, + '' + "Tickets opened: 2", + html=True, + ) + self.assertContains( + b_user_response, + '' + "Tickets opened: 4", + html=True, + ) + -class ViewTests(TestCase): - def setUp(self): - self.credentials = {"username": "a-user", "password": "password"} - self.user = User.objects.create_user(**self.credentials) +class ViewsTests(TestCase): - @mock.patch("accounts.views.get_user_stats") - def test_user_profile(self, mock_user_stats): - response = self.client.get(reverse("user_profile", host="www", args=["a-user"])) - self.assertContains(response, "a-user") - mock_user_stats.assert_called_once_with(self.user) + @classmethod + def setUpTestData(cls): + cls.credentials = {"username": "a-user", "password": "password"} + cls.user = User.objects.create_user(**cls.credentials) def test_login_redirect(self): response = self.client.post(reverse("login"), self.credentials) diff --git a/djangoproject/templates/accounts/user_profile.html b/djangoproject/templates/accounts/user_profile.html index 142b8e171..c72d15b15 100644 --- a/djangoproject/templates/accounts/user_profile.html +++ b/djangoproject/templates/accounts/user_profile.html @@ -46,7 +46,11 @@

{% translate "Lies, damned lies, and statistics:" %}

{% endif %} diff --git a/tracdb/stats.py b/tracdb/stats.py index 51a837c1d..b6ff897e7 100644 --- a/tracdb/stats.py +++ b/tracdb/stats.py @@ -3,15 +3,22 @@ """ import operator -from collections import OrderedDict +from collections import OrderedDict, namedtuple -import django.db +from djangoproject import settings -from .models import Attachment, Revision, Ticket, TicketChange +from .models import Revision, Ticket, TicketChange _statfuncs = [] +StatData = namedtuple("StatData", ["count", "link"]) + + +def get_trac_link(query): + return f"{settings.TRAC_URL}query?{query}&desc=1&order=changetime" + + def stat(title): """ Register a function as a "stat" @@ -36,23 +43,28 @@ def get_user_stats(username): @stat("Commits") def commit_count(username): - return Revision.objects.filter(author=username).count() + count = Revision.objects.filter(author=username).count() + # This assumes that the username is their GitHub username, this is very + # often the case. If this is incorrect, the GitHub will show no commits. + link = f"https://github.com/django/django/commits/main/?author={username}" + return StatData(count=count, link=link) -@stat("Tickets closed") -def tickets_closed(username): - # Raw query so that we can do COUNT(DISTINCT ticket). - q = """SELECT COUNT(DISTINCT ticket) FROM ticket_change - WHERE author = %s AND field = 'status' AND newvalue = 'closed';""" - return run_single_value_query(q, username) +@stat("Tickets fixed") +def tickets_fixed(username): + count = Ticket.objects.filter(owner=username, resolution="fixed").count() + link = get_trac_link(f"owner=~{username}&resolution=fixed") + return StatData(count=count, link=link) @stat("Tickets opened") def tickets_opened(username): - return Ticket.objects.filter(reporter=username).count() + count = Ticket.objects.filter(reporter=username).count() + link = get_trac_link(f"reporter=~{username}") + return StatData(count=count, link=link) -@stat("New tickets reviewed") +@stat("New tickets triaged") def new_tickets_reviewed(username): # We don't want to de-dup as for tickets_closed: multiple reviews of the # same ticket should "count" as a review. @@ -60,18 +72,4 @@ def new_tickets_reviewed(username): author=username, field="stage", oldvalue="Unreviewed" ) qs = qs.exclude(newvalue="Unreviewed") - return qs.count() - - -@stat("Patches submitted") -def patches_submitted(username): - return Attachment.objects.filter(author=username).count() - - -def run_single_value_query(query, *params): - """ - Helper: run a query returning a single value (e.g. a COUNT) and return the value. - """ - c = django.db.connections["trac"].cursor() - c.execute(query, params) - return c.fetchone()[0] + return StatData(count=qs.count(), link=None)