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.mobin16599 -> 17659 bytes
-rw-r--r--fietsboek/locale/de/LC_MESSAGES/messages.po90
-rw-r--r--fietsboek/locale/en/LC_MESSAGES/messages.mobin15551 -> 16586 bytes
-rw-r--r--fietsboek/locale/en/LC_MESSAGES/messages.po90
-rw-r--r--fietsboek/locale/fietslog.pot90
-rw-r--r--fietsboek/routes.py9
-rw-r--r--fietsboek/scripts/fietscron.py4
-rw-r--r--fietsboek/static/theme.css20
-rw-r--r--fietsboek/static/theme.css.map2
-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
16 files changed, 583 insertions, 91 deletions
diff --git a/asset-sources/theme.scss b/asset-sources/theme.scss
index 7f89bf6..2fcb278 100644
--- a/asset-sources/theme.scss
+++ b/asset-sources/theme.scss
@@ -284,6 +284,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 a7e9b19..459d0ce 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.
@@ -252,10 +272,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 bda8bef..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 b621d1c..3319052 100644
--- a/fietsboek/locale/de/LC_MESSAGES/messages.po
+++ b/fietsboek/locale/de/LC_MESSAGES/messages.po
@@ -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"
@@ -97,30 +97,98 @@ msgstr "Bitte such einen anderen Weg."
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"
@@ -912,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 7b6e259..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 7cc4b66..4e9f302 100644
--- a/fietsboek/locale/en/LC_MESSAGES/messages.po
+++ b/fietsboek/locale/en/LC_MESSAGES/messages.po
@@ -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"
@@ -97,30 +97,98 @@ msgstr "Please choose a different path."
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"
@@ -902,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 f8e3670..1a1770b 100644
--- a/fietsboek/locale/fietslog.pot
+++ b/fietsboek/locale/fietslog.pot
@@ -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 ""
@@ -93,30 +93,98 @@ msgstr ""
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 ""
@@ -890,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 d5caef8..a327f71 100644
--- a/fietsboek/routes.py
+++ b/fietsboek/routes.py
@@ -49,10 +49,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 7687e12..923885f 100644
--- a/fietsboek/scripts/fietscron.py
+++ b/fietsboek/scripts/fietscron.py
@@ -53,10 +53,12 @@ def cli(config):
remove_old_tokens(engine)
rebuild_cache(engine, data_manager)
+ redis = mod_redis.from_url(config.redis_url)
if config.hittekaart_autogenerate:
- redis = mod_redis.from_url(config.redis_url)
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/theme.css b/fietsboek/static/theme.css
index 2298b49..f048267 100644
--- a/fietsboek/static/theme.css
+++ b/fietsboek/static/theme.css
@@ -272,6 +272,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 08ac64f..e3b2de3 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;;;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;;;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/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"]