refactor: Webform (#17232)

This commit is contained in:
Shariq Ansari 2022-07-19 15:52:15 +05:30 committed by GitHub
parent cf7cb387f3
commit a50e0ffa08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1527 additions and 806 deletions

View file

@ -3,24 +3,253 @@ context('Web Form', () => {
cy.login(); cy.login();
}); });
it('Create Web Form', () => {
cy.visit('/app/web-form/new');
cy.intercept('POST', '/api/method/frappe.desk.form.save.savedocs').as('save_form');
cy.fill_field('title', 'Note');
cy.fill_field('doc_type', 'Note', 'Link');
cy.fill_field('module', 'Website', 'Link');
cy.click_custom_action_button('Get Fields');
cy.click_custom_action_button('Publish');
cy.wait('@save_form');
cy.get_field('route').should('have.value', 'note');
cy.get('.title-area .indicator-pill').contains('Published');
});
it('Open Web Form (Logged in User)', () => {
cy.visit('/note');
cy.fill_field('title', 'Note 1');
cy.get('.web-form-actions button').contains('Save').click();
cy.url().should('include', '/note/Note%201');
cy.visit('/note');
cy.url().should('include', '/note/Note%201');
});
it('Open Web Form (Guest)', () => {
cy.request('/api/method/logout');
cy.visit('/note');
cy.url().should('include', '/note/new');
cy.fill_field('title', 'Guest Note 1');
cy.get('.web-form-actions button').contains('Save').click();
cy.url().should('include', '/note/new');
cy.visit('/note');
cy.url().should('include', '/note/new');
});
it('Login Required', () => {
cy.login();
cy.visit('/app/web-form/note');
cy.findByRole("tab", {name: "Form Settings"}).click();
cy.get('input[data-fieldname="login_required"]').check({force: true});
cy.save();
cy.visit('/note');
cy.url().should('include', '/note/Note%201');
cy.call('logout');
cy.visit('/note');
cy.get_open_dialog()
.get('.modal-message')
.contains('You are not permitted to access this page without login.');
});
it('Show List', () => {
cy.login();
cy.visit('/app/web-form/note');
cy.findByRole("tab", {name: "List Settings"}).click();
cy.get('input[data-fieldname="show_list"]').check();
cy.save();
cy.visit('/note');
cy.url().should('include', '/note/list');
cy.get('.web-list-table').should('be.visible');
});
it('Show Custom List Title', () => {
cy.visit('/app/web-form/note');
cy.findByRole("tab", {name: "List Settings"}).click();
cy.fill_field('list_title', 'Note List');
cy.save();
cy.visit('/note');
cy.url().should('include', '/note/list');
cy.get('.web-list-header h1').should('contain.text', 'Note List');
});
it('Show Custom List Columns', () => {
cy.visit('/note');
cy.url().should('include', '/note/list');
cy.get('.web-list-table thead th').contains('Name');
cy.get('.web-list-table thead th').contains('Title');
cy.visit('/app/web-form/note');
cy.findByRole("tab", {name: "List Settings"}).click();
cy.get('[data-fieldname="list_columns"] .grid-footer button').contains('Add Row').as('add-row');
cy.get('@add-row').click();
cy.get('[data-fieldname="list_columns"] .grid-body .rows').as('grid-rows');
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)');
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)');
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)');
cy.save();
cy.visit('/note');
cy.url().should('include', '/note/list');
cy.get('.web-list-table thead th').contains('Title');
cy.get('.web-list-table thead th').contains('Public');
cy.get('.web-list-table thead th').contains('Content');
});
it('Breadcrumbs', () => {
cy.visit('/note/Note 1');
cy.get('.breadcrumb-container .breadcrumb .breadcrumb-item:first a')
.should('contain.text', 'Note').click();
cy.url().should('include', '/note/list');
});
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.fill_field('breadcrumbs', '[{"label": _("Notes"), "route":"note"}]', 'Code');
cy.get('.form-section .section-head').contains('Customization').click();
cy.save();
cy.visit('/note/Note 1');
cy.get('.breadcrumb-container .breadcrumb .breadcrumb-item:first a')
.should('contain.text', 'Notes');
});
it('Read Only', () => {
cy.login();
cy.visit('/note');
cy.url().should('include', '/note/list');
// Read Only Field
cy.get('.web-list-table tbody tr[id="Note 1"]').click();
cy.get('.frappe-control[data-fieldname="title"] .control-input')
.should('have.css', 'display', 'none');
});
it('Edit Mode', () => {
cy.visit('/app/web-form/note');
cy.findByRole("tab", {name: "Form Settings"}).click();
cy.get('input[data-fieldname="allow_edit"]').check();
cy.save();
cy.visit('/note/Note 1');
cy.url().should('include', '/note/Note%201');
cy.get('.web-form-actions a').contains('Edit').click();
cy.url().should('include', '/note/Note%201/edit');
// Editable Field
cy.get_field('title').should('have.value', 'Note 1');
cy.fill_field('title', ' Edited');
cy.get('.web-form-actions button').contains('Save').click();
cy.get_field('title').should('have.value', 'Note 1 Edited');
});
it('Allow Multiple Response', () => {
cy.visit('/app/web-form/note');
cy.findByRole("tab", {name: "Form Settings"}).click();
cy.get('input[data-fieldname="allow_multiple"]').check();
cy.save();
cy.visit('/note');
cy.url().should('include', '/note/list');
cy.get('.web-list-actions a:visible').contains('New').click();
cy.url().should('include', '/note/new');
cy.fill_field('title', 'Note 2');
cy.get('.web-form-actions button').contains('Save').click();
});
it('Allow Delete', () => {
cy.visit('/app/web-form/note');
cy.findByRole("tab", {name: "Form Settings"}).click();
cy.get('input[data-fieldname="allow_delete"]').check();
cy.save();
cy.visit('/note');
cy.url().should('include', '/note/list');
cy.get('.web-list-table tbody tr[id="Note 1"] .list-col-checkbox').click();
cy.get('.web-list-table tbody tr[id="Note 2"] .list-col-checkbox').click();
cy.get('.web-list-actions button:visible').contains('Delete').click({force: true});
cy.get('.web-list-actions button').contains('Delete').should('not.be.visible');
cy.visit('/note');
cy.get('.web-list-table tbody tr[id="Note 1"]').should('not.exist');
cy.get('.web-list-table tbody tr[id="Note 2"]').should('not.exist');
cy.get('.web-list-table tbody tr[id="Guest Note 1"]').should('exist');
});
it('Navigate and Submit a WebForm', () => { it('Navigate and Submit a WebForm', () => {
cy.visit('/update-profile'); cy.visit('/update-profile');
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
cy.get('.web-form-actions a').contains('Edit').click();
cy.fill_field('last_name', '_Test User');
cy.get('.web-form-actions .btn-primary').click(); cy.get('.web-form-actions .btn-primary').click();
cy.wait(5000);
cy.url().should('include', '/me'); cy.url().should('include', '/me');
}); });
it('Navigate and Submit a MultiStep WebForm', () => { it('Navigate and Submit a MultiStep WebForm', () => {
cy.call('frappe.tests.ui_test_helpers.update_webform_to_multistep').then(() => { cy.call('frappe.tests.ui_test_helpers.update_webform_to_multistep').then(() => {
cy.visit('/update-profile-duplicate'); cy.visit('/update-profile-duplicate');
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
cy.get('.web-form-actions a').contains('Edit').click();
cy.fill_field('last_name', '_Test User');
cy.get('.btn-next').should('be.visible'); cy.get('.btn-next').should('be.visible');
cy.get('.btn-next').click(); cy.get('.btn-next').click();
cy.get('.btn-previous').should('be.visible'); cy.get('.btn-previous').should('be.visible');
cy.get('.btn-next').should('not.be.visible'); cy.get('.btn-next').should('not.be.visible');
cy.get('.web-form-actions .btn-primary').click(); cy.get('.web-form-actions .btn-primary').click();
cy.wait(5000);
cy.url().should('include', '/me'); cy.url().should('include', '/me');
}); });
}); });

View file

