Merge pull request #17548 from ankush/fix_virtual_doctype
refactor!: Virtual DocType
This commit is contained in:
commit
ac83a0fdda
15 changed files with 216 additions and 113 deletions
|
|
@ -11,6 +11,7 @@ from frappe.desk.doctype.notification_log.notification_log import (
|
|||
get_title,
|
||||
get_title_html,
|
||||
)
|
||||
from frappe.model.utils import is_virtual_doctype
|
||||
from frappe.exceptions import ImplicitCommitError
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_fullname
|
||||
|
|
@ -152,7 +153,10 @@ def get_comments_from_parent(doc):
|
|||
`_comments`
|
||||
"""
|
||||
try:
|
||||
_comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]"
|
||||
if is_virtual_doctype(doc.reference_doctype):
|
||||
_comments = "[]"
|
||||
else:
|
||||
_comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]"
|
||||
|
||||
except Exception as e:
|
||||
if frappe.db.is_missing_table_or_column(e):
|
||||
|
|
@ -175,7 +179,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
|
|||
not reference_doctype
|
||||
or not reference_name
|
||||
or frappe.db.get_value("DocType", reference_doctype, "issingle")
|
||||
or frappe.db.get_value("DocType", reference_doctype, "is_virtual")
|
||||
or is_virtual_doctype(reference_doctype)
|
||||
):
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"is_virtual": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-31 10:06:57.919697",
|
||||
"modified": "2022-07-22 03:00:59.560061",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "test",
|
||||
|
|
@ -36,7 +36,9 @@
|
|||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,43 +1,75 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import json
|
||||
""" This is a virtual doctype controller for test/demo purposes.
|
||||
|
||||
# import frappe
|
||||
- It uses a JSON file on disk as "backend".
|
||||
- Key is docname and value is the document itself.
|
||||
|
||||
Example:
|
||||
{
|
||||
"doc1": {"name": "doc1", ...}
|
||||
"doc2": {"name": "doc2", ...}
|
||||
}
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
DATA_FILE = "data_file.json"
|
||||
|
||||
|
||||
def get_current_data() -> dict[str, dict]:
|
||||
"""Read data from disk"""
|
||||
if not os.path.exists(DATA_FILE):
|
||||
return {}
|
||||
|
||||
with open(DATA_FILE) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def update_data(data: dict[str, dict]) -> None:
|
||||
"""Flush updated data to disk"""
|
||||
with open(DATA_FILE, "w+") as data_file:
|
||||
json.dump(data, data_file)
|
||||
|
||||
|
||||
class test(Document):
|
||||
def db_insert(self):
|
||||
def db_insert(self, *args, **kwargs):
|
||||
d = self.get_valid_dict(convert_dates_to_str=True)
|
||||
with open("data_file.json", "w+") as read_file:
|
||||
json.dump(d, read_file)
|
||||
|
||||
data = get_current_data()
|
||||
data[d.name] = d
|
||||
|
||||
update_data(data)
|
||||
|
||||
def load_from_db(self):
|
||||
with open("data_file.json") as read_file:
|
||||
d = json.load(read_file)
|
||||
super(Document, self).__init__(d)
|
||||
data = get_current_data()
|
||||
d = data.get(self.name)
|
||||
super(Document, self).__init__(d)
|
||||
|
||||
def db_update(self):
|
||||
d = self.get_valid_dict(convert_dates_to_str=True)
|
||||
with open("data_file.json", "w+") as read_file:
|
||||
json.dump(d, read_file)
|
||||
def db_update(self, *args, **kwargs):
|
||||
# For this example insert and update are same operation,
|
||||
# it might be different for you
|
||||
self.db_insert(*args, **kwargs)
|
||||
|
||||
def get_list(self, args):
|
||||
with open("data_file.json") as read_file:
|
||||
return [json.load(read_file)]
|
||||
def delete(self):
|
||||
data = get_current_data()
|
||||
data.pop(self.name, None)
|
||||
update_data(data)
|
||||
|
||||
def get_value(self, fields, filters, **kwargs):
|
||||
# return []
|
||||
with open("data_file.json") as read_file:
|
||||
return [json.load(read_file)]
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
data = get_current_data()
|
||||
return [frappe._dict(doc) for name, doc in data.items()]
|
||||
|
||||
def get_count(self, args):
|
||||
# return []
|
||||
with open("data_file.json") as read_file:
|
||||
return [json.load(read_file)]
|
||||
@staticmethod
|
||||
def get_count(args):
|
||||
data = get_current_data()
|
||||
return len(data)
|
||||
|
||||
def get_stats(self, args):
|
||||
# return []
|
||||
with open("data_file.json") as read_file:
|
||||
return [json.load(read_file)]
|
||||
@staticmethod
|
||||
def get_stats(args):
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,67 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
# import frappe
|
||||
import unittest
|
||||
import json
|
||||
import os
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.test.test import DATA_FILE
|
||||
from frappe.core.doctype.test.test import test as VirtDocType
|
||||
from frappe.desk.form.save import savedocs
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class Testtest(unittest.TestCase):
|
||||
pass
|
||||
class Testtest(FrappeTestCase):
|
||||
def tearDown(self):
|
||||
if os.path.exists(DATA_FILE):
|
||||
os.remove(DATA_FILE)
|
||||
|
||||
def test_insert_update_and_load_from_desk(self):
|
||||
"""Insert, update, reload and assert changes"""
|
||||
|
||||
frappe.response.docs = []
|
||||
doc = json.dumps(
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "test",
|
||||
"name": "new-test-1",
|
||||
"__islocal": 1,
|
||||
"__unsaved": 1,
|
||||
"owner": "Administrator",
|
||||
"test": "Original Data",
|
||||
}
|
||||
)
|
||||
savedocs(doc, "Save")
|
||||
|
||||
docname = frappe.response.docs[0]["name"]
|
||||
|
||||
doc = frappe.get_doc("test", docname)
|
||||
doc.test = "New Data"
|
||||
|
||||
savedocs(doc.as_json(), "Save")
|
||||
|
||||
doc.reload()
|
||||
self.assertEqual(doc.test, "New Data")
|
||||
|
||||
def test_multiple_doc_insert_and_get_list(self):
|
||||
doc1 = frappe.get_doc(doctype="test", test="first").insert()
|
||||
doc2 = frappe.get_doc(doctype="test", test="second").insert()
|
||||
|
||||
docs = {doc1.name, doc2.name}
|
||||
|
||||
doc2.reload()
|
||||
doc1.reload()
|
||||
updated_docs = {doc1.name, doc2.name}
|
||||
self.assertEqual(docs, updated_docs)
|
||||
|
||||
listed_docs = {d.name for d in VirtDocType.get_list({})}
|
||||
self.assertEqual(docs, listed_docs)
|
||||
|
||||
def test_get_count(self):
|
||||
args = {"doctype": "test", "filters": [], "fields": []}
|
||||
self.assertIsInstance(VirtDocType.get_count(args), int)
|
||||
|
||||
def test_delete_doc(self):
|
||||
doc = frappe.get_doc(doctype="test", test="data").insert()
|
||||
|
||||
frappe.delete_doc(doc.doctype, doc.name)
|
||||
|
||||
listed_docs = {d.name for d in VirtDocType.get_list({})}
|
||||
self.assertNotIn(doc.name, listed_docs)
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2021-01-13 12:47:03.572640",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"random"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "random",
|
||||
"fieldtype": "Data",
|
||||
"label": "random"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-13 12:47:03.572640",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Test rename new",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"route": "test-rename",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class Testrenamenew(Document):
|
||||
pass
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class Testrenamenew(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -11,6 +11,7 @@ import frappe.share
|
|||
import frappe.utils
|
||||
from frappe import _, _dict
|
||||
from frappe.desk.form.document_follow import is_document_followed
|
||||
from frappe.model.utils import is_virtual_doctype
|
||||
from frappe.model.utils.user_settings import get_user_settings
|
||||
from frappe.permissions import get_doc_permissions
|
||||
from frappe.utils.data import cstr
|
||||
|
|
@ -30,7 +31,7 @@ def getdoc(doctype, name, user=None):
|
|||
if not name:
|
||||
name = doctype
|
||||
|
||||
if not frappe.db.exists(doctype, name):
|
||||
if not frappe.db.exists(doctype, name) and not is_virtual_doctype(doctype):
|
||||
return []
|
||||
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ from frappe.core.doctype.access_log.access_log import make_access_log
|
|||
from frappe.model import child_table_fields, default_fields, optional_fields
|
||||
from frappe.model.base_document import get_controller
|
||||
from frappe.model.db_query import DatabaseQuery
|
||||
from frappe.model.utils import is_virtual_doctype
|
||||
from frappe.utils import add_user_info, cstr, format_duration
|
||||
from frappe.utils.caching import site_cache
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -24,7 +24,7 @@ def get():
|
|||
# If virtual doctype get data from controller het_list method
|
||||
if is_virtual_doctype(args.doctype):
|
||||
controller = get_controller(args.doctype)
|
||||
data = compress(controller(args.doctype).get_list(args))
|
||||
data = compress(controller.get_list(args))
|
||||
else:
|
||||
data = compress(execute(**args), args=args)
|
||||
return data
|
||||
|
|
@ -37,7 +37,7 @@ def get_list():
|
|||
|
||||
if is_virtual_doctype(args.doctype):
|
||||
controller = get_controller(args.doctype)
|
||||
data = controller(args.doctype).get_list(args)
|
||||
data = controller.get_list(args)
|
||||
else:
|
||||
# uncompressed (refactored from frappe.model.db_query.get_list)
|
||||
data = execute(**args)
|
||||
|
|
@ -52,7 +52,7 @@ def get_count():
|
|||
|
||||
if is_virtual_doctype(args.doctype):
|
||||
controller = get_controller(args.doctype)
|
||||
data = controller(args.doctype).get_count(args)
|
||||
data = controller.get_count(args)
|
||||
else:
|
||||
distinct = "distinct " if args.distinct == "true" else ""
|
||||
args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"]
|
||||
|
|
@ -272,7 +272,7 @@ def compress(data, args=None):
|
|||
values.append(new_row)
|
||||
|
||||
# add user info for assignments (avatar)
|
||||
if row._assign:
|
||||
if row.get("_assign", ""):
|
||||
for user in json.loads(row._assign):
|
||||
add_user_info(user, user_info)
|
||||
|
||||
|
|
@ -528,7 +528,7 @@ def get_sidebar_stats(stats, doctype, filters=None):
|
|||
if is_virtual_doctype(doctype):
|
||||
controller = get_controller(doctype)
|
||||
args = {"stats": stats, "filters": filters}
|
||||
data = controller(doctype).get_stats(args)
|
||||
data = controller.get_stats(args)
|
||||
else:
|
||||
data = get_stats(stats, doctype, filters)
|
||||
|
||||
|
|
@ -731,8 +731,3 @@ def get_filters_cond(
|
|||
else:
|
||||
cond = ""
|
||||
return cond
|
||||
|
||||
|
||||
@site_cache(maxsize=128)
|
||||
def is_virtual_doctype(doctype):
|
||||
return frappe.db.get_value("DocType", doctype, "is_virtual")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from frappe import _, get_module_path
|
|||
from frappe.desk.doctype.tag.tag import delete_tags_for_document
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.model.naming import revert_series_if_last
|
||||
from frappe.model.utils import is_virtual_doctype
|
||||
from frappe.utils.file_manager import remove_all
|
||||
from frappe.utils.global_search import delete_for_document
|
||||
from frappe.utils.password import delete_all_passwords_for
|
||||
|
|
@ -57,11 +58,16 @@ def delete_doc(
|
|||
doctype = frappe.form_dict.get("dt")
|
||||
name = frappe.form_dict.get("dn")
|
||||
|
||||
is_virtual = is_virtual_doctype(doctype)
|
||||
|
||||
names = name
|
||||
if isinstance(name, str) or isinstance(name, int):
|
||||
names = [name]
|
||||
|
||||
for name in names or []:
|
||||
if is_virtual:
|
||||
frappe.get_doc(doctype, name).delete()
|
||||
continue
|
||||
|
||||
# already deleted..?
|
||||
if not frappe.db.exists(doctype, name):
|
||||
|
|
|
|||
|
|
@ -179,7 +179,9 @@ class Document(BaseDocument):
|
|||
if hasattr(self, "__setup__"):
|
||||
self.__setup__()
|
||||
|
||||
reload = load_from_db
|
||||
def reload(self):
|
||||
"""Reload document from database"""
|
||||
self.load_from_db()
|
||||
|
||||
def get_latest(self):
|
||||
if not getattr(self, "latest", None):
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import frappe
|
|||
from frappe import _
|
||||
from frappe.build import html_to_js_template
|
||||
from frappe.utils import cstr
|
||||
from frappe.utils.caching import site_cache
|
||||
|
||||
STANDARD_FIELD_CONVERSION_MAP = {
|
||||
"name": "Link",
|
||||
|
|
@ -99,3 +100,8 @@ def get_fetch_values(doctype, fieldname, value):
|
|||
out[df.fieldname] = frappe.db.get_value(link_df.options, value, source_fieldname)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@site_cache(maxsize=128)
|
||||
def is_virtual_doctype(doctype):
|
||||
return frappe.db.get_value("DocType", doctype, "is_virtual")
|
||||
|
|
|
|||
52
frappe/model/virtual_doctype.py
Normal file
52
frappe/model/virtual_doctype.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from typing import Protocol
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
class VirtualDoctype(Protocol):
|
||||
"""This class documents requirements that must be met by a doctype controller to function as virtual doctype
|
||||
|
||||
|
||||
Additional requirements:
|
||||
- DocType controller has to inherit from `frappe.model.document.Document` class
|
||||
|
||||
Note:
|
||||
- "Backend" here means any storage service, it can be a database, flat file or network call to API.
|
||||
"""
|
||||
|
||||
# ============ class/static methods ============
|
||||
|
||||
@staticmethod
|
||||
def get_list(args) -> list[frappe._dict]:
|
||||
"""Similar to reportview.get_list"""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def get_count(args) -> int:
|
||||
"""Similar to reportview.get_count, return total count of documents on listview."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def get_stats(args):
|
||||
"""Similar to reportview.get_stats, return sidebar stats."""
|
||||
...
|
||||
|
||||
# ============ instance methods ============
|
||||
|
||||
def db_insert(self, *args, **kwargs) -> None:
|
||||
"""Serialize the `Document` object and insert it in backend."""
|
||||
...
|
||||
|
||||
def load_from_db(self) -> None:
|
||||
"""Using self.name initialize current document from backend data.
|
||||
|
||||
This is responsible for updatinng __dict__ of class with all the fields on doctype."""
|
||||
...
|
||||
|
||||
def db_update(self, *args, **kwargs) -> None:
|
||||
"""Serialize the `Document` object and update existing document in backend."""
|
||||
...
|
||||
|
||||
def delete(self, *args, **kwargs) -> None:
|
||||
"""Delete the current document from backend"""
|
||||
...
|
||||
|
|
@ -296,22 +296,25 @@ def make_boilerplate(template, doc, opts=None):
|
|||
custom_controller = "pass"
|
||||
if doc.get("is_virtual"):
|
||||
custom_controller = """
|
||||
def db_insert(self):
|
||||
def db_insert(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def load_from_db(self):
|
||||
pass
|
||||
|
||||
def db_update(self):
|
||||
def db_update(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def get_list(self, args):
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
def get_count(self, args):
|
||||
@staticmethod
|
||||
def get_count(args):
|
||||
pass
|
||||
|
||||
def get_stats(self, args):
|
||||
@staticmethod
|
||||
def get_stats(args):
|
||||
pass"""
|
||||
|
||||
with open(target_file_path, "w") as target:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue