|
1 | 1 | from django import template |
2 | 2 | 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 |
3 | 7 |
|
| 8 | +import datetime |
4 | 9 | from uuid import uuid4 |
5 | 10 |
|
6 | 11 | from pgcommitfest.commitfest.models import CommitFest, PatchOnCommitFest |
@@ -75,3 +80,141 @@ def static_file_param(): |
75 | 80 | @stringfilter |
76 | 81 | def hidemail(value): |
77 | 82 | 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 |
0 commit comments