diff --git a/cypress/integration/web_form.js b/cypress/integration/web_form.js index bd1c7e147e..74edee0eb9 100644 --- a/cypress/integration/web_form.js +++ b/cypress/integration/web_form.js @@ -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'); }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 5ee26348e2..5424e8c6e4 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -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(); diff --git a/frappe/__init__.py b/frappe/__init__.py index 3057eacd3b..cd1bfc5583 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -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 diff --git a/frappe/core/web_form/edit_profile/edit_profile.json b/frappe/core/web_form/edit_profile/edit_profile.json index c04e705820..cedef71c0e 100644 --- a/frappe/core/web_form/edit_profile/edit_profile.json +++ b/frappe/core/web_form/edit_profile/edit_profile.json @@ -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", diff --git a/frappe/patches.txt b/frappe/patches.txt index 437648bf9e..f79cadae87 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -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 diff --git a/frappe/patches/v14_0/update_webforms.py b/frappe/patches/v14_0/update_webforms.py new file mode 100644 index 0000000000..46918f216e --- /dev/null +++ b/frappe/patches/v14_0/update_webforms.py @@ -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) diff --git a/frappe/public/js/frappe-web.bundle.js b/frappe/public/js/frappe-web.bundle.js index a3bac55e23..21703f83b8 100644 --- a/frappe/public/js/frappe-web.bundle.js +++ b/frappe/public/js/frappe-web.bundle.js @@ -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"; diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js index a086b1b879..c266a928e6 100644 --- a/frappe/public/js/frappe/form/controls/datetime.js +++ b/frappe/public/js/frappe/form/controls/datetime.js @@ -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); } diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 5cf5a2f4f3..5a15b4fd45 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -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 ""; } diff --git a/frappe/public/js/frappe/web_form/web_form.js b/frappe/public/js/frappe/web_form/web_form.js index 11e0b782ae..21d88eac49 100644 --- a/frappe/public/js/frappe/web_form/web_form.js +++ b/frappe/public/js/frappe/web_form/web_form.js @@ -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); } } diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index 27e1695788..a4e7480f94 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -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 = $(`
`); + + this.make_table_head(); + this.make_table_body(); + } + + make_table_head() { + let $thead = $(` + + + + + + ${__("Sr")}. + + + `); + + 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 = $(`${__(col.label)}`); + $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 = ` - - `; - - empty_state.innerHTML = ` -
-
- Generic Empty State -
-

${__("No {0} found", [__(this.doctype)])}

- ${new_button} -
- `; - - 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 = ` + + `; - let th = document.createElement("th"); + let empty_state = $(` +
+
+
+ Generic Empty State +
+

${__("No {0} found", [__(this.doctype)])}

