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();
});
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', () => {
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.wait(5000);
cy.url().should('include', '/me');
});
it('Navigate and Submit a MultiStep WebForm', () => {
cy.call('frappe.tests.ui_test_helpers.update_webform_to_multistep').then(() => {
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').click();
cy.get('.btn-previous').should('be.visible');
cy.get('.btn-next').should('not.be.visible');
cy.get('.web-form-actions .btn-primary').click();
cy.wait(5000);
cy.url().should('include', '/me');
});
});

View file

@ -162,7 +162,12 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} 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');
});
@ -358,6 +363,10 @@ Cypress.Commands.add('open_list_filter', () => {
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) => {
cy.findByRole('button', {name: 'Actions'}).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
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):
"""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

View file

@ -18,9 +18,10 @@
"introduction_text": "",
"is_multi_step_form": 0,
"is_standard": 1,
"list_columns": [],
"login_required": 1,
"max_attachment_size": 0,
"modified": "2022-03-22 15:00:43.456738",
"modified": "2022-07-18 16:51:19.796411",
"modified_by": "Administrator",
"module": "Core",
"name": "edit-profile",
@ -29,9 +30,8 @@
"route": "update-profile",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_list": 0,
"show_sidebar": 0,
"sidebar_items": [],
"success_message": "Profile updated successfully.",
"success_url": "/me",
"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.log_settings_migration
frappe.patches.v14_0.setup_likes_from_feedback
frappe.patches.v14_0.update_webforms
[post_model_sync]
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 "./lib/moment.js";
import "./frappe/provide.js";
import "./frappe/form/formatters.js";
import "./frappe/format.js";
import "./frappe/utils/number_format.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() {
this.value = this.value == null ? undefined : this.value;
let value = frappe.datetime.convert_to_user_tz(this.value);
return frappe.datetime.str_to_obj(value);
}

View file

@ -196,7 +196,7 @@ frappe.form.formatters = {
Datetime: function(value) {
if(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 {
return "";
}

View file

@ -23,13 +23,14 @@ export default class WebForm extends frappe.ui.FieldGroup {
this.set_sections();
this.set_field_values();
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.setup_cancel_button();
this.setup_primary_action();
if (this.is_new || this.is_form_editable) {
this.setup_primary_action();
}
this.setup_footer_actions();
this.setup_previous_next_button();
this.toggle_section();
$(".link-btn").remove();
// webform 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`);
}
setup_footer_actions() {
if (this.is_multi_step_form) return;
if ($('.web-form-container').height() > 600) {
$(".web-form-footer").removeClass("hide");
}
}
setup_previous_next_button() {
let me = this;
@ -87,7 +96,7 @@ export default class WebForm extends frappe.ui.FieldGroup {
$('.btn-previous').on('click', function () {
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
@ -107,12 +116,13 @@ export default class WebForm extends frappe.ui.FieldGroup {
}
/* eslint-enable for-direction */
me.toggle_section();
return false;
});
$('.btn-next').on('click', function () {
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++) {
let is_empty = me.is_next_section_empty(idx);
@ -123,6 +133,7 @@ export default class WebForm extends frappe.ui.FieldGroup {
}
}
me.toggle_section();
return false;
});
}
@ -132,56 +143,20 @@ export default class WebForm extends frappe.ui.FieldGroup {
}
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();
delete values.new;
Object.assign(defaults, 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() {
this.add_button_to_header(this.button_label || __("Save", null, "Button in web form"), "primary", () =>
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()
);
$(".web-form-container").on("submit", () => this.save());
}
validate_section() {
@ -349,18 +324,21 @@ export default class WebForm extends frappe.ui.FieldGroup {
window.saving = false;
}
});
return true;
return false;
}
print() {
window.open(`/printview?
doctype=${this.doc_type}
&name=${this.doc.name}
&format=${this.print_format || "Standard"}`, '_blank');
edit() {
window.location.href = window.location.pathname + "/edit";
}
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) {
@ -375,12 +353,19 @@ export default class WebForm extends frappe.ui.FieldGroup {
// redirect
setTimeout(() => {
let path = window.location.pathname;
if (this.success_url) {
window.location.href = this.success_url;
} else if(this.login_required) {
window.location.href =
window.location.pathname + "?name=" + data.name;
path = this.success_url;
} else if (this.login_required) {
if (this.is_new && data.name) {
path = path.replace("/new", "");
path = path + "/" + data.name;
} else if (this.is_form_editable) {
path = path.replace("/edit", "");
}
}
}, 2000);
window.location.href = path;
}, 1000);
}
}

View file

@ -6,63 +6,74 @@ export default class WebFormList {
constructor(opts) {
Object.assign(this, opts);
frappe.web_form_list = this;
this.wrapper = document.getElementById("list-table");
this.wrapper = $(".web-list-table");
this.make_actions();
this.make_filters();
$('.link-btn').remove();
}
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.page_length = 20;
this.web_list_start = 0;
this.page_length = 10;
frappe.run_serially([
() => this.get_list_view_fields(),
() => this.get_data(),
() => this.remove_more(),
() => this.make_table(),
() => this.create_more()
]);
}
remove_more() {
$('.more').remove();
}
make_filters() {
this.filters = {};
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', {
web_form_name: this.web_form_name
}).then(response => {
let fields = response.message;
fields.length && filter_area.removeClass('hide');
fields.forEach(field => {
let col = document.createElement('div.col-sm-4');
col.classList.add('col', 'col-sm-3');
filter_area.appendChild(col);
if (field.default) this.add_filter(field.fieldname, field.default, field.fieldtype);
if (["Text Editor", "Text", "Small Text"].includes(field.fieldtype)) {
field.fieldtype = "Data";
}
if (["Table", "Signature"].includes(field.fieldtype)) {
return;
}
let input = frappe.ui.form.make_control({
df: {
fieldtype: field.fieldtype,
fieldname: field.fieldname,
options: field.options,
input_class: 'input-xs',
only_select: true,
label: __(field.label),
onchange: (event) => {
$('#more').remove();
this.add_filter(field.fieldname, input.value, field.fieldtype);
this.refresh();
}
},
parent: col,
value: field.default,
parent: filter_area,
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.refresh();
@ -73,37 +84,65 @@ export default class WebFormList {
if (!value) {
delete this.filters[field];
} 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]]));
}
}
get_list_view_fields() {
return frappe
.call({
method:
"frappe.website.doctype.web_form.web_form.get_in_list_view_fields",
args: { doctype: this.doctype }
})
.then(response => (this.fields_list = response.message));
if (this.columns) return this.columns;
if (this.list_columns) {
this.columns = this.list_columns.map(df => {
return {
label: df.label,
fieldname: df.fieldname,
fieldtype: df.fieldtype
};
});
}
}
fetch_data() {
return frappe.call({
let args = {
method: "frappe.www.list.get_list_data",
args: {
doctype: this.doctype,
fields: this.fields_list.map(df => df.fieldname),
limit_start: this.web_list_start,
limit: this.page_length,
web_form_name: this.web_form_name,
...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() {
let response = await this.fetch_data();
this.data = await response.message;
if (response) {
this.data = await response.message;
}
}
more() {
@ -118,159 +157,145 @@ export default class WebFormList {
}
make_table() {
this.columns = this.fields_list.map(df => {
return {
label: df.label,
fieldname: df.fieldname,
fieldtype: df.fieldtype
};
this.table = $(`<table class="table"></table>`);
this.make_table_head();
this.make_table_body();
}
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.table = document.createElement("table");
this.table.classList.add("table");
this.make_table_head();
}
this.columns.forEach(col => {
let $tr = $thead.find("tr");
let $th = $(`<th>${__(col.label)}</th>`);
$th.appendTo($tr);
});
$thead.appendTo(this.table);
}
make_table_body() {
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.wrapper.appendChild(this.table);
this.table.appendTo(this.wrapper);
} else {
let new_button = "";
let empty_state = document.createElement("div");
empty_state.classList.add("no-result", "text-muted", "flex", "justify-center", "align-center");
if (this.wrapper.find('.no-result').length) return;
this.wrapper.empty();
frappe.has_permission(this.doctype, "", "create", () => {
new_button = `
<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);
this.setup_empty_state();
});
}
}
make_table_head() {
// Create Heading
let thead = this.table.createTHead();
let row = thead.insertRow();
setup_empty_state() {
let new_button = `
<a
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");
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);
}
empty_state.appendTo(this.wrapper);
}
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) => {
let row_element = tbody.insertRow();
row_element.setAttribute("id", data_item.name);
let $row_element = $(`<tr id="${data_item.name}"></tr>`);
let row = new frappe.ui.WebFormListRow({
row: row_element,
row: $row_element,
doc: data_item,
columns: this.columns,
serial_number: this.rows.length + 1,
events: {
onEdit: () => this.open_form(data_item.name),
onSelect: () => this.toggle_delete()
on_edit: () => this.open_form(data_item.name),
on_select: () => {
this.toggle_new();
this.toggle_delete();
}
}
});
this.rows.push(row);
$row_element.appendTo($tbody);
});
}
make_actions() {
const actions = document.querySelector(".list-view-actions");
const actions = $(".web-list-actions");
frappe.has_permission(this.doctype, "", "delete", () => {
this.addButton(actions, "delete-rows", "danger", true, "Delete", () =>
this.delete_rows()
);
this.add_button(actions, "delete-rows", "danger", true, "Delete", () => 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) {
if (document.getElementById(id)) 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");
}
add_button(wrapper, name, type, hidden, text, action) {
if ($(`.${name}`).length) return;
button.id = id;
button.innerText = name;
button.hidden = hidden;
hidden = hidden ? "hide" : "";
type = type == "danger" ? "danger button-delete" : type;
button.onclick = action;
wrapper.appendChild(button);
let button = $(`
<button class="${name} btn btn-${type} btn-sm ml-2 ${hidden}">${text}</button>
`);
button.on("click", () => action());
button.appendTo(wrapper);
}
create_more() {
if (this.rows.length >= this.page_length) {
const footer = document.querySelector(".list-view-footer");
this.addButton(footer, "more", "secondary", false, "More", () => this.more());
const footer = $(".web-list-footer");
this.add_button(footer, "more", "secondary", false, "Load More", () => this.more());
}
}
@ -279,7 +304,12 @@ export default class WebFormList {
}
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() {
@ -287,9 +317,15 @@ export default class WebFormList {
}
toggle_delete() {
if (!this.settings.allow_delete) return
let btn = document.getElementById("delete-rows");
btn.hidden = !this.get_selected().length;
if (!this.settings.allow_delete) return;
let btn = $(".delete-rows");
!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() {
@ -305,8 +341,9 @@ export default class WebFormList {
}
})
.then(() => {
this.refresh()
this.toggle_delete()
this.refresh();
this.toggle_delete();
this.toggle_new();
});
}
};
@ -319,40 +356,37 @@ frappe.ui.WebFormListRow = class WebFormListRow {
make_row() {
// Add Checkboxes
let cell = this.row.insertCell();
cell.classList.add('list-col-checkbox');
let $cell = $(`<td class="list-col-checkbox"></td>`);
this.checkbox = document.createElement("input");
this.checkbox.type = "checkbox";
this.checkbox.onclick = event => {
this.checkbox = $(`<input type="checkbox">`);
this.checkbox.on("click", event => {
this.toggle_select(event.target.checked);
event.stopImmediatePropagation();
}
cell.appendChild(this.checkbox);
});
this.checkbox.appendTo($cell);
$cell.appendTo(this.row);
// Add Serial Number
let serialNo = this.row.insertCell();
serialNo.classList.add('list-col-serial');
serialNo.innerText = this.serial_number;
let serialNo = $(`<td class="list-col-serial">${__(this.serial_number)}</td>`);
serialNo.appendTo(this.row);
this.columns.forEach(field => {
let cell = this.row.insertCell();
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)) || "";
let cell = $(`<td>${value}</td>`);
cell.appendTo(this.row);
});
this.row.onclick = () => this.events.onEdit();
this.row.style.cursor = "pointer";
this.row.on("click", () => this.events.on_edit());
}
toggle_select(checked) {
this.checkbox.checked = checked;
this.events.onSelect(checked);
this.checkbox.prop("checked", checked);
this.events.on_select(checked);
}
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'
frappe.ready(function() {
let query_params = frappe.utils.get_query_params();
let wrapper = $(".web-form-wrapper");
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;
let web_form_doc = frappe.web_form_doc;
let reference_doc = frappe.reference_doc;
if (login_required) show_login_prompt();
else if (is_list) show_grid();
else show_form(webform_doctype, webform_name, is_new);
show_login_prompt();
document.querySelector("body").style.display = "block";
web_form_doc.is_list ? show_list() : show_form();
function show_login_prompt() {
if (frappe.session.user != "Guest" || !web_form_doc.login_required) return;
const login_required = new frappe.ui.Dialog({
title: __("Not Permitted"),
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."));
}
function show_grid() {
function show_list() {
new WebFormList({
parent: wrapper,
doctype: webform_doctype,
web_form_name: webform_name,
doctype: web_form_doc.doc_type,
web_form_name: web_form_doc.name,
list_columns: web_form_doc.list_columns,
settings: {
allow_delete
allow_delete: web_form_doc.allow_delete
}
});
}
function show_form() {
let web_form = new WebForm({
parent: wrapper,
is_new,
web_form_name: webform_name,
parent: $(".web-form-wrapper"),
is_new: web_form_doc.is_new,
is_form_editable: web_form_doc.is_form_editable,
web_form_name: web_form_doc.name,
});
let doc = reference_doc || {};
setup_fields(web_form_doc, doc);
get_data().then(r => {
const data = setup_fields(r.message);
let web_form_doc = data.web_form;
web_form.prepare(web_form_doc, doc);
web_form.make();
// if (web_form_doc.name && web_form_doc.allow_edit === 0) {
// 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();
if (web_form_doc.is_new) {
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() {
return frappe.call({
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
});
}
$(".file-size").each(function () {
$(this).text(frappe.form.formatters.FileSize($(this).text()));
});
}
function setup_fields(form_data) {
form_data.web_form.web_form_fields.map(df => {
df.is_web_form = true;
if (df.fieldtype === "Table") {
df.get_data = () => {
let data = [];
if (form_data.doc) {
data = form_data.doc[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;
function setup_fields(web_form_doc, doc_data) {
web_form_doc.web_form_fields.forEach(df => {
df.is_web_form = true;
df.read_only = !web_form_doc.is_new && !web_form_doc.is_form_editable;
if (df.fieldtype === "Table") {
df.get_data = () => {
let data = [];
if (doc_data && doc_data[df.fieldname]) {
return doc_data[df.fieldname];
}
return data;
};
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 = {};
$.each(df.fields || [], function(_i, field) {
if (field.fieldtype === "Link") {
field.only_select = true;
}
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;
}
.pointer {
cursor: pointer;
}
.inline-block {
display: inline-block;
}

View file

@ -5,37 +5,212 @@
max-width: 800px;
margin: auto;
.frappe-card {
padding: 1rem;
h1 {
font-size: 1.9rem;
margin-top: 0;
margin-bottom: 0;
}
h1 {
font-size: 1.9rem;
margin-top: 0;
margin-bottom: 0;
}
.web-form-container {
border: 1px solid var(--dark-border-color);
border-radius: var(--border-radius-md);
padding: 2rem;
.web-form-head {
margin: 0 -1rem;
padding: 0 1rem 1rem 1rem;
margin-bottom: 1rem;
.web-form-header {
display: flex;
justify-content: space-between;
margin: 0 -2rem 1rem;
padding: 0 2rem 1rem;
border-bottom: 1px solid var(--border-color);
.web-form-actions {
align-self: center;
}
}
#introduction {
margin-bottom: 2rem;
}
#introduction p {
.web-form-introduction {
color: var(--text-muted);
margin-bottom: 2rem;
p {
color: var(--text-muted);
}
}
.web-form-actions button {
margin-top: 0.1rem;
.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;
}
}
}
.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 {
min-height: 400px;
.web-list-container {
min-height: 470px;
border: 1px solid var(--dark-border-color);
border-radius: var(--border-radius-md);
padding: 2rem;
.web-list-header {
display: flex;
justify-content: space-between;
.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 {
@ -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 %}
<!-- js should be loaded in body! -->
<script>
frappe.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' }}",
}
};
frappe.boot = {{ boot }}
// for backward compatibility of some libs
frappe.sys_defaults = frappe.boot.sysdefaults;
</script>

View file

@ -8,17 +8,17 @@ from frappe.www.list import get_list_context
class TestWebform(unittest.TestCase):
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
edit_profile.published = True
edit_profile.save()
set_request(method="GET", path="update-profile")
request_data.published = True
request_data.save()
set_request(method="GET", path="request-data/new")
response = get_response()
self.assertEqual(response.status_code, 200)
# un-publish webform
edit_profile.published = False
edit_profile.save()
request_data.published = False
request_data.save()
response = get_response()
self.assertEqual(response.status_code, 404)

View file

@ -1951,6 +1951,15 @@ def generate_hash(*args, **kwargs) -> str:
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:
DATE_FORMATS = [
r"%d/%b/%y",

View file

@ -1,149 +1,129 @@
{% extends "templates/web.html" %}
{% block title %}{{ _(title) }}{% endblock %}
{% block breadcrumbs %}{% endblock %}
{% macro container_attributes() %}
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 }}"
{% macro action_buttons() %}
{% if allow_print and not is_new %}
{% set print_format_url = "/printview?doctype=" + doc_type + "&name=" + doc_name + "&format=" + print_format %}
<!-- print button -->
<a href="{{ print_format_url }}" target="_blank" class="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 %}
{% block page_content %}
{% if has_header and login_required and allow_multiple %}
<!-- breadcrumb -->
{% include "templates/includes/breadcrumbs.html" %}
{% else %}
<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 #}
<!-- breadcrumb -->
{% if has_header and login_required and show_list %}
{% include "templates/includes/breadcrumbs.html" %}
{% else %}
<div style="height: 3rem"></div>
{% endif %}
</div>
{% if allow_comments and not frappe.form_dict.new and not is_list -%}
<!-- comments -->
<div class="comments" style="margin-top: 3rem;">
{% include 'templates/includes/comments/comments.html' %}
</div>
{%- else -%}
<div style="height: 3rem"></div>
{%- endif %} {# comments #}
<!-- main card -->
<form role="form" class="web-form-container">
<div class="web-form-header">
<h1>{{ _(title) }}</h1>
<div class="web-form-actions">
{{ action_buttons() }}
</div>
</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 %}
{% block script %}
<script>
frappe.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' }}",
},
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>
<script>
frappe.boot = {{ boot }};
frappe._messages = {{ translated_messages }};
frappe.web_form_doc = {{ web_form_doc | json }};
frappe.reference_doc = {{ reference_doc | json }};
</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 %}
<script type="text/javascript" src="/assets/frappe/node_modules/vue/dist/vue.js"></script>
<script>
Vue.prototype.__ = window.__;
Vue.prototype.frappe = window.frappe;
</script>
{% if script is defined %}
{{ script }}
{% endif %}
</script>
{% endif %}
{{ include_script("controls.bundle.js") }}
{{ include_script("dialog.bundle.js") }}
{{ include_script("web_form.bundle.js") }}
{{ include_script("bootstrap-4-web.bundle.js") }}
<script>
{% if client_script %}
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 %}
{% block style %}
{% if not is_list %}
{{ include_style('web_form.bundle.css') }}
{% endif %}
<style>
body {
background-color: var(--bg-color);
}
{% if style is defined %}
{{ style }}
{% endif %}
{% if custom_css %}
{{ custom_css }}
{% endif %}
</style>
<style>
{% if style is defined %}
{{ style }}
{% endif %}
{% if custom_css %}
{{ custom_css }}
{% endif %}
</style>
{% 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 frappe
from frappe.utils import set_request
from frappe.website.doctype.web_form.web_form import accept
from frappe.website.serve import get_response_content
@ -68,8 +69,9 @@ class TestWebForm(unittest.TestCase):
)
def test_webform_render(self):
content = get_response_content("request-data")
self.assertIn("<h1>Request Data</h1>", content)
set_request(method="GET", path="manage-events/new")
content = get_response_content("manage-events/new")
self.assertIn("<h1>New Manage Events</h1>", content)
self.assertIn('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)

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", {
refresh: function(frm) {
// show is-standard only if 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) {
frm.set_read_only();
frm.disable_save();
}
render_list_settings_message(frm);
frm.add_custom_button(__('Get Fields'), () => {
let webform_fieldtypes = frappe.meta.get_field('Web Form Field', 'fieldtype').options.split('\n');
let fieldnames = (frm.doc.web_form_fields || []).map(d => d.fieldname);
frappe.model.with_doctype(frm.doc.doc_type, () => {
let meta = frappe.get_meta(frm.doc.doc_type);
for (let field of meta.fields) {
if (webform_fieldtypes.includes(field.fieldtype)
&& !fieldnames.includes(field.fieldname)) {
frm.add_child('web_form_fields', {
fieldname: field.fieldname,
label: field.label,
fieldtype: field.fieldtype,
options: field.options,
reqd: field.reqd,
default: field.default,
read_only: field.read_only || field.is_virtual,
depends_on: field.depends_on,
mandatory_depends_on: field.mandatory_depends_on,
read_only_depends_on: field.read_only_depends_on,
hidden: field.hidden,
description: field.description
frm.trigger('set_fields');
frm.trigger('add_get_fields_button');
frm.trigger('add_publish_button');
},
login_required: function(frm) {
render_list_settings_message(frm);
},
validate: function(frm) {
if (!frm.doc.login_required) {
frm.set_value("allow_multiple", 0);
frm.set_value("allow_edit", 0);
frm.set_value("show_list", 0);
}
!frm.doc.allow_multiple && frm.set_value("allow_delete", 0);
frm.doc.allow_multiple && frm.set_value("show_list", 1);
if (!frm.doc.web_form_fields) {
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) {
if (frm.doc.__islocal) {
var page_name = frm.doc.title.toLowerCase().replace(/ /g, "-");
frm.set_value("route", page_name);
frm.set_value("success_url", "/" + page_name);
}
},
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);
if (['Section Break', 'Column Break', 'Page Break'].includes(doc.fieldtype)) {
doc.fieldname = '';
doc.options = "";
frm.refresh_field("web_form_fields");
}
},
fieldname: function(frm, doctype, name) {
var doc = frappe.get_doc(doctype, name);
var df = $.map(frappe.get_doc("DocType", frm.doc.doc_type).fields, function(d) {
return doc.fieldname == d.fieldname ? d : null;
})[0];
let doc = frappe.get_doc(doctype, name);
let df = frappe.meta.get_docfield(frm.doc.doc_type, doc.fieldname);
if (!df) return;
doc.label = df.label;
doc.reqd = df.reqd;
doc.fieldtype = df.fieldtype;
doc.options = df.options;
doc.fieldtype = frappe.meta.get_docfield("Web Form Field", "fieldtype")
.options.split("\n").indexOf(df.fieldtype) === -1 ? "Data" : df.fieldtype;
doc.description = df.description;
doc["default"] = df["default"];
doc.reqd = df.reqd;
doc.default = df.default;
doc.read_only = df.read_only;
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",
"engine": "InnoDB",
"field_order": [
"title_and_route_tab",
"title",
"route",
"published",
"column_break_4",
"doc_type",
"module",
"column_break_4",
"is_standard",
"is_multi_step_form",
"published",
"introduction",
"introduction_text",
"form_settings_tab",
"login_required",
"route_to_success_link",
"allow_edit",
"is_multi_step_form",
"allow_multiple",
"apply_document_permissions",
"show_in_grid",
"allow_edit",
"allow_delete",
"column_break_18",
"apply_document_permissions",
"allow_print",
"print_format",
"allow_comments",
"show_attachments",
"allow_incomplete",
"introduction",
"introduction_text",
"fields",
"form_fields",
"web_form_fields",
"max_attachment_size",
"client_script_section",
"client_script",
"custom_css_section",
"custom_css",
"actions",
"breadcrumbs",
"button_label",
"column_break_29",
"success_message",
"route_to_success_link",
"success_url",
"sidebar_settings",
"list_settings_tab",
"list_setting_message",
"show_list",
"list_title",
"list_columns",
"sidebar_settings_tab",
"show_sidebar",
"sidebar_items",
"payments",
"website_sidebar",
"scripting_style_tab",
"client_script",
"custom_css",
"payments_tab",
"accept_payment",
"payment_gateway",
"payment_button_label",
@ -50,10 +58,7 @@
"amount_based_on_field",
"amount_field",
"amount",
"currency",
"advanced",
"web_page_link_text",
"breadcrumbs"
"currency"
],
"fields": [
{
@ -118,25 +123,18 @@
"depends_on": "login_required",
"fieldname": "allow_edit",
"fieldtype": "Check",
"label": "Allow Edit"
"label": "Allow Editing After Submit"
},
{
"default": "0",
"depends_on": "login_required",
"fieldname": "allow_multiple",
"fieldtype": "Check",
"label": "Allow Multiple"
"label": "Allow Multiple Responses"
},
{
"default": "0",
"depends_on": "allow_multiple",
"fieldname": "show_in_grid",
"fieldtype": "Check",
"label": "Show as Grid"
},
{
"default": "0",
"depends_on": "allow_multiple",
"depends_on": "eval: doc.allow_multiple && doc.login_required",
"fieldname": "allow_delete",
"fieldtype": "Check",
"label": "Allow Delete"
@ -187,11 +185,6 @@
"ignore_xss_filter": 1,
"label": "Introduction"
},
{
"fieldname": "fields",
"fieldtype": "Section Break",
"label": "Fields"
},
{
"fieldname": "web_form_fields",
"fieldtype": "Table",
@ -203,13 +196,6 @@
"fieldtype": "Int",
"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>",
"fieldname": "client_script",
@ -220,13 +206,13 @@
"collapsible": 1,
"fieldname": "actions",
"fieldtype": "Section Break",
"label": "Actions"
"label": "Customization"
},
{
"default": "Save",
"fieldname": "button_label",
"fieldtype": "Data",
"label": "Button Label"
"label": "Submit Button Label"
},
{
"description": "Message to be displayed on successful completion (only for Guest users)",
@ -235,36 +221,18 @@
"label": "Success Message"
},
{
"depends_on": "route_to_success_link",
"description": "Go to this URL after completing the form",
"fieldname": "success_url",
"fieldtype": "Data",
"label": "Success URL"
},
{
"collapsible": 1,
"fieldname": "sidebar_settings",
"fieldtype": "Section Break",
"label": "Sidebar Settings"
},
{
"default": "0",
"fieldname": "show_sidebar",
"fieldtype": "Check",
"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",
"fieldname": "accept_payment",
@ -321,18 +289,6 @@
"label": "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\"}]",
"fieldname": "breadcrumbs",
@ -345,13 +301,6 @@
"label": "Custom CSS",
"options": "CSS"
},
{
"collapsible": 1,
"collapsible_depends_on": "custom_css",
"fieldname": "custom_css_section",
"fieldtype": "Section Break",
"label": "Custom CSS"
},
{
"default": "0",
"fieldname": "apply_document_permissions",
@ -363,13 +312,93 @@
"fieldname": "is_multi_step_form",
"fieldtype": "Check",
"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,
"icon": "icon-edit",
"is_published_field": "published",
"links": [],
"modified": "2022-03-23 15:44:41.385001",
"modified": "2022-07-18 15:51:15.288860",
"modified_by": "Administrator",
"module": "Website",
"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.modules.utils import export_module_json, get_doc_module
from frappe.rate_limiter import rate_limit
from frappe.utils import cstr
from frappe.website.utils import get_comment_list
from frappe.utils import cstr, dict_with_keys, strip_html
from frappe.website.utils import get_boot_data, get_comment_list, get_sidebar_items
from frappe.website.website_generator import WebsiteGenerator
@ -32,17 +32,20 @@ class WebForm(WebsiteGenerator):
if not self.module:
self.module = frappe.db.get_value("DocType", self.doc_type, "module")
if (
not (
frappe.flags.in_install
or frappe.flags.in_patch
or frappe.flags.in_test
or frappe.flags.in_fixtures
)
and self.is_standard
and not frappe.conf.developer_mode
):
frappe.throw(_("You need to be in developer mode to edit a Standard Web Form"))
in_user_env = not (
frappe.flags.in_install
or frappe.flags.in_patch
or frappe.flags.in_test
or frappe.flags.in_fixtures
)
if in_user_env and self.is_standard and not frappe.conf.developer_mode:
# only published can be changed for standard web forms
if self.has_value_changed("published"):
published_value = self.published
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:
self.validate_fields()
@ -131,60 +134,131 @@ def get_context(context):
def get_context(self, context):
"""Build context to render the `web_form.html` template"""
context.is_form_editable = False
self.set_web_form_module()
doc, delimeter = make_route_string(frappe.form_dict)
context.doc = doc
context.delimeter = delimeter
if frappe.form_dict.is_list:
context.template = "website/doctype/web_form/templates/web_list.html"
else:
context.template = "website/doctype/web_form/templates/web_form.html"
# check permissions
if frappe.session.user == "Guest" and frappe.form_dict.name:
frappe.throw(
_("You need to be logged in to access this {0}.").format(self.doc_type), frappe.PermissionError
)
if frappe.form_dict.name:
if frappe.session.user == "Guest":
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(
self.doc_type, frappe.form_dict.name
if not frappe.db.exists(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(
_("You don't have the permissions to access this document"), frappe.PermissionError
)
context.is_form_editable = True
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()
if self.is_standard:
self.use_meta_fields()
if not frappe.session.user == "Guest":
if self.allow_edit:
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"
)
# add keys from form_dict to context
context.update(dict_with_keys(frappe.form_dict, ["is_list", "is_new", "is_edit", "is_read"]))
if not frappe.form_dict.name:
# only a single doc allowed and no existing doc, hence new
frappe.form_dict.new = 1
for df in self.web_form_fields:
if df.fieldtype == "Column Break":
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:
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
if not self.login_required or not self.allow_edit:
frappe.form_dict.new = 1
self.add_custom_context_and_script(context)
self.load_translations(context)
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)
if 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
)
@ -193,33 +267,40 @@ def get_context(context):
"'"
)
self.add_custom_context_and_script(context)
if not context.max_attachment_size:
context.max_attachment_size = get_max_file_size() / 1024 / 1024
context.show_in_grid = self.show_in_grid
self.load_translations(context)
context.link_title_doctypes = frappe.boot.get_link_title_doctypes()
# For Table fields, server-side processing for meta
for field in context.web_form_doc.web_form_fields:
if field.fieldtype == "Table":
field.fields = get_in_list_view_fields(field.options)
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)
if field.fieldtype == "Link":
field.fieldtype = "Autocomplete"
field.options = get_link_options(
self.name, field.options, field.allow_read_on_all_link_options
)
def load_document(self, context):
"""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)}]
context.reference_doc = {}
# load reference doc
if frappe.form_dict.name:
context.doc = frappe.get_doc(self.doc_type, frappe.form_dict.name)
context.title = context.doc.get(context.doc.meta.get_title_field())
context.doc.add_seen()
context.reference_doctype = context.doc.doctype
context.reference_name = context.doc.name
context.doc_name = frappe.form_dict.name
context.reference_doc = frappe.get_doc(self.doc_type, context.doc_name)
context.title = strip_html(
context.reference_doc.get(context.reference_doc.meta.get_title_field())
)
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:
context.attachments = frappe.get_all(
@ -233,7 +314,11 @@ def get_context(context):
)
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):
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 field in out.web_form.web_form_fields:
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})
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",
"allow_read_on_all_link_options",
"reqd",
"depends_on",
"read_only",
"show_in_filter",
"hidden",
@ -19,6 +18,7 @@
"max_length",
"max_value",
"property_depends_on_section",
"depends_on",
"mandatory_depends_on",
"column_break_16",
"read_only_depends_on",
@ -63,7 +63,7 @@
{
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On"
"label": "Display Depends On"
},
{
"default": "0",
@ -146,12 +146,13 @@
],
"istable": 1,
"links": [],
"modified": "2022-01-28 10:41:25.422345",
"modified": "2022-06-06 16:00:55.627950",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form Field",
"owner": "Administrator",
"permissions": [],
"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,
get_comment_list,
get_html_content_based_on_type,
get_sidebar_items,
)
from frappe.website.website_generator import WebsiteGenerator
@ -70,6 +71,9 @@ class WebPage(WebsiteGenerator):
if not self.show_title:
context["no_header"] = 1
if self.show_sidebar:
context.sidebar_items = get_sidebar_items(self.website_sidebar)
self.set_metatags(context)
self.set_breadcrumbs(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.model.document import Document
from frappe.utils import encode, get_request_site_address
from frappe.website.utils import get_boot_data
class WebsiteSettings(Document):
@ -190,6 +191,8 @@ def get_website_settings(context=None):
if settings.splash_image:
context["splash_image"] = settings.splash_image
context.boot = get_boot_data()
return context

View file

@ -1,11 +1,13 @@
import frappe
from frappe.website.page_renderers.document_page import DocumentPage
from frappe.website.router import get_page_info_from_web_form
class WebFormPage(DocumentPage):
def can_render(self):
webform_name = frappe.db.exists("Web Form", {"route": self.path, "published": 1}, cache=True)
if webform_name:
web_form = get_page_info_from_web_form(self.path)
if web_form:
self.doctype = "Web Form"
self.docname = webform_name
return bool(webform_name)
self.docname = web_form.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]
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):
"""
Use Werkzeug routing to evaluate dynamic routes like /project/<name>

View file

@ -1,5 +1,6 @@
import frappe
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.redirect_page import RedirectPage
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()
except frappe.PermissionError as e:
response = NotPermittedPage(endpoint, http_status_code, exception=e).render()
except frappe.PageDoesNotExistError:
response = NotFoundPage(endpoint, http_status_code).render()
except Exception as e:
frappe.log_error(f"{path} failed")
response = ErrorPage(exception=e).render()

View file

@ -12,7 +12,7 @@ from werkzeug.wrappers import Response
import frappe
from frappe import _
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)
H1_TAG_PATTERN = re.compile("<h1>([^<]*)")
@ -158,6 +158,20 @@ def get_home_page_via_hooks():
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():
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
sidebar_items = []

View file

@ -11,6 +11,7 @@
"apply_document_permissions": 0,
"breadcrumbs": "",
"button_label": "Request Data",
"client_script": "",
"creation": "2019-01-24 16:19:26.886096",
"currency": "INR",
"doc_type": "Personal Data Download Request",
@ -18,10 +19,12 @@
"doctype": "Web Form",
"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>",
"is_multi_step_form": 0,
"is_standard": 1,
"list_columns": [],
"login_required": 0,
"max_attachment_size": 0,
"modified": "2021-03-25 10:52:13.149538",
"modified": "2022-07-18 16:51:07.281527",
"modified_by": "Administrator",
"module": "Website",
"name": "request-data",
@ -31,9 +34,8 @@
"route": "request-data",
"route_to_success_link": 1,
"show_attachments": 0,
"show_in_grid": 0,
"show_list": 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_url": "/desk",
"title": "Request Data",

View file

@ -9,6 +9,7 @@
"amount": 0.0,
"amount_based_on_field": 0,
"apply_document_permissions": 0,
"breadcrumbs": "",
"button_label": "Submit",
"client_script": "",
"creation": "2019-01-25 14:24:12.588810",
@ -19,10 +20,12 @@
"doctype": "Web Form",
"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>",
"is_multi_step_form": 0,
"is_standard": 1,
"list_columns": [],
"login_required": 0,
"max_attachment_size": 0,
"modified": "2021-11-30 17:56:03.099870",
"modified": "2022-07-18 16:51:30.949738",
"modified_by": "Administrator",
"module": "Website",
"name": "request-to-delete-data",
@ -32,9 +35,8 @@
"route": "request-for-account-deletion",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_list": 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_url": "/",
"title": "Request for Account Deletion",