Skip to content

Commit ee62dca

Browse files
feat: feat: Add remove all users blong to an admin to tui and cil
1 parent 1eec68a commit ee62dca

File tree

3 files changed

+169
-30
lines changed

3 files changed

+169
-30
lines changed

cli/admin.py

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,58 @@ async def delete_admin(self, db, username: str):
117117

118118
# Check if admin exists
119119
admins = await admin_op.get_admins(db)
120-
if not any(admin.username == username for admin in admins):
120+
target_admin = next((admin for admin in admins if admin.username == username), None)
121+
if not target_admin:
121122
self.console.print(f"[red]Admin '{username}' not found[/red]")
122123
return
123124

125+
user_count = len(target_admin.users or [])
126+
124127
if typer.confirm(f"Are you sure you want to delete admin '{username}'?"):
128+
if user_count > 0:
129+
message = (
130+
f"Admin '{username}' owns {user_count} users. Delete all of their users before removing the admin?"
131+
)
132+
delete_users = typer.confirm(message, default=False)
133+
if delete_users:
134+
try:
135+
await admin_op.remove_all_users(db, username, SYSTEM_ADMIN)
136+
self.console.print(f"[green]Deleted {user_count} users belonging to admin '{username}'[/green]")
137+
except Exception as e:
138+
self.console.print(f"[red]Error deleting users: {e}[/red]")
139+
return
140+
125141
try:
126142
await admin_op.remove_admin(db, username, SYSTEM_ADMIN)
127143
self.console.print(f"[green]Admin '{username}' deleted successfully[/green]")
128144
except Exception as e:
129145
self.console.print(f"[red]Error deleting admin: {e}[/red]")
130146

147+
async def delete_admin_users(self, db, username: str):
148+
"""Delete all users belonging to an admin."""
149+
admin_op = get_admin_operation()
150+
151+
admins = await admin_op.get_admins(db)
152+
target_admin = next((admin for admin in admins if admin.username == username), None)
153+
if not target_admin:
154+
self.console.print(f"[red]Admin '{username}' not found[/red]")
155+
return
156+
157+
if not typer.confirm(
158+
f"Delete all users belonging to admin '{username}'? This action cannot be undone.", default=False
159+
):
160+
self.console.print("[yellow]Operation cancelled[/yellow]")
161+
return
162+
163+
try:
164+
deleted = await admin_op.remove_all_users(db, username, SYSTEM_ADMIN)
165+
if deleted == 0:
166+
self.console.print(f"[yellow]Admin '{username}' has no users to delete[/yellow]")
167+
else:
168+
self.console.print(f"[green]Deleted {deleted} users belonging to admin '{username}'[/green]")
169+
except Exception as e:
170+
self.console.print(f"[red]Error deleting users: {e}[/red]")
171+
131172
async def modify_admin(self, db, username: str, disable: bool):
132173
"""Modify an admin account."""
133174
admin_op = get_admin_operation()
@@ -193,17 +234,15 @@ async def modify_admin(self, db, username: str, disable: bool):
193234
self.console.print("\n[cyan]Notification Preferences:[/cyan]")
194235
enable_notifications = typer.confirm(
195236
"Enable user notifications for this admin?",
196-
default=any(
197-
[
198-
current_admin.notification_enable["create"],
199-
current_admin.notification_enable["modify"],
200-
current_admin.notification_enable["delete"],
201-
current_admin.notification_enable["status_change"],
202-
current_admin.notification_enable["reset_data_usage"],
203-
current_admin.notification_enable["data_reset_by_next"],
204-
current_admin.notification_enable["subscription_revoked"],
205-
]
206-
),
237+
default=any([
238+
current_admin.notification_enable["create"],
239+
current_admin.notification_enable["modify"],
240+
current_admin.notification_enable["delete"],
241+
current_admin.notification_enable["status_change"],
242+
current_admin.notification_enable["reset_data_usage"],
243+
current_admin.notification_enable["data_reset_by_next"],
244+
current_admin.notification_enable["subscription_revoked"],
245+
]),
207246
)
208247

209248
if enable_notifications:
@@ -318,6 +357,15 @@ async def delete_admin(username: str):
318357
console.print(f"[red]Error: {e}[/red]")
319358

320359

