feat: add permission-aware DocType discovery
This commit is contained in:
@@ -18,7 +18,7 @@ def capabilities() -> dict[str, Any]:
|
||||
"installed_apps": sorted(frappe.get_installed_apps()),
|
||||
"capabilities": {
|
||||
"system_information": True,
|
||||
"doctype_discovery": False,
|
||||
"doctype_discovery": True,
|
||||
"document_retrieval": False,
|
||||
"rag_indexing": False,
|
||||
"write_operations": False,
|
||||
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.permissions import get_doctypes_with_read
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
DEFAULT_PAGE_LENGTH = 50
|
||||
MAX_PAGE_LENGTH = 100
|
||||
|
||||
SAFE_DOCTYPE_FIELDS = (
|
||||
"name",
|
||||
"module",
|
||||
"custom",
|
||||
"issingle",
|
||||
"is_submittable",
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["GET"])
|
||||
def doctypes(
|
||||
limit_start: int | str = 0,
|
||||
limit_page_length: int | str = DEFAULT_PAGE_LENGTH,
|
||||
) -> dict[str, Any]:
|
||||
"""Return role-readable, non-child DocTypes for the authenticated user."""
|
||||
|
||||
user = frappe.session.user
|
||||
|
||||
if not user or user == "Guest":
|
||||
frappe.throw(
|
||||
_("Authentication is required."),
|
||||
frappe.AuthenticationError,
|
||||
)
|
||||
|
||||
start = max(cint(limit_start), 0)
|
||||
|
||||
requested_page_length = (
|
||||
cint(limit_page_length) or DEFAULT_PAGE_LENGTH
|
||||
)
|
||||
page_length = min(
|
||||
max(requested_page_length, 1),
|
||||
MAX_PAGE_LENGTH,
|
||||
)
|
||||
|
||||
readable_names = sorted(
|
||||
set(get_doctypes_with_read(user=user))
|
||||
)
|
||||
|
||||
if not readable_names:
|
||||
return {
|
||||
"api_version": "v1",
|
||||
"user": user,
|
||||
"permission_basis": "frappe_role_permissions",
|
||||
"discovery_only": True,
|
||||
"total": 0,
|
||||
"limit_start": start,
|
||||
"limit_page_length": page_length,
|
||||
"items": [],
|
||||
}
|
||||
|
||||
rows = frappe.get_all(
|
||||
"DocType",
|
||||
filters={
|
||||
"name": ["in", readable_names],
|
||||
"istable": 0,
|
||||
},
|
||||
fields=list(SAFE_DOCTYPE_FIELDS),
|
||||
order_by="name asc",
|
||||
)
|
||||
|
||||
total = len(rows)
|
||||
page = rows[start : start + page_length]
|
||||
|
||||
items = [
|
||||
{
|
||||
"name": row.name,
|
||||
"module": row.module,
|
||||
"custom": bool(cint(row.custom)),
|
||||
"is_single": bool(cint(row.issingle)),
|
||||
"is_submittable": bool(
|
||||
cint(row.is_submittable)
|
||||
),
|
||||
}
|
||||
for row in page
|
||||
]
|
||||
|
||||
return {
|
||||
"api_version": "v1",
|
||||
"user": user,
|
||||
"permission_basis": "frappe_role_permissions",
|
||||
"discovery_only": True,
|
||||
"total": total,
|
||||
"limit_start": start,
|
||||
"limit_page_length": page_length,
|
||||
"items": items,
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class TestCapabilities(FrappeTestCase):
|
||||
result["capabilities"],
|
||||
{
|
||||
"system_information": True,
|
||||
"doctype_discovery": False,
|
||||
"doctype_discovery": True,
|
||||
"document_retrieval": False,
|
||||
"rag_indexing": False,
|
||||
"write_operations": False,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import frappe
|
||||
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from enuxia_ai_bridge.api.discovery import doctypes
|
||||
|
||||
|
||||
class TestDoctypeDiscovery(FrappeTestCase):
|
||||
def setUp(self) -> None:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_requires_authentication(self) -> None:
|
||||
frappe.set_user("Guest")
|
||||
|
||||
with self.assertRaises(frappe.AuthenticationError):
|
||||
doctypes()
|
||||
|
||||
def test_returns_sorted_paginated_doctypes(self) -> None:
|
||||
result = doctypes(
|
||||
limit_start=0,
|
||||
limit_page_length=10,
|
||||
)
|
||||
|
||||
self.assertEqual(result["api_version"], "v1")
|
||||
self.assertEqual(result["user"], "Administrator")
|
||||
self.assertTrue(result["discovery_only"])
|
||||
self.assertEqual(
|
||||
result["permission_basis"],
|
||||
"frappe_role_permissions",
|
||||
)
|
||||
|
||||
self.assertLessEqual(len(result["items"]), 10)
|
||||
self.assertGreaterEqual(
|
||||
result["total"],
|
||||
len(result["items"]),
|
||||
)
|
||||
|
||||
names = [
|
||||
item["name"]
|
||||
for item in result["items"]
|
||||
]
|
||||
self.assertEqual(names, sorted(names))
|
||||
|
||||
for item in result["items"]:
|
||||
self.assertFalse(
|
||||
frappe.get_meta(item["name"]).istable
|
||||
)
|
||||
|
||||
def test_caps_page_length(self) -> None:
|
||||
result = doctypes(
|
||||
limit_page_length=1000,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
result["limit_page_length"],
|
||||
100,
|
||||
)
|
||||
self.assertLessEqual(
|
||||
len(result["items"]),
|
||||
100,
|
||||
)
|
||||
|
||||
def test_negative_start_becomes_zero(self) -> None:
|
||||
result = doctypes(
|
||||
limit_start=-50,
|
||||
limit_page_length=1,
|
||||
)
|
||||
|
||||
self.assertEqual(result["limit_start"], 0)
|
||||
Reference in New Issue
Block a user