Skip to content

Commit 5fc4f99

Browse files
committed
Improve rendering of relative timestamps
This improves a few things about the way we render timestamps based on feedback on the mailinglist: 1. Allow users to configure if they want to see relative timestamps or not 2. Only show one level of precision, so no "1 month, 1 day" 3. Add precision at the "seconds" level 4. Add a special case for "yesterday" Also changes how we render
1 parent d24f858 commit 5fc4f99

File tree

6 files changed

+181
-9
lines changed

6 files changed

+181
-9
lines changed

pgcommitfest/commitfest/templates/commitfest.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ <h3>{{p.is_open|yesno:"Active patches,Closed patches"}}</h3>
6565
{%if not cfb %}
6666
<span class="label label-default">Not processed</span>
6767
{%elif p.needs_rebase_since %}
68-
<a href="{{cfb.apply_url}}" title="View git apply logs. Needs rebase since {{p.needs_rebase_since|timesince}}. {%if p.failing_since and p.failing_since != p.needs_rebase_since %}Failing since {{p.failing_since|timesince}}.{%endif%}">
68+
<a href="{{cfb.apply_url}}" title="View git apply logs. Needs rebase {% cfsince p.needs_rebase_since %}. {%if p.failing_since and p.failing_since != p.needs_rebase_since %}Failing {% cfsince p.failing_since %}.{%endif%}">
6969
<span class="label label-warning">Needs rebase!</span>
7070
</a>
7171
{%else%}
7272
<a href="https://github.com/postgresql-cfbot/postgresql/compare/cf/{{p.id}}~1...cf/{{p.id}}" title="View last patch set on GitHub"><img class="github-logo" src="/media/commitfest/github-mark.svg"/></a>
7373
<a href="https://cirrus-ci.com/github/postgresql-cfbot/postgresql/cf%2F{{p.id}}"
74-
title="View CI history. {%if p.failing_since%}Failing since {{p.failing_since|timesince}}. {%endif%}{%if cfb.failed_task_names %}Failed jobs: {{cfb.failed_task_names}}{%endif%}">
74+
title="View CI history. {%if p.failing_since%}Failing {% cfsince p.failing_since %}. {%endif%}{%if cfb.failed_task_names %}Failed jobs: {{cfb.failed_task_names}}{%endif%}">
7575
{%if cfb.failed > 0 or cfb.branch_status == 'failed' or cfb.branch_status == 'timeout' %}
7676
<img src="/media/commitfest/new_failure.svg"/>
7777
{%elif cfb.completed < cfb.total %}
@@ -95,7 +95,7 @@ <h3>{{p.is_open|yesno:"Active patches,Closed patches"}}</h3>
9595
<td>{{p.reviewer_names|default:''}}</td>
9696
<td>{{p.committer|default:''}}</td>
9797
<td>{{p.num_cfs}}</td>
98-
<td style="white-space: nowrap;" title="{{p.lastmail}}">{%if p.lastmail %}{{p.lastmail|timesince}} ago{%endif%}</td>
98+
<td style="white-space: nowrap;" title="{{p.lastmail}}">{%if p.lastmail and user.userprofile.show_relative_timestamps %}{% cfwhen p.lastmail %}{%elif p.lastmail %}{{p.lastmail|date:"Y-m-d"}}<br/>{{p.lastmail|date:"H:i"}}{%endif%}</td>
9999
{%if user.is_staff%}
100100
<td style="white-space: nowrap;"><input type="checkbox" class="sender_checkbox" id="send_authors_{{p.id}}">Author<br/><input type="checkbox" class="sender_checkbox" id="send_reviewers_{{p.id}}">Reviewer</td>
101101
{%endif%}

