Merge branch 'develop' into get-all-mod

This commit is contained in:
Aradhya Tripathi 2022-08-24 19:46:46 +05:30 committed by GitHub
commit 80e64c7143
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1012 additions and 774 deletions

View file

@ -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

View file

@ -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");

View file

@ -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,

View file

@ -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),
}
)

View file

@ -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)

View file

@ -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):

View file

@ -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",

View file

@ -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():

View file

@ -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

View file

@ -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
}
}

View file

@ -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."""

View file

@ -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()

View file

@ -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):

View file

@ -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(",")

View file

@ -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"""

View file

@ -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:

View file

@ -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)

View file

@ -32,10 +32,7 @@ frappe.get_modal = function (title, content) {
${content}
</div>
<div class="modal-footer hidden">
<button type="button" class="btn btn-default btn-sm btn-modal-close" data-dismiss="modal">
<i class="octicon octicon-x visible-xs" style="padding: 1px 0px;"></i>
<span class="hidden-xs">${__("Close")}</span>
</button>
<button type="button" class="btn btn-sm btn-secondary hidden"></button>
<button type="button" class="btn btn-sm btn-primary hidden"></button>
</div>
</div>
@ -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) {

View file

@ -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);

View file

@ -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();

View file

@ -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);
}

View file

@ -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() {

View file

@ -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,
});

View file

@ -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;
}

View file

@ -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 = `<div class="ql-editor read-mode">${formatted_value}</div>`;
}
} catch (e) {

View file

@ -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;

View file

@ -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();
}

View file