@ -162,7 +162,12 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
if (fieldtype === 'Select') { if (fieldtype === 'Select') {
cy.get('@input').select(value); cy.get('@input').select(value);
} else { } else {
cy.get('@input').type(value, {waitForAnimations: false, force: true, delay: 100}); cy.get('@input').type(value, {
waitForAnimations: false,
parseSpecialCharSequences: false,
force: true,
delay: 100
});
} }
return cy.get('@input'); return cy.get('@input');
}); });
@ -358,6 +363,10 @@ Cypress.Commands.add('open_list_filter', () => {
cy.get('.filter-popover').should('exist'); cy.get('.filter-popover').should('exist');
}); });
Cypress.Commands.add('click_custom_action_button', (name) => {
cy.get(`.custom-actions [data-label="${encodeURIComponent(name)}"]`).click();
});
Cypress.Commands.add('click_action_button', (name) => { Cypress.Commands.add('click_action_button', (name) => {
cy.findByRole('button', {name: 'Actions'}).click(); cy.findByRole('button', {name: 'Actions'}).click();
cy.get(`.actions-btn-group [data-label="${encodeURIComponent(name)}"]`).click(); cy.get(`.actions-btn-group [data-label="${encodeURIComponent(name)}"]`).click();

View file

@ -1796,6 +1796,14 @@ def respond_as_web_page(
local.response["context"] = context local.response["context"] = context
def redirect(url):
"""Raise a 301 redirect to url"""
from frappe.exceptions import Redirect
flags.redirect_location = url
raise Redirect
def redirect_to_message(title, html, http_status_code=None, context=None, indicator_color=None): def redirect_to_message(title, html, http_status_code=None, context=None, indicator_color=None):
"""Redirects to /message?id=random """Redirects to /message?id=random
Similar to respond_as_web_page, but used to 'redirect' and show message pages like success, failure, etc. with a detailed message Similar to respond_as_web_page, but used to 'redirect' and show message pages like success, failure, etc. with a detailed message

View file

@ -18,9 +18,10 @@
"introduction_text": "", "introduction_text": "",
"is_multi_step_form": 0, "is_multi_step_form": 0,
"is_standard": 1, "is_standard": 1,
"list_columns": [],
"login_required": 1, "login_required": 1,
"max_attachment_size": 0, "max_attachment_size": 0,
"modified": "2022-03-22 15:00:43.456738", "modified": "2022-07-18 16:51:19.796411",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "edit-profile", "name": "edit-profile",
@ -29,9 +30,8 @@
"route": "update-profile", "route": "update-profile",
"route_to_success_link": 0, "route_to_success_link": 0,
"show_attachments": 0, "show_attachments": 0,
"show_in_grid": 0, "show_list": 0,
"show_sidebar": 0, "show_sidebar": 0,
"sidebar_items": [],
"success_message": "Profile updated successfully.", "success_message": "Profile updated successfully.",
"success_url": "/me", "success_url": "/me",
"title": "Update Profile", "title": "Update Profile",

View file

@ -194,6 +194,7 @@ frappe.patches.v14_0.remove_is_first_startup
frappe.patches.v14_0.clear_long_pending_stale_logs frappe.patches.v14_0.clear_long_pending_stale_logs
frappe.patches.v14_0.log_settings_migration frappe.patches.v14_0.log_settings_migration
frappe.patches.v14_0.setup_likes_from_feedback frappe.patches.v14_0.setup_likes_from_feedback
frappe.patches.v14_0.update_webforms
[post_model_sync] [post_model_sync]
frappe.patches.v14_0.drop_data_import_legacy frappe.patches.v14_0.drop_data_import_legacy

View file

@ -0,0 +1,14 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import frappe
def execute():
frappe.reload_doc("website", "doctype", "web_form_list_column")
frappe.reload_doctype("Web Form")
for web_form in frappe.db.get_all("Web Form", fields=["*"]):
if web_form.allow_multiple and not web_form.show_list:
frappe.db.set_value("Web Form", web_form.name, "show_list", True)

View file

@ -3,6 +3,7 @@ import "./frappe/class.js";
import "./frappe/polyfill.js"; import "./frappe/polyfill.js";
import "./lib/moment.js"; import "./lib/moment.js";
import "./frappe/provide.js"; import "./frappe/provide.js";
import "./frappe/form/formatters.js";
import "./frappe/format.js"; import "./frappe/format.js";
import "./frappe/utils/number_format.js"; import "./frappe/utils/number_format.js";
import "./frappe/utils/utils.js"; import "./frappe/utils/utils.js";

View file

@ -14,6 +14,7 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
} }
get_start_date() { get_start_date() {
this.value = this.value == null ? undefined : this.value;
let value = frappe.datetime.convert_to_user_tz(this.value); let value = frappe.datetime.convert_to_user_tz(this.value);
return frappe.datetime.str_to_obj(value); return frappe.datetime.str_to_obj(value);
} }

View file

@ -196,7 +196,7 @@ frappe.form.formatters = {
Datetime: function(value) { Datetime: function(value) {
if(value) { if(value) {
return moment(frappe.datetime.convert_to_user_tz(value)) return moment(frappe.datetime.convert_to_user_tz(value))
.format(frappe.boot.sysdefaults.date_format.toUpperCase() + ' ' + frappe.boot.sysdefaults.time_format || 'HH:mm:ss'); .format(frappe.boot.sysdefaults.date_format.toUpperCase() + ' ' + (frappe.boot.sysdefaults.time_format || 'HH:mm:ss'));
} else { } else {
return ""; return "";
} }

View file

@ -23,13 +23,14 @@ export default class WebForm extends frappe.ui.FieldGroup {
this.set_sections(); this.set_sections();
this.set_field_values(); this.set_field_values();
this.setup_listeners(); this.setup_listeners();
if (this.introduction_text) this.set_form_description(this.introduction_text);
if (this.allow_print && !this.is_new) this.setup_print_button(); if (this.is_new || this.is_form_editable) {
if (this.is_new) this.setup_cancel_button(); this.setup_primary_action();
this.setup_primary_action(); }
this.setup_footer_actions();
this.setup_previous_next_button(); this.setup_previous_next_button();
this.toggle_section(); this.toggle_section();
$(".link-btn").remove();
// webform client script // webform client script
frappe.init_client_script && frappe.init_client_script(); frappe.init_client_script && frappe.init_client_script();
@ -70,6 +71,14 @@ export default class WebForm extends frappe.ui.FieldGroup {
this.sections = $(`.form-section`); this.sections = $(`.form-section`);
} }
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() { setup_previous_next_button() {
let me = this; let me = this;
@ -87,7 +96,7 @@ export default class WebForm extends frappe.ui.FieldGroup {
$('.btn-previous').on('click', function () { $('.btn-previous').on('click', function () {
let is_validated = me.validate_section(); let is_validated = me.validate_section();
if (!is_validated) return; if (!is_validated) return false;
/** /**
The eslint utility cannot figure out if this is an infinite loop in backwards and The eslint utility cannot figure out if this is an infinite loop in backwards and
@ -107,12 +116,13 @@ export default class WebForm extends frappe.ui.FieldGroup {
} }
/* eslint-enable for-direction */ /* eslint-enable for-direction */
me.toggle_section(); me.toggle_section();
return false;
}); });
$('.btn-next').on('click', function () { $('.btn-next').on('click', function () {
let is_validated = me.validate_section(); let is_validated = me.validate_section();
if (!is_validated) return; if (!is_validated) return false;
for (let idx = me.current_section; idx < me.sections.length; idx++) { for (let idx = me.current_section; idx < me.sections.length; idx++) {
let is_empty = me.is_next_section_empty(idx); let is_empty = me.is_next_section_empty(idx);
@ -123,6 +133,7 @@ export default class WebForm extends frappe.ui.FieldGroup {
} }
} }
me.toggle_section(); me.toggle_section();
return false;
}); });
} }
@ -132,56 +143,20 @@ export default class WebForm extends frappe.ui.FieldGroup {
} }
set_default_values() { set_default_values() {
let defaults = {};
for (let df of this.fields) {
if (df.default) {
defaults[df.fieldname] = df.default;
}
}
let values = frappe.utils.get_query_params(); let values = frappe.utils.get_query_params();
delete values.new; delete values.new;
Object.assign(defaults, values);
this.set_values(values); this.set_values(values);
} }
set_form_description(intro) {
let intro_wrapper = document.getElementById('introduction');
intro_wrapper.innerHTML = intro;
intro_wrapper.classList.remove('hidden');
}
add_button(name, type, action, wrapper_class=".web-form-actions") {
const button = document.createElement("button");
button.classList.add("btn", "btn-" + type, "btn-sm", "ml-2");
button.innerHTML = name;
button.onclick = action;
document.querySelector(wrapper_class).appendChild(button);
}
add_button_to_footer(name, type, action) {
this.add_button(name, type, action, '.web-form-footer');
}
add_button_to_header(name, type, action) {
this.add_button(name, type, action, '.web-form-actions');
}
setup_primary_action() { setup_primary_action() {
this.add_button_to_header(this.button_label || __("Save", null, "Button in web form"), "primary", () => $(".web-form-container").on("submit", () => this.save());
this.save()
);
if (!this.is_multi_step_form && $('.frappe-card').height() > 600) {
// add button on footer if page is long
this.add_button_to_footer(this.button_label || __("Save", null, "Button in web form"), "primary", () =>
this.save()
);
}
}
setup_cancel_button() {
this.add_button_to_header(__("Cancel", null, "Button in web form"), "light", () => this.cancel());
}
setup_print_button() {
this.add_button_to_header(
frappe.utils.icon('print'),
"light",
() => this.print()
);
} }
validate_section() { validate_section() {
@ -349,18 +324,21 @@ export default class WebForm extends frappe.ui.FieldGroup {
window.saving = false; window.saving = false;
} }
}); });
return true; return false;
} }
print() { edit() {
window.open(`/printview? window.location.href = window.location.pathname + "/edit";
doctype=${this.doc_type}
&name=${this.doc.name}
&format=${this.print_format || "Standard"}`, '_blank');
} }
cancel() { cancel() {
window.location.href = window.location.pathname; 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) { handle_success(data) {
@ -375,12 +353,19 @@ export default class WebForm extends frappe.ui.FieldGroup {
// redirect // redirect
setTimeout(() => { setTimeout(() => {
let path = window.location.pathname;
if (this.success_url) { if (this.success_url) {
window.location.href = this.success_url; path = this.success_url;
} else if(this.login_required) { } else if (this.login_required) {
window.location.href = if (this.is_new && data.name) {
window.location.pathname + "?name=" + data.name; path = path.replace("/new", "");
path = path + "/" + data.name;
} else if (this.is_form_editable) {
path = path.replace("/edit", "");
}
} }
}, 2000); window.location.href = path;
}, 1000);
} }
} }

View file

