aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--asset-sources/theme.scss20
-rw-r--r--fietsboek/data.py27
-rw-r--r--fietsboek/locale/de/LC_MESSAGES/messages.mobin16196 -> 17659 bytes
-rw-r--r--fietsboek/locale/de/LC_MESSAGES/messages.po202
-rw-r--r--fietsboek/locale/en/LC_MESSAGES/messages.mobin15148 -> 16586 bytes
-rw-r--r--fietsboek/locale/en/LC_MESSAGES/messages.po202
-rw-r--r--fietsboek/locale/fietslog.pot202
-rw-r--r--fietsboek/routes.py9
-rw-r--r--fietsboek/scripts/fietscron.py3
-rw-r--r--fietsboek/static/DeadEnd.svg44
-rw-r--r--fietsboek/static/NoEntry.svg37
-rw-r--r--fietsboek/static/theme.css20
-rw-r--r--fietsboek/static/theme.css.map2
-rw-r--r--fietsboek/templates/403.jinja216
-rw-r--r--fietsboek/templates/404.jinja214
-rw-r--r--fietsboek/templates/admin.jinja249
-rw-r--r--fietsboek/templates/admin_badges.jinja245
-rw-r--r--fietsboek/templates/admin_overview.jinja291
-rw-r--r--fietsboek/util.py14
-rw-r--r--fietsboek/views/admin.py123
-rw-r--r--fietsboek/views/errors.py32
-rw-r--r--fietsboek/views/notfound.py19
-rw-r--r--tests/playwright/test_profiles.py2
-rw-r--r--tests/playwright/test_share.py3
24 files changed, 930 insertions, 246 deletions
diff --git a/asset-sources/theme.scss b/asset-sources/theme.scss
index cfe8adb..ba05782 100644
--- a/asset-sources/theme.scss
+++ b/asset-sources/theme.scss
@@ -317,6 +317,26 @@ strong {
text-align: center;
}
+/* Admin view layout: We have an extra sidebar for the navigation */
+#adminContainer {
+ display: grid;
+ grid-template-areas: "sidebar main";
+ grid-template-columns: 1fr 5fr;
+ gap: 1rem;
+}
+
+#adminNavigation {
+ grid-area: sidebar;
+}
+
+#adminContent {
+ grid-area: main;
+}
+
+.admin-stat {
+ font-size: 120%;
+}
+
.list-group.list-group-root {
padding: 0;
overflow: hidden;
diff --git a/fietsboek/data.py b/fietsboek/data.py
index d1268bf..9d5a133 100644
--- a/fietsboek/data.py
+++ b/fietsboek/data.py
@@ -9,7 +9,6 @@ the database itself. This module makes access to such data objects easier.
# pylint: disable=deprecated-argument
import datetime
import logging
-import os
import random
import shutil
import string
@@ -130,6 +129,27 @@ class DataManager:
raise FileNotFoundError(f"The path {path} is not a directory") from None
return UserDataDir(user_id, path)
+ def size(self) -> int:
+ """Returns the size of all data.
+
+ :return: The size of all data in bytes.
+ """
+ return util.recursive_size(self.data_dir)
+
+ def list_tracks(self) -> list[int]:
+ """Returns a list of all tracks.
+
+ :return: A list of all track IDs.
+ """
+ return [int(track.name) for track in self._track_data_dir(".").iterdir()]
+
+ def list_users(self) -> list[int]:
+ """Returns a list of all users.
+
+ :return: A list of all user IDs.
+ """
+ return [int(user.name) for user in self._user_data_dir(".").iterdir()]
+
class TrackDataDir:
"""Manager for a single track's data.
@@ -261,10 +281,7 @@ class TrackDataDir:
:return: The size of bytes that this track consumes.
"""
- size = 0
- for root, _, files in os.walk(self.path):
- size += sum(os.path.getsize(os.path.join(root, fname)) for fname in files)
- return size
+ return util.recursive_size(self.path)
def gpx_path(self) -> Path:
"""Returns the path of the GPX file.
diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.mo b/fietsboek/locale/de/LC_MESSAGES/messages.mo
index 9de152c..f2d0f60 100644
--- a/fietsboek/locale/de/LC_MESSAGES/messages.mo
+++ b/fietsboek/locale/de/LC_MESSAGES/messages.mo
Binary files differ
diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.po b/fietsboek/locale/de/LC_MESSAGES/messages.po
index ccb46a3..3319052 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: 2025-01-30 21:50+0100\n"
+"POT-Creation-Date: 2025-05-06 21:46+0200\n"
"PO-Revision-Date: 2022-07-02 17:35+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n"
@@ -29,11 +29,11 @@ msgstr ""
"\n"
"Falls Du kein Konto angelegt hast, ignoriere diese E-Mail."
-#: fietsboek/util.py:333
+#: fietsboek/util.py:334
msgid "password_constraint.mismatch"
msgstr "Passwörter stimmen nicht überein"
-#: fietsboek/util.py:335
+#: fietsboek/util.py:336
msgid "password_constraint.length"
msgstr "Passwort zu kurz"
@@ -69,34 +69,126 @@ msgstr "Maximalgeschwindigkeit"
msgid "tooltip.table.avg_speed"
msgstr "Durchschnittsgeschwindigkeit"
+#: fietsboek/templates/403.jinja2:5
+msgid "403.title"
+msgstr "Zugang verboten"
+
+#: fietsboek/templates/403.jinja2:9
+msgid "403.no_access"
+msgstr "Du hast keinen Zugang zu dieser Ressource."
+
+#: fietsboek/templates/403.jinja2:12
+msgid "403.try_log_in"
+msgstr "Falls Du Zugang haben solltest, stelle sicher, dass du korrekt angemeldet bist."
+
+#: fietsboek/templates/404.jinja2:5
+msgid "404.title"
+msgstr "Sackgasse"
+
+#: fietsboek/templates/404.jinja2:9
+msgid "404.path_not_found"
+msgstr "Der gesuchte Weg wurde nicht gefunden."
+
+#: fietsboek/templates/404.jinja2:12
+msgid "404.choose_different"
+msgstr "Bitte such einen anderen Weg."
+
#: fietsboek/templates/admin.jinja2:5
msgid "page.admin.title"
msgstr "Administration"
-#: fietsboek/templates/admin.jinja2:7
+#: fietsboek/templates/admin.jinja2:10
+msgid "page.admin.nav.overview"
+msgstr "Übersicht"
+
+#: fietsboek/templates/admin.jinja2:11
+msgid "page.admin.nav.badges"
+msgstr "Wappen"
+
+#: fietsboek/templates/admin_badges.jinja2:5
msgid "page.admin.badges"
msgstr "Wappen"
-#: fietsboek/templates/admin.jinja2:23
+#: fietsboek/templates/admin_badges.jinja2:21
msgid "page.admin.badge.edit"
msgstr "Bearbeiten"
-#: fietsboek/templates/admin.jinja2:29
+#: fietsboek/templates/admin_badges.jinja2:27
msgid "page.admin.badge.delete_badge"
msgstr "Löschen"
-#: fietsboek/templates/admin.jinja2:37
+#: fietsboek/templates/admin_badges.jinja2:35
msgid "page.admin.badges.badge_title"
msgstr "Titel"
-#: fietsboek/templates/admin.jinja2:41
+#: fietsboek/templates/admin_badges.jinja2:39
msgid "page.admin.badges.badge_image"
msgstr "Bild"
-#: fietsboek/templates/admin.jinja2:45
+#: fietsboek/templates/admin_badges.jinja2:43
msgid "page.admin.badges.add_badge"
msgstr "Hinzufügen"
+#: fietsboek/templates/admin_overview.jinja2:5
+msgid "admin.overview.instance_has"
+msgstr "Diese Instanz hat"
+
+#: fietsboek/templates/admin_overview.jinja2:9
+msgid "admin.overview.stat.user"
+msgid_plural "admin.overview.stat.users"
+msgstr[0] "%(num)d Nutzer:in"
+msgstr[1] "%(num)d Nutzer:innen"
+
+#: fietsboek/templates/admin_overview.jinja2:13
+msgid "admin.overview.stat.track"
+msgid_plural "admin.overview.stat.tracks"
+msgstr[0] "%(num)d Strecke"
+msgstr[1] "%(num)d Strecken"
+
+#: fietsboek/templates/admin_overview.jinja2:17
+msgid "admin.overview.stats.mib"
+msgstr "MiB an Daten"
+
+#: fietsboek/templates/admin_overview.jinja2:24
+msgid "admin.overview.system_overview"
+msgstr "Systemübersicht"
+
+#: fietsboek/templates/admin_overview.jinja2:28
+msgid "admin.overview.fietsboek_version"
+msgstr "Fietsboek-Version"
+
+#: fietsboek/templates/admin_overview.jinja2:32
+msgid "admin.overview.python_version"
+msgstr "Python-Version"
+
+#: fietsboek/templates/admin_overview.jinja2:36
+msgid "admin.overview.kernel_version"
+msgstr "Kernel-Version"
+
+#: fietsboek/templates/admin_overview.jinja2:40
+msgid "admin.overview.distro_version"
+msgstr "Distribution"
+
+#: fietsboek/templates/admin_overview.jinja2:44
+msgid "admin.overview.last_cronjob"
+msgstr "Letzter Cronjob"
+
+#: fietsboek/templates/admin_overview.jinja2:55
+msgid "admin.overview.storage_graph.label.gpx"
+msgstr "GPX"
+
+#: fietsboek/templates/admin_overview.jinja2:56
+msgid "admin.overview.storage_graph.label.images"
+msgstr "Bilder"
+
+#: fietsboek/templates/admin_overview.jinja2:57
+msgid "admin.overview.storage_graph.label.user_maps"
+msgstr "Nutzerkarten"
+
+#: fietsboek/templates/admin_overview.jinja2:82
+msgid "admin.overview.storage_graph.title"
+msgstr "Speicherübersicht"
+
#: fietsboek/templates/browse.jinja2:4
msgid "page.browse.title"
msgstr "Stöbern"
@@ -194,52 +286,52 @@ msgid "page.browse.synthetic_tooltip"
msgstr "Dies ist eine geplante Strecke"
#: fietsboek/templates/browse.jinja2:158 fietsboek/templates/details.jinja2:103
-#: fietsboek/templates/profile.jinja2:15
+#: fietsboek/templates/profile_overview.jinja2:16
msgid "page.details.date"
msgstr "Datum"
#: fietsboek/templates/browse.jinja2:160 fietsboek/templates/details.jinja2:117
-#: fietsboek/templates/profile.jinja2:17
+#: fietsboek/templates/profile_overview.jinja2:18
msgid "page.details.length"
msgstr "Länge"
#: fietsboek/templates/browse.jinja2:165 fietsboek/templates/details.jinja2:108
-#: fietsboek/templates/profile.jinja2:21
+#: fietsboek/templates/profile_overview.jinja2:22
msgid "page.details.start_time"
msgstr "Startzeit"
#: fietsboek/templates/browse.jinja2:167 fietsboek/templates/details.jinja2:112
-#: fietsboek/templates/profile.jinja2:23
+#: fietsboek/templates/profile_overview.jinja2:24
msgid "page.details.end_time"
msgstr "Endzeit"
#: fietsboek/templates/browse.jinja2:172 fietsboek/templates/details.jinja2:121
-#: fietsboek/templates/profile.jinja2:27
+#: fietsboek/templates/profile_overview.jinja2:28
msgid "page.details.uphill"
msgstr "Bergauf"
#: fietsboek/templates/browse.jinja2:174 fietsboek/templates/details.jinja2:125
-#: fietsboek/templates/profile.jinja2:29
+#: fietsboek/templates/profile_overview.jinja2:30
msgid "page.details.downhill"
msgstr "Bergab"
#: fietsboek/templates/browse.jinja2:179 fietsboek/templates/details.jinja2:130
-#: fietsboek/templates/profile.jinja2:33
+#: fietsboek/templates/profile_overview.jinja2:34
msgid "page.details.moving_time"
msgstr "Fahrzeit"
#: fietsboek/templates/browse.jinja2:181 fietsboek/templates/details.jinja2:134
-#: fietsboek/templates/profile.jinja2:35
+#: fietsboek/templates/profile_overview.jinja2:36
msgid "page.details.stopped_time"
msgstr "Haltezeit"
#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:138
-#: fietsboek/templates/profile.jinja2:39
+#: fietsboek/templates/profile_overview.jinja2:40
msgid "page.details.max_speed"
msgstr "maximale Geschwindigkeit"
#: fietsboek/templates/browse.jinja2:187 fietsboek/templates/details.jinja2:142
-#: fietsboek/templates/profile.jinja2:41
+#: fietsboek/templates/profile_overview.jinja2:42
msgid "page.details.avg_speed"
msgstr "durchschnittliche Geschwindigkeit"
@@ -544,13 +636,13 @@ msgstr ""
"Links, um sie fortzusetzen:"
#: fietsboek/templates/home.jinja2:44 fietsboek/templates/home.jinja2:53
-#: fietsboek/templates/home.jinja2:97
+#: fietsboek/templates/home.jinja2:99
msgid "page.home.summary.track"
msgid_plural "page.home.summary.tracks"
msgstr[0] "%(num)d Strecke"
msgstr[1] "%(num)d Strecken"
-#: fietsboek/templates/home.jinja2:97
+#: fietsboek/templates/home.jinja2:99
msgid "page.home.total"
msgstr "Gesamt"
@@ -654,91 +746,91 @@ msgstr "Passwörter stimmen nicht überein"
msgid "page.password_reset.reset"
msgstr "Zurücksetzen"
-#: fietsboek/templates/profile.jinja2:64
+#: fietsboek/templates/profile.jinja2:10
msgid "page.profile.tabbar.overview"
msgstr "Übersicht"
-#: fietsboek/templates/profile.jinja2:69
+#: fietsboek/templates/profile.jinja2:15
msgid "page.profile.tabbar.graphs"
msgstr "Diagramme"
-#: fietsboek/templates/profile.jinja2:74
+#: fietsboek/templates/profile.jinja2:20
msgid "page.profile.tabbar.calendar"
msgstr "Kalender"
-#: fietsboek/templates/profile.jinja2:88
+#: fietsboek/templates/profile_calendar.jinja2:9
+msgid "page.profile.calendar.previous"
+msgstr "Vorheriger Monat"
+
+#: fietsboek/templates/profile_calendar.jinja2:11
+msgid "page.profile.calendar.next"
+msgstr "Nächster Monat"
+
+#: fietsboek/templates/profile_graphs.jinja2:6
+msgid "page.profile.graph.km_per_month"
+msgstr "Kilometer pro Monat"
+
+#: fietsboek/templates/profile_overview.jinja2:66
msgid "page.profile.length"
msgstr "Länge"
-#: fietsboek/templates/profile.jinja2:92
+#: fietsboek/templates/profile_overview.jinja2:70
msgid "page.profile.avg_length"
msgstr "durchschnittliche Länge"
-#: fietsboek/templates/profile.jinja2:96
+#: fietsboek/templates/profile_overview.jinja2:74
msgid "page.profile.uphill"
msgstr "Bergauf"
-#: fietsboek/templates/profile.jinja2:100
+#: fietsboek/templates/profile_overview.jinja2:78
msgid "page.profile.downhill"
msgstr "Bergab"
-#: fietsboek/templates/profile.jinja2:104
+#: fietsboek/templates/profile_overview.jinja2:82
msgid "page.profile.moving_time"
msgstr "Fahrzeit"
-#: fietsboek/templates/profile.jinja2:108
+#: fietsboek/templates/profile_overview.jinja2:86
msgid "page.profile.stopped_time"
msgstr "Haltezeit"
-#: fietsboek/templates/profile.jinja2:112
+#: fietsboek/templates/profile_overview.jinja2:90
msgid "page.profile.avg_duration"
msgstr "durchschnittliche Dauer"
-#: fietsboek/templates/profile.jinja2:116
+#: fietsboek/templates/profile_overview.jinja2:94
msgid "page.profile.max_speed"
msgstr "maximale Geschwindigkeit"
-#: fietsboek/templates/profile.jinja2:120
+#: fietsboek/templates/profile_overview.jinja2:98
msgid "page.profile.avg_speed"
msgstr "durchschnittliche Geschwindigkeit"
-#: fietsboek/templates/profile.jinja2:124
+#: fietsboek/templates/profile_overview.jinja2:102
msgid "page.profile.number_of_tracks"
msgstr "Anzahl der Strecken"
-#: fietsboek/templates/profile.jinja2:130
+#: fietsboek/templates/profile_overview.jinja2:108
msgid "page.profile.longest_distance_track"
msgstr "Weiteste Strecke"
-#: fietsboek/templates/profile.jinja2:135
+#: fietsboek/templates/profile_overview.jinja2:113
msgid "page.profile.shortest_distance_track"
msgstr "Kürzeste Strecke"
-#: fietsboek/templates/profile.jinja2:140
+#: fietsboek/templates/profile_overview.jinja2:118
msgid "page.profile.longest_duration_track"
msgstr "Am Längsten Dauernde Strecke"
-#: fietsboek/templates/profile.jinja2:145
+#: fietsboek/templates/profile_overview.jinja2:123
msgid "page.profile.shortest_duration_track"
msgstr "Am Kürzesten Dauernde Strecke"
-#: fietsboek/templates/profile.jinja2:152
-msgid "page.profile.graph.km_per_month"
-msgstr "Kilometer pro Monat"
-
-#: fietsboek/templates/profile.jinja2:161
-msgid "page.profile.calendar.previous"
-msgstr "Vorheriger Monat"
-
-#: fietsboek/templates/profile.jinja2:163
-msgid "page.profile.calendar.next"
-msgstr "Nächster Monat"
-
-#: fietsboek/templates/profile.jinja2:218
+#: fietsboek/templates/profile_overview.jinja2:144
msgid "page.profile.heatmap"
msgstr "Heatmap"
-#: fietsboek/templates/profile.jinja2:223
+#: fietsboek/templates/profile_overview.jinja2:152
msgid "page.profile.tilehunt"
msgstr "Kacheljäger"
@@ -888,15 +980,15 @@ msgstr "Ungültige E-Mail-Adresse"
msgid "flash.a_confirmation_link_has_been_sent"
msgstr "Ein Bestätigungslink wurde versandt"
-#: fietsboek/views/admin.py:49
+#: fietsboek/views/admin.py:157
msgid "flash.badge_added"
msgstr "Wappen hinzugefügt"
-#: fietsboek/views/admin.py:73
+#: fietsboek/views/admin.py:181
msgid "flash.badge_modified"
msgstr "Wappen bearbeitet"
-#: fietsboek/views/admin.py:93
+#: fietsboek/views/admin.py:201
msgid "flash.badge_deleted"
msgstr "Wappen gelöscht"
diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.mo b/fietsboek/locale/en/LC_MESSAGES/messages.mo
index 5f8edc6..fd67c57 100644
--- a/fietsboek/locale/en/LC_MESSAGES/messages.mo
+++ b/fietsboek/locale/en/LC_MESSAGES/messages.mo
Binary files differ
diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.po b/fietsboek/locale/en/LC_MESSAGES/messages.po
index 981d134..4e9f302 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: 2025-01-30 21:50+0100\n"
+"POT-Creation-Date: 2025-05-06 21:46+0200\n"
"PO-Revision-Date: 2023-04-03 20:42+0200\n"
"Last-Translator: \n"
"Language: en\n"
@@ -29,11 +29,11 @@ msgstr ""
"\n"
"If you did not create an account, ignore this email."
-#: fietsboek/util.py:333
+#: fietsboek/util.py:334
msgid "password_constraint.mismatch"
msgstr "Passwords don't match"
-#: fietsboek/util.py:335
+#: fietsboek/util.py:336
msgid "password_constraint.length"
msgstr "Password not long enough"
@@ -69,34 +69,126 @@ msgstr "Max Speed"
msgid "tooltip.table.avg_speed"
msgstr "Average Speed"
+#: fietsboek/templates/403.jinja2:5
+msgid "403.title"
+msgstr "No entry"
+
+#: fietsboek/templates/403.jinja2:9
+msgid "403.no_access"
+msgstr "You are not allowed to access this resource."
+
+#: fietsboek/templates/403.jinja2:12
+msgid "403.try_log_in"
+msgstr "If you should have access, make sure you are logged in with the right credentials."
+
+#: fietsboek/templates/404.jinja2:5
+msgid "404.title"
+msgstr "Dead end"
+
+#: fietsboek/templates/404.jinja2:9
+msgid "404.path_not_found"
+msgstr "The path you have chosen was not found."
+
+#: fietsboek/templates/404.jinja2:12
+msgid "404.choose_different"
+msgstr "Please choose a different path."
+
#: fietsboek/templates/admin.jinja2:5
msgid "page.admin.title"
msgstr "Administration"
-#: fietsboek/templates/admin.jinja2:7
+#: fietsboek/templates/admin.jinja2:10
+msgid "page.admin.nav.overview"
+msgstr "Overview"
+
+#: fietsboek/templates/admin.jinja2:11
+msgid "page.admin.nav.badges"
+msgstr "Badges"
+
+#: fietsboek/templates/admin_badges.jinja2:5
msgid "page.admin.badges"
msgstr "Badges"
-#: fietsboek/templates/admin.jinja2:23
+#: fietsboek/templates/admin_badges.jinja2:21
msgid "page.admin.badge.edit"
msgstr "Edit"
-#: fietsboek/templates/admin.jinja2:29
+#: fietsboek/templates/admin_badges.jinja2:27
msgid "page.admin.badge.delete_badge"
msgstr "Delete badge"
-#: fietsboek/templates/admin.jinja2:37
+#: fietsboek/templates/admin_badges.jinja2:35
msgid "page.admin.badges.badge_title"
msgstr "Badge Title"
-#: fietsboek/templates/admin.jinja2:41
+#: fietsboek/templates/admin_badges.jinja2:39
msgid "page.admin.badges.badge_image"
msgstr "Badge Image"
-#: fietsboek/templates/admin.jinja2:45
+#: fietsboek/templates/admin_badges.jinja2:43
msgid "page.admin.badges.add_badge"
msgstr "Add Badge"
+#: fietsboek/templates/admin_overview.jinja2:5
+msgid "admin.overview.instance_has"
+msgstr "This instance has"
+
+#: fietsboek/templates/admin_overview.jinja2:9
+msgid "admin.overview.stat.user"
+msgid_plural "admin.overview.stat.users"
+msgstr[0] "%(num)d user"
+msgstr[1] "%(num)d users"
+
+#: fietsboek/templates/admin_overview.jinja2:13
+msgid "admin.overview.stat.track"
+msgid_plural "admin.overview.stat.tracks"
+msgstr[0] "%(num)d track"
+msgstr[1] "%(num)d tracks"
+
+#: fietsboek/templates/admin_overview.jinja2:17
+msgid "admin.overview.stats.mib"
+msgstr "MiB of data"
+
+#: fietsboek/templates/admin_overview.jinja2:24
+msgid "admin.overview.system_overview"
+msgstr "System information"
+
+#: fietsboek/templates/admin_overview.jinja2:28
+msgid "admin.overview.fietsboek_version"
+msgstr "Fietsboek version"
+
+#: fietsboek/templates/admin_overview.jinja2:32
+msgid "admin.overview.python_version"
+msgstr "Python version"
+
+#: fietsboek/templates/admin_overview.jinja2:36
+msgid "admin.overview.kernel_version"
+msgstr "Linux version"
+
+#: fietsboek/templates/admin_overview.jinja2:40
+msgid "admin.overview.distro_version"
+msgstr "Distribution"
+
+#: fietsboek/templates/admin_overview.jinja2:44
+msgid "admin.overview.last_cronjob"
+msgstr "Last cronjob"
+
+#: fietsboek/templates/admin_overview.jinja2:55
+msgid "admin.overview.storage_graph.label.gpx"
+msgstr "GPX"
+
+#: fietsboek/templates/admin_overview.jinja2:56
+msgid "admin.overview.storage_graph.label.images"
+msgstr "Images"
+
+#: fietsboek/templates/admin_overview.jinja2:57
+msgid "admin.overview.storage_graph.label.user_maps"
+msgstr "User maps"
+
+#: fietsboek/templates/admin_overview.jinja2:82
+msgid "admin.overview.storage_graph.title"
+msgstr "Storage breakdown"
+
#: fietsboek/templates/browse.jinja2:4
msgid "page.browse.title"
msgstr "Browse"
@@ -194,52 +286,52 @@ msgid "page.browse.synthetic_tooltip"
msgstr "This is a pre-planned track"
#: fietsboek/templates/browse.jinja2:158 fietsboek/templates/details.jinja2:103
-#: fietsboek/templates/profile.jinja2:15
+#: fietsboek/templates/profile_overview.jinja2:16
msgid "page.details.date"
msgstr "Date"
#: fietsboek/templates/browse.jinja2:160 fietsboek/templates/details.jinja2:117
-#: fietsboek/templates/profile.jinja2:17
+#: fietsboek/templates/profile_overview.jinja2:18
msgid "page.details.length"
msgstr "Length"
#: fietsboek/templates/browse.jinja2:165 fietsboek/templates/details.jinja2:108
-#: fietsboek/templates/profile.jinja2:21
+#: fietsboek/templates/profile_overview.jinja2:22
msgid "page.details.start_time"
msgstr "Record Start"
#: fietsboek/templates/browse.jinja2:167 fietsboek/templates/details.jinja2:112
-#: fietsboek/templates/profile.jinja2:23
+#: fietsboek/templates/profile_overview.jinja2:24
msgid "page.details.end_time"
msgstr "Record End"
#: fietsboek/templates/browse.jinja2:172 fietsboek/templates/details.jinja2:121
-#: fietsboek/templates/profile.jinja2:27
+#: fietsboek/templates/profile_overview.jinja2:28
msgid "page.details.uphill"
msgstr "Uphill"
#: fietsboek/templates/browse.jinja2:174 fietsboek/templates/details.jinja2:125
-#: fietsboek/templates/profile.jinja2:29
+#: fietsboek/templates/profile_overview.jinja2:30
msgid "page.details.downhill"
msgstr "Downhill"
#: fietsboek/templates/browse.jinja2:179 fietsboek/templates/details.jinja2:130
-#: fietsboek/templates/profile.jinja2:33
+#: fietsboek/templates/profile_overview.jinja2:34
msgid "page.details.moving_time"
msgstr "Moving Time"
#: fietsboek/templates/browse.jinja2:181 fietsboek/templates/details.jinja2:134
-#: fietsboek/templates/profile.jinja2:35
+#: fietsboek/templates/profile_overview.jinja2:36
msgid "page.details.stopped_time"
msgstr "Stopped Time"
#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:138
-#: fietsboek/templates/profile.jinja2:39
+#: fietsboek/templates/profile_overview.jinja2:40
msgid "page.details.max_speed"
msgstr "Max Speed"
#: fietsboek/templates/browse.jinja2:187 fietsboek/templates/details.jinja2:142
-#: fietsboek/templates/profile.jinja2:41
+#: fietsboek/templates/profile_overview.jinja2:42
msgid "page.details.avg_speed"
msgstr "Average Speed"
@@ -538,13 +630,13 @@ msgid "page.home.unfinished_uploads"
msgstr "You have unfinished uploads. Click on the links below to resume them:"
#: fietsboek/templates/home.jinja2:44 fietsboek/templates/home.jinja2:53
-#: fietsboek/templates/home.jinja2:97
+#: fietsboek/templates/home.jinja2:99
msgid "page.home.summary.track"
msgid_plural "page.home.summary.tracks"
msgstr[0] "%(num)d track"
msgstr[1] "%(num)d tracks"
-#: fietsboek/templates/home.jinja2:97
+#: fietsboek/templates/home.jinja2:99
msgid "page.home.total"
msgstr "Total"
@@ -648,91 +740,91 @@ msgstr "Passwords must match"
msgid "page.password_reset.reset"
msgstr "Reset"
-#: fietsboek/templates/profile.jinja2:64
+#: fietsboek/templates/profile.jinja2:10
msgid "page.profile.tabbar.overview"
msgstr "Overview"
-#: fietsboek/templates/profile.jinja2:69
+#: fietsboek/templates/profile.jinja2:15
msgid "page.profile.tabbar.graphs"
msgstr "Graphs"
-#: fietsboek/templates/profile.jinja2:74
+#: fietsboek/templates/profile.jinja2:20
msgid "page.profile.tabbar.calendar"
msgstr "Calendar"
-#: fietsboek/templates/profile.jinja2:88
+#: fietsboek/templates/profile_calendar.jinja2:9
+msgid "page.profile.calendar.previous"
+msgstr "Previous month"
+
+#: fietsboek/templates/profile_calendar.jinja2:11
+msgid "page.profile.calendar.next"
+msgstr "Next month"
+
+#: fietsboek/templates/profile_graphs.jinja2:6
+msgid "page.profile.graph.km_per_month"
+msgstr "Kilometers per month"
+
+#: fietsboek/templates/profile_overview.jinja2:66
msgid "page.profile.length"
msgstr "Length"
-#: fietsboek/templates/profile.jinja2:92
+#: fietsboek/templates/profile_overview.jinja2:70
msgid "page.profile.avg_length"
msgstr "Average Length"
-#: fietsboek/templates/profile.jinja2:96
+#: fietsboek/templates/profile_overview.jinja2:74
msgid "page.profile.uphill"
msgstr "Uphill"
-#: fietsboek/templates/profile.jinja2:100
+#: fietsboek/templates/profile_overview.jinja2:78
msgid "page.profile.downhill"
msgstr "Downhill"
-#: fietsboek/templates/profile.jinja2:104
+#: fietsboek/templates/profile_overview.jinja2:82
msgid "page.profile.moving_time"
msgstr "Moving Time"
-#: fietsboek/templates/profile.jinja2:108
+#: fietsboek/templates/profile_overview.jinja2:86
msgid "page.profile.stopped_time"
msgstr "Stopped Time"
-#: fietsboek/templates/profile.jinja2:112
+#: fietsboek/templates/profile_overview.jinja2:90
msgid "page.profile.avg_duration"
msgstr "Average Duration"
-#: fietsboek/templates/profile.jinja2:116
+#: fietsboek/templates/profile_overview.jinja2:94
msgid "page.profile.max_speed"
msgstr "Max Speed"
-#: fietsboek/templates/profile.jinja2:120
+#: fietsboek/templates/profile_overview.jinja2:98
msgid "page.profile.avg_speed"
msgstr "Average Speed"
-#: fietsboek/templates/profile.jinja2:124
+#: fietsboek/templates/profile_overview.jinja2:102
msgid "page.profile.number_of_tracks"
msgstr "Number of tracks"
-#: fietsboek/templates/profile.jinja2:130
+#: fietsboek/templates/profile_overview.jinja2:108
msgid "page.profile.longest_distance_track"
msgstr "Longest Track"
-#: fietsboek/templates/profile.jinja2:135
+#: fietsboek/templates/profile_overview.jinja2:113
msgid "page.profile.shortest_distance_track"
msgstr "Shortest Track"
-#: fietsboek/templates/profile.jinja2:140
+#: fietsboek/templates/profile_overview.jinja2:118
msgid "page.profile.longest_duration_track"
msgstr "Most Time-Consuming Track"
-#: fietsboek/templates/profile.jinja2:145
+#: fietsboek/templates/profile_overview.jinja2:123
msgid "page.profile.shortest_duration_track"
msgstr "Quickest Track"
-#: fietsboek/templates/profile.jinja2:152
-msgid "page.profile.graph.km_per_month"
-msgstr "Kilometers per month"
-
-#: fietsboek/templates/profile.jinja2:161
-msgid "page.profile.calendar.previous"
-msgstr "Previous month"
-
-#: fietsboek/templates/profile.jinja2:163
-msgid "page.profile.calendar.next"
-msgstr "Next month"
-
-#: fietsboek/templates/profile.jinja2:218
+#: fietsboek/templates/profile_overview.jinja2:144
msgid "page.profile.heatmap"
msgstr "Heat Map"
-#: fietsboek/templates/profile.jinja2:223
+#: fietsboek/templates/profile_overview.jinja2:152
msgid "page.profile.tilehunt"
msgstr "Tilehunt"
@@ -878,15 +970,15 @@ msgstr "Invalid email"
msgid "flash.a_confirmation_link_has_been_sent"
msgstr "A confirmation link has been sent"
-#: fietsboek/views/admin.py:49
+#: fietsboek/views/admin.py:157
msgid "flash.badge_added"
msgstr "Badge has been added"
-#: fietsboek/views/admin.py:73
+#: fietsboek/views/admin.py:181
msgid "flash.badge_modified"
msgstr "Badge has been modified"
-#: fietsboek/views/admin.py:93
+#: fietsboek/views/admin.py:201
msgid "flash.badge_deleted"
msgstr "Badge has been deleted"
diff --git a/fietsboek/locale/fietslog.pot b/fietsboek/locale/fietslog.pot
index 60c77a5..1a1770b 100644
--- a/fietsboek/locale/fietslog.pot
+++ b/fietsboek/locale/fietslog.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2025-01-30 21:50+0100\n"
+"POT-Creation-Date: 2025-05-06 21:46+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"
@@ -25,11 +25,11 @@ msgstr ""
msgid "email.verify.text"
msgstr ""
-#: fietsboek/util.py:333
+#: fietsboek/util.py:334
msgid "password_constraint.mismatch"
msgstr ""
-#: fietsboek/util.py:335
+#: fietsboek/util.py:336
msgid "password_constraint.length"
msgstr ""
@@ -65,34 +65,126 @@ msgstr ""
msgid "tooltip.table.avg_speed"
msgstr ""
+#: fietsboek/templates/403.jinja2:5
+msgid "403.title"
+msgstr ""
+
+#: fietsboek/templates/403.jinja2:9
+msgid "403.no_entry"
+msgstr ""
+
+#: fietsboek/templates/403.jinja2:12
+msgid "403.try_log_in"
+msgstr ""
+
+#: fietsboek/templates/404.jinja2:5
+msgid "404.title"
+msgstr ""
+
+#: fietsboek/templates/404.jinja2:9
+msgid "404.path_not_found"
+msgstr ""
+
+#: fietsboek/templates/404.jinja2:12
+msgid "404.choose_different"
+msgstr ""
+
#: fietsboek/templates/admin.jinja2:5
msgid "page.admin.title"
msgstr ""
-#: fietsboek/templates/admin.jinja2:7
+#: fietsboek/templates/admin.jinja2:10
+msgid "page.admin.nav.overview"
+msgstr ""
+
+#: fietsboek/templates/admin.jinja2:11
+msgid "page.admin.nav.badges"
+msgstr ""
+
+#: fietsboek/templates/admin_badges.jinja2:5
msgid "page.admin.badges"
msgstr ""
-#: fietsboek/templates/admin.jinja2:23
+#: fietsboek/templates/admin_badges.jinja2:21
msgid "page.admin.badge.edit"
msgstr ""
-#: fietsboek/templates/admin.jinja2:29
+#: fietsboek/templates/admin_badges.jinja2:27
msgid "page.admin.badge.delete_badge"
msgstr ""
-#: fietsboek/templates/admin.jinja2:37
+#: fietsboek/templates/admin_badges.jinja2:35
msgid "page.admin.badges.badge_title"
msgstr ""
-#: fietsboek/templates/admin.jinja2:41
+#: fietsboek/templates/admin_badges.jinja2:39
msgid "page.admin.badges.badge_image"
msgstr ""
-#: fietsboek/templates/admin.jinja2:45
+#: fietsboek/templates/admin_badges.jinja2:43
msgid "page.admin.badges.add_badge"
msgstr ""
+#: fietsboek/templates/admin_overview.jinja2:5
+msgid "admin.overview.instance_has"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:9
+msgid "admin.overview.stat.user"
+msgid_plural "admin.overview.stat.users"
+msgstr[0] ""
+msgstr[1] ""
+
+#: fietsboek/templates/admin_overview.jinja2:13
+msgid "admin.overview.stat.track"
+msgid_plural "admin.overview.stat.tracks"
+msgstr[0] ""
+msgstr[1] ""
+
+#: fietsboek/templates/admin_overview.jinja2:17
+msgid "admin.overview.stats.mib"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:24
+msgid "admin.overview.system_overview"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:28
+msgid "admin.overview.fietsboek_version"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:32
+msgid "admin.overview.python_version"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:36
+msgid "admin.overview.kernel_version"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:40
+msgid "admin.overview.distro_version"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:44
+msgid "admin.overview.last_cronjob"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:55
+msgid "admin.overview.storage_graph.label.gpx"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:56
+msgid "admin.overview.storage_graph.label.images"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:57
+msgid "admin.overview.storage_graph.label.user_maps"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:82
+msgid "admin.overview.storage_graph.title"
+msgstr ""
+
#: fietsboek/templates/browse.jinja2:4
msgid "page.browse.title"
msgstr ""
@@ -190,52 +282,52 @@ msgid "page.browse.synthetic_tooltip"
msgstr ""
#: fietsboek/templates/browse.jinja2:158 fietsboek/templates/details.jinja2:103
-#: fietsboek/templates/profile.jinja2:15
+#: fietsboek/templates/profile_overview.jinja2:16
msgid "page.details.date"
msgstr ""
#: fietsboek/templates/browse.jinja2:160 fietsboek/templates/details.jinja2:117
-#: fietsboek/templates/profile.jinja2:17
+#: fietsboek/templates/profile_overview.jinja2:18
msgid "page.details.length"
msgstr ""
#: fietsboek/templates/browse.jinja2:165 fietsboek/templates/details.jinja2:108
-#: fietsboek/templates/profile.jinja2:21
+#: fietsboek/templates/profile_overview.jinja2:22
msgid "page.details.start_time"
msgstr ""
#: fietsboek/templates/browse.jinja2:167 fietsboek/templates/details.jinja2:112
-#: fietsboek/templates/profile.jinja2:23
+#: fietsboek/templates/profile_overview.jinja2:24
msgid "page.details.end_time"
msgstr ""
#: fietsboek/templates/browse.jinja2:172 fietsboek/templates/details.jinja2:121
-#: fietsboek/templates/profile.jinja2:27
+#: fietsboek/templates/profile_overview.jinja2:28
msgid "page.details.uphill"
msgstr ""
#: fietsboek/templates/browse.jinja2:174 fietsboek/templates/details.jinja2:125
-#: fietsboek/templates/profile.jinja2:29
+#: fietsboek/templates/profile_overview.jinja2:30
msgid "page.details.downhill"
msgstr ""
#: fietsboek/templates/browse.jinja2:179 fietsboek/templates/details.jinja2:130
-#: fietsboek/templates/profile.jinja2:33
+#: fietsboek/templates/profile_overview.jinja2:34
msgid "page.details.moving_time"
msgstr ""
#: fietsboek/templates/browse.jinja2:181 fietsboek/templates/details.jinja2:134
-#: fietsboek/templates/profile.jinja2:35
+#: fietsboek/templates/profile_overview.jinja2:36
msgid "page.details.stopped_time"
msgstr ""
#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:138
-#: fietsboek/templates/profile.jinja2:39
+#: fietsboek/templates/profile_overview.jinja2:40
msgid "page.details.max_speed"
msgstr ""
#: fietsboek/templates/browse.jinja2:187 fietsboek/templates/details.jinja2:142
-#: fietsboek/templates/profile.jinja2:41
+#: fietsboek/templates/profile_overview.jinja2:42
msgid "page.details.avg_speed"
msgstr ""
@@ -532,13 +624,13 @@ msgid "page.home.unfinished_uploads"
msgstr ""
#: fietsboek/templates/home.jinja2:44 fietsboek/templates/home.jinja2:53
-#: fietsboek/templates/home.jinja2:97
+#: fietsboek/templates/home.jinja2:99
msgid "page.home.summary.track"
msgid_plural "page.home.summary.tracks"
msgstr[0] ""
msgstr[1] ""
-#: fietsboek/templates/home.jinja2:97
+#: fietsboek/templates/home.jinja2:99
msgid "page.home.total"
msgstr ""
@@ -642,91 +734,91 @@ msgstr ""
msgid "page.password_reset.reset"
msgstr ""
-#: fietsboek/templates/profile.jinja2:64
+#: fietsboek/templates/profile.jinja2:10
msgid "page.profile.tabbar.overview"
msgstr ""
-#: fietsboek/templates/profile.jinja2:69
+#: fietsboek/templates/profile.jinja2:15
msgid "page.profile.tabbar.graphs"
msgstr ""
-#: fietsboek/templates/profile.jinja2:74
+#: fietsboek/templates/profile.jinja2:20
msgid "page.profile.tabbar.calendar"
msgstr ""
-#: fietsboek/templates/profile.jinja2:88
+#: fietsboek/templates/profile_calendar.jinja2:9
+msgid "page.profile.calendar.previous"
+msgstr ""
+
+#: fietsboek/templates/profile_calendar.jinja2:11
+msgid "page.profile.calendar.next"
+msgstr ""
+
+#: fietsboek/templates/profile_graphs.jinja2:6
+msgid "page.profile.graph.km_per_month"
+msgstr ""
+
+#: fietsboek/templates/profile_overview.jinja2:66
msgid "page.profile.length"
msgstr ""
-#: fietsboek/templates/profile.jinja2:92
+#: fietsboek/templates/profile_overview.jinja2:70
msgid "page.profile.avg_length"
msgstr ""
-#: fietsboek/templates/profile.jinja2:96
+#: fietsboek/templates/profile_overview.jinja2:74
msgid "page.profile.uphill"
msgstr ""
-#: fietsboek/templates/profile.jinja2:100
+#: fietsboek/templates/profile_overview.jinja2:78
msgid "page.profile.downhill"
msgstr ""
-#: fietsboek/templates/profile.jinja2:104
+#: fietsboek/templates/profile_overview.jinja2:82
msgid "page.profile.moving_time"
msgstr ""
-#: fietsboek/templates/profile.jinja2:108
+#: fietsboek/templates/profile_overview.jinja2:86
msgid "page.profile.stopped_time"
msgstr ""
-#: fietsboek/templates/profile.jinja2:112
+#: fietsboek/templates/profile_overview.jinja2:90
msgid "page.profile.avg_duration"
msgstr ""
-#: fietsboek/templates/profile.jinja2:116
+#: fietsboek/templates/profile_overview.jinja2:94
msgid "page.profile.max_speed"
msgstr ""
-#: fietsboek/templates/profile.jinja2:120
+#: fietsboek/templates/profile_overview.jinja2:98
msgid "page.profile.avg_speed"
msgstr ""
-#: fietsboek/templates/profile.jinja2:124
+#: fietsboek/templates/profile_overview.jinja2:102
msgid "page.profile.number_of_tracks"
msgstr ""
-#: fietsboek/templates/profile.jinja2:130
+#: fietsboek/templates/profile_overview.jinja2:108
msgid "page.profile.longest_distance_track"
msgstr ""
-#: fietsboek/templates/profile.jinja2:135
+#: fietsboek/templates/profile_overview.jinja2:113
msgid "page.profile.shortest_distance_track"
msgstr ""
-#: fietsboek/templates/profile.jinja2:140
+#: fietsboek/templates/profile_overview.jinja2:118
msgid "page.profile.longest_duration_track"
msgstr ""
-#: fietsboek/templates/profile.jinja2:145
+#: fietsboek/templates/profile_overview.jinja2:123
msgid "page.profile.shortest_duration_track"
msgstr ""
-#: fietsboek/templates/profile.jinja2:152
-msgid "page.profile.graph.km_per_month"
-msgstr ""
-
-#: fietsboek/templates/profile.jinja2:161
-msgid "page.profile.calendar.previous"
-msgstr ""
-
-#: fietsboek/templates/profile.jinja2:163
-msgid "page.profile.calendar.next"
-msgstr ""
-
-#: fietsboek/templates/profile.jinja2:218
+#: fietsboek/templates/profile_overview.jinja2:144
msgid "page.profile.heatmap"
msgstr ""
-#: fietsboek/templates/profile.jinja2:223
+#: fietsboek/templates/profile_overview.jinja2:152
msgid "page.profile.tilehunt"
msgstr ""
@@ -866,15 +958,15 @@ msgstr ""
msgid "flash.a_confirmation_link_has_been_sent"
msgstr ""
-#: fietsboek/views/admin.py:49
+#: fietsboek/views/admin.py:157
msgid "flash.badge_added"
msgstr ""
-#: fietsboek/views/admin.py:73
+#: fietsboek/views/admin.py:181
msgid "flash.badge_modified"
msgstr ""
-#: fietsboek/views/admin.py:93
+#: fietsboek/views/admin.py:201
msgid "flash.badge_deleted"
msgstr ""
diff --git a/fietsboek/routes.py b/fietsboek/routes.py
index f1d4ee2..4286e92 100644
--- a/fietsboek/routes.py
+++ b/fietsboek/routes.py
@@ -54,10 +54,11 @@ def includeme(config):
config.add_route("badge", "/badge/{badge_id}", factory="fietsboek.models.Badge.factory")
- config.add_route("admin", "/admin")
- config.add_route("admin-badge-add", "/admin/add-badge")
- config.add_route("admin-badge-edit", "/admin/edit-badge")
- config.add_route("admin-badge-delete", "/admin/delete-badge")
+ config.add_route("admin", "/admin/")
+ config.add_route("admin-badge", "/admin/badges/")
+ config.add_route("admin-badge-add", "/admin/badges/add")
+ config.add_route("admin-badge-edit", "/admin/badges/edit")
+ config.add_route("admin-badge-delete", "/admin/badges/delete")
config.add_route("user-data", "/me")
config.add_route("add-friend", "/me/send-friend-request")
diff --git a/fietsboek/scripts/fietscron.py b/fietsboek/scripts/fietscron.py
index 68b731f..a3f3f54 100644
--- a/fietsboek/scripts/fietscron.py
+++ b/fietsboek/scripts/fietscron.py
@@ -58,9 +58,12 @@ def cli(config):
rebuild_cache(engine, data_manager)
build_previews(engine, data_manager, redis, config)
+ redis = mod_redis.from_url(config.redis_url)
if config.hittekaart_autogenerate:
run_hittekaart(engine, data_manager, redis, config)
+ redis.set("last-cronjob", datetime.datetime.now(datetime.UTC).timestamp())
+
def remove_old_uploads(engine: Engine):
"""Removes old uploads from the database."""
diff --git a/fietsboek/static/DeadEnd.svg b/fietsboek/static/DeadEnd.svg
new file mode 100644
index 0000000..b65f171
--- /dev/null
+++ b/fietsboek/static/DeadEnd.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 421.0267 630.72443"
+ height="630.72443"
+ width="421.0267"
+ xml:space="preserve"
+ id="svg5733"
+ version="1.1"><metadata
+ id="metadata5739"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs5737" /><g
+ transform="matrix(1.3333333,0,0,-1.3333333,0,630.72445)"
+ id="g5741"><g
+ id="g5743"><path
+ id="path5745"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="M 21.594,0.375 H 294.59 c 20.906,0 20.805,17.051 20.805,17.051 v 438.41 c 0,0 0.136,16.828 -17.977,16.828 H 18.945 c 0,0 -18.531,0.309 -18.57,-18.507 V 19.11 C 0.375,0.328 21.594,0.375 21.594,0.375 Z" /><path
+ id="path5747"
+ style="fill:#154889;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 18.422,8.723 h 279.019 c 0,0 10.153,0.027 10.153,10.27 V 454.86 c 0,0 -0.008,10.058 -11.235,10.058 H 19.504 c 0,0 -11.152,0 -11.152,-10.054 V 18.352 C 8.391,8.621 18.422,8.723 18.422,8.723 Z" /><path
+ id="path5749"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 18.945,473.043 c -0.007,0 -4.711,0.078 -9.433,-2.238 C 4.781,468.485 0.02,463.688 0,454.161 V 19.11 C 0,9.582 5.441,4.731 10.832,2.356 16.223,-0.019 21.594,0 21.594,0 H 294.59 c 10.555,0 15.918,4.348 18.562,8.707 2.645,4.356 2.618,8.715 2.618,8.715 v 438.414 c 0,0.004 0.035,4.293 -2.243,8.594 -2.277,4.297 -6.937,8.613 -16.109,8.613 z m 0,-0.379 h 278.473 c 18.113,0 17.977,-16.828 17.977,-16.828 V 17.422 c 0,0 0.101,-17.047 -20.805,-17.047 H 21.594 c 0,0 -21.219,-0.047 -21.219,18.731 v 435.051 c 0.039,18.816 18.57,18.507 18.57,18.507 z" /><path
+ id="path5751"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="M 53.969,418.61 V 265.614 h 96.164 v -34.368 h -66 V 181.43 H 137.16 V 6.571 h 41.59 l -0.066,174.859 h 53.027 l -0.066,49.816 h -66 v 34.368 h 96.156 V 418.61 Z" /><path
+ id="path5753"
+ style="fill:#cc0000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="M 88.68,226.7 H 227.098 V 186.442 H 88.68 Z" /><path
+ id="path5755"
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 165.359,390.379 c 0,-5.152 -4.175,-9.332 -9.332,-9.332 -5.152,0 -9.328,4.18 -9.328,9.332 0,5.153 4.176,9.332 9.328,9.332 5.157,0 9.332,-4.179 9.332,-9.332 z" /><path
+ id="path5757"
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 128.367,336.723 c 25.278,39.441 22.879,40.543 32.328,40.824 5.5,-0.164 5.664,-1.664 14.996,-8.996 11.5,-8.449 10.664,-8.551 10.5,-17.832 -0.05,-11.164 0.715,-10.328 -8.168,-14.496 v 18.832 l -7.832,6.996 v -26.496 l -18.66,0.168 -0.168,20.996 c -15.332,-23.66 -14.332,-22.328 -22.996,-19.996 z" /><path
+ id="path5759"
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 151.863,332.891 h 18.164 c 0,-4.168 1.332,-3.832 10.996,-21.996 10.165,-18.715 10.332,-17.445 4.5,-28.16 l -25.16,45.156 c -23.832,-46.105 -20.832,-44.707 -33.996,-43.824 l 13.164,24.66 c 11.282,20.383 11.883,20.449 12.332,24.164 z" /></g></g></svg> \ No newline at end of file
diff --git a/fietsboek/static/NoEntry.svg b/fietsboek/static/NoEntry.svg
new file mode 100644
index 0000000..1cc18a4
--- /dev/null
+++ b/fietsboek/static/NoEntry.svg
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="600.99628" height="600.99628">
+ <defs>
+ <path id="a" d="M23.809 456.512h.051v-.047h-.051v.047z"/>
+ <path id="b" d="M23.809 456.465v.047l.05-.047h-.05zm.05 0-.05.047v-.047h.05z"/>
+ <path id="c" d="M23.859 456.465h-.05.023v.047l.027-.047z"/>
+ <path id="d" d="M23.809 456.465v.047-.047l.05.047-.05-.047z"/>
+ <path id="e" d="M23.859 456.512v-.047h-.05l.05.047z"/>
+ </defs>
+ <g transform="matrix(1.25 0 0 -1.25 0 600.99628)">
+ <path fill="#fff" d="M480.398 240.399c0-132.551-107.449-240-240-240-132.55 0-240 107.449-240 240 0 132.55 107.45 240 240 240 132.551 0 240-107.45 240-240z"/>
+ <path fill="#c1121c" d="M240.402 472.402c-127.75 0-232-104.25-232-232s104.25-232 232-232c127.746 0 232 104.25 232 232s-104.254 232-232 232zm-208-196h416v-72h-416v72z"/>
+ <path d="M240.398 480.797C107.633 480.797 0 373.164 0 240.399 0 107.633 107.633 0 240.398 0c132.766 0 240.399 107.633 240.399 240.399 0 132.765-107.633 240.398-240.399 240.398zm0-.398c132.551 0 240-107.45 240-240 0-132.551-107.449-240-240-240-132.55 0-240 107.449-240 240 0 132.55 107.45 240 240 240zM23.832 456.512v-.024h-.023l.023.024z"/>
+ <path d="M23.809 456.512h.023v-.023h-.023v.023z"/>
+ <path d="M23.809 456.488v.024l.023-.024h-.023zm.023 0-.023.024v-.024h.023z"/>
+ <path d="M23.859 456.488h-.05.023v.024l.027-.024z"/>
+ <path d="M23.809 456.488v.024-.024l.023.024-.023-.024z"/>
+ <path d="M23.809 456.512h.023v-.023h-.023v.023zm.05 0v-.047h-.05l.05.047z"/>
+ <use xlink:href="#a"/>
+ <use xlink:href="#b"/>
+ <use xlink:href="#c"/>
+ <use xlink:href="#d"/>
+ <use xlink:href="#a"/>
+ <use xlink:href="#e"/>
+ <use xlink:href="#a"/>
+ <use xlink:href="#b"/>
+ <use xlink:href="#c"/>
+ <use xlink:href="#d"/>
+ <use xlink:href="#a"/>
+ <use xlink:href="#e"/>
+ <use xlink:href="#a"/>
+ <use xlink:href="#b"/>
+ <use xlink:href="#c"/>
+ <use xlink:href="#d"/>
+ <use xlink:href="#a"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/fietsboek/static/theme.css b/fietsboek/static/theme.css
index 0e932dd..65e6881 100644
--- a/fietsboek/static/theme.css
+++ b/fietsboek/static/theme.css
@@ -301,6 +301,26 @@ strong {
text-align: center;
}
+/* Admin view layout: We have an extra sidebar for the navigation */
+#adminContainer {
+ display: grid;
+ grid-template-areas: "sidebar main";
+ grid-template-columns: 1fr 5fr;
+ gap: 1rem;
+}
+
+#adminNavigation {
+ grid-area: sidebar;
+}
+
+#adminContent {
+ grid-area: main;
+}
+
+.admin-stat {
+ font-size: 120%;
+}
+
.list-group.list-group-root {
padding: 0;
overflow: hidden;
diff --git a/fietsboek/static/theme.css.map b/fietsboek/static/theme.css.map
index 6773b79..3ddb9bc 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;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;;;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
diff --git a/fietsboek/templates/403.jinja2 b/fietsboek/templates/403.jinja2
new file mode 100644
index 0000000..9f4478e
--- /dev/null
+++ b/fietsboek/templates/403.jinja2
@@ -0,0 +1,16 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="container">
+ <h1>{{ _("403.title") }}</h1>
+ <div style="text-align: center;">
+ <img src="{{ request.static_url('fietsboek:static/NoEntry.svg') }}" style="width: min(100%, 300px); margin: auto;">
+ <p>
+ {{ _("403.no_access") }}
+ </p>
+ <p>
+ {{ _("403.try_log_in") }}
+ </p>
+ </div>
+</div>
+{% endblock content %}
diff --git a/fietsboek/templates/404.jinja2 b/fietsboek/templates/404.jinja2
index aaf1241..9c7cc72 100644
--- a/fietsboek/templates/404.jinja2
+++ b/fietsboek/templates/404.jinja2
@@ -1,8 +1,16 @@
{% extends "layout.jinja2" %}
{% block content %}
-<div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1>
- <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+<div class="container">
+ <h1>{{ _("404.title") }}</h1>
+ <div style="text-align: center;">
+ <img src="{{ request.static_url('fietsboek:static/DeadEnd.svg') }}" style="width: min(100%, 300px); margin: auto;">
+ <p>
+ {{ _("404.path_not_found") }}
+ </p>
+ <p>
+ {{ _("404.choose_different") }}
+ </p>
+ </div>
</div>
{% endblock content %}
diff --git a/fietsboek/templates/admin.jinja2 b/fietsboek/templates/admin.jinja2
index 3201d8d..e05e9f0 100644
--- a/fietsboek/templates/admin.jinja2
+++ b/fietsboek/templates/admin.jinja2
@@ -4,45 +4,18 @@
<div class="container">
<h1>{{ _("page.admin.title") }}</h1>
- <h2>{{ _("page.admin.badges") }}</h2>
+ <div id="adminContainer">
+ <aside id="adminNavigation">
+ <nav class="nav nav-pills nav-fill flex-column">
+ <a class="nav-link{% if admin_index == 0 %} active text-bg-dark{% endif %}" href="{{ request.route_url('admin') }}">{{ _("page.admin.nav.overview") }}</a>
+ <a class="nav-link{% if admin_index == 1 %} active text-bg-dark{% endif %}" href="{{ request.route_url('admin-badge') }}">{{ _("page.admin.nav.badges") }}</a>
+ </nav>
+ </aside>
- <div class="list-group">
- {% for badge in badges %}
- <span href="#" class="list-group-item list-group-item-action d-flex admin-badge-list">
- {{ util.render_badge(badge) }}
- <form method="POST" enctype="multipart/form-data" action="{{ request.route_path('admin-badge-edit') }}">
- <input type="hidden" name="badge-edit-id" value="{{ badge.id }}">
- <div class="mb-3">
- <input type="text" class="form-control" name="badge-title" value="{{ badge.title }}">
- </div>
- <div class="mb-3">
- <input class="form-control" type="file" name="badge-image">
- </div>
- {{ util.hidden_csrf_input() }}
- <div class="mb-3">
- <button class="btn btn-primary">{{ _("page.admin.badge.edit") }}</button>
- </div>
- </form>
- <form method="POST" action="{{ request.route_path('admin-badge-delete') }}">
- <input type="hidden" name="badge-delete-id" value="{{ badge.id }}">
- {{ util.hidden_csrf_input() }}
- <button class="btn btn-danger"><i class="bi bi-trash"></i> {{ _("page.admin.badge.delete_badge") }}</button>
- </form>
- </span>
- {% endfor %}
- </div>
-
- <form method="POST" enctype="multipart/form-data" action="{{ request.route_path('admin-badge-add') }}">
- <div class="mb-3">
- <label for="badge-title" class="form-label">{{ _("page.admin.badges.badge_title") }}</label>
- <input type="text" class="form-control" id="badge-title" name="badge-title">
+ <div id="adminContent">
+ {% block admin_content %}
+ {% endblock %}
</div>
- <div class="mb-3">
- <label for="badge-image" class="form-label">{{ _("page.admin.badges.badge_image") }}</label>
- <input class="form-control" type="file" name="badge-image">
- </div>
- {{ util.hidden_csrf_input() }}
- <button type="submit" class="btn btn-primary">{{ _("page.admin.badges.add_badge") }}</button>
- </form>
+ </div>
</div>
{% endblock %}
diff --git a/fietsboek/templates/admin_badges.jinja2 b/fietsboek/templates/admin_badges.jinja2
new file mode 100644
index 0000000..efe8d2c
--- /dev/null
+++ b/fietsboek/templates/admin_badges.jinja2
@@ -0,0 +1,45 @@
+{% set admin_index = 1 %}
+{% extends "admin.jinja2" %}
+{% import "util.jinja2" as util with context %}
+{% block admin_content %}
+<h2>{{ _("page.admin.badges") }}</h2>
+
+<div class="list-group">
+ {% for badge in badges %}
+ <span href="#" class="list-group-item list-group-item-action d-flex admin-badge-list">
+ {{ util.render_badge(badge) }}
+ <form method="POST" enctype="multipart/form-data" action="{{ request.route_path('admin-badge-edit') }}">
+ <input type="hidden" name="badge-edit-id" value="{{ badge.id }}">
+ <div class="mb-3">
+ <input type="text" class="form-control" name="badge-title" value="{{ badge.title }}">
+ </div>
+ <div class="mb-3">
+ <input class="form-control" type="file" name="badge-image">
+ </div>
+ {{ util.hidden_csrf_input() }}
+ <div class="mb-3">
+ <button class="btn btn-success"><i class="bi bi-pencil"></i> {{ _("page.admin.badge.edit") }}</button>
+ <button class="btn btn-danger" form="deleteBadge{{ badge.id }}"><i class="bi bi-trash"></i> {{ _("page.admin.badge.delete_badge") }}</button>
+ </div>
+ </form>
+ <form method="POST" id="deleteBadge{{ badge.id }}" action="{{ request.route_path('admin-badge-delete') }}">
+ <input type="hidden" name="badge-delete-id" value="{{ badge.id }}">
+ {{ util.hidden_csrf_input() }}
+ </form>
+ </span>
+ {% endfor %}
+</div>
+
+<form method="POST" enctype="multipart/form-data" action="{{ request.route_path('admin-badge-add') }}">
+ <div class="mb-3">
+ <label for="badge-title" class="form-label">{{ _("page.admin.badges.badge_title") }}</label>
+ <input type="text" class="form-control" id="badge-title" name="badge-title">
+ </div>
+ <div class="mb-3">
+ <label for="badge-image" class="form-label">{{ _("page.admin.badges.badge_image") }}</label>
+ <input class="form-control" type="file" name="badge-image">
+ </div>
+ {{ util.hidden_csrf_input() }}
+ <button type="submit" class="btn btn-primary">{{ _("page.admin.badges.add_badge") }}</button>
+</form>
+{% endblock %}
diff --git a/fietsboek/templates/admin_overview.jinja2 b/fietsboek/templates/admin_overview.jinja2
new file mode 100644
index 0000000..5337a69
--- /dev/null
+++ b/fietsboek/templates/admin_overview.jinja2
@@ -0,0 +1,91 @@
+{% set admin_index = 0 %}
+{% extends "admin.jinja2" %}
+{% block admin_content %}
+<p class="admin-stat">
+ {{ _("admin.overview.instance_has") }}&hellip;
+</p>
+
+<p class="admin-stat">
+ &hellip; {{ ngettext("admin.overview.stat.user", "admin.overview.stat.users", user_count) }}
+</p>
+
+<p class="admin-stat">
+ &hellip; {{ ngettext("admin.overview.stat.track", "admin.overview.stat.tracks", track_count) }}
+</p>
+
+<p class="admin-stat">
+ &hellip; {{ (total_size / 1024 / 1024) | round(2) }} {{ _("admin.overview.stats.mib") }}
+</p>
+
+<div style="position: relative; height: 500px; margin: auto; width: 75%;">
+ <canvas id="graph-size-breakdown"></canvas>
+</div>
+
+<h2>{{ _("admin.overview.system_overview") }}</h2>
+
+<table class="table">
+ <tr>
+ <td>{{ _("admin.overview.fietsboek_version") }}</td>
+ <td>{{ versions["fietsboek"] }}</td>
+ </tr>
+ <tr>
+ <td>{{ _("admin.overview.python_version") }}</td>
+ <td>{{ versions["python"] }}</td>
+ </tr>
+ <tr>
+ <td>{{ _("admin.overview.kernel_version") }}</td>
+ <td>{{ versions["linux"] }}</td>
+ </tr>
+ <tr>
+ <td>{{ _("admin.overview.distro_version") }}</td>
+ <td>{{ versions["distro"] }}</td>
+ </tr>
+ <tr class="{% if cron_good %}table-success{% else %}table-warning{% endif %}">
+ <td>{{ _("admin.overview.last_cronjob") }} {% if not cron_good %}<i class="bi bi-exclamation-triangle-fill"></i>{% endif %}</td>
+ <td>{{ last_cronjob }}</td>
+ </tr>
+</table>
+{% endblock %}
+
+{% block latescripts %}
+<script>
+ (function() {
+ const data = {
+ labels: [
+ {{ _("admin.overview.storage_graph.label.gpx") | tojson }},
+ {{ _("admin.overview.storage_graph.label.images") | tojson }},
+ {{ _("admin.overview.storage_graph.label.user_maps") | tojson }}
+ ],
+ datasets: [
+ {
+ label: "MiB",
+ data: [
+ {{ (size_breakdown.gpx_files / 1024 / 1024) | tojson }},
+ {{ (size_breakdown.image_files / 1024 / 1024) | tojson }},
+ {{ (size_breakdown.user_maps / 1024 / 1024) | tojson }}
+ ]
+ }
+ ]
+ };
+
+ const config = {
+ type: 'pie',
+ data: data,
+ options: {
+ responsive: true,
+ plugins: {
+ legend: {
+ position: 'top',
+ },
+ title: {
+ display: true,
+ text: {{ _("admin.overview.storage_graph.title") | tojson }}
+ }
+ }
+ }
+ };
+
+ new Chart("graph-size-breakdown", config);
+ })();
+</script>
+{% endblock %}
diff --git a/fietsboek/util.py b/fietsboek/util.py
index 9284ce2..5611c51 100644
--- a/fietsboek/util.py
+++ b/fietsboek/util.py
@@ -7,6 +7,7 @@ import os
import re
import secrets
import unicodedata
+from pathlib import Path
from typing import Optional, TypeVar, Union
import babel
@@ -504,6 +505,18 @@ def secure_filename(filename: str) -> str:
return filename
+def recursive_size(path: Path) -> int:
+ """Recursively determines the size of the given directory.
+
+ :param path: The directory.
+ :return: The combined size, in bytes.
+ """
+ size = 0
+ for root, _folders, files in os.walk(path):
+ size += sum(os.path.getsize(os.path.join(root, fname)) for fname in files)
+ return size
+
+
__all__ = [
"ALLOWED_TAGS",
"ALLOWED_ATTRIBUTES",
@@ -529,4 +542,5 @@ __all__ = [
"tile_url",
"encode_gpx",
"secure_filename",
+ "recursive_size",
]
diff --git a/fietsboek/views/admin.py b/fietsboek/views/admin.py
index d078794..0589cd0 100644
--- a/fietsboek/views/admin.py
+++ b/fietsboek/views/admin.py
@@ -1,21 +1,126 @@
"""Admin views."""
+import datetime
+import platform
+import stat
+from dataclasses import dataclass
+from pathlib import Path
+
from pyramid.httpexceptions import HTTPFound
from pyramid.i18n import TranslationString as _
+from pyramid.request import Request
from pyramid.view import view_config
-from sqlalchemy import select
+from sqlalchemy import func, select
+
+from .. import models, util
+
+GOOD_CRON_THRESHOLD = datetime.timedelta(hours=1)
+
+
+def _safe_size(path: Path) -> int:
+ try:
+ res = path.stat()
+ if stat.S_ISDIR(res.st_mode):
+ return util.recursive_size(path)
+ if stat.S_ISREG(res.st_mode):
+ return res.st_size
+ return 0
+ except FileNotFoundError:
+ return 0
+
+
+@dataclass
+class SizeBreakdown:
+ """A breakdown of what objects take how much storage."""
+
+ gpx_files: int = 0
+ image_files: int = 0
+ user_maps: int = 0
-from .. import models
+
+def _get_size_breakdown(data_manager):
+ breakdown = SizeBreakdown()
+
+ for track_id in data_manager.list_tracks():
+ track = data_manager.open(track_id)
+ breakdown.gpx_files += _safe_size(track.gpx_path())
+ for image_id in track.images():
+ breakdown.image_files += _safe_size(track.image_path(image_id))
+
+ for user_id in data_manager.list_users():
+ user = data_manager.open_user(user_id)
+ breakdown.user_maps += _safe_size(user.heatmap_path())
+ breakdown.user_maps += _safe_size(user.tilehunt_path())
+
+ return breakdown
+
+
+def _get_fietsboek_version():
+ # pylint: disable=import-outside-toplevel
+ from fietsboek import __VERSION__
+
+ return __VERSION__
@view_config(
route_name="admin",
- renderer="fietsboek:templates/admin.jinja2",
+ renderer="fietsboek:templates/admin_overview.jinja2",
+ request_method="GET",
+ permission="admin",
+)
+def admin(request: Request):
+ """Renders the admin overview.
+
+ :param request: The Pyramid request.
+ :return: The HTTP response.
+ """
+ # False-positive with func.count()
+ # pylint: disable=not-callable
+ user_count = request.dbsession.execute(select(func.count()).select_from(models.User)).scalar()
+ track_count = request.dbsession.execute(select(func.count()).select_from(models.Track)).scalar()
+ size_total = request.data_manager.size()
+ size_breakdown = _get_size_breakdown(request.data_manager)
+
+ try:
+ distro = platform.freedesktop_os_release()["PRETTY_NAME"]
+ except OSError:
+ distro = None
+
+ try:
+ last_cronjob_timestamp = float(request.redis.get("last-cronjob"))
+ except (TypeError, ValueError):
+ last_cronjob = None
+ cron_good = False
+ else:
+ last_cronjob = datetime.datetime.fromtimestamp(last_cronjob_timestamp, datetime.UTC)
+ cron_good = (datetime.datetime.now(datetime.UTC) - last_cronjob) < GOOD_CRON_THRESHOLD
+
+ versions = {
+ "fietsboek": _get_fietsboek_version(),
+ "python": platform.python_version(),
+ "linux": platform.platform(),
+ "distro": distro,
+ }
+
+ return {
+ "user_count": user_count,
+ "track_count": track_count,
+ "total_size": size_total,
+ "size_breakdown": size_breakdown,
+ "versions": versions,
+ "last_cronjob": last_cronjob,
+ "cron_good": cron_good,
+ }
+
+
+@view_config(
+ route_name="admin-badge",
+ renderer="fietsboek:templates/admin_badges.jinja2",
request_method="GET",
permission="admin",
)
-def admin(request):
- """Renders the main admin overview.
+def admin_badges(request):
+ """Renders the badges editor.
:param request: The Pyramid request.
:type request: pyramid.request.Request
@@ -47,7 +152,7 @@ def do_badge_add(request):
request.dbsession.add(badge)
request.session.flash(request.localizer.translate(_("flash.badge_added")))
- return HTTPFound(request.route_url("admin"))
+ return HTTPFound(request.route_url("admin-badge"))
@view_config(route_name="admin-badge-edit", permission="admin", request_method="POST")
@@ -71,7 +176,7 @@ def do_badge_edit(request):
badge.title = request.params["badge-title"]
request.session.flash(request.localizer.translate(_("flash.badge_modified")))
- return HTTPFound(request.route_url("admin"))
+ return HTTPFound(request.route_url("admin-badge"))
@view_config(route_name="admin-badge-delete", permission="admin", request_method="POST")
@@ -91,7 +196,7 @@ def do_badge_delete(request):
request.dbsession.delete(badge)
request.session.flash(request.localizer.translate(_("flash.badge_deleted")))
- return HTTPFound(request.route_url("admin"))
+ return HTTPFound(request.route_url("admin-badge"))
-__all__ = ["admin", "do_badge_add", "do_badge_edit", "do_badge_delete"]
+__all__ = ["admin", "admin_badges", "do_badge_add", "do_badge_edit", "do_badge_delete"]
diff --git a/fietsboek/views/errors.py b/fietsboek/views/errors.py
new file mode 100644
index 0000000..39af14e
--- /dev/null
+++ b/fietsboek/views/errors.py
@@ -0,0 +1,32 @@
+"""Error views."""
+
+from pyramid.view import forbidden_view_config, notfound_view_config
+
+
+@notfound_view_config(renderer="fietsboek:templates/404.jinja2")
+def notfound_view(request):
+ """Renders the 404 response.
+
+ :param request: The Pyramid request.
+ :type request: pyramid.request.Request
+ :return: The HTTP response.
+ :rtype: pyramid.response.Response
+ """
+ request.response.status = 404
+ return {}
+
+
+@forbidden_view_config(renderer="fietsboek:templates/403.jinja2")
+def forbidden_view(request):
+ """Renders the 403 response.
+
+ :param request: The Pyramid request.
+ :type request: pyramid.request.Request
+ :return: The HTTP response.
+ :rtype: pyramid.response.Response
+ """
+ request.response.status = 403
+ return {}
+
+
+__all__ = ["notfound_view", "forbidden_view"]
diff --git a/fietsboek/views/notfound.py b/fietsboek/views/notfound.py
deleted file mode 100644
index 2ec6c6c..0000000
--- a/fietsboek/views/notfound.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Error views."""
-
-from pyramid.view import notfound_view_config
-
-
-@notfound_view_config(renderer="fietsboek:templates/404.jinja2")
-def notfound_view(request):
- """Renders the 404 response.
-
- :param request: The Pyramid request.
- :type request: pyramid.request.Request
- :return: The HTTP response.
- :rtype: pyramid.response.Response
- """
- request.response.status = 404
- return {}
-
-
-__all__ = ["notfound_view"]
diff --git a/tests/playwright/test_profiles.py b/tests/playwright/test_profiles.py
index 7e5fb3c..ffbaab0 100644
--- a/tests/playwright/test_profiles.py
+++ b/tests/playwright/test_profiles.py
@@ -5,7 +5,7 @@ def test_forbidden(page: Page, playwright_helper):
john = playwright_helper.john_doe()
with page.expect_response(lambda resp: resp.status == 403):
- page.goto(f"/user/{john.id}")
+ page.goto(f"/user/{john.id}/")
def test_profile(page: Page, playwright_helper):
diff --git a/tests/playwright/test_share.py b/tests/playwright/test_share.py
index de288a0..dcba899 100644
--- a/tests/playwright/test_share.py
+++ b/tests/playwright/test_share.py
@@ -29,7 +29,8 @@ def test_view_wrong_link(page: Page, playwright_helper, dbaccess):
with page.expect_response(lambda resp: resp.status == 403):
page.goto(f"/track/{track.id}?secret=foobar")
- assert "Forbidden" in page.content()
+ assert "No entry" in page.content()
+ assert "not allowed to access" in page.content()
def test_change_link(page: Page, playwright_helper, dbaccess):