360+
async def delete_admin_users(username: str):
361+
"""Delete all users belonging to an admin."""
362+
async with GetDB() as db:
363+
try:
364+
await admin_cli.delete_admin_users(db, username)
365+
except Exception as e:
366+
console.print(f"[red]Error: {e}[/red]")
367+
368+
321369
async def modify_admin(
322370
username: str,
323371
disable: Annotated[bool, typer.Option(..., "--disable", help="Disable or enable the admin account.")] = None,

cli/main.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import typer
1212

1313
from cli import console
14-
from cli.admin import create_admin, delete_admin, list_admins, modify_admin, reset_admin_usage
14+
from cli.admin import create_admin, delete_admin, delete_admin_users, list_admins, modify_admin, reset_admin_usage
1515
from cli.system import show_status
1616

1717
# Initialize Typer app
@@ -37,18 +37,23 @@ def admins(
3737
create: Optional[str] = typer.Option(None, "--create", "-c", help="Create new admin"),
3838
sudo: bool = typer.Option(False, "--sudo", "-s", help="Create a sudo admin."),
3939
delete: Optional[str] = typer.Option(None, "--delete", "-d", help="Delete admin"),
40+
delete_users: Optional[str] = typer.Option(
41+
None, "--delete-users", "-u", help="Delete all users belonging to an admin"
42+
),
4043
modify: Optional[str] = typer.Option(None, "--modify", "-m", help="Modify admin"),
4144
disable: Optional[bool] = typer.Option(None, "--disable", help="Disable or enable the admin account."),
4245
reset_usage: Optional[str] = typer.Option(None, "--reset-usage", "-r", help="Reset admin usage"),
4346
):
4447
"""List & manage admin accounts."""
4548

46-
if list or not any([create, delete, modify, reset_usage]):
49+
if list or not any([create, delete, delete_users, modify, reset_usage]):
4750
asyncio.run(list_admins())
4851
elif create:
4952
asyncio.run(create_admin(create, sudo))
5053
elif delete:
5154
asyncio.run(delete_admin(delete))
55+
elif delete_users:
56+
asyncio.run(delete_admin_users(delete_users))
5257
elif modify:
5358
asyncio.run(modify_admin(modify, disable))
5459
elif reset_usage:

tui/admin.py

Lines changed: 102 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,40 @@
2727

2828
class AdminDelete(BaseModal):
2929
def __init__(
30-
self, db: AsyncSession, operation: AdminOperation, username: str, on_close: callable, *args, **kwargs
30+
self,
31+
db: AsyncSession,
32+
operation: AdminOperation,
33+
username: str,
34+
on_close: callable,
35+
user_count: int = 0,
36+
*args,
37+
**kwargs,
3138
) -> None:
3239
super().__init__(*args, **kwargs)
3340
self.db = db
3441
self.operation = operation
3542
self.username = username
3643
self.on_close = on_close
44+
self.user_count = user_count
3745

3846
async def on_mount(self) -> None:
3947
"""Ensure the first button is focused."""
40-
yes_button = self.query_one("#no")
41-
self.set_focus(yes_button)
48+
focus_target = "#delete-users" if self.user_count > 0 else "#no"
49+
self.set_focus(self.query_one(focus_target))
4250

4351
def compose(self) -> ComposeResult:
4452
with Container(classes="modal-box-delete"):
45-
yield Static("Are you sure about deleting this admin?", classes="title")
53+
yield Static(f"Delete admin '{self.username}'?", classes="title")
54+
if self.user_count > 0:
55+
yield Static(
56+
f"This admin has {self.user_count} users.\nYou must delete them to remove the admin.",
57+
classes="subtitle",
58+
)
59+
yield Horizontal(
60+
Static("Delete all users:", classes="label"),
61+
Switch(animate=False, id="delete-users"),
62+
classes="switch-container",
63+
)
4664
yield Horizontal(
4765
Button("Yes", id="yes", variant="success"),
4866
Button("No", id="no", variant="error"),
@@ -52,13 +70,69 @@ def compose(self) -> ComposeResult:
5270
async def on_button_pressed(self, event: Button.Pressed) -> None:
5371
if event.button.id == "yes":
5472
try:
73+
if self.user_count > 0:
74+
delete_users = self.query_one("#delete-users").value
75+
if delete_users:
76+
await self.operation.remove_all_users(self.db, self.username, SYSTEM_ADMIN)
77+
self.notify("Admin users deleted successfully", severity="success", title="Success")
5578
await self.operation.remove_admin(self.db, self.username, SYSTEM_ADMIN)
5679
self.on_close()
5780
except ValueError as e:
5881
self.notify(str(e), severity="error", title="Error")
5982
await self.key_escape()
6083

6184

85+
class AdminDeleteUsers(BaseModal):
86+
def __init__(
87+
self,
88+
db: AsyncSession,
89+
operation: AdminOperation,
90+
username: str,
91+
on_close: callable,
92+
user_count: int = 0,
93+
*args,
94+
**kwargs,
95+
) -> None:
96+
super().__init__(*args, **kwargs)
97+
self.db = db
98+
self.operation = operation
99+
self.username = username
100+
self.on_close = on_close
101+
self.user_count = user_count
102+
103+
async def on_mount(self) -> None:
104+
confirm_button = self.query_one("#cancel")
105+
self.set_focus(confirm_button)
106+
107+
def compose(self) -> ComposeResult:
108+
with Container(classes="modal-box-delete"):
109+
yield Static(
110+
f"Delete all users belonging to admin '{self.username}'?"
111+
f"\nFound {self.user_count} user(s). This action cannot be undone.",
112+
classes="title",
113+
)
114+
yield Horizontal(
115+
Button("Delete Users", id="delete", variant="warning"),
116+
Button("Cancel", id="cancel", variant="error"),
117+
classes="button-container",
118+
)
119+
120+
async def on_button_pressed(self, event: Button.Pressed) -> None:
121+
if event.button.id == "delete":
122+
try:
123+
deleted = await self.operation.remove_all_users(self.db, self.username, SYSTEM_ADMIN)
124+
if deleted == 0:
125+
self.notify("No users were deleted (none found)", severity="warning", title="Info")
126+
else:
127+
self.notify(
128+
f"{deleted} users deleted for admin '{self.username}'", severity="success", title="Success"
129+
)
130+
self.on_close()
131+
except ValueError as e:
132+
self.notify(str(e), severity="error", title="Error")
133+
await self.key_escape()
134+
135+
62136
class AdminResetUsage(BaseModal):
63137
def __init__(
64138
self, db: AsyncSession, operation: AdminOperation, username: str, on_close: callable, *args, **kwargs
@@ -396,17 +470,15 @@ async def on_mount(self) -> None:
396470

397471
# Load existing notification preferences (notification_enable is a dict from SQLAlchemy)
398472
notif = self.admin.notification_enable or {}
399-
master_on = any(
400-
[
401-
notif.get("create", False),
402-
notif.get("modify", False),
403-
notif.get("delete", False),
404-
notif.get("status_change", False),
405-
notif.get("reset_data_usage", False),
406-
notif.get("data_reset_by_next", False),
407-
notif.get("subscription_revoked", False),
408-
]
409-
)
473+
master_on = any([
474+
notif.get("create", False),
475+
notif.get("modify", False),
476+
notif.get("delete", False),
477+
notif.get("status_change", False),
478+
notif.get("reset_data_usage", False),
479+
notif.get("data_reset_by_next", False),
480+
notif.get("subscription_revoked", False),
481+
])
410482

411483
self.query_one("#notif_master").value = master_on
412484
self.query_one("#notif_create").value = notif.get("create", False)
@@ -549,6 +621,7 @@ def __init__(self, *args, **kwargs) -> None:
549621
("m", "modify_admin", "Modify admin"),
550622
("r", "reset_admin_usage", "Reset admin usage"),
551623
("d", "delete_admin", "Delete admin"),
624+
("u", "delete_admin_users", "Delete admin users"),
552625
("i", "import_from_env", "Import from env"),
553626
("p", "previous_page", "Previous page"),
554627
("n", "next_page", "Next page"),
@@ -648,7 +721,20 @@ def selected_admin(self):
648721
async def action_delete_admin(self):
649722
if not self.table.columns:
650723
return
651-
self.app.push_screen(AdminDelete(self.db, self.admin_operator, self.selected_admin, self._refresh_table))
724+
admin = await self.admin_operator.get_validated_admin(self.db, username=self.selected_admin)
725+
user_count = len(admin.users or [])
726+
self.app.push_screen(
727+
AdminDelete(self.db, self.admin_operator, self.selected_admin, self._refresh_table, user_count)
728+
)
729+
730+
async def action_delete_admin_users(self):
731+
if not self.table.columns:
732+
return
733+
admin = await self.admin_operator.get_validated_admin(self.db, username=self.selected_admin)
734+
user_count = len(admin.users or [])
735+
self.app.push_screen(
736+
AdminDeleteUsers(self.db, self.admin_operator, self.selected_admin, self._refresh_table, user_count)
737+
)
652738

653739
def _refresh_table(self):
654740
self.run_worker(self.admins_list)

0 commit comments

Comments
 (0)