diff options
| -rw-r--r-- | asset-sources/theme.scss | 33 | ||||
| -rw-r--r-- | babel.cfg | 1 | ||||
| -rw-r--r-- | fietsboek/locale/de/LC_MESSAGES/messages.mo | bin | 20324 -> 20685 bytes | |||
| -rw-r--r-- | fietsboek/locale/de/LC_MESSAGES/messages.po | 34 | ||||
| -rw-r--r-- | fietsboek/locale/en/LC_MESSAGES/messages.mo | bin | 19139 -> 19490 bytes | |||
| -rw-r--r-- | fietsboek/locale/en/LC_MESSAGES/messages.po | 34 | ||||
| -rw-r--r-- | fietsboek/locale/fietslog.pot | 34 | ||||
| -rw-r--r-- | fietsboek/static/theme.css | 51 | ||||
| -rw-r--r-- | fietsboek/static/theme.css.map | 2 | ||||
| -rw-r--r-- | fietsboek/summaries.py | 175 | ||||
| -rw-r--r-- | fietsboek/templates/profile_overview.jinja2 | 50 | ||||
| -rw-r--r-- | fietsboek/views/profile.py | 5 |
12 files changed, 383 insertions, 36 deletions
diff --git a/asset-sources/theme.scss b/asset-sources/theme.scss index ba05782..2faaffa 100644 --- a/asset-sources/theme.scss +++ b/asset-sources/theme.scss @@ -271,6 +271,39 @@ strong { } } +.contribs-cell { + div { + border: 1px solid rgba(0, 0, 0, 0.175); + border-radius: calc(0.375rem - (1px)); + + width: 1.1em; + height: 1.1em; + } +} + +.contribs-rowhead { + font-size: 90%; +} + +.contribs-length-0 div { + background-color: #FFFFFF; +} +.contribs-length-1 div { + background-color: #8FF0A4; +} +.contribs-length-2 div { + background-color: #75DD95; +} +.contribs-length-3 div { + background-color: #5BC987; +} +.contribs-length-4 div { + background-color: #40B678; +} +.contribs-length-5 div { + background-color: #26A269; +} + .browse-summary { font-variant: lining-nums; } @@ -4,3 +4,4 @@ jinja2 = jinja2.ext:babel_extract [jinja2: **.jinja2] [python: **.py] +keywords = _ pluralize:1,2 diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.mo b/fietsboek/locale/de/LC_MESSAGES/messages.mo Binary files differindex 356be25..25b4a89 100644 --- a/fietsboek/locale/de/LC_MESSAGES/messages.mo +++ b/fietsboek/locale/de/LC_MESSAGES/messages.mo diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.po b/fietsboek/locale/de/LC_MESSAGES/messages.po index 9367d28..bdd53a7 100644 --- a/fietsboek/locale/de/LC_MESSAGES/messages.po +++ b/fietsboek/locale/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-01-03 19:25+0100\n" +"POT-Creation-Date: 2026-08-13 17:40+0200\n" "PO-Revision-Date: 2022-07-02 17:35+0200\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language: de\n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.17.0\n" +"Generated-By: Babel 2.18.0\n" #: fietsboek/actions.py:266 msgid "email.verify_mail.subject" @@ -61,6 +61,12 @@ msgstr "Maximalgeschwindigkeit" msgid "pdf.table.avg_speed" msgstr "Durchschnittsgeschwindigkeit" +#: fietsboek/summaries.py:429 +msgid "page.profile.contribution_card.tooltip" +msgid_plural "page.profile.contribution_card.tooltips" +msgstr[0] "${count} Strecke am ${date}" +msgstr[1] "${count} Strecken am ${date}" + #: fietsboek/util.py:299 msgid "password_constraint.mismatch" msgstr "Passwörter stimmen nicht überein" @@ -1008,27 +1014,39 @@ msgstr "durchschnittliche Geschwindigkeit" msgid "page.profile.number_of_tracks" msgstr "Anzahl der Strecken" -#: fietsboek/templates/profile_overview.jinja2:113 +#: fietsboek/templates/profile_overview.jinja2:112 +msgid "page.profile.contribution_card.title" +msgstr "Streckenaktivität" + +#: fietsboek/templates/profile_overview.jinja2:147 +msgid "page.profile.contribution_card.legend.less" +msgstr "Weniger km" + +#: fietsboek/templates/profile_overview.jinja2:154 +msgid "page.profile.contribution_card.legend.more" +msgstr "Mehr km" + +#: fietsboek/templates/profile_overview.jinja2:161 msgid "page.profile.longest_distance_track" msgstr "Weiteste Strecke" -#: fietsboek/templates/profile_overview.jinja2:118 +#: fietsboek/templates/profile_overview.jinja2:166 msgid "page.profile.shortest_distance_track" msgstr "Kürzeste Strecke" -#: fietsboek/templates/profile_overview.jinja2:123 +#: fietsboek/templates/profile_overview.jinja2:171 msgid "page.profile.longest_duration_track" msgstr "Am Längsten Dauernde Strecke" -#: fietsboek/templates/profile_overview.jinja2:128 +#: fietsboek/templates/profile_overview.jinja2:176 msgid "page.profile.shortest_duration_track" msgstr "Am Kürzesten Dauernde Strecke" -#: fietsboek/templates/profile_overview.jinja2:149 +#: fietsboek/templates/profile_overview.jinja2:197 msgid "page.profile.heatmap" msgstr "Heatmap" -#: fietsboek/templates/profile_overview.jinja2:157 +#: fietsboek/templates/profile_overview.jinja2:205 msgid "page.profile.tilehunt" msgstr "Kacheljäger" diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.mo b/fietsboek/locale/en/LC_MESSAGES/messages.mo Binary files differindex f4b7bbf..b0c8a9c 100644 --- a/fietsboek/locale/en/LC_MESSAGES/messages.mo +++ b/fietsboek/locale/en/LC_MESSAGES/messages.mo diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.po b/fietsboek/locale/en/LC_MESSAGES/messages.po index d52960b..e75d0e0 100644 --- a/fietsboek/locale/en/LC_MESSAGES/messages.po +++ b/fietsboek/locale/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-01-03 19:25+0100\n" +"POT-Creation-Date: 2026-08-13 17:40+0200\n" "PO-Revision-Date: 2023-04-03 20:42+0200\n" "Last-Translator: \n" "Language: en\n" @@ -16,7 +16,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.17.0\n" +"Generated-By: Babel 2.18.0\n" #: fietsboek/actions.py:266 msgid "email.verify_mail.subject" @@ -61,6 +61,12 @@ msgstr "Max Speed" msgid "pdf.table.avg_speed" msgstr "Average Speed" +#: fietsboek/summaries.py:429 +msgid "page.profile.contribution_card.tooltip" +msgid_plural "page.profile.contribution_card.tooltips" +msgstr[0] "${count} track on ${date}" +msgstr[1] "${count} tracks on ${date}" + #: fietsboek/util.py:299 msgid "password_constraint.mismatch" msgstr "Passwords don't match" @@ -1002,27 +1008,39 @@ msgstr "Average Speed" msgid "page.profile.number_of_tracks" msgstr "Number of tracks" -#: fietsboek/templates/profile_overview.jinja2:113 +#: fietsboek/templates/profile_overview.jinja2:112 +msgid "page.profile.contribution_card.title" +msgstr "Track Activity" + +#: fietsboek/templates/profile_overview.jinja2:147 +msgid "page.profile.contribution_card.legend.less" +msgstr "Fewer km" + +#: fietsboek/templates/profile_overview.jinja2:154 +msgid "page.profile.contribution_card.legend.more" +msgstr "More km" + +#: fietsboek/templates/profile_overview.jinja2:161 msgid "page.profile.longest_distance_track" msgstr "Longest Track" -#: fietsboek/templates/profile_overview.jinja2:118 +#: fietsboek/templates/profile_overview.jinja2:166 msgid "page.profile.shortest_distance_track" msgstr "Shortest Track" -#: fietsboek/templates/profile_overview.jinja2:123 +#: fietsboek/templates/profile_overview.jinja2:171 msgid "page.profile.longest_duration_track" msgstr "Most Time-Consuming Track" -#: fietsboek/templates/profile_overview.jinja2:128 +#: fietsboek/templates/profile_overview.jinja2:176 msgid "page.profile.shortest_duration_track" msgstr "Quickest Track" -#: fietsboek/templates/profile_overview.jinja2:149 +#: fietsboek/templates/profile_overview.jinja2:197 msgid "page.profile.heatmap" msgstr "Heat Map" -#: fietsboek/templates/profile_overview.jinja2:157 +#: fietsboek/templates/profile_overview.jinja2:205 msgid "page.profile.tilehunt" msgstr "Tilehunt" diff --git a/fietsboek/locale/fietslog.pot b/fietsboek/locale/fietslog.pot index 50a43a0..53f697f 100644 --- a/fietsboek/locale/fietslog.pot +++ b/fietsboek/locale/fietslog.pot @@ -8,14 +8,14 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-01-03 19:25+0100\n" +"POT-Creation-Date: 2026-08-13 17:40+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.17.0\n" +"Generated-By: Babel 2.18.0\n" #: fietsboek/actions.py:266 msgid "email.verify_mail.subject" @@ -57,6 +57,12 @@ msgstr "" msgid "pdf.table.avg_speed" msgstr "" +#: fietsboek/summaries.py:429 +msgid "page.profile.contribution_card.tooltip" +msgid_plural "page.profile.contribution_card.tooltips" +msgstr[0] "" +msgstr[1] "" + #: fietsboek/util.py:299 msgid "password_constraint.mismatch" msgstr "" @@ -994,27 +1000,39 @@ msgstr "" msgid "page.profile.number_of_tracks" msgstr "" -#: fietsboek/templates/profile_overview.jinja2:113 +#: fietsboek/templates/profile_overview.jinja2:112 +msgid "page.profile.contribution_card.title" +msgstr "" + +#: fietsboek/templates/profile_overview.jinja2:147 +msgid "page.profile.contribution_card.legend.less" +msgstr "" + +#: fietsboek/templates/profile_overview.jinja2:154 +msgid "page.profile.contribution_card.legend.more" +msgstr "" + +#: fietsboek/templates/profile_overview.jinja2:161 msgid "page.profile.longest_distance_track" msgstr "" -#: fietsboek/templates/profile_overview.jinja2:118 +#: fietsboek/templates/profile_overview.jinja2:166 msgid "page.profile.shortest_distance_track" msgstr "" -#: fietsboek/templates/profile_overview.jinja2:123 +#: fietsboek/templates/profile_overview.jinja2:171 msgid "page.profile.longest_duration_track" msgstr "" -#: fietsboek/templates/profile_overview.jinja2:128 +#: fietsboek/templates/profile_overview.jinja2:176 msgid "page.profile.shortest_duration_track" msgstr "" -#: fietsboek/templates/profile_overview.jinja2:149 +#: fietsboek/templates/profile_overview.jinja2:197 msgid "page.profile.heatmap" msgstr "" -#: fietsboek/templates/profile_overview.jinja2:157 +#: fietsboek/templates/profile_overview.jinja2:205 msgid "page.profile.tilehunt" msgstr "" diff --git a/fietsboek/static/theme.css b/fietsboek/static/theme.css index 65e6881..d12a9b7 100644 --- a/fietsboek/static/theme.css +++ b/fietsboek/static/theme.css @@ -80,14 +80,6 @@ strong { flex-wrap: wrap; } -.track-image-preview { - position: relative; - width: min(317px, 100%); - height: 178.3125px; - border: 1px solid grey; - margin-right: 5px; - margin-bottom: 5px; -} .track-image-preview .delete-image { position: absolute; z-index: 5; @@ -120,6 +112,14 @@ strong { left: 50%; transform: translate(-50%, -50%); } +.track-image-preview { + position: relative; + width: min(317px, 100%); + height: 178.3125px; + border: 1px solid grey; + margin-right: 5px; + margin-bottom: 5px; +} .track-description img { max-width: 100%; @@ -259,6 +259,41 @@ strong { color: inherit; } +.contribs-cell div { + border: 1px solid rgba(0, 0, 0, 0.175); + border-radius: calc(0.375rem - 1px); + width: 1.1em; + height: 1.1em; +} + +.contribs-rowhead { + font-size: 90%; +} + +.contribs-length-0 div { + background-color: #FFFFFF; +} + +.contribs-length-1 div { + background-color: #8FF0A4; +} + +.contribs-length-2 div { + background-color: #75DD95; +} + +.contribs-length-3 div { + background-color: #5BC987; +} + +.contribs-length-4 div { + background-color: #40B678; +} + +.contribs-length-5 div { + background-color: #26A269; +} + .browse-summary { font-variant: lining-nums; } diff --git a/fietsboek/static/theme.css.map b/fietsboek/static/theme.css.map index 3ddb9bc..acade8d 100644 --- a/fietsboek/static/theme.css.map +++ b/fietsboek/static/theme.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../asset-sources/theme.scss"],"names":[],"mappings":"AAAA;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EAqCE;EACA;EACA;EACA;EACA;EACA;;AAzCA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAWJ;EACI;;;AAGJ;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;;AAEF;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;EACA;;AACA;EACE;;AAIJ;EACE;EACA;;;AAIJ;EACE;;;AAGF;AACA;EACE;;;AAGF;EACE;EACA;;AAEA;EAJF;IAKI,eACE;;;AAIJ;EACE;;AAGF;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;;AAIJ;EACE;EACA;;;AAIJ;EACE;;;AAGF;AACA;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE","file":"theme.css"}
\ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../../asset-sources/theme.scss"],"names":[],"mappings":"AAAA;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAIA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAlCJ;EAqCE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACI;;;AAGJ;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;;AAEF;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;EACA;;AACA;EACE;;AAIJ;EACE;EACA;;;AAKF;EACE;EACA;EAEA;EACA;;;AAIJ;EACE;;;AAGF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAGF;EACE;;;AAGF;AACA;EACE;;;AAGF;EACE;EACA;;AAEA;EAJF;IAKI,eACE;;;AAIJ;EACE;;AAGF;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;;AAIJ;EACE;EACA;;;AAIJ;EACE;;;AAGF;AACA;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE","file":"theme.css"}
\ No newline at end of file diff --git a/fietsboek/summaries.py b/fietsboek/summaries.py index 0bee6ca..502e2f9 100644 --- a/fietsboek/summaries.py +++ b/fietsboek/summaries.py @@ -2,11 +2,18 @@ import datetime from dataclasses import dataclass -from typing import Optional +from typing import Iterator, Optional + +from babel import Locale +from babel.dates import format_date +from pyramid.i18n import Localizer +from pyramid.request import Request from . import util from .models.track import TrackWithMetadata +ONE_WEEK = datetime.timedelta(days=7) + class Summary: """A summary of a user's tracks. @@ -294,4 +301,168 @@ class CumulativeStats: return util.round_to_seconds((self.moving_time + self.stopped_time) / self.count) -__all__ = ["Summary", "YearSummary", "MonthSummary", "CumulativeStats"] +class YearContributions: + """Github-contributions-tracker-like overview for tracks.""" + + def __init__(self, end_date: datetime.date): + self.end_date = end_date + self.start_date = end_date - 52 * ONE_WEEK + # Make sure the start of the range is on a Monday + self.start_date -= datetime.timedelta(days=end_date.weekday()) + assert (self.start_date - self.end_date).days <= 53 * 7 + + self.tracks: dict[datetime.date, list[TrackWithMetadata]] = {} + + def add(self, track: TrackWithMetadata): + """Adds a track to this overview. + + :param track: The track to add. + """ + if track.date is None: + return + date = track.date.date() + if self.start_date <= date <= self.end_date: + self.tracks.setdefault(date, []).append(track) + + def rows(self) -> Iterator[Iterator[DayContributions]]: + """Returns the rows for the overview table. + + Each row is a list of the cells. Seven rows will be returned, one for each weekday. + + :return: An iterator over the rows to render. + """ + for weekday in range(7): + row = [] + day = self.start_date + datetime.timedelta(days=weekday) + while day <= self.end_date: + row.append(DayContributions(day, self.tracks.get(day, []))) + day += ONE_WEEK + yield iter(row) + + def _rowheads(self, localizer: Localizer) -> Iterator[str | None]: + """Returns the row heads (i.e., weekday names) to be prepended to the rows. + + :param localizer: The localizer to use. + :return: An iterator over the column headers. + """ + loc = Locale.parse(localizer.locale_name) + days = loc.days["stand-alone"]["abbreviated"] + return iter( + [ + days[0], + None, + None, + days[3], + None, + None, + days[6], + ] + ) + + def rows_with_heads( + self, localizer: Localizer + ) -> Iterator[tuple[str | None, Iterator[DayContributions]]]: + """Returns each row with its associated heading (weekday name). + + :param localizer: The localizer to use. + :return: An iterator over (header, cells) pairs. + """ + return zip(self._rowheads(localizer), self.rows()) + + def header_row(self, localizer: Localizer) -> Iterator[str | None]: + """Returns the header row. + + Each string marks a month name (spanning three columns), and a ``None`` is an empty column. + + :param localizer: The localizer to use. + :return: An iterator over the column headers. + """ + locale = localizer.locale_name + day = self.start_date + while day <= self.end_date: + diq = day + datetime.timedelta(days=6) + if diq.day <= 7: + yield format_date(diq, "MMM", locale=locale) + day += 3 * ONE_WEEK + else: + yield None + day += ONE_WEEK + + +class DayContributions: + """A single cell (representing one day) for the YearContributions.""" + + def __init__(self, day: datetime.date, tracks: list[TrackWithMetadata]): + self.day = day + self.tracks = tracks + + def total_length(self) -> float: + """The total length of all tracks on this day, in meters.""" + return sum(track.length for track in self.tracks) + + def count(self) -> int: + """The number of tracks in this day.""" + return len(self.tracks) + + def cell_style(self) -> str: + """Returns the CSS style that should be applied to the cell. + + :return: The CSS class. + """ + length = self.total_length() / 1000 + # Arbitrary cutoffs, should maybe be the same as for the calendar + if length == 0: + return "contribs-length-0" + if length <= 25: + return "contribs-length-1" + if length <= 50: + return "contribs-length-2" + if length <= 75: + return "contribs-length-3" + if length <= 100: + return "contribs-length-4" + return "contribs-length-5" + + def tooltip(self, localizer: Localizer) -> str: + """Generates the tooltip for the cell, localized to the given language. + + :param localizer: The localizer to use. + :return: The tooltip text. + """ + formatted_date = format_date(self.day, locale=localizer.locale_name) + return localizer.pluralize( + "page.profile.contribution_card.tooltip", + "page.profile.contribution_card.tooltips", + self.count(), + mapping={ + "count": self.count(), + "date": formatted_date, + }, + ) + + def browse_url(self, request: Request) -> str: + """Returns a URL to the browse page with filters for this day applied. + + :param request: The pyramid request. + :return: The URL. + """ + datestring = self.day.strftime("%Y-%m-%d") + return request.route_url( + "browse", + _query={ + "type[]": "ORGANIC", + "show-only[]": "user-tagged", + "min-date": datestring, + "max-date": datestring, + }, + ) + + +__all__ = [ + "Summary", + "YearSummary", + "MonthSummary", + "CumulativeStats", + "YearContributions", + "DayContributions", +] diff --git a/fietsboek/templates/profile_overview.jinja2 b/fietsboek/templates/profile_overview.jinja2 index eaa10c6..a088ba5 100644 --- a/fietsboek/templates/profile_overview.jinja2 +++ b/fietsboek/templates/profile_overview.jinja2 @@ -109,6 +109,56 @@ </tr> </table> +<h2>{{ _("page.profile.contribution_card.title") }}</h2> +<div class="d-xl-flex justify-content-center" style="overflow: scroll;"> + <table class="contributions"> + <thead> + <tr> + <th></th> + {% for cell in year_contributions.header_row(request.localizer) %} + {% if cell is none %} + <th></th> + {% else %} + <th colspan="3" class="text-muted">{{ cell }}</th> + {% endif %} + {% endfor %} + </tr> + </thead> + <tbody> + {% for rowhead, row in year_contributions.rows_with_heads(request.localizer) %} + <tr> + <td class="text-muted contribs-rowhead">{{ rowhead | default("", true) }}</td> + {% for cell in row %} + <td class="contribs-cell {{ cell.cell_style() }}"> + <a href="{{ cell.browse_url(request) }}"> + <!-- Use a div because border-radius does not work on td --> + <div data-bs-toggle="tooltip" title="{{ cell.tooltip(request.localizer) }}"></div> + </a> + </td> + {% endfor %} + </tr> + {% endfor %} + </tbody> + </table> +</div> + +<div class="d-flex justify-content-end mb-3"> + <table> + <tbody> + <tr> + <td>{{ _("page.profile.contribution_card.legend.less") }}</td> + <td class="contribs-cell contribs-length-0"><div></div></td> + <td class="contribs-cell contribs-length-1"><div></div></td> + <td class="contribs-cell contribs-length-2"><div></div></td> + <td class="contribs-cell contribs-length-3"><div></div></td> + <td class="contribs-cell contribs-length-4"><div></div></td> + <td class="contribs-cell contribs-length-5"><div></div></td> + <td>{{ _("page.profile.contribution_card.legend.more") }}</td> + </tr> + </tbody> + </table> +</div> + {% if total.longest_distance_track %} <h2>{{ _("page.profile.longest_distance_track") }}</h2> {{ render_track_card(total.longest_distance_track) }} diff --git a/fietsboek/views/profile.py b/fietsboek/views/profile.py index 6d671b9..4f9abca 100644 --- a/fietsboek/views/profile.py +++ b/fietsboek/views/profile.py @@ -16,7 +16,7 @@ from sqlalchemy.orm import aliased from .. import models, util from ..data import UserDataDir from ..models.track import TrackType, TrackWithMetadata -from ..summaries import CumulativeStats, Summary +from ..summaries import CumulativeStats, Summary, YearContributions # A well-made transparent tile is actually pretty small (only 116 bytes), which # is even smaller than our HTTP 404 page. So not only is it more efficient @@ -49,6 +49,7 @@ LOGGER = logging.getLogger(__name__) def profile_data(request: Request) -> dict: """Retrieves the profile data for the given request.""" total = CumulativeStats() + year_contribs = YearContributions(datetime.date.today()) query = request.context.all_tracks_query() query = select(aliased(models.Track, query)).where(query.c.type == TrackType.ORGANIC) @@ -56,6 +57,7 @@ def profile_data(request: Request) -> dict: for track in request.dbsession.execute(query).scalars(): meta = TrackWithMetadata(track) total.add(meta) + year_contribs.add(meta) total.moving_time = util.round_to_seconds(total.moving_time) total.stopped_time = util.round_to_seconds(total.stopped_time) @@ -79,6 +81,7 @@ def profile_data(request: Request) -> dict: "mps_to_kph": util.mps_to_kph, "heatmap_url": heatmap_url, "tilehunt_url": tilehunt_url, + "year_contributions": year_contribs, } |