@ -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) {

View file

@ -72,7 +72,6 @@ frappe.warn = function (title, message_html, proceed_action, primary_label, is_m
d.$body.append(`<div class="frappe-confirm-message">${message_html}</div>`);
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;

View file

@ -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(`
<button class="btn btn-default btn-previous btn-md mr-2">${__("Previous")}</button>
`);
this.$next_button = $(`<button class="btn btn-default btn-next btn-sm ml-2">
${__("Next")}
</button>`);
$(".web-form-footer .web-form-actions .right-area").prepend(`
<button class="btn btn-default btn-next btn-md">${__("Next")}</button>
`);
this.$previous_button = $(`<button class="btn btn-default btn-previous btn-sm">
${__("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(`<div>${paging_text}</div>`);
return;
}
this.$slide_progress = $(`<div class="slides-progress"></div>`).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(`<div>${paging_text}</div>`);
}
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(`
<a href="/${this.route}/${data.name}/edit" class="edit-button btn btn-light btn-md ml-2">
$(".success-footer").append(`
<a href="/${this.route}/${data.name}/edit" class="edit-button btn btn-default btn-md">
${__("Edit your response", null, "Button in web form")}
</a>
`);
}
if (this.login_required && !this.allow_multiple && !this.show_list && data.name) {
$(".success-page").append(`
<a href="/${this.route}/${data.name}" class="view-button btn btn-light btn-md ml-2">
$(".success-footer").append(`
<a href="/${this.route}/${data.name}" class="view-button btn btn-default btn-md">
${__("View your response", null, "Button in web form")}
</a>
`);

View file

@ -381,7 +381,11 @@ frappe.ui.WebFormListRow = class WebFormListRow {
formatter(this.doc[field.fieldname], field, { only_value: 1 }, this.doc)
)) ||
"";
let cell = $(`<td>${value}</td>`);
let cell = $(`<td><p class="ellipsis">${value}</p></td>`);
if (field.fieldtype === "Text Editor") {
value = $(value).addClass("ellipsis");
cell = $("<td></td>").append(value);
}
cell.appendTo(this.row);
});

View file

@ -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 = [];

View file

@ -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";

View file

@ -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;
}
}
}

View file

@ -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:

View file

@ -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(

View file

@ -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)

View file

@ -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

View file

@ -2,49 +2,44 @@
{% block breadcrumbs %}{% endblock %}
{% block header %}
{% if banner_image %}
<!-- banner image -->
<img class="web-form-banner-image" src="{{ banner_image }}" alt="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 %}
<!-- print button -->
<a href="{{ print_format_url }}" target="_blank" class="print-btn btn btn-light btn-sm ml-2">
<svg class="icon icon-sm"><use href="#icon-printer"></use></svg>
</a>
{% if allow_edit and in_view_mode %}
<!-- edit button -->
<a href="/{{ route }}/{{ doc_name }}/edit" class="edit-button btn btn-default btn-sm">{{ _("Edit Response", null, "Button in web form") }}</a>
{% endif %}
{% if allow_edit and doc_name and not is_form_editable %}
<!-- edit button -->
<a href="/{{ route }}/{{ doc_name }}/edit" class="edit-button btn btn-primary btn-sm ml-2">{{ _("Edit", null, "Button in web form") }}</a>
{% if allow_print and in_view_mode %}
{% set print_format_url = "/printview?doctype=" + doc_type + "&name=" + doc_name + "&format=" + print_format %}
<!-- print button -->
<a href="{{ print_format_url }}" target="_blank" class="print-btn btn btn-default btn-sm ml-2">
<svg class="icon icon-sm"><use href="#icon-printer"></use></svg>
</a>
{% endif %}
{% endmacro %}
{% macro action_buttons() %}
{% if is_new or is_form_editable %}
<div class="left-area">
<!-- clear button -->
<a href="/{{ path }}" class="clear-btn btn btn-light btn-md">
{% if is_form_editable %}
{{ _("Reset Form", null, "Button in web form") }}
{% else %}
{{ _("Clear Form", null, "Button in web form") }}
{% endif %}
</a>
</div>
<div class="center-area paging"></div>
<div class="right-area">
<div class="left-area"></div>
<div class="center-area paging"></div>
<div class="right-area">
{% if not in_view_mode %}
<!-- discard button -->
<button class="discard-btn btn btn-default btn-sm">
{{ _("Discard", null, "Button in web form") }}
</button>
<!-- submit button -->
<button type="submit" class="submit-btn btn btn-primary btn-md ml-2">{{ button_label or _("Submit", null, "Button in web form") }}</button>
</div>
{% endif %}
<button type="submit" class="submit-btn btn btn-primary btn-sm ml-2">{{ button_label or _("Submit", null, "Button in web form") }}</button>
{% endif %}
</div>
{% endmacro %}
{% block page_content %}
<!-- banner image -->
{% if banner_image %}
<div class="web-form-banner-image">
<img src="{{ banner_image }}" alt="Banner Image">
</div>
{% endif %}
<!-- web form container -->
<div class="web-form-container">
<!-- breadcrumb -->
@ -61,12 +56,20 @@
{% endif %}
<div class="web-form-head">
<div class="title">
<h1>{{ _(title) }}</h1>
<div class="web-form-title ellipsis">
{% if show_list and not is_new %}
<h1 class="ellipsis">{{ _(web_form_title) }}</h1>
<p class="ellipsis">{{ _(title) }}</p>
{% else %}
<h1 class="ellipsis">{{ _(title) }}</h1>
{% endif %}
</div>
<span class="indicator-pill orange hide">Not Saved</span>
<div class="web-form-actions">
{{ header_buttons() }}
</div>
</div>
{% if is_new and introduction_text %}
{% if introduction_text and (is_new or in_edit_mode) %}
<div class="web-form-introduction">{{ introduction_text }}</div>
{% endif %}
</div>
@ -115,30 +118,37 @@
<!-- success page -->
<div class="success-page hide">
<svg class="icon">
<use href="#icon-solid-success"></use>
</svg>
<h2 class="success-title">{{ _(success_title) or _("Submitted") }}</h2>
<p class="success-message">{{ _(success_message) or _("Thank you for spending your valuable time to fill this form") }}</p>
<div class="success-header">
<svg class="success-icon icon">
<use href="#icon-solid-success"></use>
</svg>
<h2 class="success-title">{{ _(success_title) or _("Submitted") }}</h2>
</div>
{% if success_url %}
<div class="success_url_message">
<p>
<span>Click on this </span>
<a href="{{ success_url }}">{{_("URL")}}</a>
<span> if you are not redirected within </span>
<span class="time">5</span>
<span> seconds.</span>
</p>
</div>
{% else %}
{% if show_list %}
<a href="/{{ route }}/list" class="show-list-button btn btn-light btn-md mr-2">{{ _("See previous responses", null, "Button in web form") }}</a>
<div class="success-body">
<p class="success-message">{{ _(success_message) or _("Thank you for spending your valuable time to fill this form") }}</p>
</div>
<div class="success-footer">
{% if success_url %}
<div class="success_url_message">
<p>
<span>Click on this </span>
<a href="{{ success_url }}">{{_("URL")}}</a>
<span> if you are not redirected within </span>
<span class="time">5</span>
<span> seconds.</span>
</p>
</div>
{% else %}
{% if show_list %}
<a href="/{{ route }}/list" class="show-list-button btn btn-default btn-md">{{ _("See previous responses", null, "Button in web form") }}</a>
{% endif %}
{% if not login_required or allow_multiple %}
<a href="/{{ route }}/new" class="new-btn btn btn-default btn-md">{{ _("Submit another response", null, "Button in web form") }}</a>
{% endif %}
{% endif %}
{% if not login_required or allow_multiple %}
<a href="/{{ route }}/new" class="new-btn btn btn-light btn-md">{{ _("Submit another response", null, "Button in web form") }}</a>
{% endif %}
{% endif %}
</div>
</div>
{% endblock page_content %}
@ -157,10 +167,7 @@
Vue.prototype.frappe = window.frappe;
</script>
{{ include_script("controls.bundle.js") }}
{{ include_script("dialog.bundle.js") }}
{{ include_script("web_form.bundle.js") }}
{{ include_script("bootstrap-4-web.bundle.js") }}
<script>
{% if client_script %}

View file

@ -1,10 +1,10 @@
<div class="web-form-skeleton">
<div class="box-group">
<div class="box-container">
<div class="box-container col-sm-6">
<div class="box box-label"></div>
<div class="box box-area"></div>
</div>
<div class="box-container">
<div class="box-container col-sm-6">
<div class="box box-label"></div>
<div class="box box-area"></div>
</div>
@ -16,21 +16,21 @@
</div>
</div>
<div class="box-group">
<div class="box-container">
<div class="box-container col-sm-6">
<div class="box box-label"></div>
<div class="box box-area"></div>
</div>
<div class="box-container">
<div class="box-container col-sm-6">
<div class="box box-label"></div>
<div class="box box-area"></div>
</div>
</div>
<div class="box-group">
<div class="box-container">
<div class="box-container col-sm-6">
<div class="box box-label"></div>
<div class="box box-area"></div>
</div>
<div class="box-container">
<div class="box-container col-sm-6">
<div class="box box-label"></div>
<div class="box box-area"></div>
</div>

View file

@ -7,7 +7,9 @@
<div class="web-list-container">
<!-- list -->
<div class="web-list-header">
<h1>{{ _(list_title or title) }}</h1>
<div class="web-list-title ellipsis">
<h1 class="ellipsis">{{ _(list_title or title) }}</h1>
</div>
<div class="web-list-actions">
{%- if allow_multiple -%}
<a class="btn btn-primary btn-sm button-new" href="/{{ route }}/new">New</a>
@ -27,10 +29,7 @@
frappe.web_form_doc = {{ web_form_doc | json }};
</script>
{{ 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 %}

View file

@ -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("<h1>New Manage Events</h1>", content)
self.assertIn('<h1 class="ellipsis">New Manage Events</h1>', content)
self.assertIn('data-doctype="Web Form"', content)
self.assertIn('data-path="manage-events/new"', content)
self.assertIn('source-type="Generator"', content)

View file

@ -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 = `<span class="bold" style="color: var(${prefix[value]})">${value}</span>`;
}
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 = `
<span class="bold pointer" title="${__("Switch to Form Settings Tab")}">
${__("Form Settings Tab")}
</span>
const go_to_login_required_field = `
<code class="pointer" title="${__("Go to Login Required field")}">
${__("login_required")}
</code>
`;
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(
$(
`<div class="form-message blue">
${__(
"Login is required to see web form list view. Enable <code>login_required</code> from {0} to see list settings",
[switch_to_form_settings_tab]
)}
</div>`
)
)
.find("span")
.html($(`<div class="form-message blue">${message}</div>`))
.find("code")
.click(() => frm.scroll_to_field("login_required"));
} else {
$(frm.fields_dict["list_setting_message"].wrapper).empty();

View file

@ -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",

View file

@ -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)

View file

@ -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",

View file

@ -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",

View file

@ -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")

View file

@ -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