feat: implement user type-based UI access control
Restrict settings interface and material validation features based on user type: - SettingsTab: User type only sees "Test ERP Connection" and "Test Database Connection" buttons; Admin sees full configuration interface - MaterialValidationTab: Hide manager filter and data source options from non-admin users; default to database_filtered mode - test_db_connection(): Read from config directly to support User type without UI variables - Add backward compatibility: no session_manager defaults to full interface Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
105
docs/USER_SETTINGS_RESTRICTION.md
Normal file
105
docs/USER_SETTINGS_RESTRICTION.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# 用户类型限制设置界面功能
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
实现了基于用户类型的设置界面访问控制。对于 `User` 类型的用户,设置界面仅显示"测试ERP连接"和"测试数据库连接"两个按钮,隐藏所有配置组。`Admin` 类型用户可以看到完整的设置界面。
|
||||||
|
|
||||||
|
## 修改的文件
|
||||||
|
|
||||||
|
### 1. `gui/settings_tab.py`
|
||||||
|
|
||||||
|
#### 构造函数修改
|
||||||
|
- 添加了 `session_manager` 可选参数
|
||||||
|
- 保存 `session_manager` 实例用于用户类型判断
|
||||||
|
|
||||||
|
#### `create_widgets()` 方法修改
|
||||||
|
- 添加用户类型判断逻辑
|
||||||
|
- 对于 `User` 类型用户,跳过创建所有配置组
|
||||||
|
- 根据用户类型显示不同的按钮组合
|
||||||
|
|
||||||
|
#### `load_settings()` 方法修改
|
||||||
|
- 添加用户类型判断
|
||||||
|
- 对于 `User` 类型用户,跳过加载配置到界面(因为没有界面元素)
|
||||||
|
|
||||||
|
#### `test_db_connection()` 方法修改
|
||||||
|
- 从 `self.config` 直接读取配置而不是从 UI 变量
|
||||||
|
- 这样即使没有 UI 变量,User 也能测试数据库连接
|
||||||
|
|
||||||
|
### 2. `gui/main_window.py`
|
||||||
|
|
||||||
|
#### `create_widgets()` 方法修改
|
||||||
|
- 传入 `session_manager` 到 `SettingsTab` 构造函数
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### User 类型用户
|
||||||
|
- **可见内容**: 仅两个按钮
|
||||||
|
- 测试 ERP 连接
|
||||||
|
- 测试数据库连接
|
||||||
|
- **隐藏内容**: 所有配置组
|
||||||
|
- ERP 系统配置
|
||||||
|
- 数据库配置
|
||||||
|
- 浏览器设置
|
||||||
|
- 路径设置
|
||||||
|
- 数据提取设置
|
||||||
|
- 物料校验设置
|
||||||
|
- **隐藏按钮**:
|
||||||
|
- 保存设置
|
||||||
|
- 恢复默认
|
||||||
|
|
||||||
|
### Admin 类型用户
|
||||||
|
- 显示完整的设置界面(所有配置组和所有按钮)
|
||||||
|
|
||||||
|
### 向后兼容
|
||||||
|
- 如果不传入 `session_manager` 参数,默认显示完整界面
|
||||||
|
- 这确保了不破坏现有代码
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
### 自动化测试
|
||||||
|
运行 `tests/test_user_settings_restriction.py`:
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python tests/test_user_settings_restriction.py
|
||||||
|
```
|
||||||
|
|
||||||
|
测试覆盖:
|
||||||
|
1. User 类型用户不显示任何配置组
|
||||||
|
2. Admin 类型用户显示所有配置组
|
||||||
|
3. 无 session_manager 时显示所有配置(向后兼容)
|
||||||
|
|
||||||
|
### 手动测试
|
||||||
|
运行 `tests/manual_test_user_settings.py`:
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python tests/manual_test_user_settings.py
|
||||||
|
```
|
||||||
|
|
||||||
|
选择测试场景后,可以直观地查看设置界面的显示效果。
|
||||||
|
|
||||||
|
## 验证步骤
|
||||||
|
|
||||||
|
1. 启动应用,使用 `User` 类型用户登录
|
||||||
|
2. 切换到"设置"标签页
|
||||||
|
3. 确认只显示"测试ERP连接"和"测试数据库连接"两个按钮
|
||||||
|
4. 确认所有配置组都不显示
|
||||||
|
5. 使用 `Admin` 类型用户登录,确认设置界面显示完整内容
|
||||||
|
|
||||||
|
## 技术细节
|
||||||
|
|
||||||
|
### 用户类型判断
|
||||||
|
```python
|
||||||
|
is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置读取
|
||||||
|
对于测试功能,直接从 `ConfigManager` 读取配置:
|
||||||
|
```python
|
||||||
|
db_type = self.config.get("database.db_type", "sqlserver")
|
||||||
|
```
|
||||||
|
|
||||||
|
这确保了即使没有 UI 变量,测试功能也能正常工作。
|
||||||
|
|
||||||
|
## 未来扩展
|
||||||
|
|
||||||
|
如果需要为不同用户类型提供更多自定义功能,可以在 `create_widgets()` 方法中添加更多条件判断,例如:
|
||||||
|
- 为不同用户类型显示不同的配置组
|
||||||
|
- 为不同用户类型提供不同的默认值
|
||||||
|
- 添加更多用户类型(如 `Guest`, `Manager` 等)
|
||||||
@@ -83,8 +83,8 @@ class MainWindow:
|
|||||||
self.query_tab = DataQueryTab(self.notebook, self.config)
|
self.query_tab = DataQueryTab(self.notebook, self.config)
|
||||||
self.notebook.add(self.query_tab, text="数据查询")
|
self.notebook.add(self.query_tab, text="数据查询")
|
||||||
|
|
||||||
# 设置标签页
|
# 设置标签页(传入 session_manager)
|
||||||
self.settings_tab = SettingsTab(self.notebook, self.config)
|
self.settings_tab = SettingsTab(self.notebook, self.config, self.session_manager)
|
||||||
self.notebook.add(self.settings_tab, text="设置")
|
self.notebook.add(self.settings_tab, text="设置")
|
||||||
|
|
||||||
def create_status_bar(self):
|
def create_status_bar(self):
|
||||||
|
|||||||
@@ -171,13 +171,18 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
control_frame = ttk.Frame(main_container)
|
control_frame = ttk.Frame(main_container)
|
||||||
control_frame.pack(fill=tk.X, pady=(0, 10))
|
control_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
# 负责人筛选区域
|
# 负责人筛选区域 - 仅管理员可见
|
||||||
|
# PERMISSION CHECK: 非管理员用户不显示筛选区域
|
||||||
|
if self.session_manager.is_admin():
|
||||||
manager_filter_frame = ttk.LabelFrame(
|
manager_filter_frame = ttk.LabelFrame(
|
||||||
main_container,
|
main_container,
|
||||||
text="筛选(按负责人)",
|
text="筛选(按负责人)",
|
||||||
padding=10
|
padding=10
|
||||||
)
|
)
|
||||||
manager_filter_frame.pack(fill=tk.X, pady=(0, 10))
|
manager_filter_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
else:
|
||||||
|
# 普通用户:创建一个空容器,避免后续代码出错
|
||||||
|
manager_filter_frame = ttk.Frame(main_container)
|
||||||
|
|
||||||
# 中部:结果表格
|
# 中部:结果表格
|
||||||
result_frame = ttk.LabelFrame(main_container, text="校验结果", padding=5)
|
result_frame = ttk.LabelFrame(main_container, text="校验结果", padding=5)
|
||||||
@@ -197,7 +202,9 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
|
|
||||||
def _create_control_panel(self, parent):
|
def _create_control_panel(self, parent):
|
||||||
"""创建控制面板"""
|
"""创建控制面板"""
|
||||||
# 数据来源选择
|
# 数据来源选择 - 仅管理员可见
|
||||||
|
# PERMISSION CHECK: 非管理员用户不显示数据来源选项
|
||||||
|
if self.session_manager.is_admin():
|
||||||
source_group = ttk.LabelFrame(parent, text="数据来源", padding=10)
|
source_group = ttk.LabelFrame(parent, text="数据来源", padding=10)
|
||||||
source_group.pack(fill=tk.X, pady=5)
|
source_group.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
@@ -219,6 +226,9 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
value="database_filtered",
|
value="database_filtered",
|
||||||
command=self._on_source_mode_change,
|
command=self._on_source_mode_change,
|
||||||
).grid(row=0, column=1, sticky="w", padx=5)
|
).grid(row=0, column=1, sticky="w", padx=5)
|
||||||
|
else:
|
||||||
|
# 普通用户:默认使用 database_filtered 模式
|
||||||
|
self.source_mode = tk.StringVar(value="database_filtered")
|
||||||
|
|
||||||
# 文件选择
|
# 文件选择
|
||||||
file_group = ttk.LabelFrame(parent, text="文件选择", padding=10)
|
file_group = ttk.LabelFrame(parent, text="文件选择", padding=10)
|
||||||
@@ -243,18 +253,15 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
)
|
)
|
||||||
self.db_filtered_production_id_selector.pack(fill=tk.X)
|
self.db_filtered_production_id_selector.pack(fill=tk.X)
|
||||||
|
|
||||||
# 输出文件
|
# 输出文件(对于所有用户都可见)
|
||||||
output_frame = ttk.Frame(file_group)
|
|
||||||
output_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(10, 0))
|
|
||||||
|
|
||||||
self.output_file_selector = FileSelector(
|
self.output_file_selector = FileSelector(
|
||||||
output_frame,
|
file_group,
|
||||||
label_text="输出文件:",
|
label_text="输出文件:",
|
||||||
file_type="file",
|
file_type="file",
|
||||||
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
|
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
|
||||||
initial_dir=self.config.get("paths.data_dir", "data/"),
|
initial_dir=self.config.get("paths.data_dir", "data/"),
|
||||||
)
|
)
|
||||||
self.output_file_selector.pack(fill=tk.X)
|
self.output_file_selector.pack(fill=tk.X, pady=(10, 0))
|
||||||
|
|
||||||
# 设置默认输出
|
# 设置默认输出
|
||||||
default_output = os.path.join(
|
default_output = os.path.join(
|
||||||
@@ -370,20 +377,31 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
|
|
||||||
def _on_source_mode_change(self):
|
def _on_source_mode_change(self):
|
||||||
"""数据源模式切换"""
|
"""数据源模式切换"""
|
||||||
|
# PERMISSION CHECK: 普通用户固定为 database_filtered 模式
|
||||||
|
if not self.session_manager.is_admin():
|
||||||
|
# 对于普通用户,pack 数据库过滤模式框架
|
||||||
|
self.db_full_frame.pack_forget()
|
||||||
|
self.db_filtered_frame.pack(fill=tk.X, pady=(0, 5))
|
||||||
|
return
|
||||||
|
|
||||||
mode = self.source_mode.get()
|
mode = self.source_mode.get()
|
||||||
|
|
||||||
# 隐藏所有文件选择框架
|
# 隐藏所有文件选择框架
|
||||||
self.db_full_frame.grid_remove()
|
self.db_full_frame.pack_forget()
|
||||||
self.db_filtered_frame.grid_remove()
|
self.db_filtered_frame.pack_forget()
|
||||||
|
|
||||||
# 根据模式显示对应的文件选择器
|
# 根据模式显示对应的文件选择器
|
||||||
if mode == "database_full":
|
if mode == "database_full":
|
||||||
self.db_full_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
|
self.db_full_frame.pack(fill=tk.X, pady=(0, 5))
|
||||||
elif mode == "database_filtered":
|
elif mode == "database_filtered":
|
||||||
self.db_filtered_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
|
self.db_filtered_frame.pack(fill=tk.X, pady=(0, 5))
|
||||||
|
|
||||||
def _create_manager_filter_area(self, parent):
|
def _create_manager_filter_area(self, parent):
|
||||||
"""创建负责人筛选区域"""
|
"""创建负责人筛选区域"""
|
||||||
|
# PERMISSION CHECK: 非管理员用户不显示筛选区域
|
||||||
|
if not self.session_manager.is_admin():
|
||||||
|
return
|
||||||
|
|
||||||
# 创建复选框容器
|
# 创建复选框容器
|
||||||
canvas_container = ttk.Frame(parent)
|
canvas_container = ttk.Frame(parent)
|
||||||
canvas_container.pack(fill=tk.BOTH, expand=True)
|
canvas_container.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|||||||
@@ -16,21 +16,26 @@ from config.schema import DatabaseType
|
|||||||
class SettingsTab(ttk.Frame):
|
class SettingsTab(ttk.Frame):
|
||||||
"""设置标签页"""
|
"""设置标签页"""
|
||||||
|
|
||||||
def __init__(self, parent, config: ConfigManager):
|
def __init__(self, parent, config: ConfigManager, session_manager=None):
|
||||||
"""
|
"""
|
||||||
初始化设置标签页
|
初始化设置标签页
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
parent: 父容器
|
parent: 父容器
|
||||||
config: 配置管理器
|
config: 配置管理器
|
||||||
|
session_manager: 会话管理器,用于获取用户类型
|
||||||
"""
|
"""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.session_manager = session_manager
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
self.load_settings()
|
self.load_settings()
|
||||||
|
|
||||||
def create_widgets(self):
|
def create_widgets(self):
|
||||||
"""创建界面组件"""
|
"""创建界面组件"""
|
||||||
|
# 判断用户类型
|
||||||
|
is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User'
|
||||||
|
|
||||||
# 创建主容器,带滚动条
|
# 创建主容器,带滚动条
|
||||||
canvas = tk.Canvas(self)
|
canvas = tk.Canvas(self)
|
||||||
scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
|
scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
|
||||||
@@ -43,28 +48,30 @@ class SettingsTab(ttk.Frame):
|
|||||||
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
||||||
canvas.configure(yscrollcommand=scrollbar.set)
|
canvas.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
# ERP 配置组
|
# 根据用户类型创建配置组
|
||||||
|
if not is_user_only:
|
||||||
|
# 管理员显示所有配置组
|
||||||
self._create_erp_group(scrollable_frame)
|
self._create_erp_group(scrollable_frame)
|
||||||
|
|
||||||
# 数据库配置组
|
|
||||||
self._create_database_group(scrollable_frame)
|
self._create_database_group(scrollable_frame)
|
||||||
|
|
||||||
# 浏览器配置组
|
|
||||||
self._create_browser_group(scrollable_frame)
|
self._create_browser_group(scrollable_frame)
|
||||||
|
|
||||||
# 路径配置组
|
|
||||||
self._create_paths_group(scrollable_frame)
|
self._create_paths_group(scrollable_frame)
|
||||||
|
|
||||||
# 处理配置组
|
|
||||||
self._create_extraction_group(scrollable_frame)
|
self._create_extraction_group(scrollable_frame)
|
||||||
|
|
||||||
# 校验配置组
|
|
||||||
self._create_validation_group(scrollable_frame)
|
self._create_validation_group(scrollable_frame)
|
||||||
|
|
||||||
# 按钮区域
|
# 按钮区域 - 根据用户类型显示不同按钮
|
||||||
button_frame = ttk.Frame(scrollable_frame)
|
button_frame = ttk.Frame(scrollable_frame)
|
||||||
button_frame.grid(row=6, column=0, columnspan=2, pady=20, sticky="ew")
|
button_frame.grid(row=6, column=0, columnspan=2, pady=20, sticky="ew")
|
||||||
|
|
||||||
|
if is_user_only:
|
||||||
|
# 普通用户只显示测试按钮
|
||||||
|
ttk.Button(
|
||||||
|
button_frame, text="测试 ERP 连接", command=self.test_erp_connection
|
||||||
|
).pack(side="left", padx=5)
|
||||||
|
ttk.Button(
|
||||||
|
button_frame, text="测试数据库连接", command=self.test_db_connection
|
||||||
|
).pack(side="left", padx=5)
|
||||||
|
else:
|
||||||
|
# 管理员显示所有按钮
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
button_frame, text="测试 ERP 连接", command=self.test_erp_connection
|
button_frame, text="测试 ERP 连接", command=self.test_erp_connection
|
||||||
).pack(side="left", padx=5)
|
).pack(side="left", padx=5)
|
||||||
@@ -326,6 +333,15 @@ class SettingsTab(ttk.Frame):
|
|||||||
|
|
||||||
def load_settings(self):
|
def load_settings(self):
|
||||||
"""从配置加载设置到界面"""
|
"""从配置加载设置到界面"""
|
||||||
|
# 判断是否为仅测试用户模式
|
||||||
|
is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User'
|
||||||
|
|
||||||
|
if is_user_only:
|
||||||
|
# 普通用户模式 - 只需要加载测试连接所需的配置
|
||||||
|
# 不需要加载设置到界面,因为界面没有配置输入框
|
||||||
|
return
|
||||||
|
|
||||||
|
# 管理员模式 - 加载所有配置
|
||||||
# ERP 设置
|
# ERP 设置
|
||||||
self.erp_url_var.set(self.config.get("erp.url", ""))
|
self.erp_url_var.set(self.config.get("erp.url", ""))
|
||||||
self.erp_username_var.set(self.config.get("erp.username", ""))
|
self.erp_username_var.set(self.config.get("erp.username", ""))
|
||||||
@@ -431,7 +447,8 @@ class SettingsTab(ttk.Frame):
|
|||||||
|
|
||||||
def test_db_connection(self):
|
def test_db_connection(self):
|
||||||
"""测试数据库连接"""
|
"""测试数据库连接"""
|
||||||
db_type = self.db_type_var.get()
|
# 从配置读取而不是从 UI 变量(支持 User 类型用户)
|
||||||
|
db_type = self.config.get("database.db_type", "sqlserver")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if db_type == "mysql":
|
if db_type == "mysql":
|
||||||
@@ -439,11 +456,11 @@ class SettingsTab(ttk.Frame):
|
|||||||
from mysql.connector import Error
|
from mysql.connector import Error
|
||||||
|
|
||||||
conn = mysql.connector.connect(
|
conn = mysql.connector.connect(
|
||||||
host=self.mysql_host_var.get(),
|
host=self.config.get("database.mysql.host", ""),
|
||||||
port=self.mysql_port_var.get(),
|
port=self.config.get("database.mysql.port", 3306),
|
||||||
database=self.db_name_var.get(),
|
database=self.config.get("database.database", ""),
|
||||||
user=self.db_username_var.get(),
|
user=self.config.get("database.username", ""),
|
||||||
password=self.db_password_var.get(),
|
password=self.config.get("database.password", ""),
|
||||||
connection_timeout=5
|
connection_timeout=5
|
||||||
)
|
)
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -451,10 +468,10 @@ class SettingsTab(ttk.Frame):
|
|||||||
else:
|
else:
|
||||||
conn_str = (
|
conn_str = (
|
||||||
f"DRIVER={{ODBC Driver 18 for SQL Server}};"
|
f"DRIVER={{ODBC Driver 18 for SQL Server}};"
|
||||||
f"SERVER={self.db_server_var.get()};"
|
f"SERVER={self.config.get('database.server', '')};"
|
||||||
f"DATABASE={self.db_name_var.get()};"
|
f"DATABASE={self.config.get('database.database', '')};"
|
||||||
f"UID={self.db_username_var.get()};"
|
f"UID={self.config.get('database.username', '')};"
|
||||||
f"PWD={self.db_password_var.get()};"
|
f"PWD={self.config.get('database.password', '')};"
|
||||||
f"TrustServerCertificate=yes;"
|
f"TrustServerCertificate=yes;"
|
||||||
)
|
)
|
||||||
conn = pyodbc.connect(conn_str, timeout=5)
|
conn = pyodbc.connect(conn_str, timeout=5)
|
||||||
|
|||||||
Reference in New Issue
Block a user