Skip to content

Commit

Permalink
Added links for user-profile stats.
Browse files Browse the repository at this point in the history
  • Loading branch information
sarahboyce committed Nov 16, 2024
1 parent 2d05221 commit d5e98ed
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 45 deletions.
129 changes: 116 additions & 13 deletions accounts/tests.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,126 @@
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


@override_settings(TRAC_URL="https://code.djangoproject.com/")
class UserProfileTests(TracDBCreateDatabaseMixin, TestCase):
databases = {"default", "trac"}

@classmethod
def setUpTestData(cls):
User.objects.create_user(username="user1", password="password")
User.objects.create_user(username="user2", password="password")
cls.user1_url = reverse("user_profile", args=["user1"])
cls.user2_url = reverse("user_profile", args=["user2"])

def test_username(self):
user1_response = self.client.get(self.user1_url)
user2_response = self.client.get(self.user2_url)
self.assertContains(user1_response, "user1")
self.assertContains(user2_response, "user2")

def test_stat_commits(self):
revision_data = {
"rev": "91c879eda595c12477bbfa6f51115e88b75ddf88",
"_time": 1731669560,
}
Revision.objects.create(author="user1", **revision_data)
Revision.objects.create(author="user1", **revision_data)
Revision.objects.create(author="user2", **revision_data)

class ViewTests(TestCase):
def setUp(self):
self.credentials = {"username": "a-user", "password": "password"}
self.user = User.objects.create_user(**self.credentials)
user1_response = self.client.get(self.user1_url)
user2_response = self.client.get(self.user2_url)
self.assertContains(
user1_response,
'<a href="https://github.com/django/django/commits/main/'
'?author=user1">Commits: 2.</a>',
html=True,
)
self.assertContains(
user2_response,
'<a href="https://github.com/django/django/commits/main/'
'?author=user2">Commits: 1.</a>',
html=True,
)

@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)
def test_stat_tickets(self):
Ticket.objects.create(status="new", reporter="user1")
Ticket.objects.create(status="new", reporter="user2")
Ticket.objects.create(
status="closed", reporter="user1", owner="user1", resolution="fixed"
)
Ticket.objects.create(
status="closed", reporter="user2", owner="user1", resolution="fixed"
)
Ticket.objects.create(
status="closed", reporter="user2", owner="user2", resolution="fixed"
)
Ticket.objects.create(
status="closed", reporter="user2", owner="user1", resolution="wontfix"
)

user1_response = self.client.get(self.user1_url)
user2_response = self.client.get(self.user2_url)
self.assertContains(
user1_response,
'<a href="https://code.djangoproject.com/query?'
'owner=user1&resolution=fixed&desc=1&order=changetime">'
"Tickets fixed: 2.</a>",
html=True,
)
self.assertContains(
user2_response,
'<a href="https://code.djangoproject.com/query?'
'owner=user2&resolution=fixed&desc=1&order=changetime">'
"Tickets fixed: 1.</a>",
html=True,
)
self.assertContains(
user1_response,
'<a href="https://code.djangoproject.com/query?'
'reporter=user1&desc=1&order=changetime">'
"Tickets opened: 2.</a>",
html=True,
)
self.assertContains(
user2_response,
'<a href="https://code.djangoproject.com/query?'
'reporter=user2&desc=1&order=changetime">'
"Tickets opened: 4.</a>",
html=True,
)

def test_stat_tickets_triaged(self):
ticket_1 = Ticket.objects.create()
ticket_2 = Ticket.objects.create()
ticket_3 = Ticket.objects.create()
accepts_ticket = {
"field": "stage",
"oldvalue": "Unreviewed",
"newvalue": "Accepted",
"_time": 1731669560,
}
TicketChange.objects.create(author="user1", ticket=ticket_1, **accepts_ticket)
TicketChange.objects.create(author="user2", ticket=ticket_2, **accepts_ticket)
TicketChange.objects.create(author="user2", ticket=ticket_3, _time=1731669560)

user1_response = self.client.get(self.user1_url)
user2_response = self.client.get(self.user2_url)
self.assertContains(user1_response, "New tickets triaged: 1.")
self.assertContains(user2_response, "New tickets triaged: 1.")


class ViewsTests(TestCase):

def test_login_redirect(self):
response = self.client.post(reverse("login"), self.credentials)
credentials = {"username": "a-user", "password": "password"}
User.objects.create_user(**credentials)

response = self.client.post(reverse("login"), credentials)
self.assertRedirects(response, "/accounts/edit/")

def test_profile_view_reversal(self):
Expand Down
4 changes: 2 additions & 2 deletions dashboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import datetime

import requests
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import connections, models
from django.utils.translation import gettext_lazy as _
from django_hosts.resolvers import reverse

from tracdb.models import Ticket
from tracdb.stats import get_trac_link

METRIC_PERIOD_INSTANT = "instant"
METRIC_PERIOD_DAILY = "daily"
Expand Down Expand Up @@ -130,7 +130,7 @@ def fetch(self):
return queryset.count()

def link(self):
return f"{settings.TRAC_URL}query?{self.query}&desc=1&order=changetime"
return get_trac_link(self.query)


class GithubItemCountMetric(Metric):
Expand Down
8 changes: 6 additions & 2 deletions djangoproject/templates/accounts/user_profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,14 @@ <h1>
</h1>

{% if stats %}
<h2>{% translate "Lies, damned lies, and statistics:" %}</h2>
<h2>{% translate "Statistics on Django core contributions:" %}</h2>
<ul>
{% for stat, value in stats.items %}
<li>{{ stat }}: {{ value|intcomma }}.</li>
<li>
{% if value.link %}<a href="{{ value.link }}">{{ stat }}: {{ value.count|intcomma }}.</a>
{% else %}{{ stat }}: {{ value.count|intcomma }}.
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
Expand Down
56 changes: 28 additions & 28 deletions tracdb/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@
"""

import operator
from collections import OrderedDict
from collections import OrderedDict, namedtuple

import django.db
from django.conf 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"
The function should take a username and return a number.
The function should take a username and return a StatData object.
"""

def _inner(f):
Expand All @@ -36,42 +43,35 @@ 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):
query = f"owner={username}&resolution=fixed"
count = Ticket.objects.from_querystring(query).count()
link = get_trac_link(query)
return StatData(count=count, link=link)


@stat("Tickets opened")
def tickets_opened(username):
return Ticket.objects.filter(reporter=username).count()
query = f"reporter={username}"
count = Ticket.objects.from_querystring(query).count()
link = get_trac_link(query)
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.
qs = TicketChange.objects.filter(
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)

0 comments on commit d5e98ed

Please sign in to comment.