diff options
-rw-r--r-- | asset-sources/theme.scss | 20 | ||||
-rw-r--r-- | fietsboek/data.py | 27 | ||||
-rw-r--r-- | fietsboek/locale/de/LC_MESSAGES/messages.mo | bin | 16599 -> 17659 bytes | |||
-rw-r--r-- | fietsboek/locale/de/LC_MESSAGES/messages.po | 90 | ||||
-rw-r--r-- | fietsboek/locale/en/LC_MESSAGES/messages.mo | bin | 15551 -> 16586 bytes | |||
-rw-r--r-- | fietsboek/locale/en/LC_MESSAGES/messages.po | 90 | ||||
-rw-r--r-- | fietsboek/locale/fietslog.pot | 90 | ||||
-rw-r--r-- | fietsboek/routes.py | 9 | ||||
-rw-r--r-- | fietsboek/scripts/fietscron.py | 4 | ||||
-rw-r--r-- | fietsboek/static/theme.css | 20 | ||||
-rw-r--r-- | fietsboek/static/theme.css.map | 2 | ||||
-rw-r--r-- | fietsboek/templates/admin.jinja2 | 49 | ||||
-rw-r--r-- | fietsboek/templates/admin_badges.jinja2 | 45 | ||||
-rw-r--r-- | fietsboek/templates/admin_overview.jinja2 | 91 | ||||
-rw-r--r-- | fietsboek/util.py | 14 | ||||
-rw-r--r-- | fietsboek/views/admin.py | 123 |
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 Binary files differindex bda8bef..f2d0f60 100644 --- a/fietsboek/locale/de/LC_MESSAGES/messages.mo +++ b/fietsboek/locale/de/LC_MESSAGES/messages.mo diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.po b/fietsboek/locale/de/LC_MESSAGES/messages.po index 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 Binary files differindex 7b6e259..fd67c57 100644 --- a/fietsboek/locale/en/LC_MESSAGES/messages.mo +++ b/fietsboek/locale/en/LC_MESSAGES/messages.mo diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.po b/fietsboek/locale/en/LC_MESSAGES/messages.po index 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") }}… +</p> + +<p class="admin-stat"> + … {{ ngettext("admin.overview.stat.user", "admin.overview.stat.users", user_count) }} +</p> + +<p class="admin-stat"> + … {{ ngettext("admin.overview.stat.track", "admin.overview.stat.tracks", track_count) }} +</p> + +<p class="admin-stat"> + … {{ (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"] |