feat(backend): validate OA person field and whitelist type
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
6aa6c6e73e
commit
330dd9e2ce
4 files changed
+239
No files matched your search
@@ -1552,6 +1552,7 @@ def start_instance(
|
||||
|
||||
from app.services.oa_dept_select import validate_dept_form_values
|
||||
from app.services.oa_money_field import validate_money_form_values
|
||||
from app.services.oa_person_select import validate_person_form_values
|
||||
|
||||
dept_errs = validate_dept_form_values(schema, data)
|
||||
if dept_errs:
|
||||
@@ -1561,6 +1562,10 @@ def start_instance(
|
||||
if money_errs:
|
||||
raise OaEngineError(money_errs[0])
|
||||
|
||||
person_errs = validate_person_form_values(schema, data)
|
||||
if person_errs:
|
||||
raise OaEngineError(person_errs[0])
|
||||
|
||||
user = (
|
||||
db.query(SysUser)
|
||||
.filter(SysUser.id == initiator_user_id, SysUser.del_flag == 0)
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
"""OA 表单设计器:人员选择控件 form_data 校验"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
|
||||
def _walk_rules(rules: Any, out: List[dict]) -> None:
|
||||
if not isinstance(rules, list):
|
||||
return
|
||||
for r in rules:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
out.append(r)
|
||||
children = r.get("children") or (r.get("props") or {}).get("rule") or []
|
||||
if isinstance(children, list):
|
||||
_walk_rules(children, out)
|
||||
|
||||
|
||||
def _schema_rules(schema: Any) -> List[dict]:
|
||||
if isinstance(schema, list):
|
||||
out: List[dict] = []
|
||||
_walk_rules(schema, out)
|
||||
return out
|
||||
if not isinstance(schema, dict):
|
||||
return []
|
||||
out: List[dict] = []
|
||||
_walk_rules(schema.get("rule") or schema.get("rules") or [], out)
|
||||
return out
|
||||
|
||||
|
||||
def _rule_props(rule: dict) -> dict:
|
||||
props = rule.get("props")
|
||||
return props if isinstance(props, dict) else {}
|
||||
|
||||
|
||||
def _field_label(rule: dict) -> str:
|
||||
return str(rule.get("title") or rule.get("label") or rule.get("field") or "人员")
|
||||
|
||||
|
||||
def _is_required(rule: dict) -> bool:
|
||||
props = _rule_props(rule)
|
||||
if rule.get("$required") is True:
|
||||
return True
|
||||
if rule.get("required") is True:
|
||||
return True
|
||||
if props.get("required") is True:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_multiple(rule: dict) -> bool:
|
||||
props = _rule_props(rule)
|
||||
multiple = props.get("multiple")
|
||||
if multiple is None:
|
||||
return True
|
||||
return multiple is True
|
||||
|
||||
|
||||
def _max_count(rule: dict) -> Optional[int]:
|
||||
raw = _rule_props(rule).get("max")
|
||||
if raw is None or raw == "":
|
||||
return None
|
||||
try:
|
||||
value = int(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if value <= 0:
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def _is_valid_person_item(item: Any) -> bool:
|
||||
if not isinstance(item, dict):
|
||||
return False
|
||||
person_id = item.get("id")
|
||||
name = item.get("name")
|
||||
if not isinstance(person_id, str) or not person_id.strip():
|
||||
return False
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _validate_person_rule(rule: dict, data: Dict[str, Any]) -> List[str]:
|
||||
field = str(rule.get("field") or "").strip()
|
||||
if not field:
|
||||
return []
|
||||
|
||||
label = _field_label(rule)
|
||||
required = _is_required(rule)
|
||||
multiple = _is_multiple(rule)
|
||||
max_count = _max_count(rule)
|
||||
value = data.get(field)
|
||||
errors: List[str] = []
|
||||
|
||||
if multiple:
|
||||
if value is None:
|
||||
if required:
|
||||
errors.append(f"「{label}」为必填人员字段")
|
||||
return errors
|
||||
if not isinstance(value, list):
|
||||
errors.append(f"「{label}」人员字段格式不正确,应为人员列表")
|
||||
return errors
|
||||
if required and len(value) == 0:
|
||||
errors.append(f"「{label}」为必填人员字段")
|
||||
return errors
|
||||
for item in value:
|
||||
if not _is_valid_person_item(item):
|
||||
errors.append(f"「{label}」人员项格式不正确,需包含有效的 id 与 name")
|
||||
break
|
||||
if max_count is not None and len(value) > max_count:
|
||||
errors.append(f"「{label}」选择人员数量超过最大上限 {max_count} 个")
|
||||
return errors
|
||||
|
||||
if value is None or value == "":
|
||||
if required:
|
||||
errors.append(f"「{label}」为必填人员字段")
|
||||
return errors
|
||||
if isinstance(value, list):
|
||||
errors.append(f"「{label}」人员字段格式不正确,应为单个人员对象")
|
||||
return errors
|
||||
if not _is_valid_person_item(value):
|
||||
errors.append(f"「{label}」人员字段格式不正确,需包含有效的 id 与 name")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_person_form_values(schema: Any, form_data: Optional[Dict[str, Any]]) -> List[str]:
|
||||
"""校验 form_data 中人员选择控件的值格式,返回中文错误列表(空表示通过)。"""
|
||||
data = form_data or {}
|
||||
errors: List[str] = []
|
||||
for rule in _schema_rules(schema):
|
||||
if str(rule.get("type") or "") != "person":
|
||||
continue
|
||||
errors.extend(_validate_person_rule(rule, data))
|
||||
return errors
|
||||
|
||||
|
||||
def extract_person_ids(raw: Any) -> Set[str]:
|
||||
ids: Set[str] = set()
|
||||
items = raw if isinstance(raw, list) else ([raw] if isinstance(raw, dict) else [])
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
pid = str(item.get("id") or "").strip()
|
||||
if pid:
|
||||
ids.add(pid)
|
||||
return ids
|
||||
|
||||
|
||||
def person_contains_match(form_value: Any, cond_ids: Any) -> bool:
|
||||
form_ids = extract_person_ids(form_value)
|
||||
if isinstance(cond_ids, str):
|
||||
want = {cond_ids.strip()} if cond_ids.strip() else set()
|
||||
elif isinstance(cond_ids, list):
|
||||
want = {str(x).strip() for x in cond_ids if str(x).strip()}
|
||||
else:
|
||||
want = set()
|
||||
return bool(form_ids & want)
|
||||
@@ -29,6 +29,7 @@ FILLABLE_TYPES = frozenset(
|
||||
"upload",
|
||||
"money",
|
||||
"dept",
|
||||
"person",
|
||||
"region",
|
||||
"formula",
|
||||
"relatedApproval",
|
||||
@@ -80,6 +81,7 @@ CONDITION_ELIGIBLE_TYPES = frozenset(
|
||||
"select",
|
||||
"datePicker",
|
||||
"date",
|
||||
"person",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import unittest
|
||||
|
||||
from app.services.oa_person_select import (
|
||||
extract_person_ids,
|
||||
person_contains_match,
|
||||
validate_person_form_values,
|
||||
)
|
||||
|
||||
|
||||
class TestOaPersonSelect(unittest.TestCase):
|
||||
def test_validate_multi_ok(self):
|
||||
schema = {
|
||||
"rule": [
|
||||
{
|
||||
"type": "person",
|
||||
"field": "F1",
|
||||
"title": "协同人",
|
||||
"props": {"multiple": True},
|
||||
}
|
||||
]
|
||||
}
|
||||
data = {"F1": [{"id": "u1", "name": "张三"}, {"id": "u2", "name": "李四"}]}
|
||||
self.assertEqual(validate_person_form_values(schema, data), [])
|
||||
|
||||
def test_validate_required_empty(self):
|
||||
schema = {
|
||||
"rule": [
|
||||
{
|
||||
"type": "person",
|
||||
"field": "F1",
|
||||
"title": "协同人",
|
||||
"$required": True,
|
||||
"props": {"multiple": True},
|
||||
}
|
||||
]
|
||||
}
|
||||
errs = validate_person_form_values(schema, {"F1": []})
|
||||
self.assertTrue(any("必填" in e for e in errs))
|
||||
|
||||
def test_validate_max(self):
|
||||
schema = {
|
||||
"rule": [
|
||||
{
|
||||
"type": "person",
|
||||
"field": "F1",
|
||||
"title": "协同人",
|
||||
"props": {"multiple": True, "max": 1},
|
||||
}
|
||||
]
|
||||
}
|
||||
errs = validate_person_form_values(
|
||||
schema,
|
||||
{"F1": [{"id": "u1", "name": "A"}, {"id": "u2", "name": "B"}]},
|
||||
)
|
||||
self.assertTrue(any("上限" in e for e in errs))
|
||||
|
||||
def test_extract_and_contains(self):
|
||||
self.assertEqual(extract_person_ids({"id": "u1", "name": "A"}), {"u1"})
|
||||
self.assertTrue(
|
||||
person_contains_match(
|
||||
[{"id": "u1", "name": "A"}],
|
||||
["u1", "u2"],
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
person_contains_match(
|
||||
[{"id": "u3", "name": "C"}],
|
||||
["u1", "u2"],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user