pgcommitfest/commitfest/templates/me.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@ <h3>{%if user.is_authenticated%}Open patches you are subscribed to{%elif p.is_op
5959
{%if not cfb %}
6060
<span class="label label-default">Not processed</span>
6161
{%elif p.needs_rebase_since %}
62-
<a href="{{cfb.apply_url}}" title="View git apply logs. Needs rebase since {{p.needs_rebase_since|timesince}}. {%if p.failing_since and p.failing_since != p.needs_rebase_since %}Failing since {{p.failing_since|timesince}}.{%endif%}">
62+
<a href="{{cfb.apply_url}}" title="View git apply logs. Needs rebase {% cfsince p.needs_rebase_since %}. {%if p.failing_since and p.failing_since != p.needs_rebase_since %}Failing {% cfsince p.failing_since %}.{%endif%}">
6363
<span class="label label-warning">Needs rebase!</span>
6464
</a>
6565
{%else%}
6666
<a href="https://github.com/postgresql-cfbot/postgresql/compare/cf/{{p.id}}~1...cf/{{p.id}}" title="View last patch set on GitHub"><img class="github-logo" src="/media/commitfest/github-mark.svg"/></a>
6767
<a href="https://cirrus-ci.com/github/postgresql-cfbot/postgresql/cf%2F{{p.id}}"
68-
title="View CI history. {%if p.failing_since%}Failing since {{p.failing_since|timesince}}. {%endif%}{%if cfb.failed_task_names %}Failed jobs: {{cfb.failed_task_names}}{%endif%}">
68+
title="View CI history. {%if p.failing_since%}Failing {% cfsince p.failing_since %}. {%endif%}{%if cfb.failed_task_names %}Failed jobs: {{cfb.failed_task_names}}{%endif%}">
6969
{%if cfb.failed > 0 or cfb.branch_status == 'failed' or cfb.branch_status == 'timeout' %}
7070
<img src="/media/commitfest/new_failure.svg"/>
7171
{%elif cfb.completed < cfb.total %}
@@ -89,7 +89,7 @@ <h3>{%if user.is_authenticated%}Open patches you are subscribed to{%elif p.is_op
8989
<td>{{p.reviewer_names|default:''}}</td>
9090
<td>{{p.committer|default:''}}</td>
9191
<td>{{p.num_cfs}}</td>
92-
<td style="white-space: nowrap;" title="{{p.lastmail}}">{%if p.lastmail %}{{p.lastmail|timesince}} ago{%endif%}</td>
92+
<td style="white-space: nowrap;" title="{{p.lastmail}}">{%if p.lastmail and user.userprofile.show_relative_timestamps %}{% cfwhen p.lastmail %}{%elif p.lastmail %}{{p.lastmail|date:"Y-m-d"}}<br/>{{p.lastmail|date:"H:i"}}{%endif%}</td>
9393
</tr>
9494
{%if forloop.last%}
9595
</tbody>

pgcommitfest/commitfest/templates/patch.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
{%elif cfbot_branch.needs_rebase_since %}
2121
<a href="{{cfbot_branch.apply_url}}">
2222
<span class="label label-warning" title="View git apply logs">Needs rebase!</span></a>
23-
Needs rebase since {{cfbot_branch.needs_rebase_since|timesince}}. {%if cfbot_branch.failing_since and cfbot_branch.failing_since != cfbot_branch.needs_rebase_since %}Failing since {{cfbot_branch.failing_since|timesince}}. {%endif%}<br>Additional links previous successfully applied patch (outdated):<br>
23+
Needs rebase {% cfsince cfbot_branch.needs_rebase_since %}. {%if cfbot_branch.failing_since and cfbot_branch.failing_since != cfbot_branch.needs_rebase_since %}Failing {% cfsince cfbot_branch.failing_since %}. {%endif%}<br>Additional links previous successfully applied patch (outdated):<br>
2424
<a href="https://github.com/postgresql-cfbot/postgresql/compare/cf/{{patch.id}}~1...cf/{{patch.id}}" title="View previous successfully applied patch set on GitHub"><img class="github-logo" src="/media/commitfest/github-mark.svg"/></a>
2525
<a href="https://cirrus-ci.com/github/postgresql-cfbot/postgresql/cf%2F{{patch.id}}">
2626
<span class="label label-default">Summary</span></a>
@@ -73,11 +73,11 @@
7373
</tr>
7474
<tr>
7575
<th style="white-space: nowrap;">Last modified</th>
76-
<td>{{patch.modified}} ({{patch.modified|timesince}} ago)</td>
76+
<td>{{patch.modified}} ({% cfwhen patch.modified %})</td>
7777
</tr>
7878
<tr>
7979
<th style="white-space: nowrap;">Latest email</th>
80-
<td>{%if patch.lastmail%}{{patch.lastmail}} ({{patch.lastmail|timesince}} ago){%endif%}</td>
80+
<td>{%if patch.lastmail%}{{patch.lastmail}} ({% cfwhen patch.lastmail %}){%endif%}</td>
8181
</tr>
8282
<tr>
8383
<th>Status</th>

pgcommitfest/commitfest/templatetags/commitfest.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
from django import template
22
from django.template.defaultfilters import stringfilter
3+
from django.utils.html import avoid_wrapping
4+
from django.utils.timesince import MONTHS_DAYS
5+
from django.utils.timezone import is_aware
6+
from django.utils.translation import ngettext_lazy
37

8+
import datetime
49
from uuid import uuid4
510

611
from pgcommitfest.commitfest.models import CommitFest, PatchOnCommitFest
@@ -75,3 +80,141 @@ def static_file_param():
7580
@stringfilter
7681
def hidemail(value):
7782
return value.replace("@", " at ")
83+
84+
85+
TIME_STRINGS = {
86+
"year": ngettext_lazy("%(num)d year", "%(num)d years", "num"),
87+
"month": ngettext_lazy("%(num)d month", "%(num)d months", "num"),
88+
"week": ngettext_lazy("%(num)d week", "%(num)d weeks", "num"),
89+
"day": ngettext_lazy("%(num)d day", "%(num)d days", "num"),
90+
"hour": ngettext_lazy("%(num)d hour", "%(num)d hours", "num"),
91+
"minute": ngettext_lazy("%(num)d minute", "%(num)d minutes", "num"),
92+
"second": ngettext_lazy("%(num)d second", "%(num)d seconds", "num"),
93+
}
94+
95+
TIME_STRINGS_KEYS = list(TIME_STRINGS.keys())
96+
97+
TIME_CHUNKS = [
98+
60 * 60 * 24 * 7, # week
99+
60 * 60 * 24, # day
100+
60 * 60, # hour
101+
60, # minute
102+
1, # second
103+
]
104+
105+
106+
@register.simple_tag(takes_context=True)
107+
def cfsince(context, d):
108+
if (
109+
context["user"].is_authenticated
110+
and not context["user"].userprofile.show_relative_timestamps
111+
):
112+
return f"since {d}"
113+
partials = cf_duration_partials(d)
114+
if partials is None:
115+
return "since some time in the future"
116+
117+
# Find the first non-zero part (if any) and then build the result, until
118+
# depth.
119+
i = 0
120+
for i, value in enumerate(partials):
121+
if value != 0:
122+
break
123+
else:
124+
return "since now"
125+
126+
value = partials[i]
127+
name = TIME_STRINGS_KEYS[i]
128+
if name == "day" and value == 1:
129+
return avoid_wrapping("since yesterday")
130+
return avoid_wrapping("since " + TIME_STRINGS[name] % {"num": value})
131+
132+
133+
@register.simple_tag()
134+
def cfwhen(d):
135+
partials = cf_duration_partials(d)
136+
if partials is None:
137+
return "some time in the future"
138+
139+
# Find the first non-zero part (if any) and then build the result, until
140+
# depth.
141+
i = 0
142+
for i, value in enumerate(partials):
143+
if value != 0:
144+
break
145+
else:
146+
return "now"
147+
148+
value = partials[i]
149+
name = TIME_STRINGS_KEYS[i]
150+
151+
if name == "day" and value == 1:
152+
return avoid_wrapping("yesterday")
153+
154+
return avoid_wrapping(TIME_STRINGS[name] % {"num": value} + " ago")
155+
156+
157+
def cf_duration_partials(d):
158+
"""
159+
Take two datetime objects and return the time between d and now as a nicely
160+
formatted string, e.g. "10 minutes". If d occurs after now, return
161+
"0 minutes".
162+
163+
Units used are years, months, weeks, days, hours, and minutes.
164+
Seconds and microseconds are ignored.
165+
166+
The algorithm takes into account the varying duration of years and months.
167+
There is exactly "1 year, 1 month" between 2013/02/10 and 2014/03/10,
168+
but also between 2007/08/10 and 2008/09/10 despite the delta being 393 days
169+
in the former case and 397 in the latter.
170+
171+
Adapted from Django's timesince function.
172+
"""
173+
# Convert datetime.date to datetime.datetime for comparison.
174+
if not isinstance(d, datetime.datetime):
175+
d = datetime.datetime(d.year, d.month, d.day)
176+
177+
now = datetime.datetime.now(d.tzinfo if is_aware(d) else None)
178+
179+
delta = now - d
180+
181+
# Ignore microseconds.
182+
since = delta.days * 24 * 60 * 60 + delta.seconds
183+
if since <= 0:
184+
# d is in the future compared to now, stop processing.
185+
return "in the future"
186+
187+
# Get years and months.
188+
total_months = (now.year - d.year) * 12 + (now.month - d.month)
189+
if d.day > now.day or (d.day == now.day and d.time() > now.time()):
190+
total_months -= 1
191+
years, months = divmod(total_months, 12)
192+
193+
# Calculate the remaining time.
194+
# Create a "pivot" datetime shifted from d by years and months, then use
195+
# that to determine the other parts.
196+
if years or months:
197+
pivot_year = d.year + years
198+
pivot_month = d.month + months
199+
if pivot_month > 12:
200+
pivot_month -= 12
201+
pivot_year += 1
202+
pivot = datetime.datetime(
203+
pivot_year,
204+
pivot_month,
205+
min(MONTHS_DAYS[pivot_month - 1], d.day),
206+
d.hour,
207+
d.minute,
208+
d.second,
209+
tzinfo=d.tzinfo,
210+
)
211+
else:
212+
pivot = d
213+
remaining_time = (now - pivot).total_seconds()
214+
partials = [years, months]
215+
for chunk in TIME_CHUNKS:
216+
count = int(remaining_time // chunk)
217+
partials.append(count)
218+
remaining_time -= chunk * count
219+
220+
return partials
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 4.2.19 on 2025-03-04 22:15
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
10+
("userprofile", "0003_emails_managed_upstream"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="userprofile",
16+
name="show_relative_timestamps",
17+
field=models.BooleanField(
18+
default=True,
19+
verbose_name="Show relative timestamps throughout the site",
20+
),
21+
),
22+
]

pgcommitfest/userprofile/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,12 @@ class UserProfile(models.Model):
5252
verbose_name="Notify on all where committer",
5353
)
5454

55+
show_relative_timestamps = models.BooleanField(
56+
null=False,
57+
blank=False,
58+
default=True,
59+
verbose_name="Show relative timestamps throughout the site",
60+
)
61+
5562
def __str__(self):
5663
return str(self.user)

0 commit comments

Comments
 (0)