Merge branch 'develop' into editable-form

This commit is contained in:
Shariq Ansari 2022-08-12 11:22:35 +05:30 committed by GitHub
commit 52c5270f00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 529 additions and 304 deletions

View file

@ -11,6 +11,7 @@ The following 3rd-party software packages may be used by or distributed with <ht
- Leaflet.Locate - (c) 2016 Dominik Moritz
- Leaflet.draw - (c) 2012-2017, Jacob Toye, Jon West, Smartrak
- Leaflet.EasyButton - MIT License, (C) 2014 Daniel Montague
- Identicons - MIT License, (C) 2015, <https://github.com/evuez/identicons>
### Icon Fonts

View file

@ -142,10 +142,9 @@ context("Web Form", () => {
it("Custom Breadcrumbs", () => {
cy.visit("/app/web-form/note");
cy.findByRole("tab", { name: "Form Settings" }).click();
cy.get(".form-section .section-head").contains("Customization").click();
cy.findByRole("tab", { name: "Customization" }).click();
cy.fill_field("breadcrumbs", '[{"label": _("Notes"), "route":"note"}]', "Code");
cy.get(".form-section .section-head").contains("Customization").click();
cy.get(".form-tabs .nav-item .nav-link").contains("Customization").click();
cy.save();
cy.visit("/note/Note 1");
@ -188,6 +187,7 @@ context("Web Form", () => {
cy.fill_field("title", " Edited");
cy.get(".web-form-actions button").contains("Save").click();
cy.get(".success-page .edit-button").click();
cy.get_field("title").should("have.value", "Note 1 Edited");
});

View file

@ -493,7 +493,7 @@ def init_singles():
doc.flags.ignore_mandatory = True
doc.flags.ignore_validate = True
doc.save()
except ImportError:
except (ImportError, frappe.DoesNotExistError):
# The doctype exists, but controller is deleted,
# no need to attempt to init such single, ref: #16917
continue

View file

@ -38,21 +38,14 @@ $.extend(frappe.perm, {
has_perm: (doctype, permlevel, ptype, doc) => {
if (!permlevel) permlevel = 0;
if (!frappe.perm.doctype_perm[doctype]) {
frappe.perm.doctype_perm[doctype] = frappe.perm.get_perm(doctype);
frappe.perm.doctype_perm[doctype] = frappe.perm.get_perm(doctype, doc);
}
let perms = frappe.perm.doctype_perm[doctype];
if (!perms || !perms[permlevel]) return false;
let perm = !!perms[permlevel][ptype];
if (permlevel === 0 && perm && doc) {
let docinfo = frappe.model.get_docinfo(doctype, doc.name);
if (docinfo && !docinfo.permissions[ptype]) perm = false;
}
return perm;
return !!perms[permlevel][ptype];
},
get_perm: (doctype, doc) => {

View file

@ -982,6 +982,20 @@ Object.assign(frappe.utils, {
}
});
},
setup_timer(start, end, $element) {
const increment = end > start;
let counter = start;
let interval = setInterval(() => {
increment ? counter++ : counter--;
if (increment ? counter > end : counter < end) {
clearInterval(interval);
return;
}
$element.text(counter);
}, 1000);
},
deep_equal(a, b) {
return deep_equal(a, b);
},

View file

@ -20,6 +20,7 @@ export default class WebForm extends frappe.ui.FieldGroup {
}
make() {
this.parent.empty();
super.make();
this.set_page_breaks();
this.set_field_values();
@ -29,7 +30,6 @@ export default class WebForm extends frappe.ui.FieldGroup {
this.setup_primary_action();
}
this.setup_footer_actions();
this.setup_previous_next_button();
this.toggle_section();
@ -73,14 +73,6 @@ export default class WebForm extends frappe.ui.FieldGroup {
this.is_multi_step_form = true;
}
setup_footer_actions() {
if (this.is_multi_step_form) return;
if ($(".web-form-container").height() > 600) {
$(".web-form-footer").removeClass("hide");
}
}
setup_previous_next_button() {
let me = this;
@ -380,45 +372,45 @@ export default class WebForm extends frappe.ui.FieldGroup {
return false;
}
edit() {
window.location.href = window.location.pathname + "/edit";
}
cancel() {
let path = window.location.pathname;
if (this.is_new) {
path = path.replace("/new", "");
} else {
path = path.replace("/edit", "");
}
window.location.href = path;
}
handle_success(data) {
// TODO: remove this (used for payments app)
if (this.accept_payment && !this.doc.paid) {
window.location.href = data;
}
const success_message = this.success_message || __("Submitted");
if (!this.is_new) {
$(".success-title").text(__("Updated"));
$(".success-message").text(__("Your form has been successfully updated"));
}
frappe.toast({ message: success_message, indicator: "green" });
$(".web-form-container").hide();
$(".success-page").removeClass("hide");
// redirect
setTimeout(() => {
let path = window.location.pathname;
if (this.success_url) {
frappe.utils.setup_timer(5, 0, $(".time"));
setTimeout(() => {
window.location.href = this.success_url;
}, 5000);
} else {
this.render_success_page(data);
}
}
if (this.success_url) {
path = this.success_url;
} else if (this.login_required) {
if (this.is_new && data.name) {
path = path.replace("/new", "");
path = path + "/" + data.name;
} else if (this.is_form_editable) {
path = path.replace("/edit", "");
}
}
window.location.href = path;
}, 3000);
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">
${__("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">
${__("View your response", null, "Button in web form")}
</a>
`);
}
}
}

View file

@ -6,36 +6,69 @@
margin: auto;
h1 {
font-size: 1.9rem;
font-size: 2.25rem;
margin-top: 0;
margin-bottom: 0;
}
.web-form-container {
.web-form-banner-image {
margin: -4rem -14rem 5rem;
padding-top: 3rem;
position: relative;
img {
position: absolute;
object-fit: cover;
width: 100%;
height: 250px;
z-index: -1;
}
}
.web-form-header {
border: 1px solid var(--dark-border-color);
border-radius: var(--border-radius-md);
padding: 2rem;
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;
.web-form-header {
display: flex;
justify-content: space-between;
margin: 0 -2rem 1rem;
padding: 0 2rem 1rem;
border-bottom: 1px solid var(--border-color);
.breadcrumb-container {
padding: 0px;
margin: 0 0 2rem;
.web-form-actions {
align-self: center;
ol.breadcrumb {
padding: 0px;
}
}
.web-form-introduction {
color: var(--text-muted);
margin-bottom: 2rem;
.web-form-head {
border-bottom: 1px solid var(--dark-border-color);
padding-bottom: 1.25rem;
p {
.title {
display: flex;
justify-content: space-between;
}
.web-form-introduction {
color: var(--text-muted);
margin-top: 1.25rem;
p {
color: var(--text-muted);
}
}
}
}
.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 {
@ -52,7 +85,7 @@
}
.form-column {
padding: 0 var(--padding-md);
padding: 0 var(--padding-sm);
&:first-child {
padding-left: 0;
@ -66,6 +99,34 @@
padding: 0;
}
}
.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);
}
.box-label {
height: 20px;
width: 100px;
margin-bottom: 0.5rem;
}
.box-area {
height: 34px;
width: 100%;
}
}
}
}
}
.web-form-footer {
@ -83,33 +144,115 @@
padding: 0.5rem;
display: flex;
align-items: center;
}
}
}
.attachments {
margin: 1rem -2rem 0;
padding: 1rem 2rem 0;
border-top: 1px solid var(--border-color);
.slides-progress {
display: flex;
margin-right: .5rem;
.attachment {
display: flex;
justify-content: space-between;
gap: 6px;
max-width: 300px;
color: var(--text-muted);
font-size: var(--text-md);
.slide-step {
@include flex(flex, center, center, null);
&:hover {
text-decoration: none;
.file-name span {
text-decoration: underline;
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);
}
}
}
}
}
}
}
}
}
.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);
@ -119,6 +262,8 @@
.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;
@ -128,9 +273,7 @@
.web-list-filters {
display: flex;
flex-wrap: wrap;
margin: 1rem -2rem 0;
padding: 1rem 2rem 0;
border-top: 1px solid var(--border-color);
margin: 1.25rem 0;
gap: 10px;
.form-group.frappe-control {
@ -158,7 +301,6 @@
.web-list-table {
overflow: auto;
margin: 1rem -2rem 0;
.table {
border-bottom: 1px solid var(--border-color);
@ -171,14 +313,6 @@
font-weight: normal;
color: var(--text-muted);
&:first-child {
padding-left: 1.5rem;
}
&:last-child {
padding-right: 1.5rem;
}
input[type="checkbox"] {
margin-bottom: -2px;
}
@ -192,19 +326,10 @@
td {
font-size: 13px;
border-top: 1px solid var(--border-color);
&:first-child {
padding-left: 1.5rem;
}
&:last-child {
padding-right: 1.5rem;
}
}
}
input[type="checkbox"] {
margin-left: 0.5rem;
margin-top: 2px;
}
@ -234,63 +359,4 @@
}
}
}
.slides-progress {
display: flex;
margin-right: .5rem;
.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);
}
}
}
}
}
}

View file

@ -0,0 +1,4 @@
# File for testing lazy_import util via test_lazy_import_module
import time
print("Module `frappe.tests.data.load_sleep` loaded")

View file

@ -713,3 +713,12 @@ class TestBenchBuild(BaseTestCommands):
CURRENT_SIZE * (1 + JS_ASSET_THRESHOLD),
f"Default JS bundle size increased by {JS_ASSET_THRESHOLD:.2%} or more",
)
class TestCommandUtils(unittest.TestCase):
def test_bench_helper(self):
from frappe.utils.bench_helper import get_app_groups
app_groups = get_app_groups()
self.assertIn("frappe", app_groups)
self.assertIsInstance(app_groups["frappe"], click.Group)

View file

@ -4,10 +4,12 @@
import io
import json
import os
import sys
import unittest
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from enum import Enum
from io import StringIO
from mimetypes import guess_type
from unittest.mock import patch
@ -16,15 +18,18 @@ from PIL import Image
import frappe
from frappe.installer import parse_app_name
from frappe.model.document import Document
from frappe.utils import (
ceil,
evaluate_filters,
floor,
format_timedelta,
get_bench_path,
get_gravatar,
get_url,
money_in_words,
parse_timedelta,
random_string,
scrub_urls,
validate_email_address,
validate_url,
@ -42,10 +47,25 @@ from frappe.utils.data import (
)
from frappe.utils.dateutils import get_dates_from_timegrain
from frappe.utils.diff import _get_value_from_version, get_version_diff, version_query
from frappe.utils.identicon import Identicon
from frappe.utils.image import optimize_image, strip_exif_data
from frappe.utils.make_random import can_make, get_random, how_many
from frappe.utils.response import json_handler
class Capturing(list):
# ref: https://stackoverflow.com/a/16571630/10309266
def __enter__(self):
self._stdout = sys.stdout
sys.stdout = self._stringio = StringIO()
return self
def __exit__(self, *args):
self.extend(self._stringio.getvalue().splitlines())
del self._stringio
sys.stdout = self._stdout
class TestFilters(unittest.TestCase):
def test_simple_dict(self):
self.assertTrue(evaluate_filters({"doctype": "User", "status": "Open"}, {"status": "Open"}))
@ -677,3 +697,53 @@ class TestIntrospectionMagic(unittest.TestCase):
# No args
self.assertEqual(frappe.get_newargs(lambda: None, args), {})
class TestMakeRandom(unittest.TestCase):
def test_get_random(self):
self.assertIsInstance(get_random("DocType", doc=True), Document)
self.assertIsInstance(get_random("DocType"), str)
def test_can_make(self):
self.assertIsInstance(can_make("User"), bool)
def test_how_many(self):
self.assertIsInstance(how_many("User"), int)
class TestLazyLoader(unittest.TestCase):
def test_lazy_import_module(self):
from frappe.utils.lazy_loader import lazy_import
with Capturing() as output:
ls = lazy_import("frappe.tests.data.load_sleep")
self.assertEqual(output, [])
with Capturing() as output:
ls.time
self.assertEqual(["Module `frappe.tests.data.load_sleep` loaded"], output)
class TestIdenticon(unittest.TestCase):
def test_get_gravatar(self):
# developers@frappe.io has a gravatar linked so str URL will be returned
frappe.flags.in_test = False
gravatar_url = get_gravatar("developers@frappe.io")
frappe.flags.in_test = True
self.assertIsInstance(gravatar_url, str)
self.assertTrue(gravatar_url.startswith("http"))
# random email will require Identicon to be generated, which will be a base64 string
gravatar_url = get_gravatar(f"developers{random_string(6)}@frappe.io")
self.assertIsInstance(gravatar_url, str)
self.assertTrue(gravatar_url.startswith("data:image/png;base64,"))
def test_generate_identicon(self):
identicon = Identicon(random_string(6))
with patch.object(identicon.image, "show") as show:
identicon.generate()
show.assert_called_once()
identicon_bs64 = identicon.base64()
self.assertIsInstance(identicon_bs64, str)
self.assertTrue(identicon_bs64.startswith("data:image/png;base64,"))

View file

@ -283,12 +283,7 @@ def get_gravatar_url(email):
def get_gravatar(email):
from frappe.utils.identicon import Identicon
gravatar_url = has_gravatar(email)
if not gravatar_url:
gravatar_url = Identicon(email).base64()
return gravatar_url
return has_gravatar(email) or Identicon(email).base64()
def get_traceback(with_context=False) -> str:

View file

@ -3,6 +3,7 @@ import json
import os
import traceback
import warnings
from pathlib import Path
import click
@ -18,22 +19,18 @@ def main():
click.Group(commands=commands)(prog_name="bench")
def get_app_groups():
def get_app_groups() -> dict[str, click.Group]:
"""Get all app groups, put them in main group "frappe" since bench is
designed to only handle that"""
commands = dict()
commands = {}
for app in get_apps():
app_commands = get_app_commands(app)
if app_commands:
commands.update(app_commands)
ret = dict(frappe=click.group(name="frappe", commands=commands)(app_group))
return ret
if app_commands := get_app_commands(app):
commands |= app_commands
return dict(frappe=click.group(name="frappe", commands=commands)(app_group))
def get_app_group(app):
app_commands = get_app_commands(app)
if app_commands:
def get_app_group(app: str) -> click.Group:
if app_commands := get_app_commands(app):
return click.group(name=app, commands=app_commands)(app_group)
@ -48,7 +45,7 @@ def app_group(ctx, site=False, force=False, verbose=False, profile=False):
ctx.info_name = ""
def get_sites(site_arg):
def get_sites(site_arg: str) -> list[str]:
if site_arg == "all":
return frappe.utils.get_sites()
elif site_arg:
@ -57,25 +54,23 @@ def get_sites(site_arg):
return [os.environ.get("FRAPPE_SITE")]
elif os.path.exists("currentsite.txt"):
with open("currentsite.txt") as f:
site = f.read().strip()
if site:
if site := f.read().strip():
return [site]
return []
def get_app_commands(app):
if os.path.exists(os.path.join("..", "apps", app, app, "commands.py")) or os.path.exists(
os.path.join("..", "apps", app, app, "commands", "__init__.py")
):
try:
app_command_module = importlib.import_module(app + ".commands")
except Exception:
traceback.print_exc()
return []
else:
return []
def get_app_commands(app: str) -> dict:
ret = {}
app_path = Path("..", "apps", app, app)
if not ((app_path / "commands.py").exists() or (app_path / "commands" / "__init__.py").exists()):
return ret
try:
app_command_module = importlib.import_module(f"{app}.commands")
except Exception:
traceback.print_exc()
return ret
for command in getattr(app_command_module, "commands", []):
ret[command.name] = command
return ret

View file

@ -1,7 +1,13 @@
import base64
import random
"""
This module provides a class Identicon that can be used to generate identicons
from strings.
It provides a (slighltly modified) version of https://github.com/evuez/identicons
which has been released under the MIT license, as described in attributions.md.
"""
from base64 import b64encode
from hashlib import md5
from io import StringIO
from io import BytesIO
from PIL import Image, ImageDraw
@ -33,32 +39,7 @@ class Identicon:
First three bytes are used to generate the color,
remaining bytes are used to create the drawing
"""
# color = (self.hash & 0xff, self.hash >> 8 & 0xff, self.hash >> 16 & 0xff)
color = random.choice(
(
(254, 196, 197),
(253, 138, 139),
(254, 231, 206),
(254, 208, 159),
(210, 211, 253),
(163, 165, 252),
(247, 213, 247),
(242, 172, 238),
(235, 247, 206),
(217, 241, 157),
(211, 248, 237),
(167, 242, 221),
(255, 249, 207),
(254, 245, 161),
(211, 241, 254),
(168, 228, 254),
(207, 245, 210),
(159, 235, 164),
)
)
# print color
# color = (254, 232, 206)
color = (self.hash & 0xFF, self.hash >> 8 & 0xFF, self.hash >> 16 & 0xFF)
self.hash >>= 24 # skip first three bytes
square_x = square_y = 0 # init square position
for x in range(GRID_SIZE * (GRID_SIZE + 1) // 2):
@ -86,22 +67,17 @@ class Identicon:
def base64(self, format="PNG"):
"""
usage: i = Identicon('xx')
print(i.base64())
return: this image's base64 code
created by: liuzheng712
bug report: https://github.com/liuzheng712/identicons/issues
Return the identicon's base64
Created by: liuzheng712
Bug report: https://github.com/liuzheng712/identicons/issues
"""
self.calculate()
fp = StringIO()
self.image.encoderinfo = {}
self.image.encoderconfig = ()
if format.upper() not in Image.SAVE:
Image.init()
save_handler = Image.SAVE[format.upper()]
try:
save_handler(self.image, fp, "")
finally:
fp.seek(0)
return f"data:image/png;base64,{base64.b64encode(fp.read())}" # noqa
buff = BytesIO()
self.image.save(buff, format=format.upper())
return f"data:image/png;base64,{b64encode(buff.getvalue()).decode()}"

View file

@ -1,7 +1,11 @@
import random
from typing import TYPE_CHECKING
import frappe
if TYPE_CHECKING:
from frappe.model.document import Document
settings = frappe._dict(
prob={
"default": {"make": 0.6, "qty": (1, 5)},
@ -9,7 +13,7 @@ settings = frappe._dict(
)
def add_random_children(doc, fieldname, rows, randomize, unique=None):
def add_random_children(doc: "Document", fieldname: str, rows, randomize: dict, unique=None):
nrows = rows
if rows > 1:
nrows = random.randrange(1, rows)
@ -29,15 +33,13 @@ def add_random_children(doc, fieldname, rows, randomize, unique=None):
doc.append(fieldname, d)
def get_random(doctype, filters=None, doc=False):
def get_random(doctype: str, filters: dict = None, doc: bool = False):
condition = []
if filters:
for key, val in filters.items():
condition.append("{}='{}'".format(key, str(val).replace("'", "'")))
if condition:
condition = " where " + " and ".join(condition)
else:
condition = ""
condition.extend(
"{}='{}'".format(key, str(val).replace("'", "'")) for key, val in filters.items()
)
condition = " where " + " and ".join(condition) if condition else ""
out = frappe.db.multisql(
{
@ -54,13 +56,12 @@ def get_random(doctype, filters=None, doc=False):
if doc and out:
return frappe.get_doc(doctype, out)
else:
return out
return out
def can_make(doctype):
def can_make(doctype: str) -> bool:
return random.random() < settings.prob.get(doctype, settings.prob["default"])["make"]
def how_many(doctype):
def how_many(doctype: str) -> int:
return random.randrange(*settings.prob.get(doctype, settings.prob["default"])["qty"])

View file

@ -20,7 +20,7 @@
{% macro action_buttons() %}
{% if is_new or is_form_editable %}
<div class="left-area">
<!-- cancel button -->
<!-- clear button -->
<a href="/{{ path }}" class="clear-btn btn btn-light btn-md">
{% if is_form_editable %}
{{ _("Reset Form", null, "Button in web form") }}
@ -38,33 +38,53 @@
{% endmacro %}
{% block page_content %}
<!-- breadcrumb -->
{% if has_header and login_required and show_list %}
{% include "templates/includes/breadcrumbs.html" %}
{% else %}
<div style="height: 3rem"></div>
<!-- banner image -->
{% if banner_image %}
<div class="web-form-banner-image">
<img src="{{ banner_image }}" alt="Banner Image">
</div>
{% endif %}
<!-- main card -->
<form role="form" class="web-form-container">
<!-- web form container -->
<div class="web-form-container">
<!-- breadcrumb -->
{% if not banner_image and has_header and login_required and show_list %}
{% include "templates/includes/breadcrumbs.html" %}
{% else %}
<div style="height: 3rem"></div>
{% endif %}
<!-- header -->
<div class="web-form-header">
<h1>{{ _(title) }}</h1>
<div class="web-form-actions">
{{ header_buttons() }}
{% if banner_image and has_header and login_required and show_list %}
{% include "templates/includes/breadcrumbs.html" %}
{% endif %}
<div class="web-form-head">
<div class="title">
<h1>{{ _(title) }}</h1>
<div class="web-form-actions">
{{ header_buttons() }}
</div>
</div>
{% if is_new and introduction_text %}
<div class="web-form-introduction">{{ introduction_text }}</div>
{% endif %}
</div>
</div>
<div class="web-form-body">
{% if is_new and introduction_text %}
<div class="web-form-introduction">{{ introduction_text }}</div>
{% endif %}
<div class="web-form-wrapper"></div>
<!-- main card -->
<form role="form" class="web-form">
<div class="web-form-body">
<div class="web-form-wrapper">
{% include "website/doctype/web_form/templates/web_form_skeleton.html" %}
</div>
</div>
<div class="web-form-footer">
<div class="web-form-actions">
{{ action_buttons() }}
</div>
</div>
</div>
</form>
<!-- attachments -->
{% if show_attachments and not is_new and attachments %}
@ -81,17 +101,45 @@
{% endfor %}
</div>
{% endif %} {# attachments #}
</form>
<!-- comments -->
{% if allow_comments and not is_new and not is_list -%}
<div class="comments">
<h3>{{ _("Comments") }}</h3>
{% include 'templates/includes/comments/comments.html' %}
</div>
{%- else -%}
<div style="height: 3rem"></div>
{%- endif %} {# comments #}
<!-- comments -->
{% if allow_comments and not is_new and not is_list -%}
<div class="comments">
<h3>{{ _("Comments") }}</h3>
{% include 'templates/includes/comments/comments.html' %}
</div>
{%- else -%}
<div style="height: 3rem"></div>
{%- endif %} {# comments #}
</div>
<!-- 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>
{% 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>
{% 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>
{% endblock page_content %}

View file

@ -0,0 +1,38 @@
<div class="web-form-skeleton">
<div class="box-group">
<div class="box-container">
<div class="box box-label"></div>
<div class="box box-area"></div>
</div>
<div class="box-container">
<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 box-label"></div>
<div class="box box-area"></div>
</div>
</div>
<div class="box-group">
<div class="box-container">
<div class="box box-label"></div>
<div class="box box-area"></div>
</div>
<div class="box-container">
<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 box-label"></div>
<div class="box box-area"></div>
</div>
<div class="box-container">
<div class="box box-label"></div>
<div class="box box-area"></div>
</div>
</div>
</div>

View file

@ -30,12 +30,6 @@
"form_fields",
"web_form_fields",
"max_attachment_size",
"actions",
"breadcrumbs",
"button_label",
"column_break_29",
"success_message",
"success_url",
"list_settings_tab",
"list_setting_message",
"show_list",
@ -44,6 +38,16 @@
"sidebar_settings_tab",
"show_sidebar",
"website_sidebar",
"customization_tab",
"button_label",
"banner_image",
"column_break_37",
"breadcrumbs",
"section_break_43",
"success_title",
"success_url",
"column_break_41",
"success_message",
"scripting_style_tab",
"client_script",
"custom_css"
@ -162,7 +166,7 @@
},
{
"fieldname": "introduction_text",
"fieldtype": "Small Text",
"fieldtype": "Text Editor",
"ignore_xss_filter": 1,
"label": "Introduction"
},
@ -183,12 +187,6 @@
"fieldtype": "Code",
"label": "Client Script"
},
{
"collapsible": 1,
"fieldname": "actions",
"fieldtype": "Section Break",
"label": "Customization"
},
{
"default": "Save",
"fieldname": "button_label",
@ -196,7 +194,7 @@
"label": "Submit Button Label"
},
{
"description": "Message to be displayed on successful completion (only for Guest users)",
"description": "Message to be displayed on successful completion",
"fieldname": "success_message",
"fieldtype": "Text",
"label": "Success Message"
@ -217,7 +215,8 @@
"description": "List as [{\"label\": _(\"Jobs\"), \"route\":\"jobs\"}]",
"fieldname": "breadcrumbs",
"fieldtype": "Code",
"label": "Breadcrumbs"
"label": "Breadcrumbs",
"max_height": "140px"
},
{
"fieldname": "custom_css",
@ -272,10 +271,6 @@
"label": "Website Sidebar",
"options": "Website Sidebar"
},
{
"fieldname": "column_break_29",
"fieldtype": "Column Break"
},
{
"fieldname": "list_setting_message",
"fieldtype": "HTML",
@ -303,13 +298,41 @@
"fieldname": "scripting_style_tab",
"fieldtype": "Tab Break",
"label": "Scripting / Style"
},
{
"fieldname": "customization_tab",
"fieldtype": "Tab Break",
"label": "Customization"
},
{
"fieldname": "success_title",
"fieldtype": "Data",
"label": "Success Title"
},
{
"fieldname": "banner_image",
"fieldtype": "Attach Image",
"label": "Banner Image"
},
{
"fieldname": "column_break_41",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_43",
"fieldtype": "Section Break",
"label": "After Submission"
},
{
"fieldname": "column_break_37",
"fieldtype": "Column Break"
}
],
"has_web_view": 1,
"icon": "icon-edit",
"is_published_field": "published",
"links": [],
"modified": "2022-08-10 15:38:28.611328",
"modified": "2022-08-11 16:27:25.914627",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form",

View file

@ -281,7 +281,7 @@ def get_context(context):
context.title = strip_html(
context.reference_doc.get(context.reference_doc.meta.get_title_field())
)
if context.is_form_editable:
if context.is_form_editable and context.parents:
context.parents.append(
{
"label": _(context.title),