@ -6,63 +6,74 @@ export default class WebFormList {
constructor(opts) { constructor(opts) {
Object.assign(this, opts); Object.assign(this, opts);
frappe.web_form_list = this; frappe.web_form_list = this;
this.wrapper = document.getElementById("list-table"); this.wrapper = $(".web-list-table");
this.make_actions(); this.make_actions();
this.make_filters(); this.make_filters();
$('.link-btn').remove();
} }
refresh() { refresh() {
if (this.table) {
Array.from(this.table.tBodies).forEach(tbody => tbody.remove());
let check = document.getElementById('select-all');
if (check)
check.checked = false;
}
this.rows = []; this.rows = [];
this.page_length = 20;
this.web_list_start = 0; this.web_list_start = 0;
this.page_length = 10;
frappe.run_serially([ frappe.run_serially([
() => this.get_list_view_fields(), () => this.get_list_view_fields(),
() => this.get_data(), () => this.get_data(),
() => this.remove_more(),
() => this.make_table(), () => this.make_table(),
() => this.create_more() () => this.create_more()
]); ]);
} }
remove_more() {
$('.more').remove();
}
make_filters() { make_filters() {
this.filters = {}; this.filters = {};
this.filter_input = []; this.filter_input = [];
const filter_area = document.getElementById('list-filters'); let filter_area = $('.web-list-filters');
frappe.call('frappe.website.doctype.web_form.web_form.get_web_form_filters', { frappe.call('frappe.website.doctype.web_form.web_form.get_web_form_filters', {
web_form_name: this.web_form_name web_form_name: this.web_form_name
}).then(response => { }).then(response => {
let fields = response.message; let fields = response.message;
fields.length && filter_area.removeClass('hide');
fields.forEach(field => { fields.forEach(field => {
let col = document.createElement('div.col-sm-4'); if (["Text Editor", "Text", "Small Text"].includes(field.fieldtype)) {
col.classList.add('col', 'col-sm-3'); field.fieldtype = "Data";
filter_area.appendChild(col); }
if (field.default) this.add_filter(field.fieldname, field.default, field.fieldtype);
if (["Table", "Signature"].includes(field.fieldtype)) {
return;
}
let input = frappe.ui.form.make_control({ let input = frappe.ui.form.make_control({
df: { df: {
fieldtype: field.fieldtype, fieldtype: field.fieldtype,
fieldname: field.fieldname, fieldname: field.fieldname,
options: field.options, options: field.options,
input_class: 'input-xs',
only_select: true, only_select: true,
label: __(field.label), label: __(field.label),
onchange: (event) => { onchange: (event) => {
$('#more').remove();
this.add_filter(field.fieldname, input.value, field.fieldtype); this.add_filter(field.fieldname, input.value, field.fieldtype);
this.refresh(); this.refresh();
} }
}, },
parent: col, parent: filter_area,
value: field.default,
render_input: 1, render_input: 1,
only_input: field.fieldtype == "Check" ? false : true,
}); });
$(input.wrapper)
.addClass('col-md-2')
.attr("title", __(field.label)).tooltip({
delay: { "show": 600, "hide": 100},
trigger: "hover"
});
input.$input.attr("placeholder", __(field.label));
this.filter_input.push(input); this.filter_input.push(input);
}); });
this.refresh(); this.refresh();
@ -73,37 +84,65 @@ export default class WebFormList {
if (!value) { if (!value) {
delete this.filters[field]; delete this.filters[field];
} else { } else {
if (fieldtype === 'Data') value = ['like', value + '%']; if (["Data", "Currency", "Float", "Int"].includes(fieldtype)) {
value = ['like', '%' + value + '%'];
}
Object.assign(this.filters, Object.fromEntries([[field, value]])); Object.assign(this.filters, Object.fromEntries([[field, value]]));
} }
} }
get_list_view_fields() { get_list_view_fields() {
return frappe if (this.columns) return this.columns;
.call({
method: if (this.list_columns) {
"frappe.website.doctype.web_form.web_form.get_in_list_view_fields", this.columns = this.list_columns.map(df => {
args: { doctype: this.doctype } return {
}) label: df.label,
.then(response => (this.fields_list = response.message)); fieldname: df.fieldname,
fieldtype: df.fieldtype
};
});
}
} }
fetch_data() { fetch_data() {
return frappe.call({ let args = {
method: "frappe.www.list.get_list_data", method: "frappe.www.list.get_list_data",
args: { args: {
doctype: this.doctype, doctype: this.doctype,
fields: this.fields_list.map(df => df.fieldname),
limit_start: this.web_list_start, limit_start: this.web_list_start,
limit: this.page_length,
web_form_name: this.web_form_name, web_form_name: this.web_form_name,
...this.filters ...this.filters
} }
}); };
if (this.no_change(args)) {
// console.log('throttled');
return Promise.resolve();
}
return frappe.call(args);
}
no_change(args) {
// returns true if arguments are same for the last 3 seconds
// this helps in throttling if called from various sources
if (this.last_args && JSON.stringify(args) === this.last_args) {
return true;
}
this.last_args = JSON.stringify(args);
setTimeout(() => {
this.last_args = null;
}, 3000);
return false;
} }
async get_data() { async get_data() {
let response = await this.fetch_data(); let response = await this.fetch_data();
this.data = await response.message; if (response) {
this.data = await response.message;
}
} }
more() { more() {
@ -118,159 +157,145 @@ export default class WebFormList {
} }
make_table() { make_table() {
this.columns = this.fields_list.map(df => { this.table = $(`<table class="table"></table>`);
return {
label: df.label, this.make_table_head();
fieldname: df.fieldname, this.make_table_body();
fieldtype: df.fieldtype }
};
make_table_head() {
let $thead = $(`
<thead>
<tr>
<th>
<input type="checkbox" class="select-all">
</th>
<th>${__("Sr")}.</th>
</tr>
</thead>
`);
this.check_all = $thead.find('input.select-all');
this.check_all.on("click", event => {
this.toggle_select_all(event.target.checked);
}); });
if (!this.table) { this.columns.forEach(col => {
this.table = document.createElement("table"); let $tr = $thead.find("tr");
this.table.classList.add("table"); let $th = $(`<th>${__(col.label)}</th>`);
this.make_table_head(); $th.appendTo($tr);
} });
$thead.appendTo(this.table);
}
make_table_body() {
if (this.data.length) { if (this.data.length) {
this.wrapper.empty();
if (this.table) {
this.table.find('tbody').remove();
if (this.check_all.length) {
this.check_all.prop("checked", false);
}
}
this.append_rows(this.data); this.append_rows(this.data);
this.wrapper.appendChild(this.table); this.table.appendTo(this.wrapper);
} else { } else {
let new_button = ""; if (this.wrapper.find('.no-result').length) return;
let empty_state = document.createElement("div");
empty_state.classList.add("no-result", "text-muted", "flex", "justify-center", "align-center");
this.wrapper.empty();
frappe.has_permission(this.doctype, "", "create", () => { frappe.has_permission(this.doctype, "", "create", () => {
new_button = ` this.setup_empty_state();
<a
class="btn btn-primary btn-sm btn-new-doc hidden-xs"
href="${window.location.pathname}?new=1">
${__("Create a new {0}", [__(this.doctype)])}
</a>
`;
empty_state.innerHTML = `
<div class="text-center">
<div>
<img
src="/assets/frappe/images/ui-states/list-empty-state.svg"
alt="Generic Empty State"
class="null-state">
</div>
<p class="small mb-2">${__("No {0} found", [__(this.doctype)])}</p>
${new_button}
</div>
`;
this.wrapper.appendChild(empty_state);
}); });
} }
} }
make_table_head() { setup_empty_state() {
// Create Heading let new_button = `
let thead = this.table.createTHead(); <a
let row = thead.insertRow(); class="btn btn-primary btn-sm btn-new-doc hidden-xs"
href="${location.pathname.replace('/list', '')}/new">
${__("Create a new {0}", [__(this.doctype)])}
</a>
`;
let th = document.createElement("th"); let empty_state = $(`
<div class="no-result text-muted flex justify-center align-center">
<div class="text-center">
<div>
<img
src="/assets/frappe/images/ui-states/list-empty-state.svg"
alt="Generic Empty State"
class="null-state">
</div>
<p class="small mb-2">${__("No {0} found", [__(this.doctype)])}</p>
${new_button}
</div>
</div>
`);
let checkbox = document.createElement("input"); empty_state.appendTo(this.wrapper);
checkbox.type = "checkbox";
checkbox.id = "select-all";
checkbox.onclick = event =>
this.toggle_select_all(event.target.checked);
th.appendChild(checkbox);
row.appendChild(th);
add_heading(row, __("Sr"));
this.columns.forEach(col => {
add_heading(row, __(col.label));
});
function add_heading(row, label) {
let th = document.createElement("th");
th.innerText = label;
row.appendChild(th);
}
} }
append_rows(row_data) { append_rows(row_data) {
const tbody = this.table.childNodes[1] || this.table.createTBody(); let $tbody = this.table.find('tbody');
if (!$tbody.length) {
$tbody = $(`<tbody></tbody>`);
$tbody.appendTo(this.table);
}
row_data.forEach((data_item) => { row_data.forEach((data_item) => {
let row_element = tbody.insertRow(); let $row_element = $(`<tr id="${data_item.name}"></tr>`);
row_element.setAttribute("id", data_item.name);
let row = new frappe.ui.WebFormListRow({ let row = new frappe.ui.WebFormListRow({
row: row_element, row: $row_element,
doc: data_item, doc: data_item,
columns: this.columns, columns: this.columns,
serial_number: this.rows.length + 1, serial_number: this.rows.length + 1,
events: { events: {
onEdit: () => this.open_form(data_item.name), on_edit: () => this.open_form(data_item.name),
onSelect: () => this.toggle_delete() on_select: () => {
this.toggle_new();
this.toggle_delete();
}
} }
}); });
this.rows.push(row); this.rows.push(row);
$row_element.appendTo($tbody);
}); });
} }
make_actions() { make_actions() {
const actions = document.querySelector(".list-view-actions"); const actions = $(".web-list-actions");
frappe.has_permission(this.doctype, "", "delete", () => { frappe.has_permission(this.doctype, "", "delete", () => {
this.addButton(actions, "delete-rows", "danger", true, "Delete", () => this.add_button(actions, "delete-rows", "danger", true, "Delete", () => this.delete_rows());
this.delete_rows()
);
}); });
this.addButton(
actions,
"new",
"primary",
false,
"New",
() => (window.location.href = window.location.pathname + "?new=1")
);
} }
addButton(wrapper, id, type, hidden, name, action) { add_button(wrapper, name, type, hidden, text, action) {
if (document.getElementById(id)) return; if ($(`.${name}`).length) return;
const button = document.createElement("button");
if (type == "secondary") {
button.classList.add(
"btn",
"btn-secondary",
"btn-sm",
"ml-2"
);
}
else if (type == "danger") {
button.classList.add(
"btn",
"btn-danger",
"button-delete",
"btn-sm",
"ml-2"
);
}
else {
button.classList.add("btn", "btn-primary", "btn-sm", "ml-2");
}
button.id = id; hidden = hidden ? "hide" : "";
button.innerText = name; type = type == "danger" ? "danger button-delete" : type;
button.hidden = hidden;
button.onclick = action; let button = $(`
wrapper.appendChild(button); <button class="${name} btn btn-${type} btn-sm ml-2 ${hidden}">${text}</button>
`);
button.on("click", () => action());
button.appendTo(wrapper);
} }
create_more() { create_more() {
if (this.rows.length >= this.page_length) { if (this.rows.length >= this.page_length) {
const footer = document.querySelector(".list-view-footer"); const footer = $(".web-list-footer");
this.addButton(footer, "more", "secondary", false, "More", () => this.more()); this.add_button(footer, "more", "secondary", false, "Load More", () => this.more());
} }
} }
@ -279,7 +304,12 @@ export default class WebFormList {
} }
open_form(name) { open_form(name) {
window.location.href = window.location.pathname + "?name=" + name; let path = window.location.pathname;
if (path.includes('/list')) {
path = path.replace('/list', '');
}
window.location.href = path + "/" + name;
} }
get_selected() { get_selected() {
@ -287,9 +317,15 @@ export default class WebFormList {
} }
toggle_delete() { toggle_delete() {
if (!this.settings.allow_delete) return if (!this.settings.allow_delete) return;
let btn = document.getElementById("delete-rows"); let btn = $(".delete-rows");
btn.hidden = !this.get_selected().length; !this.get_selected().length ? btn.addClass('hide') : btn.removeClass('hide');
}
toggle_new() {
if (!this.settings.allow_delete) return;
let btn = $(".button-new");
this.get_selected().length ? btn.addClass('hide') : btn.removeClass('hide');
} }
delete_rows() { delete_rows() {
@ -305,8 +341,9 @@ export default class WebFormList {
} }
}) })
.then(() => { .then(() => {
this.refresh() this.refresh();
this.toggle_delete() this.toggle_delete();
this.toggle_new();
}); });
} }
}; };
@ -319,40 +356,37 @@ frappe.ui.WebFormListRow = class WebFormListRow {
make_row() { make_row() {
// Add Checkboxes // Add Checkboxes
let cell = this.row.insertCell(); let $cell = $(`<td class="list-col-checkbox"></td>`);
cell.classList.add('list-col-checkbox');
this.checkbox = document.createElement("input"); this.checkbox = $(`<input type="checkbox">`);
this.checkbox.type = "checkbox"; this.checkbox.on("click", event => {
this.checkbox.onclick = event => {
this.toggle_select(event.target.checked); this.toggle_select(event.target.checked);
event.stopImmediatePropagation(); event.stopImmediatePropagation();
} });
this.checkbox.appendTo($cell);
cell.appendChild(this.checkbox); $cell.appendTo(this.row);
// Add Serial Number // Add Serial Number
let serialNo = this.row.insertCell(); let serialNo = $(`<td class="list-col-serial">${__(this.serial_number)}</td>`);
serialNo.classList.add('list-col-serial'); serialNo.appendTo(this.row);
serialNo.innerText = this.serial_number;
this.columns.forEach(field => { this.columns.forEach(field => {
let cell = this.row.insertCell();
let formatter = frappe.form.get_formatter(field.fieldtype); let formatter = frappe.form.get_formatter(field.fieldtype);
cell.innerHTML = this.doc[field.fieldname] && let value = this.doc[field.fieldname] &&
__(formatter(this.doc[field.fieldname], field, {only_value: 1}, this.doc)) || ""; __(formatter(this.doc[field.fieldname], field, {only_value: 1}, this.doc)) || "";
let cell = $(`<td>${value}</td>`);
cell.appendTo(this.row);
}); });
this.row.onclick = () => this.events.onEdit(); this.row.on("click", () => this.events.on_edit());
this.row.style.cursor = "pointer";
} }
toggle_select(checked) { toggle_select(checked) {
this.checkbox.checked = checked; this.checkbox.prop("checked", checked);
this.events.onSelect(checked); this.events.on_select(checked);
} }
is_selected() { is_selected() {
return this.checkbox.checked; return this.checkbox.prop("checked");
} }
}; };

View file

@ -2,23 +2,15 @@ import WebFormList from './web_form_list'
import WebForm from './web_form' import WebForm from './web_form'
frappe.ready(function() { frappe.ready(function() {
let query_params = frappe.utils.get_query_params(); let web_form_doc = frappe.web_form_doc;
let wrapper = $(".web-form-wrapper"); let reference_doc = frappe.reference_doc;
let is_list = parseInt(wrapper.data('is-list')) || query_params.is_list;
let webform_doctype = wrapper.data('web-form-doctype');
let webform_name = wrapper.data('web-form');
let login_required = parseInt(wrapper.data('login-required'));
let allow_delete = parseInt(wrapper.data('allow-delete'));
let doc_name = query_params.name || '';
let is_new = query_params.new;
if (login_required) show_login_prompt(); show_login_prompt();
else if (is_list) show_grid();
else show_form(webform_doctype, webform_name, is_new);
document.querySelector("body").style.display = "block"; web_form_doc.is_list ? show_list() : show_form();
function show_login_prompt() { function show_login_prompt() {
if (frappe.session.user != "Guest" || !web_form_doc.login_required) return;
const login_required = new frappe.ui.Dialog({ const login_required = new frappe.ui.Dialog({
title: __("Not Permitted"), title: __("Not Permitted"),
primary_action_label: __("Login"), primary_action_label: __("Login"),
@ -30,102 +22,79 @@ frappe.ready(function() {
login_required.set_message(__("You are not permitted to access this page without login.")); login_required.set_message(__("You are not permitted to access this page without login."));
} }
function show_grid() { function show_list() {
new WebFormList({ new WebFormList({
parent: wrapper, doctype: web_form_doc.doc_type,
doctype: webform_doctype, web_form_name: web_form_doc.name,
web_form_name: webform_name, list_columns: web_form_doc.list_columns,
settings: { settings: {
allow_delete allow_delete: web_form_doc.allow_delete
} }
}); });
} }
function show_form() { function show_form() {
let web_form = new WebForm({ let web_form = new WebForm({
parent: wrapper, parent: $(".web-form-wrapper"),
is_new, is_new: web_form_doc.is_new,
web_form_name: webform_name, 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);
get_data().then(r => { web_form.prepare(web_form_doc, doc);
const data = setup_fields(r.message); web_form.make();
let web_form_doc = data.web_form;
// if (web_form_doc.name && web_form_doc.allow_edit === 0) { if (web_form_doc.is_new) {
// if (!window.location.href.includes("?new=1")) {
// window.location.replace(window.location.pathname + "?new=1");
// }
// }
let doc = r.message.doc || build_doc(r.message);
web_form.prepare(web_form_doc, r.message.doc && web_form_doc.allow_edit === 1 ? r.message.doc : {});
web_form.make();
web_form.set_default_values(); web_form.set_default_values();
})
function build_doc(form_data) {
let doc = {};
form_data.web_form.web_form_fields.forEach(df => {
if (df.default) return doc[df.fieldname] = df.default;
});
return doc;
} }
function get_data() { $(".file-size").each(function () {
return frappe.call({ $(this).text(frappe.form.formatters.FileSize($(this).text()));
method: "frappe.website.doctype.web_form.web_form.get_form_data", });
args: { }
doctype: webform_doctype,
docname: doc_name,
web_form_name: webform_name
},
freeze: true
});
}
function setup_fields(form_data) { function setup_fields(web_form_doc, doc_data) {
form_data.web_form.web_form_fields.map(df => { web_form_doc.web_form_fields.forEach(df => {
df.is_web_form = true; df.is_web_form = true;
if (df.fieldtype === "Table") { df.read_only = !web_form_doc.is_new && !web_form_doc.is_form_editable;
df.get_data = () => { if (df.fieldtype === "Table") {
let data = []; df.get_data = () => {
if (form_data.doc) { let data = [];
data = form_data.doc[df.fieldname]; if (doc_data && doc_data[df.fieldname]) {
} return doc_data[df.fieldname];
return data;
};
df.fields = form_data[df.fieldname];
$.each(df.fields || [], function(_i, field) {
if (field.fieldtype === "Link") {
field.only_select = true;
}
field.is_web_form = true;
});
if (df.fieldtype === "Attach") {
df.is_private = true;
} }
return data;
};
delete df.parent; $.each(df.fields || [], function(_i, field) {
delete df.parentfield; if (field.fieldtype === "Link") {
delete df.parenttype; field.only_select = true;
delete df.doctype;
return df;
}
if (df.fieldtype === "Link") {
df.only_select = true;
}
if (["Attach", "Attach Image"].includes(df.fieldtype)) {
if (typeof df.options !== "object") {
df.options = {};
} }
df.options.disable_file_browser = true; field.is_web_form = true;
} });
});
return form_data; if (df.fieldtype === "Attach") {
} df.is_private = true;
}
delete df.parent;
delete df.parentfield;
delete df.parenttype;
delete df.doctype;
return df;
}
if (df.fieldtype === "Link") {
df.only_select = true;
}
if (["Attach", "Attach Image"].includes(df.fieldtype)) {
if (typeof df.options !== "object") {
df.options = {};
}
df.options.disable_file_browser = true;
}
});
} }
}); });

View file

@ -76,6 +76,10 @@ a.badge-hover {
text-decoration: underline; text-decoration: underline;
} }
.pointer {
cursor: pointer;
}
.inline-block { .inline-block {
display: inline-block; display: inline-block;
} }

View file

@ -5,37 +5,212 @@
max-width: 800px; max-width: 800px;
margin: auto; margin: auto;
.frappe-card { h1 {
padding: 1rem; font-size: 1.9rem;
margin-top: 0;
margin-bottom: 0;
}
h1 { .web-form-container {
font-size: 1.9rem; border: 1px solid var(--dark-border-color);
margin-top: 0; border-radius: var(--border-radius-md);
margin-bottom: 0; padding: 2rem;
}
.web-form-head { .web-form-header {
margin: 0 -1rem; display: flex;
padding: 0 1rem 1rem 1rem; justify-content: space-between;
margin-bottom: 1rem; margin: 0 -2rem 1rem;
padding: 0 2rem 1rem;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
.web-form-actions {
align-self: center;
}
} }
#introduction { .web-form-introduction {
margin-bottom: 2rem;
}
#introduction p {
color: var(--text-muted); color: var(--text-muted);
margin-bottom: 2rem;
p {
color: var(--text-muted);
}
} }
.web-form-actions button { .web-form-wrapper {
margin-top: 0.1rem; .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-md);
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
@include media-breakpoint-down(sm) {
padding: 0;
}
}
}
.web-form-footer {
text-align: right;
}
.attachments {
margin: 1rem -2rem 0;
padding: 1rem 2rem 0;
border-top: 1px solid var(--border-color);
.attachment {
display: flex;
justify-content: space-between;
gap: 6px;
max-width: 300px;
color: var(--text-muted);
font-size: var(--text-md);
&:hover {
text-decoration: none;
.file-name span {
text-decoration: underline;
}
}
}
} }
} }
.frappe-card.list-card { .web-list-container {
min-height: 400px; 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;
.web-list-actions {
align-self: center;
}
}
.web-list-filters {
display: flex;
flex-wrap: wrap;
margin: 1rem -2rem 0;
padding: 1rem 2rem 0;
border-top: 1px solid var(--border-color);
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;
margin: 1rem -2rem 0;
.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);
&:first-child {
padding-left: 1.5rem;
}
&:last-child {
padding-right: 1.5rem;
}
input[type="checkbox"] {
margin-bottom: -2px;
}
}
}
tbody tr {
color: var(--text-color);
cursor: pointer;
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;
}
.list-col-checkbox {
width: 1rem;
}
.list-col-serial {
width: 1.5rem;
}
}
.no-result {
min-height: 330px;
border-top: 1px solid var(--border-color);
}
}
.web-list-footer {
text-align: right;
}
} }
.breadcrumb-container.container { .breadcrumb-container.container {
@ -45,76 +220,3 @@
} }
} }
} }
.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-md);
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
@include media-breakpoint-down(sm) {
padding: 0;
}
}
}
.list-table {
margin-left: -1rem;
margin-right: -1rem;
.table {
thead {
th {
border: 0;
font-size: 13px;
font-weight: normal;
color: var(--text-muted);
input[type="checkbox"] {
margin-bottom: -2px;
}
}
}
tr {
color: var(--text-color);
td {
font-size: 13px;
border-top: 1px solid var(--border-color);
}
}
input[type="checkbox"] {
margin-left: 0.5rem;
margin-top: 2px;
}
.list-col-checkbox {
width: 1rem;
}
.list-col-serial {
width: 1.5rem;
}
}
}

View file

@ -96,12 +96,7 @@
{% block base_scripts %} {% block base_scripts %}
<!-- js should be loaded in body! --> <!-- js should be loaded in body! -->
<script> <script>
frappe.boot = { frappe.boot = {{ boot }}
sysdefaults: {
float_precision: parseInt("{{ frappe.get_system_settings('float_precision') or 3 }}"),
date_format: "{{ frappe.get_system_settings('date_format') or 'yyyy-mm-dd' }}",
}
};
// for backward compatibility of some libs // for backward compatibility of some libs
frappe.sys_defaults = frappe.boot.sysdefaults; frappe.sys_defaults = frappe.boot.sysdefaults;
</script> </script>

View file

@ -8,17 +8,17 @@ from frappe.www.list import get_list_context
class TestWebform(unittest.TestCase): class TestWebform(unittest.TestCase):
def test_webform_publish_functionality(self): def test_webform_publish_functionality(self):
edit_profile = frappe.get_doc("Web Form", "edit-profile") request_data = frappe.get_doc("Web Form", "request-data")
# publish webform # publish webform
edit_profile.published = True request_data.published = True
edit_profile.save() request_data.save()
set_request(method="GET", path="update-profile") set_request(method="GET", path="request-data/new")
response = get_response() response = get_response()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# un-publish webform # un-publish webform
edit_profile.published = False request_data.published = False
edit_profile.save() request_data.save()
response = get_response() response = get_response()
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)

View file

@ -1951,6 +1951,15 @@ def generate_hash(*args, **kwargs) -> str:
return frappe.generate_hash(*args, **kwargs) return frappe.generate_hash(*args, **kwargs)
def dict_with_keys(dict, keys):
"""Returns a new dict with a subset of keys"""
out = {}
for key in dict:
if key in keys:
out[key] = dict[key]
return out
def guess_date_format(date_string: str) -> str: def guess_date_format(date_string: str) -> str:
DATE_FORMATS = [ DATE_FORMATS = [
r"%d/%b/%y", r"%d/%b/%y",

View file

@ -1,149 +1,129 @@
{% extends "templates/web.html" %} {% extends "templates/web.html" %}
{% block title %}{{ _(title) }}{% endblock %}
{% block breadcrumbs %}{% endblock %} {% block breadcrumbs %}{% endblock %}
{% macro container_attributes() %} {% macro action_buttons() %}
data-web-form="{{ name }}" data-web-form-doctype="{{ doc_type }}" data-login-required="{{ frappe.utils.cint(login_required and frappe.session.user=='Guest') }}" data-is-list="{{ frappe.utils.cint(is_list) }}" data-allow-delete="{{ allow_delete }}" {% 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="btn btn-light btn-sm ml-2">
<svg class="icon icon-sm"><use href="#icon-printer"></use></svg>
</a>
{% endif %}
{% if allow_edit and doc_name and not is_form_editable %}
<!-- edit button -->
<a href="/{{ route }}/{{ doc_name }}/edit" class="btn btn-primary btn-sm ml-2">{{ _("Edit", null, "Button in web form") }}</a>
{% endif %}
{% if is_new or is_form_editable %}
<!-- cancel button -->
<a href="/{{ route }}/{% if doc_name %}{{ doc_name }}{% endif %}" class="btn btn-light btn-sm ml-2">{{ _("Cancel", null, "Button in web form") }}</a>
<!-- submit button -->
<button type="submit" class="btn btn-primary btn-sm ml-2">{{ button_label or _("Save", null, "Button in web form") }}</button>
{% endif %}
{% endmacro %} {% endmacro %}
{% block page_content %} {% block page_content %}
{% if has_header and login_required and allow_multiple %} <!-- breadcrumb -->
<!-- breadcrumb --> {% if has_header and login_required and show_list %}
{% include "templates/includes/breadcrumbs.html" %} {% include "templates/includes/breadcrumbs.html" %}
{% else %} {% else %}
<div style="height: 3rem"></div> <div style="height: 3rem"></div>
{% endif %}
<!-- main card -->
<div class="frappe-card {{ frappe.utils.cint(is_list) and 'list-card' or '' }}">
{% if is_list %}
<!-- list -->
<div class="d-flex justify-content-between">
<h3 class="mt-0">{{ _(title) }}</h3>
<div class="list-view-actions"></div>
</div>
<div class="web-form-wrapper" {{ container_attributes() }}></div>
<div id="list-filters" class="mt-4 row"></div>
<div id="list-table" class="pt-4 overflow-auto list-table"></div>
<div class="text-right list-view-footer"></div>
{% else %}
<!-- web form -->
<div class="d-flex justify-content-between web-form-head">
<h1>{{ _(title) }}</h1>
<div class="web-form-actions"></div>
</div>
<div role="form">
<div id="introduction" class="hidden text-muted"></div>
<div class="web-form-wrapper" {{ container_attributes() }}></div>
<div class="text-right web-form-footer"></div>
</div>
{% if show_attachments and not frappe.form_dict.new and attachments %}
<div class="attachments">
<h5>{{ _("Attachments") }}</h5>
{% for attachment in attachments %}
<div class="attachment">
<a class="no-underline attachment-link" href="{{ attachment.file_url }}" target="blank">
<div class="row">
<div class="col-9">
<span class="file-name">{{ attachment.file_name }}</span>
</div>
<div class="col-3">
<span class="pull-right file-size">{{ attachment.file_size }}</span>
</div>
</div>
</a>
</div>
{% endfor %}
</div>
{% endif %} {# attachments #}
{% endif %} {% endif %}
</div>
{% if allow_comments and not frappe.form_dict.new and not is_list -%} <!-- main card -->
<!-- comments --> <form role="form" class="web-form-container">
<div class="comments" style="margin-top: 3rem;"> <div class="web-form-header">
{% include 'templates/includes/comments/comments.html' %} <h1>{{ _(title) }}</h1>
</div> <div class="web-form-actions">
{%- else -%} {{ action_buttons() }}
<div style="height: 3rem"></div> </div>
{%- endif %} {# comments #} </div>
<div class="web-form-body">
{% if introduction_text %}
<div class="web-form-introduction">{{ introduction_text }}</div>
{% endif %}
<div class="web-form-wrapper"></div>
<div class="web-form-footer hide">
{{ action_buttons() }}
</div>
</div>
<!-- attachments -->
{% if show_attachments and not is_new and attachments %}
<div class="attachments">
<h5 class="mb-3">{{ _("Attachments") }}</h5>
{% for attachment in attachments %}
<a class="attachment attachment-link" href="{{ attachment.file_url }}" target="blank">
<div class="file-name ellipsis">
<svg class="icon icon-sm"><use href="#icon-attachment"></use></svg>
<span>{{ attachment.file_name }}</span>
</div>
<div class="file-size">{{ attachment.file_size }}</div>
</a>
{% 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 #}
{% endblock page_content %} {% endblock page_content %}
{% block script %} {% block script %}
<script> <script>
frappe.boot = { frappe.boot = {{ boot }};
sysdefaults: { frappe._messages = {{ translated_messages }};
float_precision: parseInt("{{ frappe.get_system_settings('float_precision') or 3 }}"), frappe.web_form_doc = {{ web_form_doc | json }};
date_format: "{{ frappe.get_system_settings('date_format') or 'yyyy-mm-dd' }}", frappe.reference_doc = {{ reference_doc | json }};
}, </script>
time_zone: {
system: "{{ frappe.utils.get_time_zone() }}",
user: "{{ frappe.db.get_value('User', frappe.session.user, 'time_zone') or frappe.utils.get_time_zone() }}"
},
link_title_doctypes: {{ link_title_doctypes }}
};
// for backward compatibility of some libs
frappe.sys_defaults = frappe.boot.sysdefaults;
frappe._messages = {{ translated_messages }};
$(".file-size").each(function() {
$(this).text(frappe.form.formatters.FileSize($(this).text()));
});
</script>
{{ include_script("controls.bundle.js") }}
{% if is_list %} <!-- web form list -->
{{ include_script("dialog.bundle.js") }}
{{ include_script("web_form.bundle.js") }}
{{ include_script("bootstrap-4-web.bundle.js") }}
{% else %} <!-- web form -->
{{ include_script("dialog.bundle.js") }}
<script type="text/javascript" src="/assets/frappe/node_modules/vue/dist/vue.js"></script>
<script>
Vue.prototype.__ = window.__;
Vue.prototype.frappe = window.frappe;
</script>
{{ include_script("web_form.bundle.js") }}
{{ include_script("bootstrap-4-web.bundle.js") }}
<script>
{% if client_script %} <script type="text/javascript" src="/assets/frappe/node_modules/vue/dist/vue.js"></script>
frappe.init_client_script = () => { <script>
try { Vue.prototype.__ = window.__;
{{ client_script }} Vue.prototype.frappe = window.frappe;
} catch(e) { </script>
console.error('Error in web form client script');
console.error(e);
}
}
{% endif %}
{% if script is defined %} {{ include_script("controls.bundle.js") }}
{{ script }} {{ include_script("dialog.bundle.js") }}
{% endif %} {{ include_script("web_form.bundle.js") }}
</script> {{ include_script("bootstrap-4-web.bundle.js") }}
{% endif %}
<script>
{% if client_script %}
frappe.init_client_script = () => {
try {
{{ client_script }}
} catch(e) {
console.error('Error in web form client script');
console.error(e);
}
}
{% endif %}
{% if script is defined %}
{{ script }}
{% endif %}
</script>
{% endblock script %} {% endblock script %}
{% block style %} {% block style %}
{% if not is_list %} <style>
{{ include_style('web_form.bundle.css') }} {% if style is defined %}
{% endif %} {{ style }}
{% endif %}
<style> {% if custom_css %}
body { {{ custom_css }}
background-color: var(--bg-color); {% endif %}
} </style>
{% if style is defined %}
{{ style }}
{% endif %}
{% if custom_css %}
{{ custom_css }}
{% endif %}
</style>
{% endblock %} {% endblock %}

View file

@ -1,4 +0,0 @@
<div>
<a href={{ route }}>{{ title }}</a>
</div>
<!-- this is a sample default list template -->

View file

@ -0,0 +1,45 @@
{% extends "templates/web.html" %}
{% block breadcrumbs %}{% endblock %}
{% block page_content %}
<!-- main card -->
<div class="web-list-container">
<!-- list -->
<div class="web-list-header">
<h1>{{ _(list_title or title) }}</h1>
<div class="web-list-actions">
{%- if allow_multiple -%}
<a class="btn btn-primary btn-sm button-new" href="/{{ route }}/new">New</a>
{%- endif -%}
</div>
</div>
<div class="web-list-filters hide"></div>
<div class="web-list-table"></div>
<div class="web-list-footer"></div>
</div>
{% endblock page_content %}
{% block script %}
<script>
frappe.boot = {{ boot }};
frappe._messages = {{ translated_messages }};
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 %}
<style>
{% if style is defined %}
{{ style }}
{% endif %}
{% if custom_css %}
{{ custom_css }}
{% endif %}
</style>
{% endblock %}

View file

@ -4,6 +4,7 @@ import json
import unittest import unittest
import frappe import frappe
from frappe.utils import set_request
from frappe.website.doctype.web_form.web_form import accept from frappe.website.doctype.web_form.web_form import accept
from frappe.website.serve import get_response_content from frappe.website.serve import get_response_content
@ -68,8 +69,9 @@ class TestWebForm(unittest.TestCase):
) )
def test_webform_render(self): def test_webform_render(self):
content = get_response_content("request-data") set_request(method="GET", path="manage-events/new")
self.assertIn("<h1>Request Data</h1>", content) content = get_response_content("manage-events/new")
self.assertIn("<h1>New Manage Events</h1>", content)
self.assertIn('data-doctype="Web Form"', content) self.assertIn('data-doctype="Web Form"', content)
self.assertIn('data-path="request-data"', content) self.assertIn('data-path="manage-events/new"', content)
self.assertIn('source-type="Generator"', content) self.assertIn('source-type="Generator"', content)

View file

@ -1,89 +1,149 @@
frappe.web_form = {
set_fieldname_select: function(frm) {
return new Promise(resolve => {
var me = this,
doc = frm.doc;
if (doc.doc_type) {
frappe.model.with_doctype(doc.doc_type, function() {
var fields = $.map(frappe.get_doc("DocType", frm.doc.doc_type).fields, function(d) {
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
d.fieldtype === 'Table') {
return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname };
} else {
return null;
}
});
var currency_fields = $.map(frappe.get_doc("DocType", frm.doc.doc_type).fields, function(d) {
if (d.fieldtype === 'Currency' || d.fieldtype === 'Float') {
return { label: d.label, value: d.fieldname };
} else {
return null;
}
});
frm.fields_dict.web_form_fields.grid.update_docfield_property(
'fieldname', 'options', fields
);
frappe.meta.get_docfield("Web Form", "amount_field", frm.doc.name).options = [""].concat(currency_fields);
frm.refresh_field("amount_field");
resolve();
});
}
});
}
};
frappe.ui.form.on("Web Form", { frappe.ui.form.on("Web Form", {
refresh: function(frm) { refresh: function(frm) {
// show is-standard only if developer mode // show is-standard only if developer mode
frm.get_field("is_standard").toggle(frappe.boot.developer_mode); frm.get_field("is_standard").toggle(frappe.boot.developer_mode);
frappe.web_form.set_fieldname_select(frm);
if (frm.doc.is_standard && !frappe.boot.developer_mode) { if (frm.doc.is_standard && !frappe.boot.developer_mode) {
frm.set_read_only(); frm.set_read_only();
frm.disable_save(); frm.disable_save();
} }
render_list_settings_message(frm);
frm.add_custom_button(__('Get Fields'), () => { frm.trigger('set_fields');
let webform_fieldtypes = frappe.meta.get_field('Web Form Field', 'fieldtype').options.split('\n'); frm.trigger('add_get_fields_button');
let fieldnames = (frm.doc.web_form_fields || []).map(d => d.fieldname); frm.trigger('add_publish_button');
frappe.model.with_doctype(frm.doc.doc_type, () => { },
let meta = frappe.get_meta(frm.doc.doc_type);
for (let field of meta.fields) { login_required: function(frm) {
if (webform_fieldtypes.includes(field.fieldtype) render_list_settings_message(frm);
&& !fieldnames.includes(field.fieldname)) { },
frm.add_child('web_form_fields', {
fieldname: field.fieldname, validate: function(frm) {
label: field.label, if (!frm.doc.login_required) {
fieldtype: field.fieldtype, frm.set_value("allow_multiple", 0);
options: field.options, frm.set_value("allow_edit", 0);
reqd: field.reqd, frm.set_value("show_list", 0);
default: field.default, }
read_only: field.read_only || field.is_virtual,
depends_on: field.depends_on, !frm.doc.allow_multiple && frm.set_value("allow_delete", 0);
mandatory_depends_on: field.mandatory_depends_on, frm.doc.allow_multiple && frm.set_value("show_list", 1);
read_only_depends_on: field.read_only_depends_on,
hidden: field.hidden, if (!frm.doc.web_form_fields) {
description: field.description frm.scroll_to_field('web_form_fields');
frappe.throw(__("Atleast one field is required in Web Form Fields Table"));
}
},
add_publish_button(frm) {
frm.add_custom_button(frm.doc.published ? __("Unpublish") : __("Publish"), () => {
frm.set_value("published", !frm.doc.published);
frm.save();
});
},
add_get_fields_button(frm) {
frm.add_custom_button(__("Get Fields"), () => {
let webform_fieldtypes = frappe.meta
.get_field("Web Form Field", "fieldtype")
.options.split("\n");
let added_fields = (frm.doc.fields || []).map(d => d.fieldname);
get_fields_for_doctype(frm.doc.doc_type).then(fields => {
for (let df of fields) {
if (
webform_fieldtypes.includes(df.fieldtype) &&
!added_fields.includes(df.fieldname) &&
!df.hidden
) {
frm.add_child("web_form_fields", {
fieldname: df.fieldname,
label: df.label,
fieldtype: df.fieldtype,
options: df.options,
reqd: df.reqd,
default: df.default,
read_only: df.read_only,
depends_on: df.depends_on,
mandatory_depends_on: df.mandatory_depends_on,
read_only_depends_on: df.read_only_depends_on,
}); });
} }
} }
frm.refresh(); frm.refresh_field('web_form_fields');
frm.scroll_to_field('web_form_fields');
}); });
}); });
}, },
set_fields(frm) {
let doc = frm.doc;
let update_options = options => {
[
frm.fields_dict.web_form_fields.grid,
frm.fields_dict.list_columns.grid
].forEach(obj => {
obj.update_docfield_property("fieldname", "options", options);
});
};
if (!doc.doc_type) {
update_options([]);
frm.set_df_property("amount_field", "options", []);
return;
}
update_options([`Fetching fields from ${doc.doc_type}...`]);
get_fields_for_doctype(doc.doc_type).then(fields => {
let as_select_option = df => ({
label: df.label + " (" + df.fieldtype + ")",
value: df.fieldname
});
update_options(fields.map(as_select_option));
let currency_fields = fields
.filter(df => ["Currency", "Float"].includes(df.fieldtype))
.map(as_select_option);
if (!currency_fields.length) {
currency_fields = [
{
label: `No currency fields in ${doc.doc_type}`,
value: "",
disabled: true
}
];
}
frm.set_df_property("amount_field", "options", currency_fields);
});
},
title: function(frm) { title: function(frm) {
if (frm.doc.__islocal) { if (frm.doc.__islocal) {
var page_name = frm.doc.title.toLowerCase().replace(/ /g, "-"); var page_name = frm.doc.title.toLowerCase().replace(/ /g, "-");
frm.set_value("route", page_name); frm.set_value("route", page_name);
frm.set_value("success_url", "/" + page_name);
} }
}, },
doc_type: function(frm) { doc_type: function(frm) {
frappe.web_form.set_fieldname_select(frm); frm.trigger('set_fields');
},
allow_multiple: function(frm) {
frm.doc.allow_multiple && frm.set_value("show_list", 1);
}
});
frappe.ui.form.on("Web Form List Column", {
fieldname: function(frm, doctype, name) {
let doc = frappe.get_doc(doctype, name);
let df = frappe.meta.get_docfield(frm.doc.doc_type, doc.fieldname);
if (!df) return;
doc.fieldtype = df.fieldtype;
doc.label = df.label;
frm.refresh_field("list_columns");
} }
}); });
@ -93,22 +153,61 @@ frappe.ui.form.on("Web Form Field", {
var doc = frappe.get_doc(doctype, name); var doc = frappe.get_doc(doctype, name);
if (['Section Break', 'Column Break', 'Page Break'].includes(doc.fieldtype)) { if (['Section Break', 'Column Break', 'Page Break'].includes(doc.fieldtype)) {
doc.fieldname = ''; doc.fieldname = '';
doc.options = "";
frm.refresh_field("web_form_fields"); frm.refresh_field("web_form_fields");
} }
}, },
fieldname: function(frm, doctype, name) { fieldname: function(frm, doctype, name) {
var doc = frappe.get_doc(doctype, name); let doc = frappe.get_doc(doctype, name);
var df = $.map(frappe.get_doc("DocType", frm.doc.doc_type).fields, function(d) { let df = frappe.meta.get_docfield(frm.doc.doc_type, doc.fieldname);
return doc.fieldname == d.fieldname ? d : null; if (!df) return;
})[0];
doc.label = df.label; doc.label = df.label;
doc.reqd = df.reqd; doc.fieldtype = df.fieldtype;
doc.options = df.options; doc.options = df.options;
doc.fieldtype = frappe.meta.get_docfield("Web Form Field", "fieldtype") doc.reqd = df.reqd;
.options.split("\n").indexOf(df.fieldtype) === -1 ? "Data" : df.fieldtype; doc.default = df.default;
doc.description = df.description; doc.read_only = df.read_only;
doc["default"] = df["default"]; doc.depends_on = df.depends_on;
doc.mandatory_depends_on = df.mandatory_depends_on;
doc.read_only_depends_on = df.read_only_depends_on;
frm.refresh_field("web_form_fields");
} }
}); });
function get_fields_for_doctype(doctype) {
return new Promise(resolve =>
frappe.model.with_doctype(doctype, resolve)
).then(() => {
return frappe.meta.get_docfields(doctype).filter(df => {
return (
(frappe.model.is_value_type(df.fieldtype) &&
!["lft", "rgt"].includes(df.fieldname)) ||
["Table", "Table Multiselect"].includes(df.fieldtype)
);
});
});
}
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>
`;
$(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')
.click(() => frm.scroll_to_field('login_required'));
} else {
$(frm.fields_dict['list_setting_message'].wrapper).empty();
}
}

View file

@ -5,43 +5,51 @@
"document_type": "Document", "document_type": "Document",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title_and_route_tab",
"title", "title",
"route", "route",
"published",
"column_break_4",
"doc_type", "doc_type",
"module", "module",
"column_break_4",
"is_standard", "is_standard",
"is_multi_step_form", "introduction",
"published", "introduction_text",
"form_settings_tab",
"login_required", "login_required",
"route_to_success_link", "is_multi_step_form",
"allow_edit",
"allow_multiple", "allow_multiple",
"apply_document_permissions", "allow_edit",
"show_in_grid",
"allow_delete", "allow_delete",
"column_break_18",
"apply_document_permissions",
"allow_print", "allow_print",
"print_format", "print_format",
"allow_comments", "allow_comments",
"show_attachments", "show_attachments",
"allow_incomplete", "allow_incomplete",
"introduction", "form_fields",
"introduction_text",
"fields",
"web_form_fields", "web_form_fields",
"max_attachment_size", "max_attachment_size",
"client_script_section",
"client_script",
"custom_css_section",
"custom_css",
"actions", "actions",
"breadcrumbs",
"button_label", "button_label",
"column_break_29",
"success_message", "success_message",
"route_to_success_link",
"success_url", "success_url",
"sidebar_settings", "list_settings_tab",
"list_setting_message",
"show_list",
"list_title",
"list_columns",
"sidebar_settings_tab",
"show_sidebar", "show_sidebar",
"sidebar_items", "website_sidebar",
"payments", "scripting_style_tab",
"client_script",
"custom_css",
"payments_tab",
"accept_payment", "accept_payment",
"payment_gateway", "payment_gateway",
"payment_button_label", "payment_button_label",
@ -50,10 +58,7 @@
"amount_based_on_field", "amount_based_on_field",
"amount_field", "amount_field",
"amount", "amount",
"currency", "currency"
"advanced",
"web_page_link_text",
"breadcrumbs"
], ],
"fields": [ "fields": [
{ {
@ -118,25 +123,18 @@
"depends_on": "login_required", "depends_on": "login_required",
"fieldname": "allow_edit", "fieldname": "allow_edit",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Edit" "label": "Allow Editing After Submit"
}, },
{ {
"default": "0", "default": "0",
"depends_on": "login_required", "depends_on": "login_required",
"fieldname": "allow_multiple", "fieldname": "allow_multiple",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Multiple" "label": "Allow Multiple Responses"
}, },
{ {
"default": "0", "default": "0",
"depends_on": "allow_multiple", "depends_on": "eval: doc.allow_multiple && doc.login_required",
"fieldname": "show_in_grid",
"fieldtype": "Check",
"label": "Show as Grid"
},
{
"default": "0",
"depends_on": "allow_multiple",
"fieldname": "allow_delete", "fieldname": "allow_delete",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Delete" "label": "Allow Delete"
@ -187,11 +185,6 @@
"ignore_xss_filter": 1, "ignore_xss_filter": 1,
"label": "Introduction" "label": "Introduction"
}, },
{
"fieldname": "fields",
"fieldtype": "Section Break",
"label": "Fields"
},
{ {
"fieldname": "web_form_fields", "fieldname": "web_form_fields",
"fieldtype": "Table", "fieldtype": "Table",
@ -203,13 +196,6 @@
"fieldtype": "Int", "fieldtype": "Int",
"label": "Max Attachment Size (in MB)" "label": "Max Attachment Size (in MB)"
}, },
{
"collapsible": 1,
"collapsible_depends_on": "client_script",
"fieldname": "client_script_section",
"fieldtype": "Section Break",
"label": "Client Script"
},
{ {
"description": "For help see <a href=\"https://frappeframework.com/docs/user/en/guides/portal-development/web-forms\" target=\"_blank\">Client Script API and Examples</a>", "description": "For help see <a href=\"https://frappeframework.com/docs/user/en/guides/portal-development/web-forms\" target=\"_blank\">Client Script API and Examples</a>",
"fieldname": "client_script", "fieldname": "client_script",
@ -220,13 +206,13 @@
"collapsible": 1, "collapsible": 1,
"fieldname": "actions", "fieldname": "actions",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Actions" "label": "Customization"
}, },
{ {
"default": "Save", "default": "Save",
"fieldname": "button_label", "fieldname": "button_label",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Button Label" "label": "Submit Button Label"
}, },
{ {
"description": "Message to be displayed on successful completion (only for Guest users)", "description": "Message to be displayed on successful completion (only for Guest users)",
@ -235,36 +221,18 @@
"label": "Success Message" "label": "Success Message"
}, },
{ {
"depends_on": "route_to_success_link",
"description": "Go to this URL after completing the form", "description": "Go to this URL after completing the form",
"fieldname": "success_url", "fieldname": "success_url",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Success URL" "label": "Success URL"
}, },
{
"collapsible": 1,
"fieldname": "sidebar_settings",
"fieldtype": "Section Break",
"label": "Sidebar Settings"
},
{ {
"default": "0", "default": "0",
"fieldname": "show_sidebar", "fieldname": "show_sidebar",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show Sidebar" "label": "Show Sidebar"
}, },
{
"fieldname": "sidebar_items",
"fieldtype": "Table",
"label": "Sidebar Items",
"options": "Portal Menu Item"
},
{
"collapsible": 1,
"collapsible_depends_on": "accept_payment",
"fieldname": "payments",
"fieldtype": "Section Break",
"label": "Payments"
},
{ {
"default": "0", "default": "0",
"fieldname": "accept_payment", "fieldname": "accept_payment",
@ -321,18 +289,6 @@
"label": "Currency", "label": "Currency",
"options": "Currency" "options": "Currency"
}, },
{
"collapsible": 1,
"fieldname": "advanced",
"fieldtype": "Section Break",
"label": "Advanced"
},
{
"description": "Text to be displayed for Link to Web Page if this form has a web page. Link route will be automatically generated based on `page_name` and `parent_website_route`",
"fieldname": "web_page_link_text",
"fieldtype": "Data",
"label": "Web Page Link Text"
},
{ {
"description": "List as [{\"label\": _(\"Jobs\"), \"route\":\"jobs\"}]", "description": "List as [{\"label\": _(\"Jobs\"), \"route\":\"jobs\"}]",
"fieldname": "breadcrumbs", "fieldname": "breadcrumbs",
@ -345,13 +301,6 @@
"label": "Custom CSS", "label": "Custom CSS",
"options": "CSS" "options": "CSS"
}, },
{
"collapsible": 1,
"collapsible_depends_on": "custom_css",
"fieldname": "custom_css_section",
"fieldtype": "Section Break",
"label": "Custom CSS"
},
{ {
"default": "0", "default": "0",
"fieldname": "apply_document_permissions", "fieldname": "apply_document_permissions",
@ -363,13 +312,93 @@
"fieldname": "is_multi_step_form", "fieldname": "is_multi_step_form",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Multi Step Form" "label": "Is Multi Step Form"
},
{
"default": "0",
"depends_on": "login_required",
"fieldname": "show_list",
"fieldtype": "Check",
"label": "Show List"
},
{
"depends_on": "eval: doc.login_required && doc.show_list",
"fieldname": "list_title",
"fieldtype": "Data",
"label": "Title"
},
{
"depends_on": "eval: doc.login_required && doc.show_list",
"fieldname": "list_columns",
"fieldtype": "Table",
"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",
"label": "Website Sidebar",
"options": "Website Sidebar"
},
{
"fieldname": "column_break_29",
"fieldtype": "Column Break"
},
{
"fieldname": "list_setting_message",
"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"
},
{
"collapsible": 1,
"collapsible_depends_on": "accept_payment",
"fieldname": "payments_tab",
"fieldtype": "Tab Break",
"label": "Payments"
} }
], ],
"has_web_view": 1, "has_web_view": 1,
"icon": "icon-edit", "icon": "icon-edit",
"is_published_field": "published", "is_published_field": "published",
"links": [], "links": [],
"modified": "2022-03-23 15:44:41.385001", "modified": "2022-07-18 15:51:15.288860",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "Web Form", "name": "Web Form",

View file

@ -13,8 +13,8 @@ from frappe.desk.form.meta import get_code_files_via_hooks
from frappe.integrations.utils import get_payment_gateway_controller from frappe.integrations.utils import get_payment_gateway_controller
from frappe.modules.utils import export_module_json, get_doc_module from frappe.modules.utils import export_module_json, get_doc_module
from frappe.rate_limiter import rate_limit from frappe.rate_limiter import rate_limit
from frappe.utils import cstr from frappe.utils import cstr, dict_with_keys, strip_html
from frappe.website.utils import get_comment_list from frappe.website.utils import get_boot_data, get_comment_list, get_sidebar_items
from frappe.website.website_generator import WebsiteGenerator from frappe.website.website_generator import WebsiteGenerator
@ -32,17 +32,20 @@ class WebForm(WebsiteGenerator):
if not self.module: if not self.module:
self.module = frappe.db.get_value("DocType", self.doc_type, "module") self.module = frappe.db.get_value("DocType", self.doc_type, "module")
if ( in_user_env = not (
not ( frappe.flags.in_install
frappe.flags.in_install or frappe.flags.in_patch
or frappe.flags.in_patch or frappe.flags.in_test
or frappe.flags.in_test or frappe.flags.in_fixtures
or frappe.flags.in_fixtures )
) if in_user_env and self.is_standard and not frappe.conf.developer_mode:
and self.is_standard # only published can be changed for standard web forms
and not frappe.conf.developer_mode if self.has_value_changed("published"):
): published_value = self.published
frappe.throw(_("You need to be in developer mode to edit a Standard Web Form")) self.reload()
self.published = published_value
else:
frappe.throw(_("You need to be in developer mode to edit a Standard Web Form"))
if not frappe.flags.in_import: if not frappe.flags.in_import:
self.validate_fields() self.validate_fields()
@ -131,60 +134,131 @@ def get_context(context):
def get_context(self, context): def get_context(self, context):
"""Build context to render the `web_form.html` template""" """Build context to render the `web_form.html` template"""
context.is_form_editable = False
self.set_web_form_module() self.set_web_form_module()
doc, delimeter = make_route_string(frappe.form_dict) if frappe.form_dict.is_list:
context.doc = doc context.template = "website/doctype/web_form/templates/web_list.html"
context.delimeter = delimeter else:
context.template = "website/doctype/web_form/templates/web_form.html"
# check permissions # check permissions
if frappe.session.user == "Guest" and frappe.form_dict.name: if frappe.form_dict.name:
frappe.throw( if frappe.session.user == "Guest":
_("You need to be logged in to access this {0}.").format(self.doc_type), frappe.PermissionError frappe.throw(
) _("You need to be logged in to access this {0}.").format(self.doc_type),
frappe.PermissionError,
)
if frappe.form_dict.name and not self.has_web_form_permission( if not frappe.db.exists(self.doc_type, frappe.form_dict.name):
self.doc_type, frappe.form_dict.name raise frappe.PageDoesNotExistError()
if not self.has_web_form_permission(self.doc_type, frappe.form_dict.name):
frappe.throw(
_("You don't have the permissions to access this document"), frappe.PermissionError
)
if frappe.local.path == self.route:
path = f"/{self.route}/list" if self.show_list else f"/{self.route}/new"
frappe.redirect(path)
if frappe.form_dict.is_list and not self.show_list:
frappe.redirect(f"/{self.route}/new")
if frappe.form_dict.is_edit and not self.allow_edit:
frappe.redirect(f"/{self.route}/{frappe.form_dict.name}")
if frappe.form_dict.is_edit:
context.is_form_editable = True
if (
not frappe.form_dict.is_edit
and not frappe.form_dict.is_read
and self.allow_edit
and frappe.form_dict.name
): ):
frappe.throw( context.is_form_editable = True
_("You don't have the permissions to access this document"), frappe.PermissionError frappe.redirect(f"/{frappe.local.path}/edit")
)
if (
frappe.session.user != "Guest"
and not self.allow_multiple
and not frappe.form_dict.name
and not frappe.form_dict.is_list
):
name = frappe.db.get_value(self.doc_type, {"owner": frappe.session.user}, "name")
if name:
frappe.redirect(f"/{self.route}/{name}")
# Show new form when
# - User is Guest
# - Login not required
route_to_new = frappe.session.user == "Guest" and not self.login_required
if not frappe.form_dict.is_new and route_to_new:
frappe.redirect(f"/{self.route}/new")
self.reset_field_parent() self.reset_field_parent()
if self.is_standard: if self.is_standard:
self.use_meta_fields() self.use_meta_fields()
if not frappe.session.user == "Guest": # add keys from form_dict to context
if self.allow_edit: context.update(dict_with_keys(frappe.form_dict, ["is_list", "is_new", "is_edit", "is_read"]))
if self.allow_multiple:
if not frappe.form_dict.name and not frappe.form_dict.new:
# list data is queried via JS
context.is_list = True
else:
if frappe.session.user != "Guest" and not frappe.form_dict.name:
frappe.form_dict.name = frappe.db.get_value(
self.doc_type, {"owner": frappe.session.user}, "name"
)
if not frappe.form_dict.name: for df in self.web_form_fields:
# only a single doc allowed and no existing doc, hence new if df.fieldtype == "Column Break":
frappe.form_dict.new = 1 context.has_column_break = True
break
# 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"]))
if self.show_sidebar and self.website_sidebar:
context.sidebar_items = get_sidebar_items(self.website_sidebar)
if frappe.form_dict.is_list: if frappe.form_dict.is_list:
context.is_list = True self.load_list_data(context)
else:
self.load_form_data(context)
# always render new form if login is not required or doesn't allow editing existing ones self.add_custom_context_and_script(context)
if not self.login_required or not self.allow_edit: self.load_translations(context)
frappe.form_dict.new = 1
context.boot = get_boot_data()
context.boot["link_title_doctypes"] = frappe.boot.get_link_title_doctypes()
def load_translations(self, context):
translated_messages = frappe.translate.get_dict("doctype", self.doc_type)
# Sr is not added by default, had to be added manually
translated_messages["Sr"] = _("Sr")
context.translated_messages = frappe.as_json(translated_messages)
def load_list_data(self, context):
if not self.list_columns:
self.list_columns = get_in_list_view_fields(self.doc_type)
context.web_form_doc.list_columns = self.list_columns
def load_form_data(self, context):
"""Load document `doc` and `layout` properties for template"""
context.parents = []
if self.show_list:
context.parents.append(
{
"label": _(self.title),
"route": f"{self.route}/list",
}
)
self.load_document(context)
context.parents = self.get_parents(context) context.parents = self.get_parents(context)
if self.breadcrumbs: if self.breadcrumbs:
context.parents = frappe.safe_eval(self.breadcrumbs, {"_": _}) context.parents = frappe.safe_eval(self.breadcrumbs, {"_": _})
context.has_header = (frappe.form_dict.name or frappe.form_dict.new) and ( if frappe.form_dict.is_new:
context.title = _("New {0}").format(context.title)
context.has_header = (frappe.form_dict.name or frappe.form_dict.is_new) and (
frappe.session.user != "Guest" or not self.login_required frappe.session.user != "Guest" or not self.login_required
) )
@ -193,33 +267,40 @@ def get_context(context):
"'" "'"
) )
self.add_custom_context_and_script(context)
if not context.max_attachment_size: if not context.max_attachment_size:
context.max_attachment_size = get_max_file_size() / 1024 / 1024 context.max_attachment_size = get_max_file_size() / 1024 / 1024
context.show_in_grid = self.show_in_grid # For Table fields, server-side processing for meta
self.load_translations(context) for field in context.web_form_doc.web_form_fields:
context.link_title_doctypes = frappe.boot.get_link_title_doctypes() if field.fieldtype == "Table":
field.fields = get_in_list_view_fields(field.options)
def load_translations(self, context): if field.fieldtype == "Link":
translated_messages = frappe.translate.get_dict("doctype", self.doc_type) field.fieldtype = "Autocomplete"
# Sr is not added by default, had to be added manually field.options = get_link_options(
translated_messages["Sr"] = _("Sr") self.name, field.options, field.allow_read_on_all_link_options
context.translated_messages = frappe.as_json(translated_messages) )
def load_document(self, context): context.reference_doc = {}
"""Load document `doc` and `layout` properties for template"""
if frappe.form_dict.name or frappe.form_dict.new:
context.layout = self.get_layout()
context.parents = [{"route": self.route, "label": _(self.title)}]
# load reference doc
if frappe.form_dict.name: if frappe.form_dict.name:
context.doc = frappe.get_doc(self.doc_type, frappe.form_dict.name) context.doc_name = frappe.form_dict.name
context.title = context.doc.get(context.doc.meta.get_title_field()) context.reference_doc = frappe.get_doc(self.doc_type, context.doc_name)
context.doc.add_seen() context.title = strip_html(
context.reference_doc.get(context.reference_doc.meta.get_title_field())
context.reference_doctype = context.doc.doctype )
context.reference_name = context.doc.name if context.is_form_editable:
context.parents.append(
{
"label": _(context.title),
"route": f"{self.route}/{context.doc_name}",
}
)
context.title = _("Edit")
context.reference_doc.add_seen()
context.reference_doctype = context.reference_doc.doctype
context.reference_name = context.reference_doc.name
if self.show_attachments: if self.show_attachments:
context.attachments = frappe.get_all( context.attachments = frappe.get_all(
@ -233,7 +314,11 @@ def get_context(context):
) )
if self.allow_comments: if self.allow_comments:
context.comment_list = get_comment_list(context.doc.doctype, context.doc.name) context.comment_list = get_comment_list(
context.reference_doc.doctype, context.reference_doc.name
)
context.reference_doc = json.loads(context.reference_doc.as_json())
def get_payment_gateway_url(self, doc): def get_payment_gateway_url(self, doc):
if self.accept_payment: if self.accept_payment:
@ -594,7 +679,7 @@ def get_form_data(doctype, docname=None, web_form_name=None):
# For Table fields, server-side processing for meta # For Table fields, server-side processing for meta
for field in out.web_form.web_form_fields: for field in out.web_form.web_form_fields:
if field.fieldtype == "Table": if field.fieldtype == "Table":
field.fields = frappe.get_meta(field.options).fields field.fields = get_in_list_view_fields(field.options)
out.update({field.fieldname: field.fields}) out.update({field.fieldname: field.fields})
if field.fieldtype == "Link": if field.fieldtype == "Link":

View file

@ -0,0 +1,10 @@
frappe.listview_settings['Web Form'] = {
add_fields: ["title", "published"],
get_indicator: function(doc) {
if (doc.published) {
return [__("Published"), "green", "published,=,1"];
} else {
return [__("Not Published"), "gray", "published,=,0"];
}
}
};

View file

@ -10,7 +10,6 @@
"label", "label",
"allow_read_on_all_link_options", "allow_read_on_all_link_options",
"reqd", "reqd",
"depends_on",
"read_only", "read_only",
"show_in_filter", "show_in_filter",
"hidden", "hidden",
@ -19,6 +18,7 @@
"max_length", "max_length",
"max_value", "max_value",
"property_depends_on_section", "property_depends_on_section",
"depends_on",
"mandatory_depends_on", "mandatory_depends_on",
"column_break_16", "column_break_16",
"read_only_depends_on", "read_only_depends_on",
@ -63,7 +63,7 @@
{ {
"fieldname": "depends_on", "fieldname": "depends_on",
"fieldtype": "Code", "fieldtype": "Code",
"label": "Depends On" "label": "Display Depends On"
}, },
{ {
"default": "0", "default": "0",
@ -146,12 +146,13 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-01-28 10:41:25.422345", "modified": "2022-06-06 16:00:55.627950",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "Web Form Field", "name": "Web Form Field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
} "states": []
}

View file

@ -0,0 +1,48 @@
{
"actions": [],
"autoname": "autoincrement",
"creation": "2022-06-20 20:02:12.132569",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"fieldname",
"fieldtype",
"label"
],
"fields": [
{
"fieldname": "fieldname",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Fieldname",
"reqd": 1
},
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label"
},
{
"fieldname": "fieldtype",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldtype",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-06-21 17:22:14.978947",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form List Column",
"naming_rule": "Autoincrement",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View file

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class WebFormListColumn(Document):
pass

View file

@ -16,6 +16,7 @@ from frappe.website.utils import (
find_first_image, find_first_image,
get_comment_list, get_comment_list,
get_html_content_based_on_type, get_html_content_based_on_type,
get_sidebar_items,
) )
from frappe.website.website_generator import WebsiteGenerator from frappe.website.website_generator import WebsiteGenerator
@ -70,6 +71,9 @@ class WebPage(WebsiteGenerator):
if not self.show_title: if not self.show_title:
context["no_header"] = 1 context["no_header"] = 1
if self.show_sidebar:
context.sidebar_items = get_sidebar_items(self.website_sidebar)
self.set_metatags(context) self.set_metatags(context)
self.set_breadcrumbs(context) self.set_breadcrumbs(context)
self.set_title_and_header(context) self.set_title_and_header(context)

View file

@ -7,6 +7,7 @@ from frappe import _
from frappe.integrations.google_oauth import GoogleOAuth from frappe.integrations.google_oauth import GoogleOAuth
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import encode, get_request_site_address from frappe.utils import encode, get_request_site_address
from frappe.website.utils import get_boot_data
class WebsiteSettings(Document): class WebsiteSettings(Document):
@ -190,6 +191,8 @@ def get_website_settings(context=None):
if settings.splash_image: if settings.splash_image:
context["splash_image"] = settings.splash_image context["splash_image"] = settings.splash_image
context.boot = get_boot_data()
return context return context

View file

@ -1,11 +1,13 @@
import frappe
from frappe.website.page_renderers.document_page import DocumentPage from frappe.website.page_renderers.document_page import DocumentPage
from frappe.website.router import get_page_info_from_web_form
class WebFormPage(DocumentPage): class WebFormPage(DocumentPage):
def can_render(self): def can_render(self):
webform_name = frappe.db.exists("Web Form", {"route": self.path, "published": 1}, cache=True) web_form = get_page_info_from_web_form(self.path)
if webform_name: if web_form:
self.doctype = "Web Form" self.doctype = "Web Form"
self.docname = webform_name self.docname = web_form.name
return bool(webform_name) return True
else:
return False

View file

@ -30,6 +30,32 @@ def get_page_info_from_web_page_with_dynamic_routes(path):
return page_info[end_point] return page_info[end_point]
def get_page_info_from_web_form(path):
"""Query published web forms and evaluate if the route matches"""
rules, page_info = [], {}
web_forms = frappe.db.get_all("Web Form", ["name", "route", "modified"], {"published": 1})
for d in web_forms:
rules.append(Rule(f"/{d.route}", endpoint=d.name))
rules.append(Rule(f"/{d.route}/list", endpoint=d.name))
rules.append(Rule(f"/{d.route}/new", endpoint=d.name))
rules.append(Rule(f"/{d.route}/<name>", endpoint=d.name))
rules.append(Rule(f"/{d.route}/<name>/edit", endpoint=d.name))
d.doctype = "Web Form"
page_info[d.name] = d
end_point = evaluate_dynamic_routes(rules, path)
if end_point:
if path.endswith("/list"):
frappe.form_dict.is_list = True
elif path.endswith("/new"):
frappe.form_dict.is_new = True
elif path.endswith("/edit"):
frappe.form_dict.is_edit = True
else:
frappe.form_dict.is_read = True
return page_info[end_point]
def evaluate_dynamic_routes(rules, path): def evaluate_dynamic_routes(rules, path):
""" """
Use Werkzeug routing to evaluate dynamic routes like /project/<name> Use Werkzeug routing to evaluate dynamic routes like /project/<name>

View file

@ -1,5 +1,6 @@
import frappe import frappe
from frappe.website.page_renderers.error_page import ErrorPage from frappe.website.page_renderers.error_page import ErrorPage
from frappe.website.page_renderers.not_found_page import NotFoundPage
from frappe.website.page_renderers.not_permitted_page import NotPermittedPage from frappe.website.page_renderers.not_permitted_page import NotPermittedPage
from frappe.website.page_renderers.redirect_page import RedirectPage from frappe.website.page_renderers.redirect_page import RedirectPage
from frappe.website.path_resolver import PathResolver from frappe.website.path_resolver import PathResolver
@ -19,6 +20,8 @@ def get_response(path=None, http_status_code=200):
return RedirectPage(endpoint or path, http_status_code).render() return RedirectPage(endpoint or path, http_status_code).render()
except frappe.PermissionError as e: except frappe.PermissionError as e:
response = NotPermittedPage(endpoint, http_status_code, exception=e).render() response = NotPermittedPage(endpoint, http_status_code, exception=e).render()
except frappe.PageDoesNotExistError:
response = NotFoundPage(endpoint, http_status_code).render()
except Exception as e: except Exception as e:
frappe.log_error(f"{path} failed") frappe.log_error(f"{path} failed")
response = ErrorPage(exception=e).render() response = ErrorPage(exception=e).render()

View file

@ -12,7 +12,7 @@ from werkzeug.wrappers import Response
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import md_to_html from frappe.utils import cint, get_time_zone, md_to_html
FRONTMATTER_PATTERN = re.compile(r"^\s*(?:---|\+\+\+)(.*?)(?:---|\+\+\+)\s*(.+)$", re.S | re.M) FRONTMATTER_PATTERN = re.compile(r"^\s*(?:---|\+\+\+)(.*?)(?:---|\+\+\+)\s*(.+)$", re.S | re.M)
H1_TAG_PATTERN = re.compile("<h1>([^<]*)") H1_TAG_PATTERN = re.compile("<h1>([^<]*)")
@ -158,6 +158,20 @@ def get_home_page_via_hooks():
return home_page return home_page
def get_boot_data():
return {
"sysdefaults": {
"float_precision": cint(frappe.get_system_settings("float_precision")) or 3,
"date_format": frappe.get_system_settings("date_format") or "yyyy-mm-dd",
"time_format": frappe.get_system_settings("time_format") or "HH:mm:ss",
},
"time_zone": {
"system": get_time_zone(),
"user": frappe.db.get_value("User", frappe.session.user, "time_zone") or get_time_zone(),
},
}
def is_signup_disabled(): def is_signup_disabled():
return frappe.db.get_single_value("Website Settings", "disable_signup", True) return frappe.db.get_single_value("Website Settings", "disable_signup", True)
@ -393,7 +407,7 @@ def get_frontmatter(string):
} }
def get_sidebar_items(parent_sidebar, basepath): def get_sidebar_items(parent_sidebar, basepath=None):
import frappe.www.list import frappe.www.list
sidebar_items = [] sidebar_items = []

View file

@ -11,6 +11,7 @@
"apply_document_permissions": 0, "apply_document_permissions": 0,
"breadcrumbs": "", "breadcrumbs": "",
"button_label": "Request Data", "button_label": "Request Data",
"client_script": "",
"creation": "2019-01-24 16:19:26.886096", "creation": "2019-01-24 16:19:26.886096",
"currency": "INR", "currency": "INR",
"doc_type": "Personal Data Download Request", "doc_type": "Personal Data Download Request",
@ -18,10 +19,12 @@
"doctype": "Web Form", "doctype": "Web Form",
"idx": 0, "idx": 0,
"introduction_text": "<div class=\"ql-editor read-mode\"><p>Request a file containing your personally identifiable information (PII) that is saved on our system. The file will be in JSON format and is sent to you by email. If you would like to have your PII deleted from our system, please make a <a href=\"/request-to-delete-data\" rel=\"noopener noreferrer\">request to delete data</a>.</p></div>", "introduction_text": "<div class=\"ql-editor read-mode\"><p>Request a file containing your personally identifiable information (PII) that is saved on our system. The file will be in JSON format and is sent to you by email. If you would like to have your PII deleted from our system, please make a <a href=\"/request-to-delete-data\" rel=\"noopener noreferrer\">request to delete data</a>.</p></div>",
"is_multi_step_form": 0,
"is_standard": 1, "is_standard": 1,
"list_columns": [],
"login_required": 0, "login_required": 0,
"max_attachment_size": 0, "max_attachment_size": 0,
"modified": "2021-03-25 10:52:13.149538", "modified": "2022-07-18 16:51:07.281527",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "request-data", "name": "request-data",
@ -31,9 +34,8 @@
"route": "request-data", "route": "request-data",
"route_to_success_link": 1, "route_to_success_link": 1,
"show_attachments": 0, "show_attachments": 0,
"show_in_grid": 0, "show_list": 0,
"show_sidebar": 0, "show_sidebar": 0,
"sidebar_items": [],
"success_message": "A download link with your data will be sent to the email address associated with your account.", "success_message": "A download link with your data will be sent to the email address associated with your account.",
"success_url": "/desk", "success_url": "/desk",
"title": "Request Data", "title": "Request Data",

View file

@ -9,6 +9,7 @@
"amount": 0.0, "amount": 0.0,
"amount_based_on_field": 0, "amount_based_on_field": 0,
"apply_document_permissions": 0, "apply_document_permissions": 0,
"breadcrumbs": "",
"button_label": "Submit", "button_label": "Submit",
"client_script": "", "client_script": "",
"creation": "2019-01-25 14:24:12.588810", "creation": "2019-01-25 14:24:12.588810",
@ -19,10 +20,12 @@
"doctype": "Web Form", "doctype": "Web Form",
"idx": 0, "idx": 0,
"introduction_text": "<div class=\"ql-editor read-mode\"><p>Send a request to delete your account and personally identifiable information (PII) that is stored on our system. You will receive an email to verify your request. Once the request is verified we will take care of deleting your PII. If you just want to check what PII we have stored, you can <a href=\"/request-data\" rel=\"noopener noreferrer\">request your data</a>.</p></div>", "introduction_text": "<div class=\"ql-editor read-mode\"><p>Send a request to delete your account and personally identifiable information (PII) that is stored on our system. You will receive an email to verify your request. Once the request is verified we will take care of deleting your PII. If you just want to check what PII we have stored, you can <a href=\"/request-data\" rel=\"noopener noreferrer\">request your data</a>.</p></div>",
"is_multi_step_form": 0,
"is_standard": 1, "is_standard": 1,
"list_columns": [],
"login_required": 0, "login_required": 0,
"max_attachment_size": 0, "max_attachment_size": 0,
"modified": "2021-11-30 17:56:03.099870", "modified": "2022-07-18 16:51:30.949738",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "request-to-delete-data", "name": "request-to-delete-data",
@ -32,9 +35,8 @@
"route": "request-for-account-deletion", "route": "request-for-account-deletion",
"route_to_success_link": 0, "route_to_success_link": 0,
"show_attachments": 0, "show_attachments": 0,
"show_in_grid": 0, "show_list": 0,
"show_sidebar": 0, "show_sidebar": 0,
"sidebar_items": [],
"success_message": "An email to verify your request has been sent to your email address. Please verify your request to complete the process.", "success_message": "An email to verify your request has been sent to your email address. Please verify your request to complete the process.",
"success_url": "/", "success_url": "/",
"title": "Request for Account Deletion", "title": "Request for Account Deletion",