diff --git a/cypress/fixtures/child_table_doctype_1.js b/cypress/fixtures/child_table_doctype_1.js new file mode 100644 index 0000000000..4657d63e2e --- /dev/null +++ b/cypress/fixtures/child_table_doctype_1.js @@ -0,0 +1,59 @@ +export default { + name: "Child Table Doctype 1", + actions: [], + custom: 1, + autoname: "format: Test-{####}", + creation: "2022-02-09 20:15:21.242213", + doctype: "DocType", + editable_grid: 1, + engine: "InnoDB", + fields: [ + { + fieldname: "data", + fieldtype: "Data", + in_list_view: 1, + label: "Data" + }, + { + fieldname: "barcode", + fieldtype: "Barcode", + in_list_view: 1, + label: "Barcode" + }, + { + fieldname: "check", + fieldtype: "Check", + in_list_view: 1, + label: "Check" + }, + { + fieldname: "rating", + fieldtype: "Rating", + in_list_view: 1, + label: "Rating" + }, + { + fieldname: "duration", + fieldtype: "Duration", + in_list_view: 1, + label: "Duration" + }, + { + fieldname: "date", + fieldtype: "Date", + in_list_view: 1, + label: "Date" + } + ], + links: [], + istable: 1, + modified: "2022-02-10 12:03:12.603763", + modified_by: "Administrator", + module: "Custom", + naming_rule: "By fieldname", + owner: "Administrator", + permissions: [], + sort_field: 'modified', + sort_order: 'ASC', + track_changes: 1 +}; \ No newline at end of file diff --git a/cypress/fixtures/doctype_with_child_table.js b/cypress/fixtures/doctype_with_child_table.js index bbb2127448..014074b0b5 100644 --- a/cypress/fixtures/doctype_with_child_table.js +++ b/cypress/fixtures/doctype_with_child_table.js @@ -20,6 +20,12 @@ export default { label: "Child Table", options: "Child Table Doctype", reqd: 1 + }, + { + fieldname: "child_table_1", + fieldtype: "Table", + label: "Child Table 1", + options: "Child Table Doctype 1" } ], links: [], diff --git a/cypress/integration/dashboard_links.js b/cypress/integration/dashboard_links.js index 93d10cf1fd..019de1991d 100644 --- a/cypress/integration/dashboard_links.js +++ b/cypress/integration/dashboard_links.js @@ -1,5 +1,6 @@ import doctype_with_child_table from '../fixtures/doctype_with_child_table'; import child_table_doctype from '../fixtures/child_table_doctype'; +import child_table_doctype_1 from '../fixtures/child_table_doctype_1'; import doctype_to_link from '../fixtures/doctype_to_link'; const doctype_to_link_name = doctype_to_link.name; const child_table_doctype_name = child_table_doctype.name; @@ -9,6 +10,7 @@ context('Dashboard links', () => { cy.visit('/login'); cy.login(); cy.insert_doc('DocType', child_table_doctype, true); + cy.insert_doc('DocType', child_table_doctype_1, true); cy.insert_doc('DocType', doctype_with_child_table, true); cy.insert_doc('DocType', doctype_to_link, true); return cy.window().its('frappe').then(frappe => { diff --git a/cypress/integration/grid_search.js b/cypress/integration/grid_search.js new file mode 100644 index 0000000000..d30545a2e1 --- /dev/null +++ b/cypress/integration/grid_search.js @@ -0,0 +1,107 @@ +import doctype_with_child_table from '../fixtures/doctype_with_child_table'; +import child_table_doctype from '../fixtures/child_table_doctype'; +import child_table_doctype_1 from '../fixtures/child_table_doctype_1'; +const doctype_with_child_table_name = doctype_with_child_table.name; + +context('Grid Search', () => { + before(() => { + cy.visit('/login'); + cy.login(); + cy.visit('/app/website'); + cy.insert_doc('DocType', child_table_doctype, true); + cy.insert_doc('DocType', child_table_doctype_1, true); + cy.insert_doc('DocType', doctype_with_child_table, true); + return cy.window().its('frappe').then(frappe => { + return frappe.xcall("frappe.tests.ui_test_helpers.insert_doctype_with_child_table_record", { + name: doctype_with_child_table_name + }); + }); + }); + + it('Test search row visibility', () => { + cy.window().its('frappe').then(frappe => { + frappe.model.user_settings.save('Doctype With Child Table', 'GridView', { + 'Child Table Doctype 1': [ + {'fieldname': 'data', 'columns': 2}, + {'fieldname': 'barcode', 'columns': 1}, + {'fieldname': 'check', 'columns': 1}, + {'fieldname': 'rating', 'columns': 2}, + {'fieldname': 'duration', 'columns': 2}, + {'fieldname': 'date', 'columns': 2} + ] + }); + }); + + cy.visit(`/app/doctype-with-child-table/Test Grid Search`); + + cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table'); + cy.get('@table').find('.grid-row-check:last').click(); + cy.get('@table').find('.grid-footer').contains('Delete').click(); + cy.get('.grid-heading-row .grid-row .search').should('not.exist'); + }); + + it('test search field for different fieldtypes', () => { + cy.visit(`/app/doctype-with-child-table/Test Grid Search`); + + cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table'); + + // Index Column + cy.get('@table').find('.grid-heading-row .row-index.search input').type('3'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2); + cy.get('@table').find('.grid-heading-row .row-index.search input').clear(); + + // Data Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('Data'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 1); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').clear(); + + // Barcode Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('092'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').clear(); + + // Check Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('1'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 9); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear(); + + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('0'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 11); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear(); + + // Rating Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').type('3'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').clear(); + + // Duration Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('3d'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').clear(); + + // Date Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('2022'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').clear(); + }); + + it('test with multiple filter', () => { + cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table'); + + // Data Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('a'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 10); + + // Barcode Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('0'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 8); + + // Duration Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('d'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 5); + + // Date Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('02-'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2); + }); +}); \ No newline at end of file diff --git a/cypress/integration/list_paging.js b/cypress/integration/list_paging.js index b6832f5a53..4a59024a7b 100644 --- a/cypress/integration/list_paging.js +++ b/cypress/integration/list_paging.js @@ -31,5 +31,8 @@ context('List Paging', () => { cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click(); cy.get('.list-paging-area .list-count').should('contain.text', '500 of'); + cy.get('.list-paging-area .btn-more').click(); + + cy.get('.list-paging-area .list-count').should('contain.text', '1000 of'); }); }); diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js index 43c01e88fb..ff31aa4b74 100644 --- a/esbuild/esbuild.js +++ b/esbuild/esbuild.js @@ -286,7 +286,7 @@ function get_watch_config() { notify_redis({ error }); } else { let { - assets_json, + new_assets_json, prev_assets_json } = await write_assets_json(result.metafile); @@ -294,7 +294,7 @@ function get_watch_config() { if (prev_assets_json) { changed_files = get_rebuilt_assets( prev_assets_json, - assets_json + new_assets_json ); let timestamp = new Date().toLocaleTimeString(); @@ -384,6 +384,7 @@ let prev_assets_json; let curr_assets_json; async function write_assets_json(metafile) { + let rtl = false; prev_assets_json = curr_assets_json; let out = {}; for (let output in metafile.outputs) { @@ -392,13 +393,14 @@ async function write_assets_json(metafile) { if (info.entryPoint) { let key = path.basename(info.entryPoint); if (key.endsWith('.css') && asset_path.includes('/css-rtl/')) { + rtl = true; key = `rtl_${key}`; } out[key] = asset_path; } } - let assets_json_path = path.resolve(assets_path, "assets.json"); + let assets_json_path = path.resolve(assets_path, `assets${rtl?'-rtl':''}.json`); let assets_json; try { assets_json = await fs.promises.readFile(assets_json_path, "utf-8"); @@ -407,21 +409,21 @@ async function write_assets_json(metafile) { } assets_json = JSON.parse(assets_json); // update with new values - assets_json = Object.assign({}, assets_json, out); - curr_assets_json = assets_json; + let new_assets_json = Object.assign({}, assets_json, out); + curr_assets_json = new_assets_json; await fs.promises.writeFile( assets_json_path, - JSON.stringify(assets_json, null, 4) + JSON.stringify(new_assets_json, null, 4) ); - await update_assets_json_in_cache(assets_json); + await update_assets_json_in_cache(); return { - assets_json, + new_assets_json, prev_assets_json }; } -function update_assets_json_in_cache(assets_json) { +function update_assets_json_in_cache() { // update assets_json cache in redis, so that it can be read directly by python return new Promise(resolve => { let client = get_redis_subscriber("redis_cache"); @@ -429,7 +431,7 @@ function update_assets_json_in_cache(assets_json) { client.on("error", _ => { log_warn("Cannot connect to redis_cache to update assets_json"); }); - client.set("assets_json", JSON.stringify(assets_json), err => { + client.del("assets_json", err => { client.unref(); resolve(); }); diff --git a/frappe/__init__.py b/frappe/__init__.py index 86f8be35ea..ac0d1a3b8f 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -978,8 +978,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa def delete_doc_if_exists(doctype, name, force=0): """Delete document if exists.""" - if db.exists(doctype, name): - delete_doc(doctype, name, force=force) + delete_doc(doctype, name, force=force, ignore_missing=True) def reload_doctype(doctype, force=False, reset_permissions=False): """Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files.""" @@ -1252,9 +1251,10 @@ def get_newargs(fn, kwargs): if hasattr(fn, 'fnargs'): fnargs = fn.fnargs else: - fnargs = inspect.getfullargspec(fn).args - fnargs.extend(inspect.getfullargspec(fn).kwonlyargs) - varkw = inspect.getfullargspec(fn).varkw + fullargspec = inspect.getfullargspec(fn) + fnargs = fullargspec.args + fnargs.extend(fullargspec.kwonlyargs) + varkw = fullargspec.varkw newargs = {} for a in kwargs: @@ -1266,7 +1266,7 @@ def get_newargs(fn, kwargs): return newargs -def make_property_setter(args, ignore_validate=False, validate_fields_for_doctype=True): +def make_property_setter(args, ignore_validate=False, validate_fields_for_doctype=True, is_system_generated=True): """Create a new **Property Setter** (for overriding DocType and DocField properties). If doctype is not specified, it will create a property setter for all fields with the @@ -1297,6 +1297,7 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp 'property': args.property, 'value': args.value, 'property_type': args.property_type or "Data", + 'is_system_generated': is_system_generated, '__islocal': 1 }) ps.flags.ignore_validate = ignore_validate diff --git a/frappe/commands/site.py b/frappe/commands/site.py index b54f369e34..63da4db093 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -677,7 +677,9 @@ def _drop_site(site, db_root_username=None, db_root_password=None, archived_site try: if not no_backup: - scheduled_backup(ignore_files=False, force=True) + click.secho(f"Taking backup of {site}", fg="green") + odb = scheduled_backup(ignore_files=False, force=True, verbose=True) + odb.print_summary() except Exception as err: if force: pass @@ -692,6 +694,7 @@ def _drop_site(site, db_root_username=None, db_root_password=None, archived_site click.echo("\n".join(messages)) sys.exit(1) + click.secho("Dropping site database and user", fg="green") drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password) archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites') diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index d933c2f494..8012d8facf 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -4,8 +4,8 @@ import unittest from urllib.parse import quote import frappe -from frappe.email.doctype.email_queue.email_queue import EmailQueue from frappe.core.doctype.communication.communication import get_emails +from frappe.email.doctype.email_queue.email_queue import EmailQueue test_records = frappe.get_test_records('Communication') @@ -202,7 +202,7 @@ class TestCommunication(unittest.TestCase): self.assertIn(("Note", note.name), doc_links) - def parse_emails(self): + def test_parse_emails(self): emails = get_emails( [ 'comm_recipient+DocType+DocName@example.com', diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index d8e748a518..fb98a18d6e 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -382,7 +382,7 @@ class TestFile(unittest.TestCase): }).insert(ignore_permissions=True) test_file.make_thumbnail() - self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg') + self.assertEqual(test_file.thumbnail_url, '/files/image_small.jpg') # test web image without extension test_file = frappe.get_doc({ @@ -399,7 +399,7 @@ class TestFile(unittest.TestCase): test_file.reload() test_file.file_url = "/files/image_small.jpg" test_file.make_thumbnail(suffix="xs", crop=True) - self.assertEquals(test_file.thumbnail_url, '/files/image_small_xs.jpg') + self.assertEqual(test_file.thumbnail_url, '/files/image_small_xs.jpg') frappe.clear_messages() test_file.db_set('thumbnail_url', None) @@ -407,7 +407,7 @@ class TestFile(unittest.TestCase): test_file.file_url = frappe.utils.get_url('unknown.jpg') test_file.make_thumbnail(suffix="xs") self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found") - self.assertEquals(test_file.thumbnail_url, None) + self.assertEqual(test_file.thumbnail_url, None) def test_file_unzip(self): file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip') diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index d08755f9a8..1ad977547c 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -253,8 +253,8 @@ class User(Document): self.email_new_password(new_password) except frappe.OutgoingEmailError: - print(frappe.get_traceback()) - pass # email server not set, don't send email + # email server not set, don't send email + frappe.log_error(frappe.get_traceback()) @Document.hook def validate_reset_password(self): diff --git a/frappe/core/doctype/user_permission/user_permission.js b/frappe/core/doctype/user_permission/user_permission.js index 8d5c5c1a23..f6989db5d8 100644 --- a/frappe/core/doctype/user_permission/user_permission.js +++ b/frappe/core/doctype/user_permission/user_permission.js @@ -44,8 +44,9 @@ frappe.ui.form.on('User Permission', { set_applicable_for_constraint: frm => { frm.toggle_reqd('applicable_for', !frm.doc.apply_to_all_doctypes); + if (frm.doc.apply_to_all_doctypes && frm.doc.applicable_for) { - frm.set_value('applicable_for', null); + frm.set_value('applicable_for', null, null, true); } }, diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index f09829a688..5632da2149 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -7,6 +7,7 @@ "document_type": "Setup", "engine": "InnoDB", "field_order": [ + "is_system_generated", "dt", "module", "label", @@ -425,13 +426,20 @@ "fieldtype": "Link", "label": "Module (for export)", "options": "Module Def" + }, + { + "default": "0", + "fieldname": "is_system_generated", + "fieldtype": "Check", + "label": "Is System Generated", + "read_only": 1 } ], "icon": "fa fa-glass", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-02-14 15:42:21.885999", + "modified": "2022-02-28 22:22:54.893269", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index cb1ea2c54d..af10c6d76a 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -119,7 +119,7 @@ def create_custom_field_if_values_exist(doctype, df): frappe.db.count(dt=doctype, filters=IfNull(df.fieldname, "") != ""): create_custom_field(doctype, df) -def create_custom_field(doctype, df, ignore_validate=False): +def create_custom_field(doctype, df, ignore_validate=False, is_system_generated=True): df = frappe._dict(df) if not df.fieldname and df.label: df.fieldname = frappe.scrub(df.label) @@ -130,8 +130,7 @@ def create_custom_field(doctype, df, ignore_validate=False): "permlevel": 0, "fieldtype": 'Data', "hidden": 0, - # Looks like we always use this programatically? - # "is_standard": 1 + "is_system_generated": is_system_generated }) custom_field.update(df) custom_field.flags.ignore_validate = ignore_validate diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 4862185b99..7c2a7a2aff 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -14,7 +14,6 @@ frappe.ui.form.on("Customize Form", { }, onload: function(frm) { - frm.disable_save(); frm.set_query("doc_type", function() { return { translate_values: false, @@ -110,7 +109,7 @@ frappe.ui.form.on("Customize Form", { }, refresh: function(frm) { - frm.disable_save(); + frm.disable_save(true); frm.page.clear_icons(); if (frm.doc.doc_type) { @@ -169,7 +168,7 @@ frappe.ui.form.on("Customize Form", { doc_type = localStorage.getItem("customize_doctype"); } if (doc_type) { - setTimeout(() => frm.set_value("doc_type", doc_type), 1000); + setTimeout(() => frm.set_value("doc_type", doc_type, false, true), 1000); } }, @@ -243,7 +242,8 @@ frappe.ui.form.on("Customize Form Field", { }, fields_add: function(frm, cdt, cdn) { var f = frappe.model.get_doc(cdt, cdn); - f.is_custom_field = 1; + f.is_system_generated = false; + f.is_custom_field = true; } }); @@ -341,11 +341,11 @@ frappe.customize_form.confirm = function(msg, frm) { } frappe.customize_form.clear_locals_and_refresh = function(frm) { + delete frm.doc.__unsaved; // clear doctype from locals frappe.model.clear_doc("DocType", frm.doc.doc_type); delete frappe.meta.docfield_copy[frm.doc.doc_type]; - frm.refresh(); -} +}; extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm})); diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 81cd38ff87..efee006301 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -402,7 +402,7 @@ class CustomizeForm(Document): "property": prop, "value": value, "property_type": property_type - }) + }, is_system_generated=False) def get_existing_property_value(self, property_name, fieldname=None): # check if there is any need to make property setter! diff --git a/frappe/custom/doctype/property_setter/property_setter.json b/frappe/custom/doctype/property_setter/property_setter.json index 9707f1ee1c..039826b3b7 100644 --- a/frappe/custom/doctype/property_setter/property_setter.json +++ b/frappe/custom/doctype/property_setter/property_setter.json @@ -6,6 +6,7 @@ "document_type": "Setup", "engine": "InnoDB", "field_order": [ + "is_system_generated", "help", "sb0", "doctype_or_field", @@ -103,13 +104,20 @@ { "fieldname": "section_break_9", "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "is_system_generated", + "fieldtype": "Check", + "label": "Is System Generated", + "read_only": 1 } ], "icon": "fa fa-glass", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-14 14:15:41.929071", + "modified": "2022-02-28 22:24:12.377693", "modified_by": "Administrator", "module": "Custom", "name": "Property Setter", diff --git a/frappe/database/__init__.py b/frappe/database/__init__.py index 7b26ac31b3..5db0537ed7 100644 --- a/frappe/database/__init__.py +++ b/frappe/database/__init__.py @@ -18,7 +18,8 @@ def setup_database(force, source_sql=None, verbose=None, no_mariadb_socket=False def drop_user_and_database(db_name, root_login=None, root_password=None): import frappe if frappe.conf.db_type == 'postgres': - pass + import frappe.database.postgres.setup_db + return frappe.database.postgres.setup_db.drop_user_and_database(db_name, root_login, root_password) else: import frappe.database.mariadb.setup_db return frappe.database.mariadb.setup_db.drop_user_and_database(db_name, root_login, root_password) diff --git a/frappe/database/database.py b/frappe/database/database.py index b20ca79298..24dfdd32df 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -119,6 +119,9 @@ class Database(object): if not run: return query + # remove \n \t from start and end of query + query = re.sub(r'^\s*|\s*$', '', query) + if re.search(r'ifnull\(', query, flags=re.IGNORECASE): # replaces ifnull in query with coalesce query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE) @@ -384,7 +387,7 @@ class Database(object): """ ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug, - order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct) + order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct, limit=1) if not run: return ret @@ -393,7 +396,7 @@ class Database(object): def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False, debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False, - run=True, pluck=False, distinct=False): + run=True, pluck=False, distinct=False, limit=None): """Returns multiple document properties. :param doctype: DocType name. @@ -423,14 +426,15 @@ class Database(object): if isinstance(filters, list): out = self._get_value_for_many_names( - doctype, - filters, - fieldname, - order_by, + doctype=doctype, + names=filters, + field=fieldname, + order_by=order_by, debug=debug, run=run, pluck=pluck, distinct=distinct, + limit=limit, ) else: @@ -444,17 +448,18 @@ class Database(object): if order_by: order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by out = self._get_values_from_table( - fields, - filters, - doctype, - as_dict, - debug, - order_by, - update, + fields=fields, + filters=filters, + doctype=doctype, + as_dict=as_dict, + debug=debug, + order_by=order_by, + update=update, for_update=for_update, run=run, pluck=pluck, - distinct=distinct + distinct=distinct, + limit=limit, ) except Exception as e: if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)): @@ -623,6 +628,7 @@ class Database(object): run=True, pluck=False, distinct=False, + limit=None, ): field_objects = [] @@ -641,6 +647,7 @@ class Database(object): field_objects=field_objects, fields=fields, distinct=distinct, + limit=limit, ) if ( fields == "*" @@ -654,7 +661,7 @@ class Database(object): ) return r - def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False): + def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False, limit=None): names = list(filter(None, names)) if names: return self.get_all( @@ -667,6 +674,7 @@ class Database(object): as_list=1, run=run, distinct=distinct, + limit_page_length=limit ) else: return {} @@ -882,27 +890,39 @@ class Database(object): return self.sql("select name from `tab{doctype}` limit 1".format(doctype=doctype)) def exists(self, dt, dn=None, cache=False): - """Returns true if document exists. + """Return the document name of a matching document, or None. - :param dt: DocType name. - :param dn: Document name or filter dict.""" - if isinstance(dt, str): - if dt!="DocType" and dt==dn: - return True # single always exists (!) - try: - return self.get_value(dt, dn, "name", cache=cache) - except Exception: - return None + Note: `cache` only works if `dt` and `dn` are of type `str`. - elif isinstance(dt, dict) and dt.get('doctype'): - try: - conditions = [] - for d in dt: - if d == 'doctype': continue - conditions.append([d, '=', dt[d]]) - return self.get_all(dt['doctype'], filters=conditions, as_list=1) - except Exception: - return None + ## Examples + + Pass doctype and docname (only in this case we can cache the result) + + ``` + exists("User", "jane@example.org", cache=True) + ``` + + Pass a dict of filters including the `"doctype"` key: + + ``` + exists({"doctype": "User", "full_name": "Jane Doe"}) + ``` + + Pass the doctype and a dict of filters: + + ``` + exists("User", {"full_name": "Jane Doe"}) + ``` + """ + if dt != "DocType" and dt == dn: + # single always exists (!) + return dn + + if isinstance(dt, dict): + dt = dt.copy() # don't modify the original dict + dt, dn = dt.pop("doctype"), dt + + return self.get_value(dt, dn, ignore=True, cache=cache) def count(self, dt, filters=None, debug=False, cache=False): """Returns `COUNT(*)` for given DocType and filters.""" diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index b3b2e0fd41..4b265e7660 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -95,3 +95,11 @@ def get_root_connection(root_login=None, root_password=None): frappe.local.flags.root_connection = frappe.database.get_db(user=root_login, password=root_password) return frappe.local.flags.root_connection + + +def drop_user_and_database(db_name, root_login, root_password): + root_conn = get_root_connection(frappe.flags.root_login or root_login, frappe.flags.root_password or root_password) + root_conn.commit() + root_conn.sql(f"SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = %s", (db_name, )) + root_conn.sql(f"DROP DATABASE IF EXISTS {db_name}") + root_conn.sql(f"DROP USER IF EXISTS {db_name}") diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 5ffde0c37b..abeb681a25 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -15,8 +15,6 @@ from frappe.utils.csvutils import to_csv from frappe.utils.xlsxutils import make_xlsx from frappe.desk.query_report import build_xlsx_data -max_reports_per_user = frappe.local.conf.max_reports_per_user or 3 - class AutoEmailReport(Document): def autoname(self): @@ -46,6 +44,8 @@ class AutoEmailReport(Document): def validate_report_count(self): '''check that there are only 3 enabled reports per user''' count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0] + max_reports_per_user = frappe.local.conf.max_reports_per_user or 3 + if count > max_reports_per_user + (-1 if self.flags.in_insert else 0): frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user)) diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py index f05d35be3e..f6f216ada2 100644 --- a/frappe/email/doctype/notification/test_notification.py +++ b/frappe/email/doctype/notification/test_notification.py @@ -240,7 +240,7 @@ class TestNotification(unittest.TestCase): self.assertTrue(email_queue) # check if description is changed after alert since set_property_after_alert is set - self.assertEquals(todo.description, 'Changed by Notification') + self.assertEqual(todo.description, 'Changed by Notification') recipients = [d.recipient for d in email_queue.recipients] self.assertTrue('test2@example.com' in recipients) diff --git a/frappe/handler.py b/frappe/handler.py old mode 100755 new mode 100644 index 3fd1c096e4..ebc72da937 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -225,11 +225,10 @@ def ping(): def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): """run a whitelisted controller method""" - import json - import inspect + from inspect import getfullargspec - if not args: - args = arg or "" + if not args and arg: + args = arg if dt: # not called from a doctype (from a page) if not dn: @@ -237,9 +236,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): doc = frappe.get_doc(dt, dn) else: - if isinstance(docs, str): - docs = json.loads(docs) - + docs = frappe.parse_json(docs) doc = frappe.get_doc(docs) doc._original_modified = doc.modified doc.check_if_latest() @@ -248,16 +245,16 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): throw_permission_error() try: - args = json.loads(args) + args = frappe.parse_json(args) except ValueError: - args = args + pass method_obj = getattr(doc, method) fn = getattr(method_obj, '__func__', method_obj) is_whitelisted(fn) is_valid_http_method(fn) - fnargs = inspect.getfullargspec(method_obj).args + fnargs = getfullargspec(method_obj).args if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"): response = doc.run_method(method) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 3564b1ae11..57b4777355 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -963,7 +963,7 @@ class BaseDocument(object): from frappe.model.meta import get_default_df df = get_default_df(fieldname) - if not currency and df: + if df.fieldtype == "Currency" and not currency: currency = self.get(df.get("options")) if not frappe.db.exists('Currency', currency, cache=True): currency = None diff --git a/frappe/patches.txt b/frappe/patches.txt index a666480c90..e85c837286 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -146,7 +146,7 @@ frappe.patches.v13_0.update_duration_options frappe.patches.v13_0.replace_old_data_import # 2020-06-24 frappe.patches.v13_0.create_custom_dashboards_cards_and_charts frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart -frappe.patches.v13_0.add_standard_navbar_items # 2020-12-15 +frappe.patches.v13_0.add_standard_navbar_items # 2022-03-15 frappe.patches.v13_0.generate_theme_files_in_public_folder frappe.patches.v13_0.increase_password_length frappe.patches.v12_0.fix_email_id_formatting @@ -197,4 +197,5 @@ frappe.patches.v14_0.copy_mail_data #08.03.21 frappe.patches.v14_0.update_github_endpoints #08-11-2021 frappe.patches.v14_0.remove_db_aggregation frappe.patches.v14_0.update_color_names_in_kanban_board_column +frappe.patches.v14_0.update_is_system_generated_flag frappe.patches.v14_0.update_auto_account_deletion_duration diff --git a/frappe/patches/v14_0/update_is_system_generated_flag.py b/frappe/patches/v14_0/update_is_system_generated_flag.py new file mode 100644 index 0000000000..657e02aebc --- /dev/null +++ b/frappe/patches/v14_0/update_is_system_generated_flag.py @@ -0,0 +1,17 @@ +import frappe + +def execute(): + # assuming all customization generated by Admin is system generated customization + custom_field = frappe.qb.DocType("Custom Field") + ( + frappe.qb.update(custom_field) + .set(custom_field.is_system_generated, True) + .where(custom_field.owner == 'Administrator').run() + ) + + property_setter = frappe.qb.DocType("Property Setter") + ( + frappe.qb.update(property_setter) + .set(property_setter.is_system_generated, True) + .where(property_setter.owner == 'Administrator').run() + ) diff --git a/frappe/permissions.py b/frappe/permissions.py index af17faba01..a6c17fb59f 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -594,4 +594,4 @@ def is_parent_valid(child_doctype, parent_doctype): from frappe.core.utils import find parent_meta = frappe.get_meta(parent_doctype) child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype) - return not parent_meta.istable and child_table_field_exists \ No newline at end of file + return not parent_meta.istable and child_table_field_exists diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js index bd66225171..a91058a208 100644 --- a/frappe/public/js/frappe/form/controls/attach.js +++ b/frappe/public/js/frappe/form/controls/attach.js @@ -37,8 +37,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro if(this.frm) { me.parse_validate_and_set_in_model(null); me.refresh(); - me.frm.attachments.remove_attachment_by_filename(me.value, function() { - me.parse_validate_and_set_in_model(null); + me.frm.attachments.remove_attachment_by_filename(me.value, async () => { + await me.parse_validate_and_set_in_model(null); me.refresh(); me.frm.doc.docstatus == 1 ? me.frm.save('Update') : me.frm.save(); }); @@ -110,9 +110,9 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro return this.value || null; } - on_upload_complete(attachment) { + async on_upload_complete(attachment) { if(this.frm) { - this.parse_validate_and_set_in_model(attachment.file_url); + await this.parse_validate_and_set_in_model(attachment.file_url); this.frm.attachments.update_attachment(attachment); this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save(); } diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index d440874f36..0070d384d7 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -454,7 +454,10 @@ class FormTimeline extends BaseTimeline { let edit_box = this.make_editable(edit_wrapper); let content_wrapper = comment_wrapper.find('.content'); let more_actions_wrapper = comment_wrapper.find('.more-actions'); - if (frappe.model.can_delete("Comment")) { + if (frappe.model.can_delete("Comment") && ( + frappe.session.user == doc.owner || + frappe.user.has_role("System Manager") + )) { const delete_option = $(`