import os import shutil import unittest from contextlib import contextmanager from pathlib import Path import frappe from frappe import scrub from frappe.core.doctype.doctype.test_doctype import new_doctype from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.model.meta import trim_table from frappe.modules import export_customizations, export_module_json, get_module_path from frappe.modules.utils import export_doc, sync_customizations from frappe.tests import IntegrationTestCase from frappe.utils import now_datetime def write_file(path, content): with open(path, "w") as f: f.write(content) def delete_file(path): if path: os.remove(path) def delete_path(path): if path: shutil.rmtree(path, ignore_errors=True) class TestUtils(IntegrationTestCase): def setUp(self): self._dev_mode = frappe.local.conf.developer_mode frappe.local.conf.developer_mode = True def tearDown(self): frappe.db.rollback() frappe.local.conf.developer_mode = self._dev_mode frappe.local.flags.pop("in_import", None) def test_export_module_json_no_export(self): frappe.local.flags.in_import = True doc = frappe.get_last_doc("DocType") self.assertIsNone(export_module_json(doc=doc, is_standard=True, module=doc.module)) @unittest.skipUnless( os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable" ) def test_export_module_json(self): doc = frappe.get_last_doc("DocType", {"issingle": 0, "custom": 0}) export_doc_path = os.path.join( get_module_path(doc.module), scrub(doc.doctype), scrub(doc.name), f"{scrub(doc.name)}.json", ) with open(export_doc_path) as f: export_doc_before = frappe.parse_json(f.read()) last_modified_before = os.path.getmtime(export_doc_path) self.addCleanup(write_file, path=export_doc_path, content=frappe.as_json(export_doc_before)) frappe.flags.in_import = False frappe.conf.developer_mode = True export_path = export_module_json(doc=doc, is_standard=True, module=doc.module) last_modified_after = os.path.getmtime(export_doc_path) with open(f"{export_path}.json") as f: frappe.parse_json(f.read()) # export_doc_after self.assertTrue(last_modified_after > last_modified_before) @unittest.skipUnless( os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable" ) def test_export_customizations(self): with note_customizations(): file_path = export_customizations(module="Custom", doctype="Note") self.addCleanup(delete_file, path=file_path) self.assertTrue(file_path.endswith("/custom/custom/note.json")) self.assertTrue(os.path.exists(file_path)) @unittest.skipUnless( os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable" ) def test_export_customizations_with_module_filter(self): # create two customizations, one matching the module, one under a different module with note_customizations() as (custom_field, property_setter, _doctype_link): custom_field.db_set("module", "Custom") property_setter.db_set("module", "Custom") # create module def called OtherModule other_module = frappe.new_doc("Module Def") other_module.update({"module_name": "OtherModule", "app_name": "frappe"}) other_module.save(ignore_permissions=True) self.addCleanup(other_module.delete) # create a customization belonging to another module (should be excluded) other_cf = create_custom_field( "Note", df={ "fieldname": "other_mod_field", "label": "Other Mod Field", "fieldtype": "Data", "module": "OtherModule", }, ) self.addCleanup(other_cf.delete) file_path = export_customizations( module="Custom", doctype="Note", apply_module_export_filter=True, ) self.addCleanup(delete_file, path=file_path) self.assertTrue(os.path.exists(file_path)) with open(file_path) as f: exported = frappe.parse_json(f.read()) exported_fields = {f["fieldname"] for f in exported.get("custom_fields", [])} self.assertIn("test_export_customizations_field", exported_fields) self.assertNotIn("other_mod_field", exported_fields) @unittest.skipUnless( os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable" ) def test_sync_customizations(self): with note_customizations() as (custom_field, property_setter, doctype_link): file_path = export_customizations(module="Custom", doctype="Note", sync_on_migrate=True) custom_field.db_set("modified", now_datetime()) custom_field.reload() # Untracked property setter custom_prop_setter = make_property_setter( "Note", fieldname="content", property="bold", value="1", property_type="Check" ) self.assertTrue(file_path.endswith("/custom/custom/note.json")) self.assertTrue(os.path.exists(file_path)) last_modified_before = custom_field.modified sync_customizations(app="frappe") self.assertTrue(property_setter.doctype, property_setter.name) self.assertTrue(custom_prop_setter.doctype, custom_prop_setter.name) self.assertTrue(doctype_link.doctype, doctype_link.name) self.assertTrue(file_path.endswith("/custom/custom/note.json")) self.assertTrue(os.path.exists(file_path)) custom_field.reload() last_modified_after = custom_field.modified self.assertNotEqual(last_modified_after, last_modified_before) self.addCleanup(delete_file, path=file_path) def test_reload_doc(self): frappe.db.set_value("DocType", "Note", "migration_hash", "", update_modified=False) self.assertFalse(frappe.db.get_value("DocType", "Note", "migration_hash")) frappe.db.set_value( "DocField", {"parent": "Note", "fieldname": "title"}, "fieldtype", "Text", update_modified=False, ) self.assertEqual( frappe.db.get_value("DocField", {"parent": "Note", "fieldname": "title"}, "fieldtype"), "Text", ) frappe.reload_doctype("Note") self.assertEqual( frappe.db.get_value("DocField", {"parent": "Note", "fieldname": "title"}, "fieldtype"), "Data", ) self.assertTrue(frappe.db.get_value("DocType", "Note", "migration_hash")) @unittest.skipUnless( os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable" ) def test_export_doc(self): note = frappe.new_doc("Note") note.title = frappe.generate_hash(length=10) note.save() export_doc(doctype="Note", name=note.name) exported_doc_path = Path( frappe.get_app_path("frappe", "desk", "note", note.name, f"{note.name}.json") ) self.assertTrue(os.path.exists(exported_doc_path)) self.addCleanup(delete_path, path=exported_doc_path.parent.parent) @unittest.skipUnless( os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable" ) def test_make_boilerplate(self): with temp_doctype() as doctype: scrubbed = frappe.scrub(doctype.name) path = frappe.get_app_path("frappe", "core", "doctype", scrubbed, f"{scrubbed}.json") self.assertFalse(os.path.exists(path)) doctype.custom = False doctype.save() self.assertTrue(os.path.exists(path)) @contextmanager def temp_doctype(): try: doctype = new_doctype().insert() yield doctype finally: doctype.delete(force=True) frappe.db.sql_ddl(f"DROP TABLE `tab{doctype.name}`") @contextmanager def note_customizations(): try: df = { "fieldname": "test_export_customizations_field", "label": "Custom Data Field", "fieldtype": "Data", } custom_field = create_custom_field("Note", df=df) property_setter = make_property_setter( "Note", fieldname="content", property="bold", value="1", property_type="Check" ) doctype_link = frappe.get_doc( { "doctype": "DocType Link", "parent": "Note", "parenttype": "DocType", "parentfield": "links", "link_doctype": "User", "link_fieldname": "owner", "group": "Test Group", } ).insert() yield custom_field, property_setter, doctype_link finally: custom_field.delete() property_setter.delete() doctype_link.delete() trim_table("Note", dry_run=False) delete_path(frappe.get_module_path("Desk", "Note"))