diff --git a/codecov.yml b/codecov.yml index ad60b8a6d9..125a7ef014 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,17 +2,16 @@ codecov: require_ci_to_pass: yes coverage: + range: 60..90 status: project: - default: false - server-mariadb: + default: target: auto threshold: 0.5% flags: - server-mariadb patch: - default: false - server-mariadb: + default: target: 85% threshold: 0% only_pulls: true diff --git a/cypress/integration/web_form.js b/cypress/integration/web_form.js index 9173bfaeb3..d7f210e7d2 100644 --- a/cypress/integration/web_form.js +++ b/cypress/integration/web_form.js @@ -45,7 +45,7 @@ context("Web Form", () => { cy.login(); cy.visit("/app/web-form/note"); - cy.findByRole("tab", { name: "Form Settings" }).click(); + cy.findByRole("tab", { name: "Settings" }).click(); cy.get('input[data-fieldname="login_required"]').check({ force: true }); cy.save(); @@ -65,7 +65,8 @@ context("Web Form", () => { cy.login(); cy.visit("/app/web-form/note"); - cy.findByRole("tab", { name: "List Settings" }).click(); + cy.findByRole("tab", { name: "Settings" }).click(); + cy.get(".section-head").contains("List Settings").click(); cy.get('input[data-fieldname="show_list"]').check(); cy.save(); @@ -78,7 +79,7 @@ context("Web Form", () => { it("Show Custom List Title", () => { cy.visit("/app/web-form/note"); - cy.findByRole("tab", { name: "List Settings" }).click(); + cy.findByRole("tab", { name: "Settings" }).click(); cy.fill_field("list_title", "Note List"); cy.save(); @@ -97,7 +98,7 @@ context("Web Form", () => { cy.visit("/app/web-form/note"); - cy.findByRole("tab", { name: "List Settings" }).click(); + cy.findByRole("tab", { name: "Settings" }).click(); cy.get('[data-fieldname="list_columns"] .grid-footer button') .contains("Add Row") @@ -108,19 +109,19 @@ context("Web Form", () => { cy.get("@grid-rows").find('.grid-row:first [data-fieldname="fieldname"]').click(); cy.get("@grid-rows") .find('.grid-row:first select[data-fieldname="fieldname"]') - .select("Title (Data)"); + .select("Title"); cy.get("@add-row").click(); cy.get("@grid-rows").find('.grid-row[data-idx="2"] [data-fieldname="fieldname"]').click(); cy.get("@grid-rows") .find('.grid-row[data-idx="2"] select[data-fieldname="fieldname"]') - .select("Public (Check)"); + .select("Public"); cy.get("@add-row").click(); cy.get("@grid-rows").find('.grid-row:last [data-fieldname="fieldname"]').click(); cy.get("@grid-rows") .find('.grid-row:last select[data-fieldname="fieldname"]') - .select("Content (Text Editor)"); + .select("Content"); cy.save(); @@ -171,7 +172,7 @@ context("Web Form", () => { it("Edit Mode", () => { cy.visit("/app/web-form/note"); - cy.findByRole("tab", { name: "Form Settings" }).click(); + cy.findByRole("tab", { name: "Settings" }).click(); cy.get('input[data-fieldname="allow_edit"]').check(); cy.save(); @@ -179,7 +180,7 @@ context("Web Form", () => { cy.visit("/note/Note 1"); cy.url().should("include", "/note/Note%201"); - cy.get(".web-form-actions a").contains("Edit").click(); + cy.get(".web-form-actions a").contains("Edit Response").click(); cy.url().should("include", "/note/Note%201/edit"); // Editable Field @@ -194,7 +195,7 @@ context("Web Form", () => { it("Allow Multiple Response", () => { cy.visit("/app/web-form/note"); - cy.findByRole("tab", { name: "Form Settings" }).click(); + cy.findByRole("tab", { name: "Settings" }).click(); cy.get('input[data-fieldname="allow_multiple"]').check(); cy.save(); @@ -212,7 +213,7 @@ context("Web Form", () => { it("Allow Delete", () => { cy.visit("/app/web-form/note"); - cy.findByRole("tab", { name: "Form Settings" }).click(); + cy.findByRole("tab", { name: "Settings" }).click(); cy.get('input[data-fieldname="allow_delete"]').check(); cy.save(); @@ -235,7 +236,7 @@ context("Web Form", () => { it("Navigate and Submit a WebForm", () => { cy.visit("/update-profile"); - cy.get(".web-form-actions a").contains("Edit").click(); + cy.get(".web-form-actions a").contains("Edit Response").click(); cy.fill_field("middle_name", "_Test User"); @@ -247,7 +248,7 @@ context("Web Form", () => { cy.call("frappe.tests.ui_test_helpers.update_webform_to_multistep").then(() => { cy.visit("/update-profile-duplicate"); - cy.get(".web-form-actions a").contains("Edit").click(); + cy.get(".web-form-actions a").contains("Edit Response").click(); cy.fill_field("middle_name", "_Test User"); diff --git a/frappe/commands/site.py b/frappe/commands/site.py index ab599be121..eb5a732f04 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -496,6 +496,32 @@ def add_system_manager(context, email, first_name, last_name, send_welcome_email raise SiteNotSpecifiedError +@click.command("add-user") +@click.argument("email") +@click.option("--first-name") +@click.option("--last-name") +@click.option("--password") +@click.option("--user-type") +@click.option("--add-role", multiple=True) +@click.option("--send-welcome-email", default=False, is_flag=True) +@pass_context +def add_user_for_sites( + context, email, first_name, last_name, user_type, send_welcome_email, password, add_role +): + "Add user to a site" + import frappe.utils.user + + for site in context.sites: + frappe.connect(site=site) + try: + add_new_user(email, first_name, last_name, user_type, send_welcome_email, password, add_role) + frappe.db.commit() + finally: + frappe.destroy() + if not context.sites: + raise SiteNotSpecifiedError + + @click.command("disable-user") @click.argument("email") @pass_context @@ -1275,8 +1301,38 @@ def handle_data(data: dict, format="json"): render_table(data) +def add_new_user( + email, + first_name=None, + last_name=None, + user_type="System User", + send_welcome_email=False, + password=None, + role=None, +): + user = frappe.new_doc("User") + user.update( + { + "name": email, + "email": email, + "enabled": 1, + "first_name": first_name or email, + "last_name": last_name, + "user_type": user_type, + "send_welcome_email": 1 if send_welcome_email else 0, + } + ) + user.insert() + user.add_roles(*role) + if password: + from frappe.utils.password import update_password + + update_password(user=user.name, pwd=password) + + commands = [ add_system_manager, + add_user_for_sites, backup, drop_site, install_app, diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 378044c219..7ff25118b1 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -990,10 +990,11 @@ class Column: not_exists = list(set(values) - set(exists)) if not_exists: missing_values = ", ".join(not_exists) + message = _("The following values do not exist for {0}: {1}") self.warnings.append( { "col": self.column_number, - "message": (f"The following values do not exist for {self.df.options}: {missing_values}"), + "message": message.format(self.df.options, missing_values), "type": "warning", } ) @@ -1003,17 +1004,18 @@ class Column: if not self.date_format: if self.df.fieldtype == "Time": self.date_format = "%H:%M:%S" - format = "HH:mm:ss" + date_format = "HH:mm:ss" else: self.date_format = "%Y-%m-%d" - format = "yyyy-mm-dd" + date_format = "yyyy-mm-dd" + message = _( + "{0} format could not be determined from the values in this column. Defaulting to {1}." + ) self.warnings.append( { "col": self.column_number, - "message": _( - "{0} format could not be determined from the values in this column. Defaulting to {1}." - ).format(self.df.fieldtype, format), + "message": message.format(self.df.fieldtype, date_format), "type": "info", } ) @@ -1025,13 +1027,11 @@ class Column: if invalid: valid_values = ", ".join(frappe.bold(o) for o in options) invalid_values = ", ".join(frappe.bold(i) for i in invalid) + message = _("The following values are invalid: {0}. Values must be one of {1}") self.warnings.append( { "col": self.column_number, - "message": ( - "The following values are invalid: {}. Values must be" - " one of {}".format(invalid_values, valid_values) - ), + "message": message.format(invalid_values, valid_values), } ) diff --git a/frappe/core/doctype/translation/test_translation.py b/frappe/core/doctype/translation/test_translation.py index 581cda83f0..ebed447b00 100644 --- a/frappe/core/doctype/translation/test_translation.py +++ b/frappe/core/doctype/translation/test_translation.py @@ -11,18 +11,19 @@ class TestTranslation(FrappeTestCase): def tearDown(self): frappe.local.lang = "en" - frappe.local.lang_full_dict = None + clear_translation_cache() def test_doctype(self): translation_data = get_translation_data() for key, val in translation_data.items(): frappe.local.lang = key - frappe.local.lang_full_dict = None + + clear_translation_cache() translation = create_translation(key, val) self.assertEqual(_(val[0]), val[1]) frappe.delete_doc("Translation", translation.name) - frappe.local.lang_full_dict = None + clear_translation_cache() self.assertEqual(_(val[0]), val[0]) @@ -38,20 +39,20 @@ class TestTranslation(FrappeTestCase): frappe.local.lang = "es" - frappe.local.lang_full_dict = None + clear_translation_cache() self.assertTrue(_(data[0][0]), data[0][1]) - frappe.local.lang_full_dict = None + clear_translation_cache() self.assertTrue(_(data[1][0]), data[1][1]) frappe.local.lang = "es-MX" # different translation for es-MX - frappe.local.lang_full_dict = None + clear_translation_cache() self.assertTrue(_(data[2][0]), data[2][1]) # from spanish (general) - frappe.local.lang_full_dict = None + clear_translation_cache() self.assertTrue(_(data[1][0]), data[1][1]) def test_html_content_data_translation(self): @@ -109,3 +110,8 @@ def create_translation(key, val): translation.translated_text = val[1] translation.save() return translation + + +def clear_translation_cache(): + frappe.local.lang_full_dict = None + frappe.cache().delete_key("lang_full_dict", shared=True) diff --git a/frappe/defaults.py b/frappe/defaults.py index 02076b1fda..744a3fad5d 100644 --- a/frappe/defaults.py +++ b/frappe/defaults.py @@ -6,8 +6,7 @@ from frappe.cache_manager import clear_defaults_cache, common_default_keys from frappe.desk.notifications import clear_notifications from frappe.query_builder import DocType -# Note: DefaultValue records are identified by parenttype -# __default, __global or 'User Permission' +# Note: DefaultValue records are identified by parent (e.g. __default, __global) def set_user_default(key, value, user=None, parenttype=None): diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json index 032de9de4e..529fc8bf8b 100644 --- a/frappe/desk/doctype/workspace/workspace.json +++ b/frappe/desk/doctype/workspace/workspace.json @@ -107,7 +107,8 @@ { "fieldname": "icon", "fieldtype": "Icon", - "label": "Icon" + "label": "Icon", + "read_only": 1 }, { "fieldname": "links", @@ -122,18 +123,21 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Public", + "read_only": 1, "search_index": 1 }, { "fieldname": "title", "fieldtype": "Data", "label": "Title", + "read_only": 1, "reqd": 1 }, { "fieldname": "parent_page", "fieldtype": "Data", - "label": "Parent Page" + "label": "Parent Page", + "read_only": 1 }, { "default": "[]", @@ -145,7 +149,8 @@ { "fieldname": "sequence_id", "fieldtype": "Float", - "label": "Sequence Id" + "label": "Sequence Id", + "read_only": 1 }, { "fieldname": "roles", @@ -172,7 +177,7 @@ ], "in_create": 1, "links": [], - "modified": "2022-05-12 13:00:03.925605", + "modified": "2022-08-16 18:01:42.632238", "modified_by": "Administrator", "module": "Desk", "name": "Workspace", diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 9fa99884fb..0e49d1d7e1 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -9,7 +9,7 @@ from frappe.desk.desktop import save_new_widget from frappe.desk.utils import validate_route_conflict from frappe.model.document import Document from frappe.model.rename_doc import rename_doc -from frappe.modules.export_file import export_to_files +from frappe.modules.export_file import delete_folder, export_to_files class Workspace(Document): @@ -28,8 +28,22 @@ class Workspace(Document): if disable_saving_as_public(): return - if frappe.conf.developer_mode and self.module and self.public: - export_to_files(record_list=[["Workspace", self.name]], record_module=self.module) + if frappe.conf.developer_mode and self.public: + if self.module: + export_to_files(record_list=[["Workspace", self.name]], record_module=self.module) + + if self.has_value_changed("title") or self.has_value_changed("module"): + previous = self.get_doc_before_save() + if previous and previous.get("module") and previous.get("title"): + delete_folder(previous.get("module"), "Workspace", previous.get("title")) + + def before_export(self, doc): + if doc.title != doc.label and doc.label == doc.name: + self.name = doc.name = doc.label = doc.title + + def after_delete(self): + if self.module: + delete_folder(self.module, "Workspace", self.title) @staticmethod def get_module_page_map(): diff --git a/frappe/email/doctype/email_account/email_account.js b/frappe/email/doctype/email_account/email_account.js index 3e42af7051..98160e5f46 100644 --- a/frappe/email/doctype/email_account/email_account.js +++ b/frappe/email/doctype/email_account/email_account.js @@ -158,7 +158,6 @@ frappe.ui.form.on("Email Account", { }, refresh: function (frm) { - frm.events.set_domain_fields(frm); frm.events.enable_incoming(frm); frm.events.notify_if_unreplied(frm); frm.events.show_gmail_message_for_less_secure_apps(frm); @@ -211,42 +210,24 @@ frappe.ui.form.on("Email Account", { oauth_access(frm); }, - email_id: function (frm) { - //pull domain and if no matching domain go create one - frm.events.update_domain(frm); - }, - - update_domain: function (frm) { - if (!frm.doc.email_id && !frm.doc.service) { - return; + domain: frappe.utils.debounce((frm) => { + if (frm.doc.domain) { + frappe.call({ + method: "get_domain_values", + doc: frm.doc, + args: { + domain: frm.doc.domain, + }, + callback: function (r) { + if (!r.exc) { + for (let field in r.message) { + frm.set_value(field, r.message[field]); + } + } + }, + }); } - - frappe.call({ - method: "get_domain", - doc: frm.doc, - args: { - email_id: frm.doc.email_id, - }, - callback: function (r) { - if (r.message) { - frm.events.set_domain_fields(frm, r.message); - } - }, - }); - }, - - set_domain_fields: function (frm, args) { - if (!args) { - args = frappe.route_flags.set_domain_values ? frappe.route_options : {}; - } - - for (var field in args) { - frm.set_value(field, args[field]); - } - - delete frappe.route_flags.set_domain_values; - frappe.route_options = {}; - }, + }), email_sync_option: function (frm) { // confirm if the ALL sync option is selected diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json index 6ca5e289df..da88ac680c 100644 --- a/frappe/email/doctype/email_account/email_account.json +++ b/frappe/email/doctype/email_account/email_account.json @@ -145,7 +145,7 @@ "hide_seconds": 1, "in_list_view": 1, "in_standard_filter": 1, - "label": "Domain (optional)", + "label": "Domain", "options": "Email Domain" }, { @@ -154,7 +154,7 @@ "fieldtype": "Select", "hide_days": 1, "hide_seconds": 1, - "label": "Service (optional)", + "label": "Service", "options": "\nGMail\nSendgrid\nSparkPost\nYahoo Mail\nOutlook.com\nYandex.Mail" }, { @@ -615,7 +615,7 @@ "icon": "fa fa-inbox", "index_web_pages_for_search": 1, "links": [], - "modified": "2022-08-16 13:05:45.445572", + "modified": "2022-08-23 00:31:05.305462", "modified_by": "Administrator", "module": "Email", "name": "Email Account", @@ -639,4 +639,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index b3dc72dfe5..c69d653c96 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -12,6 +12,7 @@ from poplib import error_proto import frappe from frappe import _, are_emails_muted, safe_encode from frappe.desk.form import assign_to +from frappe.email.doctype.email_domain.email_domain import EMAIL_DOMAIN_FIELDS from frappe.email.receive import EmailServer, InboundMail, SentEmailInInboxError from frappe.email.smtp import SMTPServer from frappe.email.utils import get_port @@ -179,26 +180,8 @@ class EmailAccount(Document): email_account.save() @frappe.whitelist() - def get_domain(self, email_id): - """look-up the domain and then full""" - try: - domain = email_id.split("@") - fields = [ - "name as domain", - "use_imap", - "email_server", - "use_ssl", - "use_starttls", - "smtp_server", - "use_tls", - "smtp_port", - "incoming_port", - "append_emails_to_sent_folder", - "use_ssl_for_outgoing", - ] - return frappe.db.get_value("Email Domain", domain[1], fields, as_dict=True) - except Exception: - pass + def get_domain_values(self, domain: str): + return frappe.db.get_value("Email Domain", domain, EMAIL_DOMAIN_FIELDS, as_dict=True) def get_incoming_server(self, in_receive=False, email_sync_rule="UNSEEN"): """Returns logged in POP3/IMAP connection object.""" diff --git a/frappe/email/doctype/email_domain/email_domain.py b/frappe/email/doctype/email_domain/email_domain.py index e532c87c72..528407916a 100644 --- a/frappe/email/doctype/email_domain/email_domain.py +++ b/frappe/email/doctype/email_domain/email_domain.py @@ -11,6 +11,20 @@ from frappe.email.utils import get_port from frappe.model.document import Document from frappe.utils import cint +EMAIL_DOMAIN_FIELDS = [ + "email_server", + "use_imap", + "use_ssl", + "use_starttls", + "use_tls", + "attachment_limit", + "smtp_server", + "smtp_port", + "use_ssl_for_outgoing", + "append_emails_to_sent_folder", + "incoming_port", +] + def get_error_message(event): return { @@ -52,19 +66,7 @@ class EmailDomain(Document): for email_account in frappe.get_all("Email Account", filters={"domain": self.name}): try: email_account = frappe.get_doc("Email Account", email_account.name) - for attr in [ - "email_server", - "use_imap", - "use_ssl", - "use_starttls", - "use_tls", - "attachment_limit", - "smtp_server", - "smtp_port", - "use_ssl_for_outgoing", - "append_emails_to_sent_folder", - "incoming_port", - ]: + for attr in EMAIL_DOMAIN_FIELDS: email_account.set(attr, self.get(attr, default=0)) email_account.save() diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 8d0857ac60..41fdfeeda1 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -387,6 +387,9 @@ def get_context(context): if not is_html(self.message): self.message = frappe.utils.md_to_html(self.message) + def on_trash(self): + frappe.cache().hdel("notifications", self.document_type) + @frappe.whitelist() def get_documents_for_today(notification): diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 867f541a0e..254a7ebe12 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -612,6 +612,9 @@ class DatabaseQuery: ) elif f.operator.lower() in ("in", "not in"): + # if values contain '' or falsy values then only coalesce column + can_be_null = not f.value or any(v is None or v == "" for v in f.value) + values = f.value or "" if isinstance(values, str): values = values.split(",") diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 332a4337e2..072b9a1d66 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -167,9 +167,6 @@ def delete_doc( except ImportError: pass - # delete user_permissions - frappe.defaults.clear_default(parenttype="User Permission", key=doctype, value=name) - def add_to_deleted_document(doc): """Add this document to Deleted Document table. Called after delete""" diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 0a832580cd..46a239d0aa 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -197,14 +197,6 @@ def rename_doc( if not merge: rename_password(doctype, old, new) - # update user_permissions - DefaultValue = frappe.qb.DocType("DefaultValue") - frappe.qb.update(DefaultValue).set(DefaultValue.defvalue, new).where( - (DefaultValue.parenttype == "User Permission") - & (DefaultValue.defkey == doctype) - & (DefaultValue.defvalue == old) - ).run() - if merge: new_doc.add_comment("Edit", _("merged {0} into {1}").format(frappe.bold(old), frappe.bold(new))) else: diff --git a/frappe/modules/export_file.py b/frappe/modules/export_file.py index b448c04f2f..edbf5ccaca 100644 --- a/frappe/modules/export_file.py +++ b/frappe/modules/export_file.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import os +import shutil import frappe import frappe.model @@ -92,6 +93,21 @@ def get_module_name(doc): return module +def delete_folder(module, dt, dn): + if frappe.db.get_value("Module Def", module, "custom"): + module_path = get_custom_module_path(module) + else: + module_path = get_module_path(module) + + dt, dn = scrub_dt_dn(dt, dn) + + # delete folder + folder = os.path.join(module_path, dt, dn) + + if os.path.exists(folder): + shutil.rmtree(folder) + + def create_folder(module, dt, dn, create_init): if frappe.db.get_value("Module Def", module, "custom"): module_path = get_custom_module_path(module) diff --git a/frappe/public/js/bootstrap-4-web.bundle.js b/frappe/public/js/bootstrap-4-web.bundle.js index 083b7aecaa..3845a7b185 100644 --- a/frappe/public/js/bootstrap-4-web.bundle.js +++ b/frappe/public/js/bootstrap-4-web.bundle.js @@ -32,10 +32,7 @@ frappe.get_modal = function (title, content) { ${content}
@@ -49,11 +46,19 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.Dialog { return this.$wrapper.find(".modal-footer .btn-primary"); } + get_secondary_btn() { + return this.$wrapper.find(".modal-footer .btn-secondary"); + } + set_primary_action(label, click) { this.$wrapper.find(".modal-footer").removeClass("hidden"); return super.set_primary_action(label, click).removeClass("hidden"); } + set_secondary_action(click) { + return super.set_secondary_action(click).removeClass("hidden"); + } + make() { super.make(); if (this.fields) { diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js index 8b0e19ad64..0c854f6a0a 100644 --- a/frappe/public/js/frappe/form/controls/attach.js +++ b/frappe/public/js/frappe/form/controls/attach.js @@ -89,6 +89,7 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro } set_input(value, dataurl) { + this.last_value = this.value; this.value = value; if (this.value) { this.$input.toggle(false); diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index a315abc7e0..9d9c919e97 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -33,6 +33,14 @@ frappe.ui.form.Control = class BaseControl { this.refresh(); } + get perm() { + return this.frm?.perm; + } + + set perm(_perm) { + console.error("Setting perm on controls isn't supported, update form's perm instead"); + } + // returns "Read", "Write" or "None" // as strings based on permissions get_status(explain) { @@ -82,7 +90,7 @@ frappe.ui.form.Control = class BaseControl { is_null(value) && !in_list(["HTML", "Image", "Button"], this.df.fieldtype) ) - status = "None"; + status = "Read"; return status; } @@ -270,9 +278,6 @@ frappe.ui.form.Control = class BaseControl { } else { if (this.doc) { this.doc[this.df.fieldname] = value; - } else { - // case where input is rendered on dialog where doc is not maintained - this.value = value; } this.set_input(value); return Promise.resolve(); diff --git a/frappe/public/js/frappe/form/controls/check.js b/frappe/public/js/frappe/form/controls/check.js index 3d191c0520..91409ce4e0 100644 --- a/frappe/public/js/frappe/form/controls/check.js +++ b/frappe/public/js/frappe/form/controls/check.js @@ -31,11 +31,12 @@ frappe.ui.form.ControlCheck = class ControlCheck extends frappe.ui.form.ControlD return cint(value); } set_input(value) { + this.last_value = this.value; value = cint(value); + this.value = value; if (this.input) { this.input.checked = value ? 1 : 0; } - this.last_value = value; this.set_mandatory(value); this.set_disp_area(value); } diff --git a/frappe/public/js/frappe/form/controls/color.js b/frappe/public/js/frappe/form/controls/color.js index 7fcadc6638..e9f88faec5 100644 --- a/frappe/public/js/frappe/form/controls/color.js +++ b/frappe/public/js/frappe/form/controls/color.js @@ -93,11 +93,11 @@ frappe.ui.form.ControlColor = class ControlColor extends frappe.ui.form.ControlD set_formatted_input(value) { super.set_formatted_input(value); - this.$input.val(value); - this.selected_color.css({ + this.$input?.val(value); + this.selected_color?.css({ "background-color": value || "transparent", }); - this.selected_color.toggleClass("no-value", !value); + this.selected_color?.toggleClass("no-value", !value); } get_color() { diff --git a/frappe/public/js/frappe/form/controls/table.js b/frappe/public/js/frappe/form/controls/table.js index 39aba3a234..777705634c 100644 --- a/frappe/public/js/frappe/form/controls/table.js +++ b/frappe/public/js/frappe/form/controls/table.js @@ -8,7 +8,6 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control this.grid = new Grid({ frm: this.frm, df: this.df, - perm: this.perm || (this.frm && this.frm.perm) || this.df.perm, parent: this.wrapper, control: this, }); diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index feadc08ead..db121f1c24 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -403,11 +403,17 @@ frappe.ui.form.Form = class FrappeForm { this.doc = frappe.get_doc(this.doctype, this.docname); // check permissions + this.fetch_permissions(); if (!this.has_read_permission()) { frappe.show_not_permitted(__(this.doctype) + " " + __(cstr(this.docname))); return; } + // update grids with new permissions + this.grids.forEach((table) => { + table.grid.refresh(); + }); + // read only (workflow) this.read_only = frappe.workflow.is_read_only(this.doctype, this.docname); if (this.read_only) this.set_read_only(true); @@ -1157,11 +1163,12 @@ frappe.ui.form.Form = class FrappeForm { .attr("target", "_blank"); } - has_read_permission() { - // get perm - var dt = this.parent_doctype ? this.parent_doctype : this.doctype; + fetch_permissions() { + let dt = this.parent_doctype ? this.parent_doctype : this.doctype; this.perm = frappe.perm.get_perm(dt, this.doc); + } + has_read_permission() { if (!this.perm[0].read) { return 0; } diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index e0286a4af1..7c580ff2b3 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -294,7 +294,10 @@ frappe.form.formatters = { let formatted_value = frappe.form.formatters.Text(value); // to use ql-editor styles try { - if (!$(formatted_value).find(".ql-editor").length) { + if ( + !$(formatted_value).find(".ql-editor").length && + !$(formatted_value).hasClass("ql-editor") + ) { formatted_value = `${value}
{{ _(title) }}
+ {% else %} +
+ ${__("login_required")}
+
`;
+ let message = __(
+ "Login is required to see web form list view. Enable {0} to see list settings",
+ [go_to_login_required_field]
+ );
$(frm.fields_dict["list_setting_message"].wrapper)
- .html(
- $(
- ``
- )
- )
- .find("span")
+ .html($(``))
+ .find("code")
.click(() => frm.scroll_to_field("login_required"));
} else {
$(frm.fields_dict["list_setting_message"].wrapper).empty();
diff --git a/frappe/website/doctype/web_form/web_form.json b/frappe/website/doctype/web_form/web_form.json
index 8faa263e5b..f5ab147c64 100644
--- a/frappe/website/doctype/web_form/web_form.json
+++ b/frappe/website/doctype/web_form/web_form.json
@@ -5,50 +5,50 @@
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
- "title_and_route_tab",
+ "form_tab",
"title",
"route",
"published",
- "column_break_4",
+ "column_break_1",
"doc_type",
"module",
"is_standard",
- "introduction",
+ "section_break_1",
"introduction_text",
- "form_settings_tab",
+ "web_form_fields",
+ "settings_tab",
"login_required",
"allow_multiple",
"allow_edit",
"allow_delete",
- "column_break_18",
+ "column_break_2",
"apply_document_permissions",
"allow_print",
"print_format",
"allow_comments",
"show_attachments",
"allow_incomplete",
- "form_fields",
- "web_form_fields",
+ "section_break_2",
"max_attachment_size",
- "list_settings_tab",
+ "section_break_3",
"list_setting_message",
"show_list",
"list_title",
"list_columns",
- "sidebar_settings_tab",
+ "section_break_4",
"show_sidebar",
"website_sidebar",
"customization_tab",
"button_label",
"banner_image",
- "column_break_37",
+ "column_break_3",
"breadcrumbs",
- "section_break_43",
+ "section_break_5",
"success_title",
"success_url",
- "column_break_41",
+ "column_break_4",
"success_message",
- "scripting_style_tab",
+ "section_break_6",
"client_script",
"custom_css"
],
@@ -81,10 +81,6 @@
"label": "Module",
"options": "Module Def"
},
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
{
"default": "0",
"fieldname": "is_standard",
@@ -158,12 +154,6 @@
"fieldtype": "Check",
"label": "Allow Incomplete Forms"
},
- {
- "collapsible": 1,
- "fieldname": "introduction",
- "fieldtype": "Section Break",
- "label": "Introduction"
- },
{
"fieldname": "introduction_text",
"fieldtype": "Text Editor",
@@ -250,21 +240,6 @@
"label": "List Columns",
"options": "Web Form List Column"
},
- {
- "fieldname": "title_and_route_tab",
- "fieldtype": "Tab Break",
- "label": "Title & Route"
- },
- {
- "collapsible": 1,
- "fieldname": "form_fields",
- "fieldtype": "Section Break",
- "label": "Form Fields"
- },
- {
- "fieldname": "column_break_18",
- "fieldtype": "Column Break"
- },
{
"fieldname": "website_sidebar",
"fieldtype": "Link",
@@ -276,29 +251,6 @@
"fieldtype": "HTML",
"label": "List Setting Message"
},
- {
- "fieldname": "form_settings_tab",
- "fieldtype": "Tab Break",
- "label": "Form Settings"
- },
- {
- "collapsible": 1,
- "collapsible_depends_on": "show_list",
- "fieldname": "list_settings_tab",
- "fieldtype": "Tab Break",
- "label": "List Settings"
- },
- {
- "collapsible": 1,
- "fieldname": "sidebar_settings_tab",
- "fieldtype": "Tab Break",
- "label": "Sidebar Settings"
- },
- {
- "fieldname": "scripting_style_tab",
- "fieldtype": "Tab Break",
- "label": "Scripting / Style"
- },
{
"fieldname": "customization_tab",
"fieldtype": "Tab Break",
@@ -315,24 +267,74 @@
"label": "Banner Image"
},
{
- "fieldname": "column_break_41",
+ "fieldname": "form_tab",
+ "fieldtype": "Tab Break",
+ "label": "Form"
+ },
+ {
+ "fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
- "fieldname": "section_break_43",
+ "fieldname": "section_break_1",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "settings_tab",
+ "fieldtype": "Tab Break",
+ "label": "Settings"
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "show_list",
+ "fieldname": "section_break_3",
+ "fieldtype": "Section Break",
+ "label": "List Settings"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "show_sidebar",
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "label": "Sidebar Settings"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "eval: doc.success_title || doc.success_message || doc.success_url",
+ "fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "After Submission"
},
{
- "fieldname": "column_break_37",
+ "fieldname": "column_break_4",
"fieldtype": "Column Break"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "eval: doc.client_script || doc.custom_css",
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break",
+ "label": "Scripting / Style"
}
],
"has_web_view": 1,
"icon": "icon-edit",
"is_published_field": "published",
"links": [],
- "modified": "2022-08-11 16:27:25.914627",
+ "modified": "2022-08-17 18:58:49.451658",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form",
diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py
index 718088212f..e0ae91fef7 100644
--- a/frappe/website/doctype/web_form/web_form.py
+++ b/frappe/website/doctype/web_form/web_form.py
@@ -8,7 +8,6 @@ import frappe
from frappe import _, scrub
from frappe.core.api.file import get_max_file_size
from frappe.core.doctype.file import remove_file_by_url
-from frappe.custom.doctype.customize_form.customize_form import docfield_properties
from frappe.desk.form.meta import get_code_files_via_hooks
from frappe.modules.utils import export_module_json, get_doc_module
from frappe.rate_limiter import rate_limit
@@ -22,8 +21,6 @@ class WebForm(WebsiteGenerator):
def onload(self):
super().onload()
- if self.is_standard and not frappe.conf.developer_mode:
- self.use_meta_fields()
def validate(self):
super().validate()
@@ -67,31 +64,6 @@ class WebForm(WebsiteGenerator):
for df in self.web_form_fields:
df.parent = self.doc_type
- def use_meta_fields(self):
- """Override default properties for standard web forms"""
- meta = frappe.get_meta(self.doc_type)
-
- for df in self.web_form_fields:
- meta_df = meta.get_field(df.fieldname)
-
- if not meta_df:
- continue
-
- for prop in docfield_properties:
- if df.fieldtype == meta_df.fieldtype and prop not in (
- "idx",
- "reqd",
- "default",
- "description",
- "options",
- "hidden",
- "read_only",
- "label",
- ):
- df.set(prop, meta_df.get(prop))
-
- # TODO translate options of Select fields like Country
-
# export
def on_update(self):
"""
@@ -124,7 +96,8 @@ def get_context(context):
def get_context(self, context):
"""Build context to render the `web_form.html` template"""
- context.is_form_editable = False
+ context.in_edit_mode = False
+ context.in_view_mode = False
self.set_web_form_module()
if frappe.form_dict.is_list:
@@ -156,10 +129,14 @@ def get_context(context):
frappe.redirect(f"/{self.route}/new")
if frappe.form_dict.is_edit and not self.allow_edit:
+ context.in_view_mode = True
frappe.redirect(f"/{self.route}/{frappe.form_dict.name}")
if frappe.form_dict.is_edit:
- context.is_form_editable = True
+ context.in_edit_mode = True
+
+ if frappe.form_dict.is_read:
+ context.in_view_mode = True
if (
not frappe.form_dict.is_edit
@@ -167,7 +144,7 @@ def get_context(context):
and self.allow_edit
and frappe.form_dict.name
):
- context.is_form_editable = True
+ context.in_edit_mode = True
frappe.redirect(f"/{frappe.local.path}/edit")
if (
@@ -179,6 +156,7 @@ def get_context(context):
):
name = frappe.db.get_value(self.doc_type, {"owner": frappe.session.user}, "name")
if name:
+ context.in_view_mode = True
frappe.redirect(f"/{self.route}/{name}")
# Show new form when
@@ -190,9 +168,6 @@ def get_context(context):
self.reset_field_parent()
- if self.is_standard:
- self.use_meta_fields()
-
# add keys from form_dict to context
context.update(dict_with_keys(frappe.form_dict, ["is_list", "is_new", "is_edit", "is_read"]))
@@ -203,7 +178,9 @@ def get_context(context):
# load web form doc
context.web_form_doc = self.as_dict(no_nulls=True)
- context.web_form_doc.update(dict_with_keys(context, ["is_list", "is_new", "is_form_editable"]))
+ context.web_form_doc.update(
+ dict_with_keys(context, ["is_list", "is_new", "in_edit_mode", "in_view_mode"])
+ )
if self.show_sidebar and self.website_sidebar:
context.sidebar_items = get_sidebar_items(self.website_sidebar)
@@ -278,17 +255,11 @@ def get_context(context):
if frappe.form_dict.name:
context.doc_name = frappe.form_dict.name
context.reference_doc = frappe.get_doc(self.doc_type, context.doc_name)
- context.title = strip_html(
- context.reference_doc.get(context.reference_doc.meta.get_title_field())
+ context.web_form_title = context.title
+ context.title = (
+ strip_html(context.reference_doc.get(context.reference_doc.meta.get_title_field()))
+ or context.doc_name
)
- if context.is_form_editable and context.parents:
- context.parents.append(
- {
- "label": _(context.title),
- "route": f"{self.route}/{context.doc_name}",
- }
- )
- context.title = _("Editing {0}").format(context.title)
context.reference_doc.add_seen()
context.reference_doctype = context.reference_doc.doctype
context.reference_name = context.reference_doc.name
@@ -309,7 +280,7 @@ def get_context(context):
context.reference_doc.doctype, context.reference_doc.name
)
- context.reference_doc = json.loads(context.reference_doc.as_json())
+ context.reference_doc = context.reference_doc.as_dict(no_nulls=True)
def add_custom_context_and_script(self, context):
"""Update context from module if standard and append script"""
@@ -341,62 +312,6 @@ def get_context(context):
context.style = style
- def get_layout(self):
- layout = []
-
- def add_page(df=None):
- new_page = {"sections": []}
- layout.append(new_page)
- if df and df.fieldtype == "Page Break":
- new_page.update(df.as_dict())
-
- return new_page
-
- def add_section(df=None):
- new_section = {"columns": []}
- if layout:
- layout[-1]["sections"].append(new_section)
- if df and df.fieldtype == "Section Break":
- new_section.update(df.as_dict())
-
- return new_section
-
- def add_column(df=None):
- new_col = []
- if layout:
- layout[-1]["sections"][-1]["columns"].append(new_col)
-
- return new_col
-
- page, section, column = None, None, None
- for df in self.web_form_fields:
-
- # breaks
- if df.fieldtype == "Page Break":
- page = add_page(df)
- section, column = None, None
-
- if df.fieldtype == "Section Break":
- section = add_section(df)
- column = None
-
- if df.fieldtype == "Column Break":
- column = add_column(df)
-
- # input
- if df.fieldtype not in ("Section Break", "Column Break", "Page Break"):
- if not page:
- page = add_page()
- section, column = None, None
- if not section:
- section = add_section()
- column = None
- if column is None:
- column = add_column()
- column.append(df)
-
- return layout
-
def get_parents(self, context):
parents = None
@@ -481,7 +396,7 @@ def accept(web_form, data, docname=None):
for field in web_form.web_form_fields:
fieldname = field.fieldname
df = meta.get_field(fieldname)
- value = data.get(fieldname, None)
+ value = data.get(fieldname, "")
if df and df.fieldtype in ("Attach", "Attach Image"):
if value and "data:" and "base64" in value:
@@ -597,17 +512,6 @@ def get_web_form_filters(web_form_name):
return [field for field in web_form.web_form_fields if field.show_in_filter]
-def make_route_string(parameters):
- route_string = ""
- delimeter = "?"
- if isinstance(parameters, dict):
- for key in parameters:
- if key != "web_form_name":
- route_string += route_string + delimeter + key + "=" + cstr(parameters[key])
- delimeter = "&"
- return (route_string, delimeter)
-
-
@frappe.whitelist(allow_guest=True)
def get_form_data(doctype, docname=None, web_form_name=None):
web_form = frappe.get_doc("Web Form", web_form_name)
diff --git a/frappe/website/doctype/web_form_field/web_form_field.json b/frappe/website/doctype/web_form_field/web_form_field.json
index dbadf52881..4fb566be88 100644
--- a/frappe/website/doctype/web_form_field/web_form_field.json
+++ b/frappe/website/doctype/web_form_field/web_form_field.json
@@ -32,20 +32,20 @@
"fieldname": "fieldname",
"fieldtype": "Select",
"in_list_view": 1,
- "label": "Fieldname"
+ "label": "Field"
},
{
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Fieldtype",
- "options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nDuration\nFloat\nHTML\nInt\nLink\nPassword\nRating\nSelect\nSignature\nSmall Text\nText\nText Editor\nTable\nTime\nSection Break\nColumn Break\nPage Break"
+ "options": "Attach\nAttach Image\nCheck\nCurrency\nColor\nData\nDate\nDatetime\nDuration\nFloat\nHTML\nInt\nLink\nPassword\nRating\nSelect\nSignature\nSmall Text\nText\nText Editor\nTable\nTime\nSection Break\nColumn Break\nPage Break"
},
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Label"
+ "label": "Custom Label"
},
{
"default": "0",
@@ -58,6 +58,7 @@
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
+ "in_list_view": 1,
"label": "Mandatory"
},
{
@@ -146,7 +147,7 @@
],
"istable": 1,
"links": [],
- "modified": "2022-08-10 12:59:51.170546",
+ "modified": "2022-08-22 17:22:39.026893",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form Field",
diff --git a/frappe/website/doctype/web_form_list_column/web_form_list_column.json b/frappe/website/doctype/web_form_list_column/web_form_list_column.json
index e55aeadca6..8be724f426 100644
--- a/frappe/website/doctype/web_form_list_column/web_form_list_column.json
+++ b/frappe/website/doctype/web_form_list_column/web_form_list_column.json
@@ -15,14 +15,14 @@
"fieldname": "fieldname",
"fieldtype": "Select",
"in_list_view": 1,
- "label": "Fieldname",
+ "label": "Field",
"reqd": 1
},
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Label"
+ "label": "Custom Label"
},
{
"fieldname": "fieldtype",
@@ -35,7 +35,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-06-21 17:22:14.978947",
+ "modified": "2022-08-17 19:09:01.417841",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form List Column",
diff --git a/frappe/website/doctype/website_settings/test_website_settings.py b/frappe/website/doctype/website_settings/test_website_settings.py
index 1e8410fb6e..c95d380a34 100644
--- a/frappe/website/doctype/website_settings/test_website_settings.py
+++ b/frappe/website/doctype/website_settings/test_website_settings.py
@@ -1,8 +1,29 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# License: MIT. See LICENSE
-# import frappe
+
+import frappe
from frappe.tests.utils import FrappeTestCase
+from frappe.website.doctype.website_settings.website_settings import get_website_settings
class TestWebsiteSettings(FrappeTestCase):
- pass
+ def test_child_items_in_top_bar(self):
+ ws = frappe.get_doc("Website Settings")
+ ws.append(
+ "top_bar_items",
+ {"label": "Parent Item"},
+ )
+ ws.append(
+ "top_bar_items",
+ {"parent_label": "Parent Item", "label": "Child Item"},
+ )
+ ws.save()
+
+ context = get_website_settings()
+
+ for item in context.top_bar_items:
+ if item.label == "Parent Item":
+ self.assertEqual(item.child_items[0].label, "Child Item")
+ break
+ else:
+ self.fail("Child items not found")
diff --git a/frappe/website/doctype/website_settings/website_settings.py b/frappe/website/doctype/website_settings/website_settings.py
index be9b155314..744a3c90a3 100644
--- a/frappe/website/doctype/website_settings/website_settings.py
+++ b/frappe/website/doctype/website_settings/website_settings.py
@@ -218,7 +218,7 @@ def modify_header_footer_items(items: list):
continue
if not top_bar_item.get("child_items"):
- top_bar_item["child_items"] = []
+ top_bar_item.child_items = []
top_bar_item.child_items.append(item)
break