Add User Manager panel for sys::UserService components
New user_service_view.py with table of sys::User children, add/edit/delete dialogs, SHA-1 credential hashing, 32-bit permission bitmask (4 groups x 7 flags), 8-bit provisioning byte, and undo support. Wired into main.py with context menu entry, panel management, history navigation, and live-mode DOM resolution for correct display on connected devices. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
||||
import configparser
|
||||
import os
|
||||
|
||||
VERSION = "0.0.0.116"
|
||||
VERSION = "0.0.0.117"
|
||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||
DEFAULT_GRID_SIZE = 10
|
||||
DEFAULT_ADD_TIMESTAMP = True
|
||||
|
||||
@@ -18,6 +18,7 @@ import properties
|
||||
import sax_file
|
||||
import schema
|
||||
import undo
|
||||
import user_service_view
|
||||
import wiresheet
|
||||
from sax_dom import (find_component_by_path, find_parent_element, get_component_path,
|
||||
read_meta_property, update_meta_property)
|
||||
@@ -78,6 +79,7 @@ HISTORY_LIMIT = 50
|
||||
VIEW_SHEET = "sheet"
|
||||
VIEW_PROPERTIES = "properties"
|
||||
VIEW_LINKS = "links"
|
||||
VIEW_USERS = "users"
|
||||
|
||||
# Tk class names of the widgets that own the keyboard while they have focus. Del and
|
||||
# Backspace mean "edit this text" in each of them, never "delete the selected component".
|
||||
@@ -122,6 +124,7 @@ show_left_column_var = None
|
||||
properties_panel = None
|
||||
link_panel = None
|
||||
kits_panel = None
|
||||
user_service_panel = None
|
||||
|
||||
# The sheet to put the tree back on when a sheet change is refused because of unsaved
|
||||
# edits, and the flag that stops the restoring selection from being read as a fresh
|
||||
@@ -189,6 +192,12 @@ def refresh_all_views():
|
||||
else:
|
||||
close_editor_panel(force=True)
|
||||
|
||||
if user_service_panel is not None:
|
||||
if _component_in_document(user_service_panel.comp):
|
||||
user_service_panel.refresh()
|
||||
else:
|
||||
close_editor_panel(force=True)
|
||||
|
||||
|
||||
def _component_in_document(comp):
|
||||
root = app_state.xml_root_element
|
||||
@@ -210,7 +219,7 @@ def delete_component(comp):
|
||||
than going through refresh_all_views, so the panel would otherwise be left describing
|
||||
a component the document no longer has.
|
||||
"""
|
||||
panel = properties_panel or link_panel
|
||||
panel = properties_panel or link_panel or user_service_panel
|
||||
showing = panel is not None and panel.comp is comp
|
||||
if sax_file.execute_cascading_deletion(comp) and showing:
|
||||
close_editor_panel(force=True)
|
||||
@@ -218,7 +227,8 @@ def delete_component(comp):
|
||||
|
||||
def editor_panel_showing():
|
||||
"""Whether something other than the wiresheet has the editor pane."""
|
||||
return properties_panel is not None or link_panel is not None or kits_panel is not None
|
||||
return (properties_panel is not None or link_panel is not None
|
||||
or kits_panel is not None or user_service_panel is not None)
|
||||
|
||||
|
||||
def _take_editor_pane():
|
||||
@@ -330,6 +340,38 @@ def dismiss_link_view(restore_canvas=True):
|
||||
_give_editor_pane_back()
|
||||
|
||||
|
||||
def show_user_service(comp):
|
||||
"""Puts the User Manager for a sys::UserService in the editor pane."""
|
||||
global user_service_panel
|
||||
if comp is None:
|
||||
return
|
||||
|
||||
if not close_editor_panel():
|
||||
return
|
||||
|
||||
_take_editor_pane()
|
||||
user_service_panel = user_service_view.build_user_service_panel(
|
||||
frame_editor, comp, on_change=refresh_all_views,
|
||||
on_close=dismiss_user_service,
|
||||
)
|
||||
user_service_panel.frame.pack(fill=tk.BOTH, expand=True)
|
||||
record_visit(VIEW_USERS, comp)
|
||||
app_state.set_status(f"User Manager: {comp.get('name', '?')}")
|
||||
|
||||
|
||||
def dismiss_user_service(restore_canvas=True):
|
||||
"""Takes the User Manager down and gives the pane back."""
|
||||
global user_service_panel
|
||||
if user_service_panel is None:
|
||||
return
|
||||
|
||||
user_service_panel.destroy()
|
||||
user_service_panel = None
|
||||
|
||||
if restore_canvas:
|
||||
_give_editor_pane_back()
|
||||
|
||||
|
||||
def show_kits():
|
||||
"""Puts the kit table in the editor pane, in place of the wiresheet."""
|
||||
global kits_panel
|
||||
@@ -378,6 +420,9 @@ def close_editor_panel(force=False):
|
||||
if kits_panel is not None:
|
||||
dismiss_kits()
|
||||
return True
|
||||
if user_service_panel is not None:
|
||||
dismiss_user_service()
|
||||
return True
|
||||
if properties_panel is None:
|
||||
return True
|
||||
if force:
|
||||
@@ -1207,6 +1252,11 @@ def fill_component_popup(menu, comp):
|
||||
label="Link View",
|
||||
command=lambda: show_link_view(comp),
|
||||
)
|
||||
if user_service_view.is_user_service(comp):
|
||||
menu.add_command(
|
||||
label="User Manager",
|
||||
command=lambda: show_user_service(comp),
|
||||
)
|
||||
menu.add_separator()
|
||||
menu.add_command(
|
||||
label="Delete Component",
|
||||
@@ -1644,6 +1694,8 @@ def _show_history_entry(position):
|
||||
_show_sheet_entry(element)
|
||||
elif kind == VIEW_PROPERTIES:
|
||||
show_properties(element)
|
||||
elif kind == VIEW_USERS:
|
||||
show_user_service(element)
|
||||
else:
|
||||
show_link_view(element)
|
||||
finally:
|
||||
@@ -1707,7 +1759,7 @@ def on_tree_filled():
|
||||
|
||||
# A different file has no such component, and a rebuild after a delete may have taken
|
||||
# it. Neither is a moment to ask about edits that now have nowhere to go.
|
||||
panel = properties_panel or link_panel
|
||||
panel = properties_panel or link_panel or user_service_panel
|
||||
if panel is not None and not _component_in_document(panel.comp):
|
||||
close_editor_panel(force=True)
|
||||
|
||||
|
||||
@@ -0,0 +1,466 @@
|
||||
"""The User Manager: a table of sys::User components under a sys::UserService.
|
||||
|
||||
Like the other panels it takes the editor pane over from the canvas rather than
|
||||
floating above it. `main` is what hides the canvas and puts this in its place;
|
||||
this module knows only the frame it was handed.
|
||||
|
||||
Each sys::User component carries three properties:
|
||||
cred — SHA-1 of "username:password" (20 bytes, base64 in the SAX)
|
||||
perm — 32-bit int: four groups × 7 permission flags per group
|
||||
prov — 8-bit byte: app/kits/svm provisioning flags
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import tkinter as tk
|
||||
from tkinter import font as tkfont, messagebox, ttk
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import app_state
|
||||
import undo
|
||||
from sax_dom import set_prop_value
|
||||
|
||||
HEADER_BG = "#f2f2f2"
|
||||
|
||||
PERM_FLAGS = ["or", "ow", "oi", "ar", "aw", "ai", "ua"]
|
||||
PERM_LABELS = "rwIRWIu"
|
||||
|
||||
PROV_FLAGS = [("app", 0x01), ("kits", 0x02), ("svm", 0x04)]
|
||||
|
||||
|
||||
def _hash_credentials(username, password):
|
||||
"""SHA-1 of 'username:password', the Sedona credential algorithm."""
|
||||
return base64.b64encode(
|
||||
hashlib.sha1(f"{username}:{password}".encode("utf-8")).digest()
|
||||
).decode("ascii")
|
||||
|
||||
|
||||
def _decode_perm(perm_val):
|
||||
"""32-bit int -> list of four 7-element bool lists."""
|
||||
try:
|
||||
val = int(perm_val)
|
||||
except (TypeError, ValueError):
|
||||
val = 0
|
||||
groups = []
|
||||
for g in range(4):
|
||||
byte = (val >> (g * 8)) & 0xFF
|
||||
groups.append([(byte >> bit) & 1 == 1 for bit in range(7)])
|
||||
return groups
|
||||
|
||||
|
||||
def _encode_perm(groups):
|
||||
"""List of four 7-element bool lists -> 32-bit int."""
|
||||
val = 0
|
||||
for g, flags in enumerate(groups):
|
||||
byte = 0
|
||||
for bit, on in enumerate(flags):
|
||||
if on:
|
||||
byte |= (1 << bit)
|
||||
val |= (byte << (g * 8))
|
||||
return val
|
||||
|
||||
|
||||
def _format_perm(perm_val):
|
||||
"""Human-readable: '1=rwIRWIu, 2=rwIRWIu, ...'"""
|
||||
groups = _decode_perm(perm_val)
|
||||
parts = []
|
||||
for g, flags in enumerate(groups):
|
||||
chars = "".join(PERM_LABELS[i] if on else "-" for i, on in enumerate(flags))
|
||||
parts.append(f"{g + 1}={chars}")
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
def _decode_prov(prov_val):
|
||||
"""8-bit byte -> dict of flag name -> bool."""
|
||||
try:
|
||||
val = int(prov_val)
|
||||
except (TypeError, ValueError):
|
||||
val = 0
|
||||
return {name: bool(val & mask) for name, mask in PROV_FLAGS}
|
||||
|
||||
|
||||
def _encode_prov(flags_dict):
|
||||
val = 0
|
||||
for name, mask in PROV_FLAGS:
|
||||
if flags_dict.get(name):
|
||||
val |= mask
|
||||
return val
|
||||
|
||||
|
||||
def _format_prov(prov_val):
|
||||
"""Human-readable: 'app,kits,svm' for set flags."""
|
||||
flags = _decode_prov(prov_val)
|
||||
active = [name for name, on in flags.items() if on]
|
||||
return ",".join(active) if active else "(none)"
|
||||
|
||||
|
||||
def _get_prop(comp, name):
|
||||
prop = comp.find(f"prop[@name='{name}']")
|
||||
return prop.get("val") if prop is not None else None
|
||||
|
||||
|
||||
def _get_users(service_comp):
|
||||
"""Returns child comp elements, excluding uncached placeholders."""
|
||||
return [c for c in service_comp
|
||||
if c.tag == "comp" and c.get("name", "") != "..."]
|
||||
|
||||
|
||||
def _resolve_comp(comp):
|
||||
"""Re-locate a comp element in the current DOM by id (live mode rebuilds the tree)."""
|
||||
root = app_state.xml_root_element
|
||||
if root is None:
|
||||
return comp
|
||||
comp_id = comp.get("id")
|
||||
if comp_id is None:
|
||||
return comp
|
||||
for elem in root.iter("comp"):
|
||||
if elem.get("id") == comp_id:
|
||||
return elem
|
||||
return comp
|
||||
|
||||
|
||||
def is_user_service(comp):
|
||||
"""Whether a component is a sys::UserService (or looks like one in live mode)."""
|
||||
if comp is None:
|
||||
return False
|
||||
type_name = comp.get("type", "")
|
||||
if type_name.endswith("::UserService") or type_name == "UserService":
|
||||
return True
|
||||
for child in comp:
|
||||
if child.tag == "comp" and child.get("type", "").endswith("::User"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class _UserDialog:
|
||||
"""Modal dialog for adding or editing a user."""
|
||||
|
||||
def __init__(self, parent, service_comp, user_elem, on_done):
|
||||
self._service_comp = service_comp
|
||||
self._user_elem = user_elem
|
||||
self._on_done = on_done
|
||||
self._is_edit = user_elem is not None
|
||||
self.result = None
|
||||
|
||||
self._dlg = tk.Toplevel(parent)
|
||||
self._dlg.title("Edit User" if self._is_edit else "Add User")
|
||||
self._dlg.transient(parent)
|
||||
self._dlg.grab_set()
|
||||
self._dlg.resizable(False, False)
|
||||
|
||||
main = ttk.Frame(self._dlg, padding=10)
|
||||
main.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
row = 0
|
||||
ttk.Label(main, text="Username:").grid(row=row, column=0, sticky=tk.W, pady=2)
|
||||
self._name_var = tk.StringVar(
|
||||
value=user_elem.get("name", "") if self._is_edit else "")
|
||||
name_entry = ttk.Entry(main, textvariable=self._name_var, width=24)
|
||||
name_entry.grid(row=row, column=1, columnspan=3, sticky=tk.EW, pady=2)
|
||||
if self._is_edit:
|
||||
name_entry.config(state="disabled")
|
||||
|
||||
row += 1
|
||||
ttk.Label(main, text="Password:").grid(row=row, column=0, sticky=tk.W, pady=2)
|
||||
self._pass_var = tk.StringVar()
|
||||
ttk.Entry(main, textvariable=self._pass_var, show="*", width=24).grid(
|
||||
row=row, column=1, columnspan=3, sticky=tk.EW, pady=2)
|
||||
|
||||
row += 1
|
||||
ttk.Label(main, text="Confirm:").grid(row=row, column=0, sticky=tk.W, pady=2)
|
||||
self._confirm_var = tk.StringVar()
|
||||
ttk.Entry(main, textvariable=self._confirm_var, show="*", width=24).grid(
|
||||
row=row, column=1, columnspan=3, sticky=tk.EW, pady=2)
|
||||
|
||||
# permissions
|
||||
row += 1
|
||||
ttk.Separator(main, orient=tk.HORIZONTAL).grid(
|
||||
row=row, column=0, columnspan=4, sticky=tk.EW, pady=6)
|
||||
|
||||
row += 1
|
||||
ttk.Label(main, text="Group Permissions:").grid(
|
||||
row=row, column=0, columnspan=4, sticky=tk.W, pady=2)
|
||||
|
||||
perm_groups = (_decode_perm(_get_prop(user_elem, "perm"))
|
||||
if self._is_edit else [[False] * 7 for _ in range(4)])
|
||||
self._perm_vars = []
|
||||
for g in range(4):
|
||||
row += 1
|
||||
ttk.Label(main, text=f"Group {g + 1}:").grid(row=row, column=0, sticky=tk.W)
|
||||
gframe = ttk.Frame(main)
|
||||
gframe.grid(row=row, column=1, columnspan=3, sticky=tk.W)
|
||||
gvars = []
|
||||
for f_idx, label in enumerate(PERM_LABELS):
|
||||
var = tk.BooleanVar(value=perm_groups[g][f_idx])
|
||||
ttk.Checkbutton(gframe, text=label, variable=var).pack(
|
||||
side=tk.LEFT, padx=1)
|
||||
gvars.append(var)
|
||||
self._perm_vars.append(gvars)
|
||||
|
||||
# provisioning
|
||||
row += 1
|
||||
ttk.Separator(main, orient=tk.HORIZONTAL).grid(
|
||||
row=row, column=0, columnspan=4, sticky=tk.EW, pady=6)
|
||||
|
||||
row += 1
|
||||
ttk.Label(main, text="Provisioning:").grid(row=row, column=0, sticky=tk.W, pady=2)
|
||||
prov_dict = (_decode_prov(_get_prop(user_elem, "prov"))
|
||||
if self._is_edit else {n: False for n, _ in PROV_FLAGS})
|
||||
prov_frame = ttk.Frame(main)
|
||||
prov_frame.grid(row=row, column=1, columnspan=3, sticky=tk.W)
|
||||
self._prov_vars = {}
|
||||
for name, _ in PROV_FLAGS:
|
||||
var = tk.BooleanVar(value=prov_dict.get(name, False))
|
||||
ttk.Checkbutton(prov_frame, text=name, variable=var).pack(
|
||||
side=tk.LEFT, padx=4)
|
||||
self._prov_vars[name] = var
|
||||
|
||||
# buttons
|
||||
row += 1
|
||||
btn_frame = ttk.Frame(main)
|
||||
btn_frame.grid(row=row, column=0, columnspan=4, pady=(10, 0))
|
||||
ttk.Button(btn_frame, text="OK", command=self._on_ok).pack(side=tk.LEFT, padx=4)
|
||||
ttk.Button(btn_frame, text="Cancel", command=self._on_cancel).pack(
|
||||
side=tk.LEFT, padx=4)
|
||||
|
||||
self._dlg.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
||||
name_entry.focus_set()
|
||||
self._dlg.wait_window()
|
||||
|
||||
def _on_ok(self):
|
||||
username = self._name_var.get().strip()
|
||||
password = self._pass_var.get()
|
||||
confirm = self._confirm_var.get()
|
||||
|
||||
if not username:
|
||||
messagebox.showwarning("Validation", "Username is required.", parent=self._dlg)
|
||||
return
|
||||
if not self._is_edit and not password:
|
||||
messagebox.showwarning("Validation", "Password is required for new users.",
|
||||
parent=self._dlg)
|
||||
return
|
||||
if password and password != confirm:
|
||||
messagebox.showwarning("Validation", "Passwords do not match.", parent=self._dlg)
|
||||
return
|
||||
|
||||
# check duplicate name on add
|
||||
if not self._is_edit:
|
||||
for c in _get_users(self._service_comp):
|
||||
if c.get("name", "") == username:
|
||||
messagebox.showwarning("Validation", f"User '{username}' already exists.",
|
||||
parent=self._dlg)
|
||||
return
|
||||
|
||||
perm_groups = [[v.get() for v in gvars] for gvars in self._perm_vars]
|
||||
prov_dict = {name: var.get() for name, var in self._prov_vars.items()}
|
||||
|
||||
if self._is_edit:
|
||||
self._apply_edit(username, password, perm_groups, prov_dict)
|
||||
else:
|
||||
self._apply_add(username, password, perm_groups, prov_dict)
|
||||
|
||||
self._dlg.destroy()
|
||||
if self._on_done:
|
||||
self._on_done()
|
||||
|
||||
def _apply_add(self, username, password, perm_groups, prov_dict):
|
||||
elem = ET.Element("comp", name=username, type="sys::User")
|
||||
cred = _hash_credentials(username, password)
|
||||
ET.SubElement(elem, "prop", name="cred", val=cred)
|
||||
ET.SubElement(elem, "prop", name="perm", val=str(_encode_perm(perm_groups)))
|
||||
ET.SubElement(elem, "prop", name="prov", val=str(_encode_prov(prov_dict)))
|
||||
|
||||
index = len(list(self._service_comp))
|
||||
cmd = undo.AddComponentCommand(self._service_comp, index, elem, label="add user")
|
||||
cmd.redo()
|
||||
undo.push(cmd)
|
||||
app_state.mark_dirty()
|
||||
|
||||
def _apply_edit(self, username, password, perm_groups, prov_dict):
|
||||
user = self._user_elem
|
||||
changes = []
|
||||
|
||||
new_perm = str(_encode_perm(perm_groups))
|
||||
old_perm = _get_prop(user, "perm")
|
||||
if new_perm != old_perm:
|
||||
changes.append((user, "perm", old_perm, new_perm))
|
||||
|
||||
new_prov = str(_encode_prov(prov_dict))
|
||||
old_prov = _get_prop(user, "prov")
|
||||
if new_prov != old_prov:
|
||||
changes.append((user, "prov", old_prov, new_prov))
|
||||
|
||||
if password:
|
||||
new_cred = _hash_credentials(username, password)
|
||||
old_cred = _get_prop(user, "cred")
|
||||
if new_cred != old_cred:
|
||||
changes.append((user, "cred", old_cred, new_cred))
|
||||
|
||||
if not changes:
|
||||
return
|
||||
|
||||
cmd = undo.SlotValueCommand(changes, label="edit user")
|
||||
cmd.redo()
|
||||
undo.push(cmd)
|
||||
app_state.mark_dirty()
|
||||
|
||||
def _on_cancel(self):
|
||||
self._dlg.destroy()
|
||||
|
||||
|
||||
class UserServicePanel:
|
||||
"""Table of users under a sys::UserService, with Add/Edit/Delete."""
|
||||
|
||||
def __init__(self, parent, comp, on_change=None, on_close=None):
|
||||
self.comp = comp
|
||||
self.on_change = on_change
|
||||
self.on_close = on_close
|
||||
self.dirty = False
|
||||
self.frame = tk.Frame(parent)
|
||||
|
||||
self._build_header()
|
||||
self._build_table()
|
||||
self._build_footer()
|
||||
self._refresh_table()
|
||||
|
||||
def _build_header(self):
|
||||
header = tk.Frame(self.frame, padx=8, pady=6, bg=HEADER_BG)
|
||||
header.pack(fill=tk.X)
|
||||
|
||||
name_font = tkfont.Font(family="Segoe UI", size=9, weight="bold")
|
||||
plain_font = tkfont.Font(family="Segoe UI", size=9)
|
||||
|
||||
tk.Label(header, text="User Manager", font=name_font,
|
||||
bg=HEADER_BG).pack(side=tk.LEFT)
|
||||
tk.Label(header, text=f"— {self.comp.get('name', '?')}",
|
||||
font=plain_font, fg="#555555", bg=HEADER_BG).pack(side=tk.LEFT,
|
||||
padx=(8, 0))
|
||||
self.count_var = tk.StringVar(value="")
|
||||
tk.Label(header, textvariable=self.count_var, font=plain_font, fg="#555555",
|
||||
bg=HEADER_BG).pack(side=tk.RIGHT)
|
||||
|
||||
def _build_table(self):
|
||||
area = tk.Frame(self.frame)
|
||||
area.pack(fill=tk.BOTH, expand=True, padx=8, pady=(4, 0))
|
||||
|
||||
columns = ("username", "permissions", "provisioning")
|
||||
self.table = ttk.Treeview(area, columns=columns, show="headings",
|
||||
selectmode="browse")
|
||||
self.table.heading("username", text="Username")
|
||||
self.table.heading("permissions", text="Group Permissions")
|
||||
self.table.heading("provisioning", text="Provisioning")
|
||||
|
||||
self.table.column("username", width=120, minwidth=80)
|
||||
self.table.column("permissions", width=320, minwidth=200)
|
||||
self.table.column("provisioning", width=120, minwidth=80)
|
||||
|
||||
vscroll = ttk.Scrollbar(area, orient=tk.VERTICAL, command=self.table.yview)
|
||||
self.table.configure(yscrollcommand=vscroll.set)
|
||||
|
||||
self.table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
vscroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
self.table.bind("<Double-1>", lambda _e: self._on_edit())
|
||||
self._user_elements = {}
|
||||
|
||||
def _build_footer(self):
|
||||
footer = tk.Frame(self.frame, padx=8, pady=6)
|
||||
footer.pack(side=tk.BOTTOM, fill=tk.X)
|
||||
|
||||
ttk.Button(footer, text="Close", width=9,
|
||||
command=self.close).pack(side=tk.RIGHT)
|
||||
ttk.Button(footer, text="Delete User", width=12,
|
||||
command=self._on_delete).pack(side=tk.RIGHT, padx=(0, 6))
|
||||
ttk.Button(footer, text="Edit User", width=12,
|
||||
command=self._on_edit).pack(side=tk.RIGHT, padx=(0, 6))
|
||||
ttk.Button(footer, text="Add User", width=12,
|
||||
command=self._on_add).pack(side=tk.RIGHT, padx=(0, 6))
|
||||
|
||||
def _refresh_table(self):
|
||||
for item in self.table.get_children():
|
||||
self.table.delete(item)
|
||||
self._user_elements.clear()
|
||||
|
||||
self.comp = _resolve_comp(self.comp)
|
||||
users = _get_users(self.comp)
|
||||
for user_elem in users:
|
||||
name = user_elem.get("name", "?")
|
||||
perm = _get_prop(user_elem, "perm")
|
||||
prov = _get_prop(user_elem, "prov")
|
||||
|
||||
iid = self.table.insert("", tk.END, values=(
|
||||
name,
|
||||
_format_perm(perm),
|
||||
_format_prov(prov),
|
||||
))
|
||||
self._user_elements[iid] = user_elem
|
||||
|
||||
self.count_var.set(f"{len(users)} user{'' if len(users) == 1 else 's'}")
|
||||
|
||||
def refresh(self):
|
||||
self._refresh_table()
|
||||
|
||||
def _selected_user(self):
|
||||
sel = self.table.selection()
|
||||
if not sel:
|
||||
return None, None
|
||||
return sel[0], self._user_elements.get(sel[0])
|
||||
|
||||
def _after_change(self):
|
||||
self._refresh_table()
|
||||
if self.on_change:
|
||||
self.on_change()
|
||||
|
||||
def _on_add(self):
|
||||
if app_state.live_mode:
|
||||
app_state.set_status("Add user is not available in live mode", warn=True)
|
||||
return
|
||||
_UserDialog(self.frame.winfo_toplevel(), self.comp, None, self._after_change)
|
||||
|
||||
def _on_edit(self):
|
||||
iid, user_elem = self._selected_user()
|
||||
if user_elem is None:
|
||||
messagebox.showinfo("Edit User", "Select a user first.",
|
||||
parent=self.frame.winfo_toplevel())
|
||||
return
|
||||
if app_state.live_mode:
|
||||
app_state.set_status("Edit user is not available in live mode", warn=True)
|
||||
return
|
||||
_UserDialog(self.frame.winfo_toplevel(), self.comp, user_elem, self._after_change)
|
||||
|
||||
def _on_delete(self):
|
||||
iid, user_elem = self._selected_user()
|
||||
if user_elem is None:
|
||||
messagebox.showinfo("Delete User", "Select a user first.",
|
||||
parent=self.frame.winfo_toplevel())
|
||||
return
|
||||
if app_state.live_mode:
|
||||
app_state.set_status("Delete user is not available in live mode", warn=True)
|
||||
return
|
||||
|
||||
name = user_elem.get("name", "?")
|
||||
if not messagebox.askyesno("Delete User", f"Delete user '{name}'?",
|
||||
parent=self.frame.winfo_toplevel()):
|
||||
return
|
||||
|
||||
index = list(self.comp).index(user_elem)
|
||||
cmd = undo.AddComponentCommand(self.comp, index, user_elem, label="delete user")
|
||||
cmd.undo()
|
||||
undo.push(cmd)
|
||||
app_state.mark_dirty()
|
||||
self._after_change()
|
||||
|
||||
def close(self):
|
||||
if self.on_close is not None:
|
||||
self.on_close()
|
||||
return True
|
||||
|
||||
def destroy(self):
|
||||
if self.frame.winfo_exists():
|
||||
self.frame.destroy()
|
||||
|
||||
|
||||
def build_user_service_panel(parent, comp, on_change=None, on_close=None):
|
||||
if comp is None or parent is None:
|
||||
return None
|
||||
return UserServicePanel(parent, comp, on_change=on_change, on_close=on_close)
|
||||
Reference in New Issue
Block a user