+ ${new_button} +
+
+ `); - 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.appendTo(this.table); + } + row_data.forEach((data_item) => { - let row_element = tbody.insertRow(); - row_element.setAttribute("id", data_item.name); + let $row_element = $(``); 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.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 = $(``); - this.checkbox = document.createElement("input"); - this.checkbox.type = "checkbox"; - this.checkbox.onclick = event => { + this.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 = $(`${__(this.serial_number)}`); + 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 = $(`${value}`); + 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"); } }; diff --git a/frappe/public/js/frappe/web_form/webform_script.js b/frappe/public/js/frappe/web_form/webform_script.js index 30ff03cb5d..31fecc778c 100644 --- a/frappe/public/js/frappe/web_form/webform_script.js +++ b/frappe/public/js/frappe/web_form/webform_script.js @@ -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; + } + }); } }); diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss index 1e68f374c4..0d7ca9ac06 100644 --- a/frappe/public/scss/desk/global.scss +++ b/frappe/public/scss/desk/global.scss @@ -76,6 +76,10 @@ a.badge-hover { text-decoration: underline; } +.pointer { + cursor: pointer; +} + .inline-block { display: inline-block; } diff --git a/frappe/public/scss/website/web_form.scss b/frappe/public/scss/website/web_form.scss index 010182e1e5..77c3d21880 100644 --- a/frappe/public/scss/website/web_form.scss +++ b/frappe/public/scss/website/web_form.scss @@ -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; - } - } -} diff --git a/frappe/templates/base.html b/frappe/templates/base.html index b11b775179..e3bfea559e 100644 --- a/frappe/templates/base.html +++ b/frappe/templates/base.html @@ -96,12 +96,7 @@ {% block base_scripts %} diff --git a/frappe/tests/test_webform.py b/frappe/tests/test_webform.py index d95e4a7498..51868dfdb1 100644 --- a/frappe/tests/test_webform.py +++ b/frappe/tests/test_webform.py @@ -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) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 8a970e57cc..262cc3fc7d 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -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", diff --git a/frappe/website/doctype/web_form/templates/web_form.html b/frappe/website/doctype/web_form/templates/web_form.html index 96072a19ea..5b35e6b5b4 100644 --- a/frappe/website/doctype/web_form/templates/web_form.html +++ b/frappe/website/doctype/web_form/templates/web_form.html @@ -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 %} + + + + + {% endif %} + + {% if allow_edit and doc_name and not is_form_editable %} + + {{ _("Edit", null, "Button in web form") }} + {% endif %} + + {% if is_new or is_form_editable %} + + {{ _("Cancel", null, "Button in web form") }} + + + {% endif %} {% endmacro %} {% block page_content %} -{% if has_header and login_required and allow_multiple %} - -{% include "templates/includes/breadcrumbs.html" %} -{% else %} -
-{% endif %} - - -
- {% if is_list %} - -
-

{{ _(title) }}

-
-
- -
-
-
- - {% else %} - -
-

{{ _(title) }}

-
-
-
- -
- -
- - {% if show_attachments and not frappe.form_dict.new and attachments %} -
-
{{ _("Attachments") }}
- {% for attachment in attachments %} - - {% endfor %} -
- {% endif %} {# attachments #} - + + {% if has_header and login_required and show_list %} + {% include "templates/includes/breadcrumbs.html" %} + {% else %} +
{% endif %} -
-{% if allow_comments and not frappe.form_dict.new and not is_list -%} - -
- {% include 'templates/includes/comments/comments.html' %} -
-{%- else -%} -
-{%- endif %} {# comments #} + +
+
+

{{ _(title) }}

+
+ {{ action_buttons() }} +
+
+
+ {% if introduction_text %} +
{{ introduction_text }}
+ {% endif %} +
+ +
+ + + {% if show_attachments and not is_new and attachments %} +
+
{{ _("Attachments") }}
+ {% for attachment in attachments %} + +
+ + {{ attachment.file_name }} +
+
{{ attachment.file_size }}
+
+ {% endfor %} +
+ {% endif %} {# attachments #} +
+ + + {% if allow_comments and not is_new and not is_list -%} +
+

{{ _("Comments") }}

+ {% include 'templates/includes/comments/comments.html' %} +
+ {%- else -%} +
+ {%- endif %} {# comments #} {% endblock page_content %} {% block script %} - -{{ include_script("controls.bundle.js") }} -{% if is_list %} -{{ include_script("dialog.bundle.js") }} -{{ include_script("web_form.bundle.js") }} -{{ include_script("bootstrap-4-web.bundle.js") }} -{% else %} -{{ include_script("dialog.bundle.js") }} - - -{{ include_script("web_form.bundle.js") }} -{{ include_script("bootstrap-4-web.bundle.js") }} - -{% 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 %} - -{% endif %} + {{ 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 %} -{% if not is_list %} -{{ include_style('web_form.bundle.css') }} -{% endif %} - - + {% endblock %} diff --git a/frappe/website/doctype/web_form/templates/web_form_row.html b/frappe/website/doctype/web_form/templates/web_form_row.html deleted file mode 100644 index 2b999819cb..0000000000 --- a/frappe/website/doctype/web_form/templates/web_form_row.html +++ /dev/null @@ -1,4 +0,0 @@ -
- {{ title }} -
- \ No newline at end of file diff --git a/frappe/website/doctype/web_form/templates/web_list.html b/frappe/website/doctype/web_form/templates/web_list.html new file mode 100644 index 0000000000..2ec6edaf1c --- /dev/null +++ b/frappe/website/doctype/web_form/templates/web_list.html @@ -0,0 +1,45 @@ +{% extends "templates/web.html" %} + +{% block breadcrumbs %}{% endblock %} + +{% block page_content %} + +
+ +
+

{{ _(list_title or title) }}

+
+ {%- if allow_multiple -%} + New + {%- endif -%} +
+
+
+
+ +
+{% endblock page_content %} + +{% block 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 %} + +{% endblock %} \ No newline at end of file diff --git a/frappe/website/doctype/web_form/test_web_form.py b/frappe/website/doctype/web_form/test_web_form.py index 5689bdbeef..13c73d1f14 100644 --- a/frappe/website/doctype/web_form/test_web_form.py +++ b/frappe/website/doctype/web_form/test_web_form.py @@ -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("

Request Data

", content) + set_request(method="GET", path="manage-events/new") + content = get_response_content("manage-events/new") + self.assertIn("

New Manage Events

", 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) diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index 1f27b350be..63b71d35b4 100644 --- a/frappe/website/doctype/web_form/web_form.js +++ b/frappe/website/doctype/web_form/web_form.js @@ -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 = ` + + ${__("Form Settings Tab")} + + `; + $(frm.fields_dict['list_setting_message'].wrapper) + .html($( + `
+ ${__("Login is required to see web form list view. Enable login_required from {0} to see list settings", [switch_to_form_settings_tab])} +
` + )) + .find('span') + .click(() => frm.scroll_to_field('login_required')); + } else { + $(frm.fields_dict['list_setting_message'].wrapper).empty(); + } +} diff --git a/frappe/website/doctype/web_form/web_form.json b/frappe/website/doctype/web_form/web_form.json index 08b2854059..0872c1d654 100644 --- a/frappe/website/doctype/web_form/web_form.json +++ b/frappe/website/doctype/web_form/web_form.json @@ -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 Client Script API and Examples", "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", diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index ee8861a7aa..e1c9e798e5 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -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": diff --git a/frappe/website/doctype/web_form/web_form_list.js b/frappe/website/doctype/web_form/web_form_list.js new file mode 100644 index 0000000000..f426fd9899 --- /dev/null +++ b/frappe/website/doctype/web_form/web_form_list.js @@ -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"]; + } + } +}; \ No newline at end of file diff --git a/frappe/website/doctype/web_form_field/web_form_field.json b/frappe/website/doctype/web_form_field/web_form_field.json index 36b1ca2c15..4e0d58d42d 100644 --- a/frappe/website/doctype/web_form_field/web_form_field.json +++ b/frappe/website/doctype/web_form_field/web_form_field.json @@ -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": [] +} \ No newline at end of file diff --git a/frappe/website/doctype/web_form_list_column/__init__.py b/frappe/website/doctype/web_form_list_column/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/doctype/web_form_list_column/web_form_list_column.json b/frappe/website/doctype/web_form_list_column/web_form_list_column.json new file mode 100644 index 0000000000..e55aeadca6 --- /dev/null +++ b/frappe/website/doctype/web_form_list_column/web_form_list_column.json @@ -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": [] +} \ No newline at end of file diff --git a/frappe/website/doctype/web_form_list_column/web_form_list_column.py b/frappe/website/doctype/web_form_list_column/web_form_list_column.py new file mode 100644 index 0000000000..9aff5f1ecc --- /dev/null +++ b/frappe/website/doctype/web_form_list_column/web_form_list_column.py @@ -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 diff --git a/frappe/website/doctype/web_page/web_page.py b/frappe/website/doctype/web_page/web_page.py index a94838baed..b523eb2e83 100644 --- a/frappe/website/doctype/web_page/web_page.py +++ b/frappe/website/doctype/web_page/web_page.py @@ -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) diff --git a/frappe/website/doctype/website_settings/website_settings.py b/frappe/website/doctype/website_settings/website_settings.py index bd634b4f32..fffbd94684 100644 --- a/frappe/website/doctype/website_settings/website_settings.py +++ b/frappe/website/doctype/website_settings/website_settings.py @@ -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 diff --git a/frappe/website/page_renderers/web_form.py b/frappe/website/page_renderers/web_form.py index 1953118790..74996e4a78 100644 --- a/frappe/website/page_renderers/web_form.py +++ b/frappe/website/page_renderers/web_form.py @@ -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 diff --git a/frappe/website/router.py b/frappe/website/router.py index 24a085224b..aa1e15d4c9 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -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}/", endpoint=d.name)) + rules.append(Rule(f"/{d.route}//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/ diff --git a/frappe/website/serve.py b/frappe/website/serve.py index 2c33b5df51..7eb8b017f1 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -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() diff --git a/frappe/website/utils.py b/frappe/website/utils.py index 8a0cfbab7c..508026f064 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -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("

([^<]*)") @@ -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 = [] diff --git a/frappe/website/web_form/request_data/request_data.json b/frappe/website/web_form/request_data/request_data.json index 591ef4a031..c52a2f6203 100644 --- a/frappe/website/web_form/request_data/request_data.json +++ b/frappe/website/web_form/request_data/request_data.json @@ -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": "

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 request to delete data.

", + "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", diff --git a/frappe/website/web_form/request_to_delete_data/request_to_delete_data.json b/frappe/website/web_form/request_to_delete_data/request_to_delete_data.json index 1113297df6..ce11666a34 100644 --- a/frappe/website/web_form/request_to_delete_data/request_to_delete_data.json +++ b/frappe/website/web_form/request_to_delete_data/request_to_delete_data.json @@ -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": "

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 request your data.

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