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 = `
${formatted_value}
`; } } catch (e) { diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index fa582a848f..02646a6687 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -43,6 +43,14 @@ export default class Grid { this.debounced_refresh = frappe.utils.debounce(this.debounced_refresh, 100); } + get perm() { + return this.control?.perm || this.frm?.perm || this.df.perm; + } + + set perm(_perm) { + console.error("Setting perm on grid isn't supported, update form's perm instead"); + } + allow_on_grid_editing() { if (frappe.utils.is_xs()) { return false; diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 3bd9e451db..508d35b462 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -194,9 +194,6 @@ frappe.ui.form.Layout = class Layout { this.fields_dict[fieldname].$wrapper.remove(); this.fields_list.splice(this.fields_dict[fieldname], 1, fieldobj); this.fields_dict[fieldname] = fieldobj; - if (this.frm) { - fieldobj.perm = this.frm.perm; - } this.section.fields_list.splice(this.section.fields_dict[fieldname], 1, fieldobj); this.section.fields_dict[fieldname] = fieldobj; this.refresh_fields([df]); @@ -210,9 +207,6 @@ frappe.ui.form.Layout = class Layout { const fieldobj = this.init_field(df, render); this.fields_list.push(fieldobj); this.fields_dict[df.fieldname] = fieldobj; - if (this.frm) { - fieldobj.perm = this.frm.perm; - } this.section.add_field(fieldobj); this.column.add_field(fieldobj); @@ -465,11 +459,6 @@ frappe.ui.form.Layout = class Layout { fieldobj.df = frappe.meta.get_docfield(me.doc.doctype, fieldobj.df.fieldname, me.doc.name) || fieldobj.df; - - // on form change, permissions can change - if (me.frm) { - fieldobj.perm = me.frm.perm; - } } refresh && fieldobj.df && fieldobj.refresh && fieldobj.refresh(); } diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js index 56cbfbf5a2..ed42b81b68 100644 --- a/frappe/public/js/frappe/ui/dialog.js +++ b/frappe/public/js/frappe/ui/dialog.js @@ -168,7 +168,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { set_secondary_action(click) { this.footer.removeClass("hide"); - this.get_secondary_btn().removeClass("hide").off("click").on("click", click); + return this.get_secondary_btn().removeClass("hide").off("click").on("click", click); } set_secondary_action_label(label) { diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index 8b0585b727..bbd74dba4a 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -72,7 +72,6 @@ frappe.warn = function (title, message_html, proceed_action, primary_label, is_m d.$body.append(`
${message_html}
`); d.standard_actions.find(".btn-primary").removeClass("btn-primary").addClass("btn-danger"); - d.standard_actions.find(".btn-primary").removeClass("btn-primary").addClass("btn-danger"); d.show(); return d; diff --git a/frappe/public/js/frappe/web_form/web_form.js b/frappe/public/js/frappe/web_form/web_form.js index ccd88f0ba3..b8f7c327f6 100644 --- a/frappe/public/js/frappe/web_form/web_form.js +++ b/frappe/public/js/frappe/web_form/web_form.js @@ -24,10 +24,10 @@ export default class WebForm extends frappe.ui.FieldGroup { super.make(); this.set_page_breaks(); this.set_field_values(); - this.setup_listeners(); - if (this.is_new || this.is_form_editable) { + if (this.is_new || this.in_edit_mode) { this.setup_primary_action(); + this.setup_discard_action(); } this.setup_previous_next_button(); @@ -35,6 +35,7 @@ export default class WebForm extends frappe.ui.FieldGroup { // webform client script frappe.init_client_script && frappe.init_client_script(); + this.setup_listeners(); frappe.web_form.events.trigger("after_load"); this.after_load && this.after_load(); } @@ -43,34 +44,39 @@ export default class WebForm extends frappe.ui.FieldGroup { let field = this.fields_dict[fieldname]; field.df.change = () => { handler(field, field.value); + this.make_form_dirty(); }; } setup_listeners() { - // Event listener for triggering Save/Next button for Multi Step Forms - // Do not use `on` event here since that can be used by user which will render this function useless - // setTimeout has 200ms delay so that all the base_control triggers for the fields have been run - let me = this; + // setup change event for all fields if not already set through client script + this.fields.forEach((field) => { + if (!field.change) { + field.change = () => { + this.make_form_dirty(); + }; + } + }); + } - if (!me.is_multi_step_form) { - return; - } - - for (let field of $(".input-with-feedback")) { - $(field).change((e) => { - setTimeout(() => { - e.stopPropagation(); - me.toggle_buttons(); - }, 200); - }); - } + make_form_dirty() { + frappe.form_dirty = true; + $(".indicator-pill.orange").removeClass("hide"); } set_page_breaks() { - if (this.page_breaks.length) return; + this.page_breaks = $(".page-break"); - this.page_breaks = $(`.page-break`); - this.is_multi_step_form = true; + if (this.page_breaks.length) { + this.page_breaks.each((i, page_break) => { + if (!$(page_break).find("form").length) { + $(page_break).remove(); + } + }); + } + + this.page_breaks = $(".page-break"); + this.is_multi_step_form = !!this.page_breaks.length; } setup_previous_next_button() { @@ -80,15 +86,19 @@ export default class WebForm extends frappe.ui.FieldGroup { return; } - $(".web-form-footer .web-form-actions .left-area").prepend(` - - `); + this.$next_button = $(``); - $(".web-form-footer .web-form-actions .right-area").prepend(` - - `); + this.$previous_button = $(``); - $(".btn-previous").on("click", function () { + this.$next_button.insertAfter(".web-form-footer .right-area .discard-btn"); + this.in_view_mode && $(".web-form-footer .right-area").append(this.$next_button); + $(".web-form-footer .left-area").prepend(this.$previous_button); + + this.$previous_button.on("click", () => { let is_validated = me.validate_section(); if (!is_validated) return false; @@ -115,7 +125,7 @@ export default class WebForm extends frappe.ui.FieldGroup { return false; }); - $(".btn-next").on("click", function () { + this.$next_button.on("click", () => { let is_validated = me.validate_section(); if (!is_validated) return false; @@ -155,7 +165,29 @@ export default class WebForm extends frappe.ui.FieldGroup { } setup_primary_action() { - $(".web-form-container").on("submit", () => this.save()); + $(".web-form").on("submit", () => this.save()); + } + + setup_discard_action() { + $(".web-form-footer .discard-btn").on("click", () => this.discard_form()); + } + + discard_form() { + let path = window.location.href; + // remove new or edit after last / from url + path = path.substring(0, path.lastIndexOf("/")); + + if (frappe.form_dirty) { + frappe.warn( + __("Discard?"), + __("Are you sure you want to discard the changes?"), + () => (window.location.href = path), + __("Discard") + ); + } else { + window.location.href = path; + } + return false; } validate_section() { @@ -223,8 +255,18 @@ export default class WebForm extends frappe.ui.FieldGroup { } render_progress_dots() { + if (!this.is_multi_step_form) return; $(".center-area.paging").empty(); + if (this.in_view_mode) { + let paging_text = __("Page {0} of {1}", [ + this.current_section + 1, + this.page_breaks.length + 1, + ]); + $(".center-area.paging").append(`
${paging_text}
`); + return; + } + this.$slide_progress = $(`
`).appendTo( $(".center-area.paging") ); @@ -246,12 +288,6 @@ export default class WebForm extends frappe.ui.FieldGroup { } this.$slide_progress.append($dot); } - - let paging_text = __("Page {0} of {1}", [ - this.current_section + 1, - this.page_breaks.length + 1, - ]); - $(".center-area.paging").append(`
${paging_text}
`); } toggle_buttons() { @@ -290,7 +326,7 @@ export default class WebForm extends frappe.ui.FieldGroup { show_next_and_hide_save_button() { $(".btn-next").show(); - $(".submit-btn").hide(); + !this.allow_incomplete && $(".submit-btn").hide(); } toggle_previous_button() { @@ -398,16 +434,16 @@ export default class WebForm extends frappe.ui.FieldGroup { render_success_page(data) { if (this.allow_edit && data.name) { - $(".success-page").append(` - + $(".success-footer").append(` + ${__("Edit your response", null, "Button in web form")} `); } if (this.login_required && !this.allow_multiple && !this.show_list && data.name) { - $(".success-page").append(` - + $(".success-footer").append(` + ${__("View your response", null, "Button in web form")} `); diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index 4481901e0b..6d89b8ac95 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -381,7 +381,11 @@ frappe.ui.WebFormListRow = class WebFormListRow { formatter(this.doc[field.fieldname], field, { only_value: 1 }, this.doc) )) || ""; - let cell = $(`${value}`); + let cell = $(`

${value}

`); + if (field.fieldtype === "Text Editor") { + value = $(value).addClass("ellipsis"); + cell = $("").append(value); + } cell.appendTo(this.row); }); diff --git a/frappe/public/js/frappe/web_form/webform_script.js b/frappe/public/js/frappe/web_form/webform_script.js index 75bbce9b27..f24e10cff3 100644 --- a/frappe/public/js/frappe/web_form/webform_script.js +++ b/frappe/public/js/frappe/web_form/webform_script.js @@ -36,9 +36,6 @@ frappe.ready(function () { function show_form() { let web_form = new WebForm({ parent: $(".web-form-wrapper"), - is_new: web_form_doc.is_new, - is_form_editable: web_form_doc.is_form_editable, - web_form_name: web_form_doc.name, }); let doc = reference_doc || {}; setup_fields(web_form_doc, doc); @@ -58,7 +55,7 @@ frappe.ready(function () { function setup_fields(web_form_doc, doc_data) { web_form_doc.web_form_fields.forEach((df) => { df.is_web_form = true; - df.read_only = !web_form_doc.is_new && !web_form_doc.is_form_editable; + df.read_only = !web_form_doc.is_new && !web_form_doc.in_edit_mode; if (df.fieldtype === "Table") { df.get_data = () => { let data = []; diff --git a/frappe/public/js/web_form.bundle.js b/frappe/public/js/web_form.bundle.js index ffb7b824bd..a4e9415401 100644 --- a/frappe/public/js/web_form.bundle.js +++ b/frappe/public/js/web_form.bundle.js @@ -1,3 +1,6 @@ +import "./controls.bundle.js"; +import "./dialog.bundle.js"; import "./lib/moment.js"; import "./frappe/utils/datetime.js"; import "./frappe/web_form/webform_script.js"; +import "./bootstrap-4-web.bundle.js"; diff --git a/frappe/public/scss/website/web_form.scss b/frappe/public/scss/website/web_form.scss index 0a4350e0bf..e5a0093307 100644 --- a/frappe/public/scss/website/web_form.scss +++ b/frappe/public/scss/website/web_form.scss @@ -1,361 +1,494 @@ @import "../common/form"; [data-doctype="Web Form"] { - .page_content { - max-width: 800px; - margin: auto; - - h1 { - font-size: 2.25rem; - margin-top: 0; - margin-bottom: 0; - } - - .web-form-banner-image { - margin: -4rem -14rem 5rem; - padding-top: 3rem; - position: relative; - - img { - position: absolute; - object-fit: cover; + .page-content-wrapper { + .container { + .page-header { width: 100%; - height: 250px; - z-index: -1; - } - } - .web-form-header { - border: 1px solid var(--dark-border-color); - border-bottom: none; - border-top-left-radius: var(--border-radius-md); - border-top-right-radius: var(--border-radius-md); - background-color: var(--fg-color); - padding: 2rem 2rem 0; - - .breadcrumb-container { - padding: 0px; - margin: 0 0 2rem; - - ol.breadcrumb { - padding: 0px; + img { + margin: -1rem 0rem -10.5rem; + object-fit: cover; + width: 100%; + height: 250px; + z-index: -1; } } - .web-form-head { - border-bottom: 1px solid var(--dark-border-color); - padding-bottom: 1.25rem; + .page_content { + max-width: 800px; + margin: auto; - .title { - display: flex; - justify-content: space-between; + h1 { + font-size: 2.25rem; + margin-top: 0; + margin-bottom: 0; + padding-bottom: 2px; } - .web-form-introduction { - color: var(--text-muted); - margin-top: 1.25rem; + .web-form-header { + border: 1px solid var(--dark-border-color); + border-bottom: none; + border-top-left-radius: var(--border-radius-md); + border-top-right-radius: var(--border-radius-md); + background-color: var(--fg-color); + padding: 2rem 2rem 0; - p { - color: var(--text-muted); - } - } - } - } + .breadcrumb-container { + padding: 0px; + margin: 0 0 2rem; - .web-form { - background-color: var(--fg-color); - padding: 1.25rem 2rem 2rem; - border: 1px solid var(--dark-border-color); - border-top: none; - border-bottom-left-radius: var(--border-radius); - border-bottom-right-radius: var(--border-radius); - - .web-form-wrapper { - .form-control { - color: var(--text-color); - background-color: var(--control-bg); - } - - .form-section { - .section-head { - font-weight: bold; - font-size: var(--text-xl); - padding: var(--padding-md) 0; - } - } - - .form-column { - padding: 0 var(--padding-sm); - - &:first-child { - padding-left: 0; + ol.breadcrumb { + padding: 0px; + } } - &:last-child { - padding-right: 0; - } + .web-form-head { + border-bottom: 1px solid var(--dark-border-color); + padding-bottom: 1.25rem; - @include media-breakpoint-down(sm) { - padding: 0; - } - } + .title { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; - .web-form-skeleton { - .box-group { - display: flex; - gap: 20px; - margin-bottom: 15px; - - .box-container { - width: 100%; - - .box { - background-color: var(--control-bg); - border-radius: var(--border-radius); + .web-form-title p { + margin-bottom: 0; } - .box-label { - height: 20px; - width: 100px; - margin-bottom: 0.5rem; + .indicator-pill { + margin-top: 7px; } - .box-area { - height: 34px; - width: 100%; + .web-form-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex: 1; + + .btn { + font-size: var(--text-base); + } + } + } + + .web-form-introduction { + color: var(--text-muted); + margin-top: 1.25rem; + + p { + color: var(--text-muted); } } } } - } - .web-form-footer { - margin-top: 1rem; + .web-form { + background-color: var(--fg-color); + padding: 1.25rem 2rem 2rem; + border: 1px solid var(--dark-border-color); + border-top: none; + border-bottom-left-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); - .web-form-actions { - display: flex; - justify-content: space-between; + .web-form-wrapper { + .form-control { + color: var(--text-color); + background-color: var(--control-bg); + } - .btn { - font-size: var(--font-size-base); + .form-section { + .section-head { + font-weight: bold; + font-size: var(--text-xl); + padding: var(--padding-md) 0; + } + } + + .form-column { + padding: 0 var(--padding-sm); + + .frappe-control[data-fieldtype="Rating"] { + .like-disabled-input { + background-color: unset; + padding-left: 0px; + + .rating { + cursor: default; + } + } + } + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } + + @include media-breakpoint-down(xs) { + padding: 0; + } + } + + .web-form-skeleton { + .box-group { + display: flex; + flex-wrap: wrap; + + .box-container { + width: 100%; + padding: 0 var(--padding-sm); + margin-bottom: 15px; + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } + + @include media-breakpoint-down(xs) { + padding: 0; + } + + .box { + background-color: var(--control-bg); + border-radius: var(--border-radius); + } + + .box-label { + height: 20px; + width: 100px; + margin-bottom: 0.5rem; + } + + .box-area { + height: 34px; + width: 100%; + } + } + } + } } - .center-area { - padding: 0.5rem; - display: flex; - align-items: center; + .web-form-footer { + margin-top: 1rem; - .slides-progress { + .web-form-actions { display: flex; - margin-right: .5rem; + justify-content: space-between; + flex-wrap: wrap; - .slide-step { - @include flex(flex, center, center, null); + .btn { + font-size: var(--text-base); + } - height: 18px; - width: 18px; - border-radius: var(--border-radius-full); - border: 1px solid var(--gray-300); - margin: 0 var(--margin-xs); - background-color: var(--card-bg); + .btn-link { + padding-left: 0px; + color: var(--text-color); - .slide-step-indicator { - height: 6px; - width: 6px; - background-color: var(--gray-300); - border-radius: var(--border-radius-full); + &:hover { + color: var(--text-on-light-blue); + } + } + + .left-area { + display: flex; + flex: 1; + + @include media-breakpoint-down(sm) { + order: 1 + } + } + + .center-area { + display: flex; + align-items: center; + font-size: var(--text-base); + + .slides-progress { + display: flex; + + .slide-step { + @include flex(flex, center, center, null); + + height: 18px; + width: 18px; + border-radius: var(--border-radius-full); + border: 1px solid var(--gray-300); + margin: 0 var(--margin-xs); + background-color: var(--card-bg); + + .slide-step-indicator { + height: 6px; + width: 6px; + background-color: var(--gray-300); + border-radius: var(--border-radius-full); + } + + .slide-step-complete { + display: none; + + .icon-xs { + height: 10px; + width: 10px; + } + } + + &.active { + border: 1px solid var(--primary); + + .slide-step-indicator { + display: block; + background-color: var(--primary); + } + } + + &.step-success:not(.active) { + background-color: var(--primary); + border: 1px solid var(--primary); + + .slide-step-indicator { + display: none; + } + + .slide-step-complete { + display: flex; + + .icon use { + stroke-width: 2; + stroke: var(--white); + } + } + } + + @include media-breakpoint-down(xs) { + width: 16px; + height: 16px; + } + } } - .slide-step-complete { + @include media-breakpoint-down(sm) { + order: 0; + width: 100%; + justify-content: center; + margin-bottom: 1.5rem; + } + } + + .right-area { + display: flex; + justify-content: flex-end; + flex: 1; + + @include media-breakpoint-down(sm) { + order: 2 + } + } + } + } + } + + .attachments { + margin-top: 2rem; + padding: 2rem; + border-radius: var(--border-radius); + border: 1px solid var(--dark-border-color); + + .attachment { + display: flex; + justify-content: space-between; + gap: 6px; + color: var(--text-muted); + font-size: var(--text-md); + + &:hover { + text-decoration: none; + .file-name span { + text-decoration: underline; + } + } + } + } + + .success-page { + background-color: var(--fg-color); + padding: 5rem 2rem; + margin-top: 3rem; + border: 1px solid var(--dark-border-color); + border-radius: var(--border-radius); + text-align: center; + + .success-header { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; + margin-top: 1rem; + + .success-icon { + width: 3rem; + height: 3rem; + margin: 0; + + @include media-breakpoint-down(sm) { + width: 2rem; + height: 2rem; + } + } + + .success-title { + margin-top: 0; + margin-bottom: 0; + } + } + + .success-body .success-message { + margin: 1rem 0rem 1.5rem; + } + + .success-footer a { + margin: 0rem 0.3rem 1rem; + } + } + + .web-list-container { + min-height: 470px; + border: 1px solid var(--dark-border-color); + border-radius: var(--border-radius-md); + padding: 2rem; + + .web-list-header { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; + border-bottom: 1px solid var(--dark-border-color); + padding-bottom: 1.25rem; + + .web-list-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex: 1; + } + } + + .web-list-filters { + display: flex; + flex-wrap: wrap; + margin: 1.25rem 0; + gap: 10px; + + .form-group.frappe-control { + min-width: 145px; + padding: 0px; + margin: 0px; + align-self: center; + + .checkbox { + .input-xs { + height: var(--checkbox-size); + } + + .help-box { display: none; + } + } - .icon-xs { - height: 10px; - width: 10px; + .input-xs { + height: 28px; + line-height: 1.2; + } + } + } + + .web-list-table { + overflow: auto; + + .table { + border-bottom: 1px solid var(--border-color); + border-top: 1px solid var(--border-color); + + thead tr { + th { + border: 0; + font-size: 13px; + font-weight: normal; + color: var(--text-muted); + + input[type="checkbox"] { + margin-bottom: -2px; } } + } - &.active { - border: 1px solid var(--primary); + tbody tr { + color: var(--text-color); + cursor: pointer; - .slide-step-indicator { - display: block; - background-color: var(--primary); - } - } + td { + font-size: 13px; + border-top: 1px solid var(--border-color); + max-width: 160px; - &.step-success:not(.active) { - background-color: var(--primary); - border: 1px solid var(--primary); + .ql-editor, p { + width: max-content; + max-width: 150px; + margin-bottom: 0; - .slide-step-indicator { - display: none; - } - - .slide-step-complete { - display: flex; - - .icon use { - stroke-width: 2; - stroke: var(--white); + &.read-mode { + display: inline-flex; + gap: 5px; } } } } - } - } - } - } - } - - .attachments { - margin-top: 2rem; - padding: 2rem; - border-radius: var(--border-radius); - border: 1px solid var(--dark-border-color); - - .attachment { - display: flex; - justify-content: space-between; - gap: 6px; - color: var(--text-muted); - font-size: var(--text-md); - - &:hover { - text-decoration: none; - .file-name span { - text-decoration: underline; - } - } - } - } - - .success-page { - background-color: var(--fg-color); - padding: 2rem; - border: 1px solid var(--dark-border-color); - border-radius: var(--border-radius); - text-align: center; - - svg.icon { - width: 5rem; - height: 5rem; - margin: 1rem; - } - - h2 { - margin-top: 0; - margin-bottom: 0; - } - - .success-message { - margin-bottom: 1.6rem; - } - } - - .web-list-container { - min-height: 470px; - border: 1px solid var(--dark-border-color); - border-radius: var(--border-radius-md); - padding: 2rem; - - .web-list-header { - display: flex; - justify-content: space-between; - border-bottom: 1px solid var(--dark-border-color); - padding-bottom: 1.25rem; - - .web-list-actions { - align-self: center; - } - } - - .web-list-filters { - display: flex; - flex-wrap: wrap; - margin: 1.25rem 0; - gap: 10px; - - .form-group.frappe-control { - min-width: 145px; - padding: 0px; - margin: 0px; - align-self: center; - - .checkbox { - .input-xs { - height: var(--checkbox-size); - } - - .help-box { - display: none; - } - } - - .input-xs { - height: 28px; - line-height: 1.2; - } - } - } - - .web-list-table { - overflow: auto; - - .table { - border-bottom: 1px solid var(--border-color); - border-top: 1px solid var(--border-color); - - thead tr { - th { - border: 0; - font-size: 13px; - font-weight: normal; - color: var(--text-muted); input[type="checkbox"] { - margin-bottom: -2px; + margin-top: 2px; + } + + .list-col-checkbox { + width: 1rem; + } + + .list-col-serial { + width: 1.5rem; } } - } - tbody tr { - color: var(--text-color); - cursor: pointer; - - td { - font-size: 13px; + .no-result { + min-height: 330px; border-top: 1px solid var(--border-color); } } - input[type="checkbox"] { - margin-top: 2px; - } - - .list-col-checkbox { - width: 1rem; - } - - .list-col-serial { - width: 1.5rem; + .web-list-footer { + text-align: right; } } - .no-result { - min-height: 330px; - border-top: 1px solid var(--border-color); + .breadcrumb-container.container { + @include media-breakpoint-up(sm) { + padding-left: 0; + } + } + + @include media-breakpoint-down(lg) { + padding-left: 1.5rem; + padding-right: 1.5rem; } } - .web-list-footer { - text-align: right; - } - } - - .breadcrumb-container.container { - @include media-breakpoint-up(sm) { + @include media-breakpoint-down(lg) { padding-left: 0; + padding-right: 0; } } } diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py index f97b2ee754..9fd505a9c3 100644 --- a/frappe/tests/test_commands.py +++ b/frappe/tests/test_commands.py @@ -691,6 +691,17 @@ class TestSiteMigration(BaseTestCommands): self.assertEqual(result.exception, None) +class TestAddNewUser(BaseTestCommands): + def test_create_user(self): + self.execute( + "bench --site {site} add-user test@gmail.com --first-name test --last-name test --password 123 --user-type 'System User' --add-role 'Accounts User' --add-role 'Sales User'" + ) + self.assertEqual(self.returncode, 0) + user = frappe.get_doc("User", "test@gmail.com") + roles = {r.role for r in user.roles} + self.assertEqual({"Accounts User", "Sales User"}, roles) + + class TestBenchBuild(BaseTestCommands): def test_build_assets_size_check(self): with cli(frappe.commands.utils.build, "--force --production") as result: diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index 331b8eb8b8..1e660f8a5d 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -831,6 +831,11 @@ class TestReportview(FrappeTestCase): self.assertTrue(dashboard_settings) + def test_coalesce_with_in_ops(self): + self.assertNotIn("ifnull", frappe.get_all("User", {"name": ("in", ["a", "b"])}, run=0)) + self.assertIn("ifnull", frappe.get_all("User", {"name": ("in", ["a", None])}, run=0)) + self.assertIn("ifnull", frappe.get_all("User", {"name": ("in", ["a", ""])}, run=0)) + def add_child_table_to_blog_post(): child_table = frappe.get_doc( diff --git a/frappe/tests/test_translate.py b/frappe/tests/test_translate.py index a72358847a..e546516ade 100644 --- a/frappe/tests/test_translate.py +++ b/frappe/tests/test_translate.py @@ -8,6 +8,7 @@ from unittest.mock import patch import frappe import frappe.translate from frappe import _ +from frappe.core.doctype.translation.test_translation import clear_translation_cache from frappe.tests.utils import FrappeTestCase from frappe.translate import ( extract_javascript, @@ -37,13 +38,15 @@ class TestTranslate(FrappeTestCase): def setUp(self): if self._testMethodName in self.guest_sessions_required: frappe.set_user("Guest") - frappe.local.lang_full_dict = None # reset cached translations + + clear_translation_cache() def tearDown(self): frappe.form_dict.pop("_lang", None) if self._testMethodName in self.guest_sessions_required: frappe.set_user("Administrator") - frappe.local.lang_full_dict = None # reset cached translations + + clear_translation_cache() def test_extract_message_from_file(self): data = frappe.translate.get_messages_from_file(translation_string_file) diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py index 79dfd76238..69db9a01b3 100644 --- a/frappe/tests/utils.py +++ b/frappe/tests/utils.py @@ -89,6 +89,7 @@ def _restore_thread_locals(flags): frappe.local.cache = {} frappe.local.lang = "en" frappe.local.lang_full_dict = None + frappe.local.preload_assets = {"style": [], "script": []} @contextmanager diff --git a/frappe/website/doctype/web_form/templates/web_form.html b/frappe/website/doctype/web_form/templates/web_form.html index fce6401457..0dbbe4f13e 100644 --- a/frappe/website/doctype/web_form/templates/web_form.html +++ b/frappe/website/doctype/web_form/templates/web_form.html @@ -2,49 +2,44 @@ {% block breadcrumbs %}{% endblock %} +{% block header %} + {% if banner_image %} + + Banner Image + {% endif %} +{% endblock %} + {% macro header_buttons() %} - {% if allow_print and not is_new %} - {% set print_format_url = "/printview?doctype=" + doc_type + "&name=" + doc_name + "&format=" + print_format %} - - - - + {% if allow_edit and in_view_mode %} + + {{ _("Edit Response", null, "Button in web form") }} {% endif %} - {% if allow_edit and doc_name and not is_form_editable %} - - {{ _("Edit", null, "Button in web form") }} + {% if allow_print and in_view_mode %} + {% set print_format_url = "/printview?doctype=" + doc_type + "&name=" + doc_name + "&format=" + print_format %} + + + + {% endif %} {% endmacro %} {% macro action_buttons() %} - {% if is_new or is_form_editable %} -
- - - {% if is_form_editable %} - {{ _("Reset Form", null, "Button in web form") }} - {% else %} - {{ _("Clear Form", null, "Button in web form") }} - {% endif %} - -
-
-
+
+
+
+ {% if not in_view_mode %} + + - -
- {% endif %} + + {% endif %} +
{% endmacro %} {% block page_content %} - - {% if banner_image %} -
- Banner Image -
- {% endif %} -
@@ -61,12 +56,20 @@ {% endif %}
-

{{ _(title) }}

+
+ {% if show_list and not is_new %} +

{{ _(web_form_title) }}

+

{{ _(title) }}

+ {% else %} +

{{ _(title) }}

+ {% endif %} +
+ Not Saved
{{ header_buttons() }}
- {% if is_new and introduction_text %} + {% if introduction_text and (is_new or in_edit_mode) %}
{{ introduction_text }}
{% endif %}
@@ -115,30 +118,37 @@
- - - -

{{ _(success_title) or _("Submitted") }}

-

{{ _(success_message) or _("Thank you for spending your valuable time to fill this form") }}

+
+ + + +

{{ _(success_title) or _("Submitted") }}

+
- {% if success_url %} -
-

- Click on this - {{_("URL")}} - if you are not redirected within - 5 - seconds. -

-
- {% else %} - {% if show_list %} - {{ _("See previous responses", null, "Button in web form") }} +
+

{{ _(success_message) or _("Thank you for spending your valuable time to fill this form") }}

+
+ +
{% endblock page_content %} @@ -157,10 +167,7 @@ Vue.prototype.frappe = window.frappe; - {{ include_script("controls.bundle.js") }} - {{ include_script("dialog.bundle.js") }} {{ include_script("web_form.bundle.js") }} - {{ include_script("bootstrap-4-web.bundle.js") }} - {{ include_script("controls.bundle.js") }} - {{ include_script("dialog.bundle.js") }} {{ include_script("web_form.bundle.js") }} - {{ include_script("bootstrap-4-web.bundle.js") }} {% endblock script %} {% block style %} diff --git a/frappe/website/doctype/web_form/test_web_form.py b/frappe/website/doctype/web_form/test_web_form.py index c1885d91cd..5a2269b64d 100644 --- a/frappe/website/doctype/web_form/test_web_form.py +++ b/frappe/website/doctype/web_form/test_web_form.py @@ -71,7 +71,7 @@ class TestWebForm(FrappeTestCase): def test_webform_render(self): set_request(method="GET", path="manage-events/new") content = get_response_content("manage-events/new") - self.assertIn("

New Manage Events

", content) + self.assertIn('

New Manage Events

', content) self.assertIn('data-doctype="Web Form"', content) self.assertIn('data-path="manage-events/new"', content) self.assertIn('source-type="Generator"', content) diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index 05fc7ff87b..c494781f1f 100644 --- a/frappe/website/doctype/web_form/web_form.js +++ b/frappe/website/doctype/web_form/web_form.js @@ -1,4 +1,28 @@ frappe.ui.form.on("Web Form", { + setup: function () { + frappe.meta.docfield_map["Web Form Field"].fieldtype.formatter = (value) => { + const prefix = { + "Page Break": "--red-600", + "Section Break": "--blue-600", + "Column Break": "--yellow-600", + }; + if (prefix[value]) { + value = `${value}`; + } + return value; + }; + + frappe.meta.docfield_map["Web Form Field"].fieldname.formatter = (value) => { + if (!value) return; + return frappe.unscrub(value); + }; + + frappe.meta.docfield_map["Web Form List Column"].fieldname.formatter = (value) => { + if (!value) return; + return frappe.unscrub(value); + }; + }, + refresh: function (frm) { // show is-standard only if developer mode frm.get_field("is_standard").toggle(frappe.boot.developer_mode); @@ -32,6 +56,14 @@ frappe.ui.form.on("Web Form", { frm.scroll_to_field("web_form_fields"); frappe.throw(__("Atleast one field is required in Web Form Fields Table")); } + + let page_break_count = frm.doc.web_form_fields.filter( + (f) => f.fieldtype == "Page Break" + ).length; + + if (page_break_count >= 10) { + frappe.throw(__("There can be only 9 Page Break fields in a Web Form")); + } }, add_publish_button(frm) { @@ -97,7 +129,7 @@ frappe.ui.form.on("Web Form", { get_fields_for_doctype(doc.doc_type).then((fields) => { let as_select_option = (df) => ({ - label: df.label + " (" + df.fieldtype + ")", + label: df.label, value: df.fieldname, }); update_options(fields.map(as_select_option)); @@ -147,9 +179,19 @@ frappe.ui.form.on("Web Form List Column", { frappe.ui.form.on("Web Form Field", { fieldtype: function (frm, doctype, name) { - var doc = frappe.get_doc(doctype, name); + let doc = frappe.get_doc(doctype, name); + + if (doc.fieldtype == "Page Break") { + let page_break_count = frm.doc.web_form_fields.filter( + (f) => f.fieldtype == "Page Break" + ).length; + page_break_count >= 10 && + frappe.throw(__("There can be only 9 Page Break fields in a Web Form")); + } + if (["Section Break", "Column Break", "Page Break"].includes(doc.fieldtype)) { doc.fieldname = ""; + doc.label = ""; doc.options = ""; frm.refresh_field("web_form_fields"); } @@ -188,23 +230,18 @@ function get_fields_for_doctype(doctype) { function render_list_settings_message(frm) { // render list setting message if (frm.fields_dict["list_setting_message"] && !frm.doc.login_required) { - const switch_to_form_settings_tab = ` - - ${__("Form Settings Tab")} - + const go_to_login_required_field = ` + + ${__("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( - $( - `
- ${__( - "Login is required to see web form list view. Enable login_required from {0} to see list settings", - [switch_to_form_settings_tab] - )} -
` - ) - ) - .find("span") + .html($(`
${message}
`)) + .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