From add18020a77d9c5c40fdfce854d52a81296bfdcf Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Wed, 10 May 2023 20:45:49 +0200 Subject: rename user fietsctl commands --- fietsboek/scripts/fietsctl.py | 97 +++++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/fietsboek/scripts/fietsctl.py b/fietsboek/scripts/fietsctl.py index 4255b18..caca0f9 100644 --- a/fietsboek/scripts/fietsctl.py +++ b/fietsboek/scripts/fietsctl.py @@ -38,7 +38,12 @@ def cli(): """CLI main entry point.""" -@cli.command("useradd") +@cli.group("user") +def cmd_user(): + """Management functions for user accounts.""" + + +@cmd_user.command("add") @config_option @click.option("--email", help="email address of the user", prompt=True) @click.option("--name", help="name of the user", prompt=True) @@ -51,7 +56,14 @@ def cli(): ) @click.option("--admin", help="make the new user an admin", is_flag=True) @click.pass_context -def cmd_useradd(ctx: click.Context, config: str, email: str, name: str, password: str, admin: bool): +def cmd_user_add( + ctx: click.Context, + config: str, + email: str, + name: str, + password: str, + admin: bool, +): """Create a new user. This user creation bypasses the "enable_account_registration" setting. It @@ -89,14 +101,14 @@ def cmd_useradd(ctx: click.Context, config: str, email: str, name: str, password click.echo(user_id) -@cli.command("userdel") +@cmd_user.command("del") @config_option @click.option("--force", "-f", help="override the safety check", is_flag=True) @optgroup.group("User selection", cls=RequiredMutuallyExclusiveOptionGroup) @optgroup.option("--id", "-i", "id_", help="database ID of the user", type=int) @optgroup.option("--email", "-e", help="email of the user") @click.pass_context -def cmd_userdel( +def user_del( ctx: click.Context, config: str, force: bool, @@ -131,9 +143,9 @@ def cmd_userdel( click.echo("User deleted") -@cli.command("userlist") +@cmd_user.command("list") @config_option -def cmd_userlist(config: str): +def cmd_user_list(config: str): """Prints a listing of all user accounts. The format is: @@ -156,14 +168,14 @@ def cmd_userlist(config: str): click.echo(f"{tag} {user.id} - {user.email} - {user.name}") -@cli.command("passwd") +@cmd_user.command("passwd") @config_option @click.option("--password", help="password of the user") @optgroup.group("User selection", cls=RequiredMutuallyExclusiveOptionGroup) @optgroup.option("--id", "-i", "id_", help="database ID of the user", type=int) @optgroup.option("--email", "-e", help="email of the user") @click.pass_context -def cmd_passwd( +def cms_user_passwd( ctx: click.Context, config: str, password: Optional[str], @@ -189,38 +201,7 @@ def cmd_passwd( click.echo(f"Changed password of {user.name} ({user.email})") -@cli.command("maintenance-mode") -@config_option -@click.option("--disable", help="disable the maintenance mode", is_flag=True) -@click.argument("reason", required=False) -@click.pass_context -def cmd_maintenance_mode(ctx: click.Context, config: str, disable: bool, reason: Optional[str]): - """Check the status of the maintenance mode, or activate or deactive it. - - When REASON is given, enables the maintenance mode with the given text as - reason. - - With neither --disable nor REASON given, just checks the state of the - maintenance mode. - """ - env = setup(config) - data_manager = env["request"].data_manager - if disable and reason: - click.echo("Cannot enable and disable maintenance mode at the same time", err=True) - ctx.exit(EXIT_FAILURE) - elif not disable and not reason: - maintenance = data_manager.maintenance_mode() - if maintenance is None: - click.echo("Maintenance mode is disabled") - else: - click.echo(f"Maintenance mode is enabled: {maintenance}") - elif disable: - (data_manager.data_dir / "MAINTENANCE").unlink() - else: - (data_manager.data_dir / "MAINTENANCE").write_text(reason, encoding="utf-8") - - -@cli.command("hittekaart") +@cmd_user.command("hittekaart") @config_option @click.option( "--mode", @@ -235,7 +216,7 @@ def cmd_maintenance_mode(ctx: click.Context, config: str, disable: bool, reason: @optgroup.option("--id", "-i", "id_", help="database ID of the user", type=int) @optgroup.option("--email", "-e", help="email of the user") @click.pass_context -def cmd_hittekaart( +def cmd_user_hittekaart( ctx: click.Context, config: str, modes: list[str], @@ -282,8 +263,42 @@ def cmd_hittekaart( click.echo(f"Generated {mode.value}") +@cli.command("maintenance-mode") +@config_option +@click.option("--disable", help="disable the maintenance mode", is_flag=True) +@click.argument("reason", required=False) +@click.pass_context +def cmd_maintenance_mode(ctx: click.Context, config: str, disable: bool, reason: Optional[str]): + """Check the status of the maintenance mode, or activate or deactive it. + + When REASON is given, enables the maintenance mode with the given text as + reason. + + With neither --disable nor REASON given, just checks the state of the + maintenance mode. + """ + env = setup(config) + data_manager = env["request"].data_manager + if disable and reason: + click.echo("Cannot enable and disable maintenance mode at the same time", err=True) + ctx.exit(EXIT_FAILURE) + elif not disable and not reason: + maintenance = data_manager.maintenance_mode() + if maintenance is None: + click.echo("Maintenance mode is disabled") + else: + click.echo(f"Maintenance mode is enabled: {maintenance}") + elif disable: + (data_manager.data_dir / "MAINTENANCE").unlink() + else: + (data_manager.data_dir / "MAINTENANCE").write_text(reason, encoding="utf-8") + + @cli.command("version") def cmd_version(): """Show the installed fietsboek version.""" name = __name__.split(".", 1)[0] print(f"{name} {__VERSION__}") + + +__all__ = ["cli"] -- cgit v1.2.3 From 41e9ca178b1db3d89164ae7d56d749212d5de8d9 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Wed, 10 May 2023 21:10:52 +0200 Subject: add a fietsctl user modify command Currently, there is no way to make a user an admin after creating them - so this changes that! --- fietsboek/scripts/fietsctl.py | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/fietsboek/scripts/fietsctl.py b/fietsboek/scripts/fietsctl.py index caca0f9..9bf8abe 100644 --- a/fietsboek/scripts/fietsctl.py +++ b/fietsboek/scripts/fietsctl.py @@ -16,6 +16,15 @@ EXIT_OKAY = 0 EXIT_FAILURE = 1 +def human_bool(value: bool) -> str: + """Formats a boolean for CLI output. + + :param value: The value to format. + :return: The string representing the bool. + """ + return "yes" if value else "no" + + def setup(config_path: str) -> AppEnvironment: """Sets up the logging and app environment for the scripts. @@ -201,6 +210,45 @@ def cms_user_passwd( click.echo(f"Changed password of {user.name} ({user.email})") +@cmd_user.command("modify") +@config_option +@click.option("--admin/--no-admin", help="make the user an admin", default=None) +@click.option("--verified/--no-verified", help="set the user verification status", default=None) +@optgroup.group("User selection", cls=RequiredMutuallyExclusiveOptionGroup) +@optgroup.option("--id", "-i", "id_", help="database ID of the user", type=int) +@optgroup.option("--email", "-e", help="email of the user") +@click.pass_context +def cms_user_modify( + ctx: click.Context, + config: str, + admin: Optional[bool], + verified: Optional[bool], + id_: Optional[int], + email: Optional[str], +): + """Modify a user.""" + env = setup(config) + if id_ is not None: + query = select(models.User).filter_by(id=id_) + else: + query = models.User.query_by_email(email) + with env["request"].tm: + dbsession = env["request"].dbsession + user = dbsession.execute(query).scalar_one_or_none() + if user is None: + click.echo("Error: No such user found.", err=True) + ctx.exit(EXIT_FAILURE) + + if admin is not None: + user.is_admin = admin + if verified is not None: + user.is_verified = verified + + click.echo(f"{user.name} - {user.email}") + click.echo(f"Is admin: {human_bool(user.is_admin)}") + click.echo(f"Is verified: {human_bool(user.is_verified)}") + + @cmd_user.command("hittekaart") @config_option @click.option( -- cgit v1.2.3 From 56acb746a8eecd1d8180ebd34bf757647be48efd Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Wed, 10 May 2023 21:22:23 +0200 Subject: touch up fietsctl help strings This makes them a bit more consistent with the command docstrings (even though they are not always complete sentences). --- fietsboek/scripts/fietsctl.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/fietsboek/scripts/fietsctl.py b/fietsboek/scripts/fietsctl.py index 9bf8abe..39d9e8a 100644 --- a/fietsboek/scripts/fietsctl.py +++ b/fietsboek/scripts/fietsctl.py @@ -54,8 +54,8 @@ def cmd_user(): @cmd_user.command("add") @config_option -@click.option("--email", help="email address of the user", prompt=True) -@click.option("--name", help="name of the user", prompt=True) +@click.option("--email", help="Email address of the user.", prompt=True) +@click.option("--name", help="Name of the user.", prompt=True) @click.option( "--password", help="password of the user", @@ -63,7 +63,7 @@ def cmd_user(): hide_input=True, confirmation_prompt=True, ) -@click.option("--admin", help="make the new user an admin", is_flag=True) +@click.option("--admin", help="Make the new user an admin.", is_flag=True) @click.pass_context def cmd_user_add( ctx: click.Context, @@ -112,10 +112,10 @@ def cmd_user_add( @cmd_user.command("del") @config_option -@click.option("--force", "-f", help="override the safety check", is_flag=True) +@click.option("--force", "-f", help="Override the safety check.", is_flag=True) @optgroup.group("User selection", cls=RequiredMutuallyExclusiveOptionGroup) -@optgroup.option("--id", "-i", "id_", help="database ID of the user", type=int) -@optgroup.option("--email", "-e", help="email of the user") +@optgroup.option("--id", "-i", "id_", help="Database ID of the user.", type=int) +@optgroup.option("--email", "-e", help="Email address of the user.") @click.pass_context def user_del( ctx: click.Context, @@ -179,10 +179,10 @@ def cmd_user_list(config: str): @cmd_user.command("passwd") @config_option -@click.option("--password", help="password of the user") +@click.option("--password", help="Password of the user.") @optgroup.group("User selection", cls=RequiredMutuallyExclusiveOptionGroup) -@optgroup.option("--id", "-i", "id_", help="database ID of the user", type=int) -@optgroup.option("--email", "-e", help="email of the user") +@optgroup.option("--id", "-i", "id_", help="Database ID of the user,", type=int) +@optgroup.option("--email", "-e", help="Email address of the user.") @click.pass_context def cms_user_passwd( ctx: click.Context, @@ -212,11 +212,11 @@ def cms_user_passwd( @cmd_user.command("modify") @config_option -@click.option("--admin/--no-admin", help="make the user an admin", default=None) -@click.option("--verified/--no-verified", help="set the user verification status", default=None) +@click.option("--admin/--no-admin", help="Make the user an admin.", default=None) +@click.option("--verified/--no-verified", help="Set the user verification status.", default=None) @optgroup.group("User selection", cls=RequiredMutuallyExclusiveOptionGroup) -@optgroup.option("--id", "-i", "id_", help="database ID of the user", type=int) -@optgroup.option("--email", "-e", help="email of the user") +@optgroup.option("--id", "-i", "id_", help="Database ID of the user.", type=int) +@optgroup.option("--email", "-e", help="Email address of the user.") @click.pass_context def cms_user_modify( ctx: click.Context, @@ -254,15 +254,15 @@ def cms_user_modify( @click.option( "--mode", "modes", - help="Heatmap type to generate", + help="Heatmap type to generate.", type=click.Choice([mode.value for mode in hittekaart.Mode]), multiple=True, default=["heatmap"], ) -@click.option("--delete", help="Delete the specified heatmap", is_flag=True) +@click.option("--delete", help="Delete the specified heatmap.", is_flag=True) @optgroup.group("User selection", cls=RequiredMutuallyExclusiveOptionGroup) -@optgroup.option("--id", "-i", "id_", help="database ID of the user", type=int) -@optgroup.option("--email", "-e", help="email of the user") +@optgroup.option("--id", "-i", "id_", help="Database ID of the user.", type=int) +@optgroup.option("--email", "-e", help="Email address of the user.") @click.pass_context def cmd_user_hittekaart( ctx: click.Context, @@ -313,11 +313,11 @@ def cmd_user_hittekaart( @cli.command("maintenance-mode") @config_option -@click.option("--disable", help="disable the maintenance mode", is_flag=True) +@click.option("--disable", help="Disable the maintenance mode.", is_flag=True) @click.argument("reason", required=False) @click.pass_context def cmd_maintenance_mode(ctx: click.Context, config: str, disable: bool, reason: Optional[str]): - """Check the status of the maintenance mode, or activate or deactive it. + """Controls the maintenance mode. When REASON is given, enables the maintenance mode with the given text as reason. -- cgit v1.2.3 From ec8235b950ce730adc0703f98db15d7d6f782383 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Wed, 10 May 2023 21:28:55 +0200 Subject: update fietsctl invocation in the docs --- doc/administration/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/administration/installation.rst b/doc/administration/installation.rst index 53734b5..fd0def5 100644 --- a/doc/administration/installation.rst +++ b/doc/administration/installation.rst @@ -96,7 +96,7 @@ You can use the ``fietsctl`` command line program to add administrator users: .. code:: bash - .venv/bin/fietsctl -c production.ini useradd --admin + .venv/bin/fietsctl user add -c production.ini --admin Running Fietsboek ----------------- -- cgit v1.2.3 From 673ec68f5cfc3f15790a1b979a3719d99d56a722 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 11 May 2023 23:14:45 +0200 Subject: add fietsctl track list Especially the sizes might be useful to have, we should also add this to the admin web view at some point. --- fietsboek/data.py | 11 +++++++++++ fietsboek/scripts/fietsctl.py | 35 ++++++++++++++++++++++++++++++++++- fietsboek/util.py | 19 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/fietsboek/data.py b/fietsboek/data.py index 7457986..6bb2c8c 100644 --- a/fietsboek/data.py +++ b/fietsboek/data.py @@ -5,6 +5,7 @@ the database itself. This module makes access to such data objects easier. """ import datetime import logging +import os import random import shutil import string @@ -242,6 +243,16 @@ class TrackDataDir: shutil.move(self.path, new_name) self.journal.append(("purge", new_name)) + def size(self) -> int: + """Returns the size of the data that this track entails. + + :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 + def gpx_path(self) -> Path: """Returns the path of the GPX file. diff --git a/fietsboek/scripts/fietsctl.py b/fietsboek/scripts/fietsctl.py index 39d9e8a..913da69 100644 --- a/fietsboek/scripts/fietsctl.py +++ b/fietsboek/scripts/fietsctl.py @@ -8,7 +8,7 @@ from pyramid.paster import bootstrap, setup_logging from pyramid.scripting import AppEnvironment from sqlalchemy import select -from .. import __VERSION__, hittekaart, models +from .. import __VERSION__, hittekaart, models, util from ..data import DataManager from . import config_option @@ -311,6 +311,39 @@ def cmd_user_hittekaart( click.echo(f"Generated {mode.value}") +@cli.group("track") +def cmd_track(): + """Management functions for tracks.""" + + +@cmd_track.command("list") +@config_option +def cmd_track_list(config: str): + """List all tracks that are present in the system.""" + env = setup(config) + total_size = 0 + total_tracks = 0 + with env["request"].tm: + dbsession = env["request"].dbsession + data_manager: DataManager = env["request"].data_manager + tracks = dbsession.execute(select(models.Track)).scalars() + for track in tracks: + total_tracks += 1 + try: + track_size = data_manager.open(track.id).size() + except FileNotFoundError: + size = "---" + else: + total_size += track_size + size = util.human_size(track_size) + click.echo( + f"{track.id:>4} - {size:>10} - {track.owner.name} <{track.owner.email}>" + f" - {track.title}" + ) + click.echo("-" * 80) + click.echo(f"Total: {total_tracks} - {util.human_size(total_size)}") + + @cli.command("maintenance-mode") @config_option @click.option("--disable", help="Disable the maintenance mode.", is_flag=True) diff --git a/fietsboek/util.py b/fietsboek/util.py index a296151..ecd2f31 100644 --- a/fietsboek/util.py +++ b/fietsboek/util.py @@ -202,6 +202,25 @@ def mps_to_kph(mps: float) -> float: return mps / 1000 * 60 * 60 +def human_size(num_bytes: int) -> str: + """Formats the amount of bytes for human consumption. + + :param num_bytes: The amount of bytes. + :return: The formatted amount. + """ + num_bytes = float(num_bytes) + suffixes = ["B", "KiB", "MiB", "GiB"] + for suffix in suffixes: + if num_bytes < 1024 or suffix == suffixes[-1]: + if suffix == "B": + # Don't do the decimal point for bytes + return f"{int(num_bytes)} {suffix}" + return f"{num_bytes:.1f} {suffix}" + num_bytes /= 1024 + # Unreachable: + return "" + + def month_name(request: Request, month: int) -> str: """Returns the localized name for the month with the given number. -- cgit v1.2.3 From 27963e61e0626733f3b85b4ddde373fb1b17e13a Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 11 May 2023 23:23:32 +0200 Subject: make fietsctl output a bit more colorful --- fietsboek/scripts/fietsctl.py | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/fietsboek/scripts/fietsctl.py b/fietsboek/scripts/fietsctl.py index 913da69..4950f7e 100644 --- a/fietsboek/scripts/fietsctl.py +++ b/fietsboek/scripts/fietsctl.py @@ -15,6 +15,13 @@ from . import config_option EXIT_OKAY = 0 EXIT_FAILURE = 1 +FG_USER_NAME = "yellow" +FG_USER_EMAIL = "yellow" +FG_USER_TAG = "red" +FG_SIZE = "cyan" +FG_TRACK_TITLE = "green" +FG_TRACK_SIZE = "bright_red" + def human_bool(value: bool) -> str: """Formats a boolean for CLI output. @@ -142,7 +149,7 @@ def user_del( if user is None: click.echo("Error: No such user found.", err=True) ctx.exit(EXIT_FAILURE) - click.echo(user.name) + click.secho(user.name, fg=FG_USER_NAME) click.echo(user.email) if not force: if not click.confirm("Really delete this user?"): @@ -174,7 +181,10 @@ def cmd_user_list(config: str): "a" if user.is_admin else "-", "v" if user.is_verified else "-", ) - click.echo(f"{tag} {user.id} - {user.email} - {user.name}") + tag = click.style(tag, fg=FG_USER_TAG) + user_email = click.style(user.email, fg=FG_USER_EMAIL) + user_name = click.style(user.name, fg=FG_USER_NAME) + click.echo(f"{tag} {user.id} - {user_email} - {user_name}") @cmd_user.command("passwd") @@ -207,7 +217,9 @@ def cms_user_passwd( password = click.prompt("Password", hide_input=True, confirmation_prompt=True) user.set_password(password) - click.echo(f"Changed password of {user.name} ({user.email})") + user_name = click.style(user.name, fg=FG_USER_NAME) + user_email = click.style(user.email, fg=FG_USER_EMAIL) + click.echo(f"Changed password of {user_name} ({user_email})") @cmd_user.command("modify") @@ -244,7 +256,9 @@ def cms_user_modify( if verified is not None: user.is_verified = verified - click.echo(f"{user.name} - {user.email}") + user_name = click.style(user.name, fg=FG_USER_NAME) + user_email = click.style(user.email, fg=FG_USER_EMAIL) + click.echo(f"{user_name} - {user_email}") click.echo(f"Is admin: {human_bool(user.is_admin)}") click.echo(f"Is verified: {human_bool(user.is_verified)}") @@ -302,7 +316,7 @@ def cmd_user_hittekaart( user_manager.tilehunt_path().unlink(missing_ok=True) return - click.echo(f"Generating overlay maps for {user.name}...") + click.echo(f"Generating overlay maps for {click.style(user.name, fg=FG_USER_NAME)}...") for mode in modes: hittekaart.generate_for( @@ -336,12 +350,15 @@ def cmd_track_list(config: str): else: total_size += track_size size = util.human_size(track_size) - click.echo( - f"{track.id:>4} - {size:>10} - {track.owner.name} <{track.owner.email}>" - f" - {track.title}" - ) + size = click.style(f"{size:>10}", fg=FG_TRACK_SIZE) + owner_name = click.style(track.owner.name, fg=FG_USER_NAME) + owner_email = click.style(track.owner.email, fg=FG_USER_EMAIL) + track_title = click.style(track.title, fg=FG_TRACK_TITLE) + click.echo(f"{track.id:>4} - {size} - {owner_name} <{owner_email}> - {track_title}") click.echo("-" * 80) - click.echo(f"Total: {total_tracks} - {util.human_size(total_size)}") + click.echo( + f"Total: {total_tracks} - {click.style(util.human_size(total_size), fg=FG_TRACK_SIZE)}" + ) @cli.command("maintenance-mode") -- cgit v1.2.3 From dca030a075d2a1b12f8ca902ffb44fa1eeb6b718 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Fri, 12 May 2023 23:45:25 +0200 Subject: add unit tests for util.human_size --- fietsboek/util.py | 8 ++++++-- tests/unit/test_util.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/fietsboek/util.py b/fietsboek/util.py index ecd2f31..acbb45d 100644 --- a/fietsboek/util.py +++ b/fietsboek/util.py @@ -210,12 +210,16 @@ def human_size(num_bytes: int) -> str: """ num_bytes = float(num_bytes) suffixes = ["B", "KiB", "MiB", "GiB"] + prefix = "" + if num_bytes < 0: + prefix = "-" + num_bytes = -num_bytes for suffix in suffixes: if num_bytes < 1024 or suffix == suffixes[-1]: if suffix == "B": # Don't do the decimal point for bytes - return f"{int(num_bytes)} {suffix}" - return f"{num_bytes:.1f} {suffix}" + return f"{prefix}{int(num_bytes)} {suffix}" + return f"{prefix}{num_bytes:.1f} {suffix}" num_bytes /= 1024 # Unreachable: return "" diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index 1a56911..6dc8e7d 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -88,6 +88,22 @@ def test_tour_metadata(gpx_file): def test_mps_to_kph(mps, kph): assert util.mps_to_kph(mps) == pytest.approx(kph, 0.1) +@pytest.mark.parametrize('num_bytes, expected', [ + (1, '1 B'), + (1023, '1023 B'), + (1024, '1.0 KiB'), + (1536, '1.5 KiB'), + (1024 ** 2, '1.0 MiB'), + (1024 ** 3, '1.0 GiB'), + (0, '0 B'), + # Negative sizes in itself are a bit weird, but they make sense as the + # difference between two size values, so they should still work. + (-1, '-1 B'), + (-1024, '-1.0 KiB'), +]) +def test_human_size(num_bytes, expected): + assert util.human_size(num_bytes) == expected + def test_tile_url(app_request): route_url = util.tile_url(app_request, "tile-proxy", provider="bobby") -- cgit v1.2.3 From 55f8e9eae38d908e47badaf42c456d47174fb4d9 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sat, 13 May 2023 16:01:56 +0200 Subject: add a manpage for fietsctl It's a start, now it's there and can be improved! --- doc/index.rst | 1 + doc/man.rst | 15 ++++ doc/man/fietsctl.rst | 245 +++++++++++++++++++++++++++++++++++++++++++++++++++ tox.ini | 6 +- 4 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 doc/man.rst create mode 100644 doc/man/fietsctl.rst diff --git a/doc/index.rst b/doc/index.rst index 0b69ef1..73dce3f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -13,6 +13,7 @@ Welcome to Fietsboek's documentation! administration developer user + man .. toctree:: :maxdepth: 1 diff --git a/doc/man.rst b/doc/man.rst new file mode 100644 index 0000000..03c7f78 --- /dev/null +++ b/doc/man.rst @@ -0,0 +1,15 @@ +Manpages +======== + +.. toctree:: + :maxdepth: 1 + + man/fietsctl + +In this section, you will find the manpages for the various commands that are +provided in Fietsboek. + +Each of those pages can also be turned into a "proper" manpage using +``rst2man``:: + + rst2man.py doc/man/fietsctl.rst fietsctl.1 diff --git a/doc/man/fietsctl.rst b/doc/man/fietsctl.rst new file mode 100644 index 0000000..4d3683f --- /dev/null +++ b/doc/man/fietsctl.rst @@ -0,0 +1,245 @@ +fietsctl +======== + +Control utility for Fietsboek +----------------------------- +:Manual section: 1 + +SYNPOSIS +******** + +.. code-block:: text + + fietsctl maintenance-mode + fietsctl track list + fietsctl user {add|del|hittekaart|list|modify|passwd} + fietsctl version + +DESCRIPTION +*********** + +The ``fietsctl`` script is provided for administrative purposes. It allows you +to manage the state and content of a Fietsboek instance from the command line. + +.. warning:: + + The ``fietsctl`` script does not do any access checking and does not + require and logins or passwords. You must use the permissions of your + system and database server to restrict the access to ``fietsctl`` by + restricting access to the data stores directly. + +Detailed versions of the commands are described below. + +Note that most commands expect the path to the configuration file to be given +(e.g. ``-c production.ini``). The default uses ``fietsboek.ini``. This can be +overridden using the ``-c``/``--config`` option. + +.. note:: + + All commands support the ``--help`` option, which will give you a quick + overview of how the command works and which options are available. + +USER MANAGEMENT +*************** + +You can use the ``fietsctl user`` subcommand to manage the users in the +Fietsboek system. + +Many functions which deal with existing users (delete, modify, ...) allow the +user to be specified either by their ID using the ``-i``/``--id`` option, or by +their email address using ``-e``/``--email``. You can obtain the IDs of users +using the ``fietsctl user list`` command. + +ADDING A USER +############# + +.. code-block:: text + + fietsctl user add [-c CONFIG] [--email EMAIL] [--password PASSWORD] [--admin] + +This command adds a user to the system. It can be called with no arguments, in +which case all required values are prompted for. + +If the new user should be made an admin, use the ``--admin`` flag. If not +given, the user will *not* be made an admin. In any case, the user is +automatically verified. If you want to change the admin or verification status +after creating a user, use the ``fietsctl user modify`` command (see below). + +It is advised that you do not supply the password on the command line to +prevent it from appearing in the command history. Either disable the history, +or leave out the password (in which case you will be prompted). + +Note that this function does not check the ``enable_account_registration`` +setting, so it can always be used to add new users to the system. + +Note further that this function does less checks then the web interface (e.g. +it does not require an email verification), so be careful which values you +enter. + +REMOVING A USER +############### + +.. code-block:: text + + fietsctl user del [-c CONFIG] [-f/--force] [-i/--id ID] [-e/--email EMAIL] + +Removes a user from the system. This will remove the user account and all +associated data (the user's tracks, comments, ...). + +By default, the command will ask for confirmation. You can specify the +``-f``/``--force`` flag to override this check. + +GENERATING HEATMAPS +################### + +.. code-block:: text + + fietsctl user hittekaart [-c CONFIG] [-i/--id ID] [-e/--email EMAIL] [--delete] [--mode heatmap|tilehunt] + +With ``fiettsctl user hittekaart`` you can force a hittekaart run for a +specific user. By default, only the heatmap is generated, but you can use +``--mode`` to select which overlay map you want to generate. You can also +specify ``--mode`` multiple times to generate multiple heat maps with a single +invocation. + +If you want to delete a heatmap, use the ``--delete`` option. It also respects +the ``--mode`` selection, so you can delete individual types of heatmaps. + +LISTING USERS +############# + +.. code-block:: text + + fietsctl user list [-c CONFIG] + +Outputs a list of all users in the system, one user per line. Each line +consists of: + +.. code-block:: text + + [av] ID - EMAIL - NAME + +The "a" indicates that the user has admin permissions. If the user has no admin +permissions, a "-" is shown instead. + +The "v" indicates that the user has their email address verified. If the user +has not verified their email address, a "-" is shown instead. + +MODIFYING USERS +############### + +.. code-block:: text + + fietsctl user modify [-c CONFIG] [-i/--id ID] [-e/--email EMAIL] [--admin/--no-admin] [--verified/--no-verified] + +Modifies a user. This can currently be used to set the admin and verification +status of a user. If you want to change the password, use ``fietsctl user +passwd`` (see below). You cannot currently change the email address or name of +a user with this command (note that the ``--email`` option is for user +*selection*, not *modification*). + +If you do not specifiy either ``--admin`` or ``--no-admin``, then the current +value of the user is not changed. The same goes for ``--verified`` and +``--no-verified``, if neither is given, the current value is not changed. + +CHANGING USER PASSWORDS +####################### + +.. code-block:: text + + fietsctl user passwd [-c CONFIG] [-i/--id ID] [-e/--email EMAIL] [--password PASSWORD] + +Changes the password of the specified user. If the password is not given via +``--password``, then the password is interactively prompted for. Be careful +when using ``--password`` as sensitive information might end up in the shell +history! + +Note that this function does fewer checks than the web interface, as such it is +possible to set passwords that users cannot set themselves (e.g. very short +ones). + +TRACK MANAGEMENT +**************** + +The ``fietsctl track`` subcommand can be used to manage the tracks. + +LISTING TRACKS +############## + +.. code-block:: text + + fietsctl track list [-c CONFIG] + +Lists all tracks in the system. This outputs one line per track, plus final +summary information. + +For each track, the following information is shown: + +* The track's ID +* The size of the track's data (this includes the size of the data directory, + but not the size of the database elements) +* The track's owner (both name and email address) +* The track's title. + +MAINTENANCE MODE +**************** + +The ``fietsctl maintenance-mode`` subcommand manages the maintenance mode. + +ACTIVATING MAINTENANCE +###################### + +.. code-block:: text + + fietsctl maintenance-mode [-c CONFIG] REASON + +Enables the maintenance mode with the given reason. The reason should be a +short string that describes why Fietsboek is disabled. It will be shown to the +users on the error page. + +CHECKING MAINTENANCE +#################### + +.. code-block:: text + + fietsctl maintenance-mode [-c CONFIG] + +Without the reason, the command will output the current status of the +maintenance mode. + +DEACTIVATING MAINTENANCE +######################## + +.. code-block:: text + + fietsctl maintenance-mode [-c CONFIG] --disable + +With the ``--disable`` option, the maintenance mode will be disabled. + +BUGS +**** + +Bugs can be reported either via the issue tracker +(https://gitlab.com/dunj3/fietsboek/-/issues) or via email (fietsboek at +kingdread dot de). + +AUTHOR +****** + +This program is part of Fietsboek, written by the Fietsboek authors. + +COPYRIGHT +********* + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU Affero General Public License as published by the Free +Software Foundation, either version 3 of the License, or (at your option) any +later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU Affero General Public License for more +details. + +You should have received a copy of the GNU Affero General Public License along +with this program. If not, see . diff --git a/tox.ini b/tox.ini index 9eb73d0..d922d4b 100644 --- a/tox.ini +++ b/tox.ini @@ -44,13 +44,17 @@ commands = flake8 fietsboek [testenv:sphinx] -allowlist_externals = make +allowlist_externals = + make + mkdir changedir={toxinidir}{/}doc commands_pre = poetry install -v --with docs commands = sphinx-apidoc -d 1 -f -M -e -o developer/module/ ../fietsboek "upd_*" make html + mkdir -p _build/man + rst2man.py man/fietsctl.rst _build/man/fietsctl.1 [testenv:mypy] commands_pre = -- cgit v1.2.3 From 8160c375f448f4e6e32518d632e3bc0d139f0130 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Tue, 16 May 2023 20:13:44 +0200 Subject: add fietsctl track del --- doc/man/fietsctl.rst | 17 ++++++++++++++++- fietsboek/scripts/fietsctl.py | 44 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/doc/man/fietsctl.rst b/doc/man/fietsctl.rst index 4d3683f..67cbed4 100644 --- a/doc/man/fietsctl.rst +++ b/doc/man/fietsctl.rst @@ -11,7 +11,7 @@ SYNPOSIS .. code-block:: text fietsctl maintenance-mode - fietsctl track list + fietsctl track {del|list} fietsctl user {add|del|hittekaart|list|modify|passwd} fietsctl version @@ -181,6 +181,21 @@ For each track, the following information is shown: * The track's owner (both name and email address) * The track's title. +REMOVING A TRACK +################ + +.. code-block:: text + + fietsctl track del [-c CONFIG] -i/--id ID [-f/--force] + +Deletes the specified track. The right ID can be found via the ``track list`` +command, or via the web interface. + +This command deletes the track including its pictures and comments. + +By default, the command will ask for confirmation. You can specify the +``-f``/``--force`` flag to override this check. + MAINTENANCE MODE **************** diff --git a/fietsboek/scripts/fietsctl.py b/fietsboek/scripts/fietsctl.py index 4950f7e..59462df 100644 --- a/fietsboek/scripts/fietsctl.py +++ b/fietsboek/scripts/fietsctl.py @@ -1,5 +1,6 @@ """Script to do maintenance work on a Fietsboek instance.""" # pylint: disable=too-many-arguments +import logging from typing import Optional import click @@ -12,6 +13,8 @@ from .. import __VERSION__, hittekaart, models, util from ..data import DataManager from . import config_option +LOGGER = logging.getLogger("fietsctl") + EXIT_OKAY = 0 EXIT_FAILURE = 1 @@ -361,6 +364,47 @@ def cmd_track_list(config: str): ) +@cmd_track.command("del") +@config_option +@click.option("--force", "-f", help="Override the safety check.", is_flag=True) +@click.option("--id", "-i", "id_", help="Database ID of the track.", type=int, required=True) +@click.pass_context +def cmd_track_del( + ctx: click.Context, + config: str, + force: bool, + id_: Optional[int], +): + """Delete a track. + + This command deletes the track as well as any images and comments + associated with it. + + This command is destructive and irreversibly deletes data. + """ + env = setup(config) + query = select(models.Track).filter_by(id=id_) + with env["request"].tm: + dbsession = env["request"].dbsession + track = dbsession.execute(query).scalar_one_or_none() + if track is None: + click.echo("Error: No such track found.", err=True) + ctx.exit(EXIT_FAILURE) + click.secho(track.title, fg=FG_TRACK_TITLE) + if not force: + if not click.confirm("Really delete this track?"): + click.echo("Aborted by user.") + ctx.exit(EXIT_FAILURE) + try: + data = env["request"].data_manager.open(track.id) + except FileNotFoundError: + LOGGER.warning("Data directory not found for track - ignoring") + else: + data.purge() + dbsession.delete(track) + click.echo("Track deleted") + + @cli.command("maintenance-mode") @config_option @click.option("--disable", help="Disable the maintenance mode.", is_flag=True) -- cgit v1.2.3