Merge branch 'develop' into wiki-based-desk
This commit is contained in:
commit
29616c080e
61 changed files with 942 additions and 265 deletions
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
|
|
@ -105,3 +105,5 @@ jobs:
|
|||
|
||||
- name: UI Tests
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --headless --parallel --ci-build-id $GITHUB_RUN_ID
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb
|
||||
|
|
|
|||
|
|
@ -4,13 +4,10 @@
|
|||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
* @frappe/frappe-review-team
|
||||
website/ @prssanna
|
||||
web_form/ @prssanna
|
||||
templates/ @surajshetty3416
|
||||
www/ @surajshetty3416
|
||||
integrations/ @leela
|
||||
patches/ @surajshetty3416
|
||||
dashboard/ @prssanna
|
||||
email/ @leela
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
|
|
|
|||
88
cypress/integration/form_tour.js
Normal file
88
cypress/integration/form_tour.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
context('Form Tour', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/form-tour');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call("frappe.tests.ui_test_helpers.create_form_tour");
|
||||
});
|
||||
});
|
||||
|
||||
const open_test_form_tour = () => {
|
||||
cy.visit('/app/form-tour/Test Form Tour');
|
||||
cy.get('button[data-label="Show%20Tour"]').should('be.visible').and('contain', 'Show Tour').as('show_tour');
|
||||
cy.get('@show_tour').click();
|
||||
cy.wait(500);
|
||||
cy.url().should('include', '/app/contact');
|
||||
};
|
||||
|
||||
it('jump to a form tour', open_test_form_tour);
|
||||
|
||||
it('navigates a form tour', () => {
|
||||
open_test_form_tour();
|
||||
|
||||
cy.get('#driver-popover-item').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname="first_name"]').as('first_name');
|
||||
cy.get('@first_name').should('have.class', 'driver-highlighted-element');
|
||||
cy.get('.driver-next-btn').as('next_btn');
|
||||
|
||||
// next btn shouldn't move to next step, if first name is not entered
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
cy.get('@first_name').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// after filling the field, next step should be highlighted
|
||||
cy.fill_field('first_name', 'Test Name', 'Data');
|
||||
cy.wait(500);
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert field is highlighted
|
||||
cy.get('.frappe-control[data-fieldname="last_name"]').as('last_name');
|
||||
cy.get('@last_name').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// after filling the field, next step should be highlighted
|
||||
cy.fill_field('last_name', 'Test Last Name', 'Data');
|
||||
cy.wait(500);
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert field is highlighted
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('phone_nos');
|
||||
cy.get('@phone_nos').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// move to next step
|
||||
cy.wait(500);
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert add row btn is highlighted
|
||||
cy.get('@phone_nos').find('.grid-add-row').as('add_row');
|
||||
cy.get('@add_row').should('have.class', 'driver-highlighted-element');
|
||||
|
||||
// add a row & move to next step
|
||||
cy.wait(500);
|
||||
cy.get('@add_row').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert table field is highlighted
|
||||
cy.get('.grid-row-open .frappe-control[data-fieldname="phone"]').as('phone');
|
||||
cy.get('@phone').should('have.class', 'driver-highlighted-element');
|
||||
// enter value in a table field
|
||||
cy.fill_table_field('phone_nos', '1', 'phone', '1234567890');
|
||||
|
||||
// move to collapse row step
|
||||
cy.wait(500);
|
||||
cy.get('@next_btn').click();
|
||||
cy.wait(500);
|
||||
|
||||
// collapse row
|
||||
cy.get('.grid-row-open .grid-collapse-row').click();
|
||||
cy.wait(500);
|
||||
|
||||
// assert save btn is highlighted
|
||||
cy.get('.primary-action').should('have.class', 'driver-highlighted-element');
|
||||
cy.get('@next_btn').should('contain', 'Save');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1683,7 +1683,7 @@ def get_desk_link(doctype, name):
|
|||
)
|
||||
|
||||
def bold(text):
|
||||
return '<b>{0}</b>'.format(text)
|
||||
return '<strong>{0}</strong>'.format(text)
|
||||
|
||||
def safe_eval(code, eval_globals=None, eval_locals=None):
|
||||
'''A safer `eval`'''
|
||||
|
|
|
|||
|
|
@ -661,7 +661,7 @@ def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
|
|||
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile")
|
||||
|
||||
# run for headless mode
|
||||
run_or_open = 'run --browser firefox --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
|
||||
run_or_open = 'run --browser firefox --record' if headless else 'open'
|
||||
command = '{site_env} {password_env} {cypress} {run_or_open}'
|
||||
formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)
|
||||
|
||||
|
|
@ -770,19 +770,23 @@ def set_config(context, key, value, global_=False, parse=False, as_dict=False):
|
|||
@click.command('version')
|
||||
def get_version():
|
||||
"Show the versions of all the installed apps"
|
||||
from git import Repo
|
||||
from frappe.utils.change_log import get_app_branch
|
||||
frappe.init('')
|
||||
|
||||
for m in sorted(frappe.get_all_apps()):
|
||||
branch_name = get_app_branch(m)
|
||||
module = frappe.get_module(m)
|
||||
app_hooks = frappe.get_module(m + ".hooks")
|
||||
for app in sorted(frappe.get_all_apps()):
|
||||
branch_name = get_app_branch(app)
|
||||
module = frappe.get_module(app)
|
||||
app_hooks = frappe.get_module(app + ".hooks")
|
||||
repo = Repo(frappe.get_app_path(app, ".."))
|
||||
branch = repo.head.ref.name
|
||||
commit = repo.head.ref.commit.hexsha[:7]
|
||||
|
||||
if hasattr(app_hooks, '{0}_version'.format(branch_name)):
|
||||
print("{0} {1}".format(m, getattr(app_hooks, '{0}_version'.format(branch_name))))
|
||||
click.echo("{0} {1} {2} ({3})".format(app, getattr(app_hooks, '{0}_version'.format(branch_name)), branch, commit))
|
||||
|
||||
elif hasattr(module, "__version__"):
|
||||
print("{0} {1}".format(m, module.__version__))
|
||||
click.echo("{0} {1} {2} ({3})".format(app, module.__version__, branch, commit))
|
||||
|
||||
|
||||
@click.command('rebuild-global-search')
|
||||
|
|
|
|||
|
|
@ -85,8 +85,6 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
|
|||
if attachments:
|
||||
add_attachments(comm.name, attachments)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
if cint(send_email):
|
||||
if not comm.get_outgoing_email_account():
|
||||
frappe.throw(msg=OUTGOING_EMAIL_ACCOUNT_MISSING, exc=frappe.OutgoingEmailError)
|
||||
|
|
|
|||
|
|
@ -62,9 +62,9 @@ class TestImporter(unittest.TestCase):
|
|||
data_import.reload()
|
||||
import_log = frappe.parse_json(data_import.import_log)
|
||||
self.assertEqual(import_log[0]['row_indexes'], [2,3])
|
||||
expected_error = "Error: <b>Child 1 of DocType for Import</b> Row #1: Value missing for: Child Title"
|
||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(import_log[0]['messages'][0])['message'], expected_error)
|
||||
expected_error = "Error: <b>Child 1 of DocType for Import</b> Row #2: Value missing for: Child Title"
|
||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(import_log[0]['messages'][1])['message'], expected_error)
|
||||
|
||||
self.assertEqual(import_log[1]['row_indexes'], [4])
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ def get_event_conditions(doctype, filters=None):
|
|||
|
||||
@frappe.whitelist()
|
||||
def get_events(doctype, start, end, field_map, filters=None, fields=None):
|
||||
|
||||
field_map = frappe._dict(json.loads(field_map))
|
||||
fields = frappe.parse_json(fields)
|
||||
|
||||
|
|
@ -36,8 +35,7 @@ def get_events(doctype, start, end, field_map, filters=None, fields=None):
|
|||
"color": d.fieldname
|
||||
})
|
||||
|
||||
if filters:
|
||||
filters = json.loads(filters or '')
|
||||
filters = json.loads(filters) if filters else []
|
||||
|
||||
if not fields:
|
||||
fields = [field_map.start, field_map.end, field_map.title, 'name']
|
||||
|
|
@ -52,5 +50,5 @@ def get_events(doctype, start, end, field_map, filters=None, fields=None):
|
|||
[doctype, start_date, '<=', end],
|
||||
[doctype, end_date, '>=', start],
|
||||
]
|
||||
|
||||
fields = list({field for field in fields if field})
|
||||
return frappe.get_list(doctype, fields=fields, filters=filters)
|
||||
|
|
|
|||
|
|
@ -368,6 +368,7 @@ def get_desktop_page(page):
|
|||
'allow_customization': not wspace.doc.disable_user_customization
|
||||
}
|
||||
except DoesNotExistError:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
return {}
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,45 @@
|
|||
|
||||
frappe.ui.form.on('Form Tour', {
|
||||
setup: function(frm) {
|
||||
if (!frm.doc.is_standard || frappe.boot.developer_mode) {
|
||||
frm.trigger('setup_queries');
|
||||
}
|
||||
},
|
||||
|
||||
refresh(frm) {
|
||||
if (frm.doc.is_standard && !frappe.boot.developer_mode) {
|
||||
frm.trigger("disable_form");
|
||||
}
|
||||
|
||||
frm.add_custom_button(__('Show Tour'), async () => {
|
||||
const issingle = await check_if_single(frm.doc.reference_doctype);
|
||||
|
||||
if (issingle) {
|
||||
frappe.set_route('Form', frm.doc.reference_doctype);
|
||||
} else {
|
||||
const new_name = 'new-' + frappe.scrub(frm.doc.reference_doctype) + '-1';
|
||||
frappe.set_route('Form', frm.doc.reference_doctype, new_name);
|
||||
}
|
||||
frappe.utils.sleep(500).then(() => {
|
||||
const tour_name = frm.doc.name;
|
||||
cur_frm.tour
|
||||
.init({ tour_name })
|
||||
.then(() => cur_frm.tour.start());
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
disable_form: function(frm) {
|
||||
frm.set_read_only();
|
||||
frm.fields
|
||||
.filter((field) => field.has_input)
|
||||
.forEach((field) => {
|
||||
frm.set_df_property(field.df.fieldname, "read_only", "1");
|
||||
});
|
||||
frm.disable_save();
|
||||
},
|
||||
|
||||
setup_queries(frm) {
|
||||
frm.set_query("reference_doctype", function() {
|
||||
return {
|
||||
filters: {
|
||||
|
|
@ -20,5 +59,65 @@ frappe.ui.form.on('Form Tour', {
|
|||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("parent_field", "steps", function() {
|
||||
return {
|
||||
query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
|
||||
filters: {
|
||||
doctype: frm.doc.reference_doctype,
|
||||
fieldtype: "Table",
|
||||
hidden: 0,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.trigger('reference_doctype');
|
||||
},
|
||||
|
||||
reference_doctype(frm) {
|
||||
if (!frm.doc.reference_doctype) return;
|
||||
|
||||
frappe.db.get_list('DocField', {
|
||||
filters: {
|
||||
parent: frm.doc.reference_doctype,
|
||||
parenttype: 'DocType',
|
||||
fieldtype: 'Table'
|
||||
},
|
||||
fields: ['options']
|
||||
}).then(res => {
|
||||
if (Array.isArray(res)) {
|
||||
frm.child_doctypes = res.map(r => r.options);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on('Form Tour Step', {
|
||||
parent_field(frm, cdt, cdn) {
|
||||
const child_row = locals[cdt][cdn];
|
||||
frappe.model.set_value(cdt, cdn, 'field', '');
|
||||
const field_control = get_child_field("steps", cdn, "field");
|
||||
field_control.get_query = function() {
|
||||
return {
|
||||
query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
|
||||
filters: {
|
||||
doctype: child_row.child_doctype,
|
||||
hidden: 0
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function get_child_field(child_table, child_name, fieldname) {
|
||||
// gets the field from grid row form
|
||||
const grid = cur_frm.fields_dict[child_table].grid;
|
||||
const grid_row = grid.grid_rows_by_docname[child_name];
|
||||
return grid_row.grid_form.fields_dict[fieldname];
|
||||
}
|
||||
|
||||
async function check_if_single(doctype) {
|
||||
const { message } = await frappe.db.get_value('DocType', doctype, 'issingle');
|
||||
return message.issingle || 0;
|
||||
}
|
||||
|
|
@ -8,7 +8,9 @@
|
|||
"field_order": [
|
||||
"title",
|
||||
"reference_doctype",
|
||||
"completed",
|
||||
"module",
|
||||
"is_standard",
|
||||
"save_on_complete",
|
||||
"section_break_3",
|
||||
"steps"
|
||||
],
|
||||
|
|
@ -19,23 +21,16 @@
|
|||
"in_list_view": 1,
|
||||
"label": "Reference Document",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "reference_doctype",
|
||||
"fieldname": "steps",
|
||||
"fieldtype": "Table",
|
||||
"label": "Steps",
|
||||
"options": "Form Tour Step",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.__islocal != 1",
|
||||
"fieldname": "completed",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mark as Completed"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_3",
|
||||
"fieldtype": "Section Break"
|
||||
|
|
@ -46,11 +41,32 @@
|
|||
"label": "Title",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "save_on_complete",
|
||||
"fieldtype": "Check",
|
||||
"label": "Save on Completion"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_standard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Standard"
|
||||
},
|
||||
{
|
||||
"fetch_from": "reference_doctype.module",
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Module",
|
||||
"options": "Module Def",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-26 19:36:59.093753",
|
||||
"modified": "2021-06-06 20:32:54.068774",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Form Tour",
|
||||
|
|
|
|||
|
|
@ -3,9 +3,33 @@
|
|||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules.export_file import export_to_files
|
||||
|
||||
class FormTour(Document):
|
||||
pass
|
||||
def before_insert(self):
|
||||
if not self.is_standard:
|
||||
return
|
||||
|
||||
# while syncing, set proper docfield reference
|
||||
for d in self.steps:
|
||||
if not frappe.db.exists('DocField', d.field):
|
||||
d.field = frappe.db.get_value('DocField', {
|
||||
'fieldname': d.fieldname, 'parent': self.reference_doctype, 'fieldtype': d.fieldtype
|
||||
}, "name")
|
||||
|
||||
if d.is_table_field and not frappe.db.exists('DocField', d.parent_field):
|
||||
d.parent_field = frappe.db.get_value('DocField', {
|
||||
'fieldname': d.parent_fieldname, 'parent': self.reference_doctype, 'fieldtype': 'Table'
|
||||
}, "name")
|
||||
|
||||
def on_update(self):
|
||||
if frappe.conf.developer_mode and self.is_standard:
|
||||
export_to_files([['Form Tour', self.name]], self.module)
|
||||
|
||||
def before_export(self, doc):
|
||||
for d in doc.steps:
|
||||
d.field = ""
|
||||
d.parent_field = ""
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
|
|
@ -16,17 +40,23 @@ def get_docfield_list(doctype, txt, searchfield, start, page_len, filters):
|
|||
['fieldtype', 'like', '%' + txt + '%']
|
||||
]
|
||||
|
||||
parent_doctype = filters.pop('doctype')
|
||||
excluded_fieldtypes = ['Column Break']
|
||||
excluded_fieldtypes += filters.get('excluded_fieldtypes', [])
|
||||
parent_doctype = filters.get('doctype')
|
||||
fieldtype = filters.get('fieldtype')
|
||||
if not fieldtype:
|
||||
excluded_fieldtypes = ['Column Break']
|
||||
excluded_fieldtypes += filters.get('excluded_fieldtypes', [])
|
||||
fieldtype_filter = ['not in', excluded_fieldtypes]
|
||||
else:
|
||||
fieldtype_filter = fieldtype
|
||||
|
||||
docfields = frappe.get_all(
|
||||
doctype,
|
||||
fields=["name as value", "label", "fieldtype"],
|
||||
filters={'parent': parent_doctype, 'fieldtype': ['not in', excluded_fieldtypes]},
|
||||
filters={'parent': parent_doctype, 'fieldtype': fieldtype_filter},
|
||||
or_filters=or_filters,
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
order_by="idx",
|
||||
as_list=1,
|
||||
)
|
||||
return docfields
|
||||
|
|
|
|||
|
|
@ -4,14 +4,22 @@
|
|||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"is_table_field",
|
||||
"section_break_2",
|
||||
"parent_field",
|
||||
"field",
|
||||
"title",
|
||||
"description",
|
||||
"column_break_2",
|
||||
"position",
|
||||
"fieldname",
|
||||
"label",
|
||||
"condition"
|
||||
"has_next_condition",
|
||||
"next_step_condition",
|
||||
"section_break_13",
|
||||
"fieldname",
|
||||
"parent_fieldname",
|
||||
"fieldtype",
|
||||
"child_doctype"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -30,6 +38,7 @@
|
|||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_field))",
|
||||
"fieldname": "field",
|
||||
"fieldtype": "Link",
|
||||
"label": "Field",
|
||||
|
|
@ -64,16 +73,73 @@
|
|||
"options": "Left\nLeft Center\nLeft Bottom\nTop\nTop Center\nTop Right\nRight\nRight Center\nRight Bottom\nBottom\nBottom Center\nBottom Right\nMid Center"
|
||||
},
|
||||
{
|
||||
"depends_on": "has_next_condition",
|
||||
"fieldname": "next_step_condition",
|
||||
"fieldtype": "Code",
|
||||
"label": "Next Step Condition",
|
||||
"oldfieldname": "condition",
|
||||
"options": "JS"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "has_next_condition",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Next Condition"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "field.fieldtype",
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Fieldtype",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_table_field",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Table Field"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_2",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_table_field",
|
||||
"fieldname": "parent_field",
|
||||
"fieldtype": "Link",
|
||||
"label": "Parent Field",
|
||||
"mandatory_depends_on": "is_table_field",
|
||||
"options": "DocField"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_13",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 1,
|
||||
"label": "Hidden Fields"
|
||||
},
|
||||
{
|
||||
"fetch_from": "parent_field.options",
|
||||
"fieldname": "child_doctype",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Child Doctype",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "parent_field.fieldname",
|
||||
"fieldname": "parent_fieldname",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Parent Fieldname",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-26 19:44:48.737453",
|
||||
"modified": "2021-06-06 20:52:21.076972",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Form Tour Step",
|
||||
|
|
@ -82,4 +148,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -45,19 +45,19 @@ class Workspace(Document):
|
|||
|
||||
def get_link_groups(self):
|
||||
cards = []
|
||||
current_card = {
|
||||
current_card = frappe._dict({
|
||||
"label": "Link",
|
||||
"type": "Card Break",
|
||||
"icon": None,
|
||||
"hidden": False,
|
||||
}
|
||||
})
|
||||
|
||||
card_links = []
|
||||
|
||||
for link in self.links:
|
||||
link = link.as_dict()
|
||||
if link.type == "Card Break":
|
||||
if card_links and (not current_card.only_for or current_card.only_for == frappe.get_system_settings('country')):
|
||||
if card_links and (not current_card.only_for or current_card.only_for == frappe.get_system_settings('country')):
|
||||
current_card['links'] = card_links
|
||||
cards.append(current_card)
|
||||
|
||||
|
|
|
|||
|
|
@ -83,11 +83,15 @@ class BaseDocument(object):
|
|||
|
||||
@property
|
||||
def meta(self):
|
||||
if not hasattr(self, "_meta"):
|
||||
if not getattr(self, "_meta", None):
|
||||
self._meta = frappe.get_meta(self.doctype)
|
||||
|
||||
return self._meta
|
||||
|
||||
def __getstate__(self):
|
||||
self._meta = None
|
||||
return self.__dict__
|
||||
|
||||
def update(self, d):
|
||||
""" Update multiple fields of a doctype using a dictionary of key-value pairs.
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ def get_doc_files(files, start_path):
|
|||
document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
|
||||
'website_theme', 'web_form', 'web_template', 'notification', 'print_style',
|
||||
'data_migration_mapping', 'data_migration_plan', 'workspace',
|
||||
'onboarding_step', 'module_onboarding']
|
||||
'onboarding_step', 'module_onboarding', 'form_tour']
|
||||
|
||||
for doctype in document_types:
|
||||
doctype_path = os.path.join(start_path, doctype)
|
||||
|
|
|
|||
|
|
@ -113,22 +113,20 @@ frappe.ui.form.PrintView = class {
|
|||
},
|
||||
).$input;
|
||||
|
||||
this.letterhead_selector = this.add_sidebar_item(
|
||||
this.letterhead_selector_df = this.add_sidebar_item(
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
fieldtype: 'Autocomplete',
|
||||
fieldname: 'letterhead',
|
||||
label: __('Select Letterhead'),
|
||||
options: [
|
||||
this.get_default_option_for_select(__('Select Letterhead')),
|
||||
__('No Letterhead')
|
||||
],
|
||||
placeholder: __('Select Letterhead'),
|
||||
options: [__('No Letterhead')],
|
||||
change: () => this.preview(),
|
||||
default: this.print_settings.with_letterhead
|
||||
? __('No Letterhead')
|
||||
: __('Select Letterhead')
|
||||
},
|
||||
).$input;
|
||||
|
||||
);
|
||||
this.letterhead_selector = this.letterhead_selector_df.$input;
|
||||
this.sidebar_dynamic_section = $(
|
||||
`<div class="dynamic-settings"></div>`
|
||||
).appendTo(this.sidebar);
|
||||
|
|
@ -336,23 +334,19 @@ frappe.ui.form.PrintView = class {
|
|||
}
|
||||
|
||||
set_letterhead_options() {
|
||||
let letterhead_options = [
|
||||
this.get_default_option_for_select(__('Select Letterhead')),
|
||||
__('No Letterhead')
|
||||
];
|
||||
let letterhead_options = [__('No Letterhead')];
|
||||
let default_letterhead;
|
||||
let doc_letterhead = this.frm.doc.letter_head;
|
||||
|
||||
return frappe.db
|
||||
.get_list('Letter Head', { fields: ['name', 'is_default'] })
|
||||
.get_list('Letter Head', { fields: ['name', 'is_default'], limit: 0 })
|
||||
.then((letterheads) => {
|
||||
this.letterhead_selector.empty();
|
||||
letterheads.map((letterhead) => {
|
||||
if (letterhead.is_default) default_letterhead = letterhead.name;
|
||||
return letterhead_options.push(letterhead.name);
|
||||
});
|
||||
|
||||
this.letterhead_selector.add_options(letterhead_options);
|
||||
this.letterhead_selector_df.set_data(letterhead_options);
|
||||
let selected_letterhead = doc_letterhead || default_letterhead;
|
||||
if (selected_letterhead)
|
||||
this.letterhead_selector.val(selected_letterhead);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ frappe.db = {
|
|||
if (!args.fields) {
|
||||
args.fields = ['name'];
|
||||
}
|
||||
if (!args.limit) {
|
||||
if (!('limit' in args)) {
|
||||
args.limit = 20;
|
||||
}
|
||||
return new Promise ((resolve) => {
|
||||
|
|
|
|||
|
|
@ -607,9 +607,7 @@ frappe.Application = class Application {
|
|||
let doc = JSON.parse(pasted_data);
|
||||
if (doc.doctype) {
|
||||
e.preventDefault();
|
||||
let sleep = (time) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, time));
|
||||
};
|
||||
const sleep = frappe.utils.sleep;
|
||||
|
||||
frappe.dom.freeze(__('Creating {0}', [doc.doctype]) + '...');
|
||||
// to avoid abrupt UX
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
return;
|
||||
}
|
||||
this.render_links();
|
||||
this.set_open_count();
|
||||
// this.set_open_count();
|
||||
show = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import './script_manager';
|
|||
import './script_helpers';
|
||||
import './sidebar/form_sidebar';
|
||||
import './footer/footer';
|
||||
import './form_tour';
|
||||
|
||||
frappe.ui.form.Controller = class FormController {
|
||||
constructor(opts) {
|
||||
|
|
@ -152,6 +153,10 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
parent: $('<div class="form-dashboard">').insertAfter(this.layout.wrapper.find('.form-message'))
|
||||
});
|
||||
|
||||
this.tour = new frappe.ui.form.FormTour({
|
||||
frm: this
|
||||
});
|
||||
|
||||
// workflow state
|
||||
this.states = new frappe.ui.form.States({
|
||||
frm: this
|
||||
|
|
@ -987,7 +992,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
|
||||
frappe.re_route[frappe.router.get_sub_path()] = `${encodeURIComponent(frappe.router.slug(this.doctype))}/${encodeURIComponent(name)}`;
|
||||
frappe.set_route('Form', this.doctype, name);
|
||||
!frappe._from_link && frappe.set_route('Form', this.doctype, name);
|
||||
}
|
||||
|
||||
// ACTIONS
|
||||
|
|
@ -1606,53 +1611,6 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}, 1000);
|
||||
}
|
||||
|
||||
show_tour(on_finish) {
|
||||
const tour_info = frappe.tour[this.doctype];
|
||||
|
||||
if (!Array.isArray(tour_info)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const driver = new frappe.Driver({
|
||||
className: 'frappe-driver',
|
||||
allowClose: false,
|
||||
padding: 10,
|
||||
overlayClickNext: true,
|
||||
keyboardControl: true,
|
||||
nextBtnText: 'Next',
|
||||
prevBtnText: 'Previous',
|
||||
opacity: 0.25
|
||||
});
|
||||
|
||||
this.layout.sections.forEach(section => section.collapse(false));
|
||||
|
||||
let steps = tour_info.map(step => {
|
||||
let field = this.get_docfield(step.fieldname);
|
||||
return {
|
||||
element: `.frappe-control[data-fieldname='${step.fieldname}']`,
|
||||
popover: {
|
||||
title: step.title || field.label,
|
||||
description: step.description,
|
||||
position: step.position || 'bottom'
|
||||
},
|
||||
onNext: () => {
|
||||
const next_condition_satisfied = this.layout.evaluate_depends_on_value(step.next_step_condition || true);
|
||||
if (!next_condition_satisfied) {
|
||||
driver.preventMove();
|
||||
}
|
||||
|
||||
if (!driver.hasNextStep()) {
|
||||
on_finish && on_finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
driver.defineSteps(steps);
|
||||
frappe.router.on('change', () => driver.reset());
|
||||
driver.start();
|
||||
}
|
||||
|
||||
setup_docinfo_change_listener() {
|
||||
let doctype = this.doctype;
|
||||
let docname = this.docname;
|
||||
|
|
|
|||
252
frappe/public/js/frappe/form/form_tour.js
Normal file
252
frappe/public/js/frappe/form/form_tour.js
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
frappe.ui.form.FormTour = class FormTour {
|
||||
constructor({ frm }) {
|
||||
this.frm = frm;
|
||||
this.driver_steps = [];
|
||||
|
||||
this.init_driver();
|
||||
}
|
||||
|
||||
init_driver() {
|
||||
this.driver = new frappe.Driver({
|
||||
className: 'frappe-driver',
|
||||
allowClose: false,
|
||||
padding: 10,
|
||||
overlayClickNext: true,
|
||||
keyboardControl: true,
|
||||
nextBtnText: 'Next',
|
||||
prevBtnText: 'Previous',
|
||||
opacity: 0.25,
|
||||
onHighlighted: (step) => {
|
||||
// if last step is to save, then attach a listener to save button
|
||||
if (step.options.is_save_step) {
|
||||
$(step.options.element).one('click', () => this.driver.reset());
|
||||
}
|
||||
|
||||
// focus on input
|
||||
const $input = $(step.node).find('input').get(0);
|
||||
if ($input)
|
||||
frappe.utils.sleep(200).then(() => $input.focus());
|
||||
}
|
||||
});
|
||||
|
||||
frappe.router.on('change', () => this.driver.reset());
|
||||
this.frm.layout.sections.forEach(section => section.collapse(false));
|
||||
}
|
||||
|
||||
async init({ tour_name, on_finish }) {
|
||||
if (tour_name) {
|
||||
this.tour = await frappe.db.get_doc('Form Tour', tour_name);
|
||||
} else {
|
||||
this.tour = { steps: frappe.tour[this.frm.doctype] };
|
||||
}
|
||||
|
||||
if (on_finish) this.on_finish = on_finish;
|
||||
|
||||
this.build_steps();
|
||||
this.update_driver_steps();
|
||||
}
|
||||
|
||||
build_steps() {
|
||||
this.driver_steps = [];
|
||||
this.tour.steps.forEach((step) => {
|
||||
const on_next = () => {
|
||||
if (!this.is_next_condition_satisfied(step)) {
|
||||
this.driver.preventMove();
|
||||
}
|
||||
|
||||
if (!this.driver.hasNextStep()) {
|
||||
this.on_finish && this.on_finish();
|
||||
}
|
||||
};
|
||||
|
||||
const driver_step = this.get_step(step, on_next);
|
||||
this.driver_steps.push(driver_step);
|
||||
|
||||
if (step.fieldtype == 'Table') this.handle_table_step(step);
|
||||
if (step.is_table_field) this.handle_child_table_step(step);
|
||||
});
|
||||
|
||||
if (this.tour.save_on_complete) {
|
||||
this.add_step_to_save();
|
||||
}
|
||||
}
|
||||
|
||||
is_next_condition_satisfied(step) {
|
||||
const form = step.is_table_field ? this.frm.cur_grid.grid_form : this.frm;
|
||||
return form.layout.evaluate_depends_on_value(step.next_step_condition || true);
|
||||
}
|
||||
|
||||
get_step(step_info, on_next) {
|
||||
const { name, fieldname, title, description, position, is_table_field } = step_info;
|
||||
const field = this.frm.get_field(fieldname);
|
||||
let element = field ? field.wrapper : `.frappe-control[data-fieldname='${fieldname}']`;
|
||||
|
||||
if (is_table_field) {
|
||||
element = `.grid-row-open .frappe-control[data-fieldname='${fieldname}']`;
|
||||
}
|
||||
|
||||
return {
|
||||
element,
|
||||
name,
|
||||
popover: { title, description, position: frappe.router.slug(position) },
|
||||
onNext: on_next
|
||||
};
|
||||
}
|
||||
|
||||
update_driver_steps(steps = []) {
|
||||
if (steps.length == 0) {
|
||||
steps = this.driver_steps;
|
||||
}
|
||||
this.driver.defineSteps(steps);
|
||||
}
|
||||
|
||||
start(idx = 0) {
|
||||
if (this.driver_steps.length == 0) {
|
||||
return;
|
||||
}
|
||||
this.driver.start(idx);
|
||||
}
|
||||
|
||||
get_next_step() {
|
||||
// returns the next step only if driver is active
|
||||
if (this.driver.isActivated & this.driver.hasNextStep()) {
|
||||
const current_step = this.driver.currentStep;
|
||||
return this.driver.steps[current_step + 1];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
handle_table_step(step_info) {
|
||||
const is_last_step = step_info.idx == this.tour.steps.length;
|
||||
|
||||
if (!is_last_step) {
|
||||
// if next step field is inside currently highlighted table field
|
||||
// then check if there is a row -> if not, then prompt to add row
|
||||
// then edit the first row and hightlight next step
|
||||
|
||||
const curr_step = step_info;
|
||||
const next_step = this.tour.steps[curr_step.idx];
|
||||
const is_next_field_in_curr_table = next_step.parent_field == curr_step.field;
|
||||
|
||||
if (!is_next_field_in_curr_table) return;
|
||||
|
||||
const rows = this.frm.doc[curr_step.fieldname];
|
||||
const table_has_rows = rows && rows.length > 0;
|
||||
if (table_has_rows) {
|
||||
// table already has rows
|
||||
// then just edit the first one on next step
|
||||
const curr_driver_step = this.driver_steps.find(s => s.name == curr_step.name);
|
||||
curr_driver_step.onNext = () => {
|
||||
if (this.is_next_condition_satisfied(curr_step)) {
|
||||
this.expand_row_and_proceed(curr_step, curr_step.idx);
|
||||
} else {
|
||||
this.driver.preventMove();
|
||||
}
|
||||
};
|
||||
this.update_driver_steps();
|
||||
|
||||
} else {
|
||||
this.add_new_row_step(curr_step);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add_new_row_step(step) {
|
||||
const $add_row = `.frappe-control[data-fieldname='${step.fieldname}'] .grid-add-row`;
|
||||
const add_row_step = {
|
||||
element: $add_row,
|
||||
popover: { title: __("Add a Row"), description: "" },
|
||||
onNext: () => {
|
||||
if (!cur_frm.cur_grid) {
|
||||
this.driver.preventMove();
|
||||
}
|
||||
}
|
||||
};
|
||||
this.driver_steps.push(add_row_step);
|
||||
|
||||
// setup a listener on add row button
|
||||
// so, once the row is added, move to next step automatically
|
||||
$($add_row).one('click', () => {
|
||||
this.expand_row_and_proceed(step, step.idx + 1); // +1 since add row step is added
|
||||
});
|
||||
}
|
||||
|
||||
expand_row_and_proceed(step, start_from) {
|
||||
this.open_first_row_of(step.fieldname);
|
||||
this.update_driver_steps(); // need to define again, since driver.js only considers steps which are inside DOM
|
||||
frappe.utils.sleep(300).then(() => this.driver.start(start_from));
|
||||
}
|
||||
|
||||
open_first_row_of(fieldname) {
|
||||
this.frm.fields_dict[fieldname].grid.grid_rows[0].toggle_view();
|
||||
|
||||
// setup a listener on close row button
|
||||
// so, once the row is closed, move to next step automatically
|
||||
const $close_row = '.grid-row-open .grid-collapse-row';
|
||||
$($close_row).one('click', () => {
|
||||
const next_step = this.get_next_step();
|
||||
const next_element = next_step.options.is_save_step ? null : next_step.node;
|
||||
|
||||
frappe.utils.scroll_to(next_element, true, 150, null, () => {
|
||||
this.driver.moveNext();
|
||||
frappe.flags.disable_auto_scroll = false;
|
||||
});
|
||||
frappe.flags.disable_auto_scroll = true;
|
||||
});
|
||||
}
|
||||
|
||||
handle_child_table_step(step_info) {
|
||||
const is_last_step = step_info.idx == this.tour.steps.length;
|
||||
|
||||
if (!is_last_step) {
|
||||
const curr_step = step_info;
|
||||
const next_step = this.tour.steps[curr_step.idx];
|
||||
const field = this.frm.get_field(next_step.fieldname);
|
||||
|
||||
if (!field) return;
|
||||
|
||||
// next step highlights parent field
|
||||
// so, add a step to prompt user to collapse grid form
|
||||
this.add_collapse_row_step();
|
||||
|
||||
} else if (this.tour.save_on_complete) {
|
||||
// if last step & save on complete is checked
|
||||
// add a step to prompt user to collapse grid form
|
||||
// to be able to save as a last step
|
||||
this.add_collapse_row_step();
|
||||
}
|
||||
}
|
||||
|
||||
add_collapse_row_step() {
|
||||
const $close_row = '.grid-row-open .grid-collapse-row';
|
||||
const close_row_step = {
|
||||
element: $close_row,
|
||||
popover: { title: __("Collapse"), description: "", position: "left" },
|
||||
onNext: () => {
|
||||
if (cur_frm.cur_grid) {
|
||||
this.driver.preventMove();
|
||||
}
|
||||
}
|
||||
};
|
||||
this.driver_steps.push(close_row_step);
|
||||
}
|
||||
|
||||
add_step_to_save() {
|
||||
const page_id = `#page-${this.frm.doctype}`;
|
||||
const $save_btn = `${page_id} .standard-actions .primary-action`;
|
||||
const save_step = {
|
||||
element: $save_btn,
|
||||
is_save_step: true,
|
||||
allowClose: false,
|
||||
overlayClickNext: false,
|
||||
popover: {
|
||||
title: __("Save"),
|
||||
description: "",
|
||||
position: "left",
|
||||
doneBtnText: __("Save")
|
||||
}
|
||||
};
|
||||
this.driver_steps.push(save_step);
|
||||
frappe.ui.form.on(this.frm.doctype, 'after_save', () => this.on_finish && this.on_finish());
|
||||
}
|
||||
};
|
||||
|
|
@ -543,7 +543,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
|
||||
} else if (expression.substr(0, 5)=='eval:') {
|
||||
try {
|
||||
out = frappe.utils.eval(expression.substr(5), { doc });
|
||||
out = frappe.utils.eval(expression.substr(5), { doc, parent });
|
||||
if (parent && parent.istable && expression.includes('is_submittable')) {
|
||||
out = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ frappe.ui.form.Attachments = class Attachments {
|
|||
this.parent.find(".add-attachment-btn").click(function() {
|
||||
me.new_attachment();
|
||||
});
|
||||
this.add_attachment_wrapper = this.parent.find(".add_attachment").parent();
|
||||
this.add_attachment_wrapper = this.parent.find(".add-attachment-btn");
|
||||
this.attachments_label = this.parent.find(".attachments-label");
|
||||
}
|
||||
max_reached(raise_exception=false) {
|
||||
|
|
@ -39,7 +39,7 @@ frappe.ui.form.Attachments = class Attachments {
|
|||
this.parent.find(".attachment-row").remove();
|
||||
|
||||
var max_reached = this.max_reached();
|
||||
this.add_attachment_wrapper.toggleClass("hide", !max_reached);
|
||||
this.add_attachment_wrapper.toggle(!max_reached);
|
||||
|
||||
// add attachment objects
|
||||
var attachments = this.get_attachments();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
{% } %}
|
||||
<div class="col-md-4">
|
||||
<div class="form-link-title">
|
||||
<span>{{ transactions[i].label }}<span>
|
||||
<span>{{ __(transactions[i].label) }}<span>
|
||||
</div>
|
||||
{% for (let j=0; j < transactions[i].items.length; j++) { %}
|
||||
{% let doctype = transactions[i].items[j]; %}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
{% } %}
|
||||
<div class="col-md-4">
|
||||
<div class="form-link-title">
|
||||
<span>{{ reports[i].label }}</span>
|
||||
<span>{{ __(reports[i].label) }}</span>
|
||||
</div>
|
||||
{% for (let j=0; j < reports[i].items.length; j++) { %}
|
||||
{% let report = reports[i].items[j]; %}
|
||||
|
|
|
|||
|
|
@ -116,10 +116,7 @@ frappe.ui.Capture = class {
|
|||
})
|
||||
.catch(err => {
|
||||
if (this.options.error) {
|
||||
const alert = `<span class="indicator red"/> ${
|
||||
frappe.ui.Capture.ERR_MESSAGE
|
||||
}`;
|
||||
frappe.show_alert(alert, 3);
|
||||
frappe.show_alert(frappe.ui.Capture.ERR_MESSAGE, 3);
|
||||
}
|
||||
|
||||
throw err;
|
||||
|
|
|
|||
|
|
@ -268,7 +268,9 @@ Object.assign(frappe.utils, {
|
|||
</a></p>');
|
||||
return content.html();
|
||||
},
|
||||
scroll_to: function(element, animate=true, additional_offset, element_to_be_scrolled) {
|
||||
scroll_to: function(element, animate=true, additional_offset, element_to_be_scrolled, callback) {
|
||||
if (frappe.flags.disable_auto_scroll) return;
|
||||
|
||||
element_to_be_scrolled = element_to_be_scrolled || $("html, body");
|
||||
let scroll_top = 0;
|
||||
if (element) {
|
||||
|
|
@ -289,7 +291,7 @@ Object.assign(frappe.utils, {
|
|||
}
|
||||
|
||||
if (animate) {
|
||||
element_to_be_scrolled.animate({ scrollTop: scroll_top });
|
||||
element_to_be_scrolled.animate({ scrollTop: scroll_top }).promise().then(callback);
|
||||
} else {
|
||||
element_to_be_scrolled.scrollTop(scroll_top);
|
||||
}
|
||||
|
|
@ -1332,5 +1334,9 @@ Object.assign(frappe.utils, {
|
|||
});
|
||||
!prepend && button.appendTo(wrapper);
|
||||
prepend && wrapper.prepend(button);
|
||||
},
|
||||
|
||||
sleep(time) {
|
||||
return new Promise((resolve) => setTimeout(resolve, time));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ frappe.views.CalendarView = class CalendarView extends frappe.views.ListView {
|
|||
frappe.views.Calendar = class Calendar {
|
||||
constructor(options) {
|
||||
$.extend(this, options);
|
||||
this.field_map = {
|
||||
this.field_map = this.field_map || {
|
||||
"id": "name",
|
||||
"start": "start",
|
||||
"end": "end",
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ frappe.views.CommunicationComposer = class {
|
|||
);
|
||||
});
|
||||
|
||||
if (email_accounts.length > 1) {
|
||||
if (email_accounts.length) {
|
||||
fields.unshift({
|
||||
label: __("From"),
|
||||
fieldtype: "Select",
|
||||
|
|
@ -728,7 +728,7 @@ frappe.views.CommunicationComposer = class {
|
|||
const SALUTATION_END_COMMENT = "<!-- salutation-ends -->";
|
||||
if (this.real_name && !message.includes(SALUTATION_END_COMMENT)) {
|
||||
this.message = `
|
||||
<p>${__('Dear')} ${this.real_name},</p>
|
||||
<p>${__('Dear {0},', [this.real_name], 'Salutation in new email')},</p>
|
||||
${SALUTATION_END_COMMENT}<br>
|
||||
${message}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ frappe.provide("frappe.views");
|
|||
method_name = "update_order_for_single_card";
|
||||
args = {
|
||||
board_name: this.board.name,
|
||||
docname: unescape(card.name),
|
||||
docname: card.name,
|
||||
from_colname: card.from_colname,
|
||||
to_colname: card.to_colname,
|
||||
old_index: card.old_index,
|
||||
|
|
@ -222,7 +222,7 @@ frappe.provide("frappe.views");
|
|||
var col_name = $(this).data().columnValue;
|
||||
order[col_name] = [];
|
||||
$(this).find('.kanban-card-wrapper').each(function() {
|
||||
var card_name = unescape($(this).data().name);
|
||||
var card_name = decodeURIComponent($(this).data().name);
|
||||
order[col_name].push(card_name);
|
||||
});
|
||||
});
|
||||
|
|
@ -514,7 +514,7 @@ frappe.provide("frappe.views");
|
|||
wrapper.find('.kanban-cards').height('auto');
|
||||
// update order
|
||||
const args = {
|
||||
name: $(e.item).attr('data-name'),
|
||||
name: decodeURIComponent($(e.item).attr('data-name')),
|
||||
from_colname: $(e.from).parents('.kanban-column').attr('data-column-value'),
|
||||
to_colname: $(e.to).parents('.kanban-column').attr('data-column-value'),
|
||||
old_index: e.oldIndex,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="kanban-card-wrapper {{ disable_click }}" data-name="{{escape(name)}}">
|
||||
<div class="kanban-card-wrapper {{ disable_click }}" data-name="{{encodeURIComponent(name)}}">
|
||||
<a class="kanban-card-redirect" href="#">
|
||||
<div class="kanban-card content">
|
||||
{% if(image_url) { %}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ export default class LinksWidget extends Widget {
|
|||
const opts = {
|
||||
name: item.link_to,
|
||||
type: item.link_type,
|
||||
doctype: item.doctype,
|
||||
is_query_report: item.is_query_report
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ export default class OnboardingWidget extends Widget {
|
|||
|
||||
frappe.route_hooks = {};
|
||||
frappe.route_hooks.after_load = (frm) => {
|
||||
frm.show_tour(() => {
|
||||
const on_finish = () => {
|
||||
let msg_dialog = frappe.msgprint({
|
||||
message: __("Let's take you back to onboarding"),
|
||||
title: __("Great Job"),
|
||||
|
|
@ -217,7 +217,10 @@ export default class OnboardingWidget extends Widget {
|
|||
label: () => __("Continue"),
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
frm.tour
|
||||
.init({ on_finish })
|
||||
.then(() => frm.tour.start());
|
||||
};
|
||||
|
||||
frappe.set_route(route);
|
||||
|
|
@ -290,12 +293,15 @@ export default class OnboardingWidget extends Widget {
|
|||
|
||||
frappe.route_hooks = {};
|
||||
frappe.route_hooks.after_load = (frm) => {
|
||||
frm.show_tour(() => {
|
||||
const on_finish = () => {
|
||||
frappe.msgprint({
|
||||
message: __("Awesome, now try making an entry yourself"),
|
||||
title: __("Great"),
|
||||
});
|
||||
});
|
||||
};
|
||||
frm.tour
|
||||
.init({ on_finish })
|
||||
.then(() => frm.tour.start());
|
||||
};
|
||||
|
||||
let callback = () => {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
--blue-100: #D3E9FC;
|
||||
--blue-50 : #F0F8FE;
|
||||
|
||||
--cyan-900: #006464;
|
||||
--cyan-900: #006464;
|
||||
--cyan-800: #007272;
|
||||
--cyan-700: #008b8b;
|
||||
--cyan-600: #02c5c5;
|
||||
|
|
@ -179,6 +179,10 @@
|
|||
--text-on-pink: var(--pink-500);
|
||||
--text-on-cyan: var(--cyan-600);
|
||||
|
||||
--disabled-control-bg: var(--gray-50);
|
||||
--control-bg: var(--gray-100);
|
||||
--control-bg-on-gray: var(--gray-200);
|
||||
|
||||
--awesomplete-hover-bg: var(--control-bg);
|
||||
|
||||
// Other Colors
|
||||
|
|
@ -208,5 +212,4 @@
|
|||
--checkbox-right-margin: var(--margin-xs);
|
||||
--checkbox-size: 14px;
|
||||
--checkbox-focus-shadow: 0 0 0 2px var(--gray-300);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@
|
|||
|
||||
&--time-current-hours, &--time-current-minutes, &--time-current-seconds {
|
||||
font-family: inherit;
|
||||
&:after {
|
||||
color: var(--text-color);
|
||||
background-color: var(--fg-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
&--day-name {
|
||||
|
|
@ -47,10 +51,13 @@
|
|||
background: fade(#0089FF, 10%);
|
||||
}
|
||||
|
||||
&.-focus- {
|
||||
background-color: var(--fg-hover-color);
|
||||
}
|
||||
|
||||
&.-selected-.-focus- {
|
||||
background: fade(#0089FF, 90%);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--time, &--buttons {
|
||||
|
|
@ -67,8 +74,20 @@
|
|||
&--time-row:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&--pointer {
|
||||
background: var(--fg-color);
|
||||
border-top-right-radius: 2px;
|
||||
border: 1px var(--border-color);
|
||||
border-style: solid solid hidden hidden;
|
||||
}
|
||||
|
||||
&--button {
|
||||
color: var(--brand-color);
|
||||
&:hover {
|
||||
color: var(--text-color);
|
||||
background-color: var(--fg-hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.datepicker--button {
|
||||
color: var(--brand-color);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,13 +35,14 @@
|
|||
.ql-container.ql-snow {
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ql-snow {
|
||||
.ql-editor {
|
||||
min-height: 400px;
|
||||
max-height: 600px;
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
}
|
||||
.ql-stroke {
|
||||
stroke: var(--icon-stroke);
|
||||
|
|
|
|||
|
|
@ -64,4 +64,16 @@ div#driver-popover-item {
|
|||
|
||||
input.driver-highlighted-element {
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
|
||||
.driver-fix-stacking {
|
||||
z-index: auto !important;
|
||||
position: unset !important;
|
||||
opacity: 1.0 !important;
|
||||
transform: none !important;
|
||||
filter: none !important;
|
||||
perspective: none !important;
|
||||
transform-style: flat !important;
|
||||
transform-box: border-box !important;
|
||||
will-change: unset !important;
|
||||
}
|
||||
|
|
@ -291,15 +291,19 @@ var verify_token = function (event) {
|
|||
}
|
||||
|
||||
var request_otp = function (r) {
|
||||
$('.login-content').empty().append($('<div>').attr({ 'id': 'twofactor_div' }).html(
|
||||
'<form class="form-verify">\
|
||||
<div class="page-card-head">\
|
||||
<span class="indicator blue" data-text="Verification">{{ _("Verification") }}</span>\
|
||||
</div>\
|
||||
<div id="otp_div"></div>\
|
||||
<input type="text" id="login_token" autocomplete="off" class="form-control" placeholder={{ _("Verification Code") }} required="" autofocus="">\
|
||||
<button class="btn btn-sm btn-primary btn-block" id="verify_token">{{ _("Verify") }}</button>\
|
||||
</form>'));
|
||||
$('.login-content').empty();
|
||||
$('.login-content:visible').append(
|
||||
`<div id="twofactor_div">
|
||||
<form class="form-verify">
|
||||
<div class="page-card-head">
|
||||
<span class="indicator blue" data-text="Verification">{{ _("Verification") }}</span>
|
||||
</div>
|
||||
<div id="otp_div"></div>
|
||||
<input type="text" id="login_token" autocomplete="off" class="form-control" placeholder={{ _("Verification Code") }} required="" autofocus="">
|
||||
<button class="btn btn-sm btn-primary btn-block mt-3" id="verify_token">{{ _("Verify") }}</button>
|
||||
</form>
|
||||
</div>`
|
||||
);
|
||||
// add event handler for submit button
|
||||
verify_token();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,9 @@
|
|||
<!-- base template for testing -->
|
||||
<html>
|
||||
<head>
|
||||
{%- block style %}
|
||||
{% if colocated_css -%}
|
||||
<style>{{ colocated_css }}</style>
|
||||
{%- endif %}
|
||||
{%- endblock -%}
|
||||
</head>
|
||||
<body>
|
||||
{% include "templates/includes/breadcrumbs.html" %}
|
||||
<h1>This is for testing</h1>
|
||||
{% block content %}{% endblock %}
|
||||
{%- block script %}
|
||||
{% if colocated_js -%}
|
||||
<script>{{ colocated_js }}</script>
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
20
frappe/templates/test/_test_base_breadcrumbs.html
Normal file
20
frappe/templates/test/_test_base_breadcrumbs.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!-- base template for testing -->
|
||||
<html>
|
||||
<head>
|
||||
{%- block style %}
|
||||
{% if colocated_css -%}
|
||||
<style>{{ colocated_css }}</style>
|
||||
{%- endif %}
|
||||
{%- endblock -%}
|
||||
</head>
|
||||
<body>
|
||||
{% include "templates/includes/breadcrumbs.html" %}
|
||||
<h1>This is for testing</h1>
|
||||
{% block content %}{% endblock %}
|
||||
{%- block script %}
|
||||
{% if colocated_js -%}
|
||||
<script>{{ colocated_js }}</script>
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -147,6 +147,7 @@ class TestResourceAPI(unittest.TestCase):
|
|||
response = self.delete(f"{self.DOCTYPE}/{doc_to_delete}")
|
||||
self.assertEqual(response.status_code, 202)
|
||||
self.assertDictEqual(response.json(), {"message": "ok"})
|
||||
self.GENERATED_DOCUMENTS.remove(doc_to_delete)
|
||||
|
||||
non_existent_doc = frappe.generate_hash(length=12)
|
||||
response = self.delete(f"{self.DOCTYPE}/{non_existent_doc}")
|
||||
|
|
|
|||
|
|
@ -201,6 +201,85 @@ class TestWebsite(unittest.TestCase):
|
|||
self.assertIn('<div class="print-format">', content)
|
||||
self.assertIn('<div>Language</div>', content)
|
||||
|
||||
def test_custom_base_template_path(self):
|
||||
content = get_response_content('/_test/_test_folder/_test_page')
|
||||
# assert the text in base template is rendered
|
||||
self.assertIn('<h1>This is for testing</h1>', content)
|
||||
|
||||
# assert template block rendered
|
||||
self.assertIn('<p>Test content</p>', content)
|
||||
|
||||
def test_json_sidebar_data(self):
|
||||
frappe.flags.look_for_sidebar = False
|
||||
content = get_response_content('/_test/_test_folder/_test_page')
|
||||
self.assertNotIn('Test Sidebar', content)
|
||||
clear_website_cache()
|
||||
frappe.flags.look_for_sidebar = True
|
||||
content = get_response_content('/_test/_test_folder/_test_page')
|
||||
self.assertIn('Test Sidebar', content)
|
||||
frappe.flags.look_for_sidebar = False
|
||||
|
||||
def test_base_template(self):
|
||||
content = get_response_content('/_test/_test_custom_base.html')
|
||||
|
||||
# assert the text in base template is rendered
|
||||
self.assertIn('<h1>This is for testing</h1>', content)
|
||||
|
||||
# assert template block rendered
|
||||
self.assertIn('<p>Test content</p>', content)
|
||||
|
||||
def test_index_and_next_comment(self):
|
||||
content = get_response_content('/_test/_test_folder')
|
||||
# test if {index} was rendered
|
||||
self.assertIn('<a href="/_test/_test_folder/_test_page"> Test Page</a>', content)
|
||||
|
||||
self.assertIn('<a href="/_test/_test_folder/_test_toc">Test TOC</a>', content)
|
||||
|
||||
content = get_response_content('/_test/_test_folder/_test_page')
|
||||
# test if {next} was rendered
|
||||
self.assertIn('Next: <a class="btn-next" href="/_test/_test_folder/_test_toc">Test TOC</a>', content)
|
||||
|
||||
def test_colocated_assets(self):
|
||||
content = get_response_content('/_test/_test_folder/_test_page')
|
||||
self.assertIn("<script>console.log('test data');</script>", content)
|
||||
self.assertIn("background-color: var(--bg-color);", content)
|
||||
|
||||
def test_raw_assets_are_loaded(self):
|
||||
content = get_response_content('/_test/assets/js_asset.min.js')
|
||||
# minified js files should not be passed through jinja renderer
|
||||
self.assertEqual("//{% if title %} {{title}} {% endif %}\nconsole.log('in');", content)
|
||||
|
||||
content = get_response_content('/_test/assets/css_asset.css')
|
||||
self.assertEqual("""body{color:red}""", content)
|
||||
|
||||
def test_breadcrumbs(self):
|
||||
content = get_response_content('/_test/_test_folder/_test_page')
|
||||
self.assertIn('<span itemprop="name">Test Folder</span>', content)
|
||||
self.assertIn('<span itemprop="name"> Test Page</span>', content)
|
||||
|
||||
content = get_response_content('/_test/_test_folder/index')
|
||||
self.assertIn('<span itemprop="name"> Test</span>', content)
|
||||
self.assertIn('<span itemprop="name">Test Folder</span>', content)
|
||||
|
||||
def test_get_context_without_context_object(self):
|
||||
content = get_response_content('/_test/_test_no_context')
|
||||
self.assertIn("Custom Content", content)
|
||||
|
||||
def test_caching(self):
|
||||
# to enable caching
|
||||
frappe.flags.force_website_cache = True
|
||||
|
||||
clear_website_cache()
|
||||
# first response no-cache
|
||||
response = get_response('/_test/_test_folder/_test_page')
|
||||
self.assertIn(('X-From-Cache', 'False'), list(response.headers))
|
||||
|
||||
# first response returned from cache
|
||||
response = get_response('/_test/_test_folder/_test_page')
|
||||
self.assertIn(('X-From-Cache', 'True'), list(response.headers))
|
||||
|
||||
frappe.flags.force_website_cache = False
|
||||
|
||||
|
||||
def set_home_page_hook(key, value):
|
||||
from frappe import hooks
|
||||
|
|
|
|||
|
|
@ -131,3 +131,52 @@ def insert_contact(first_name, phone_number):
|
|||
})
|
||||
doc.append('phone_nos', {'phone': phone_number})
|
||||
doc.insert()
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_form_tour():
|
||||
if frappe.db.exists('Form Tour', {'name': 'Test Form Tour'}):
|
||||
return
|
||||
|
||||
def get_docfield_name(filters):
|
||||
return frappe.db.get_value('DocField', filters, "name")
|
||||
|
||||
tour = frappe.get_doc({
|
||||
'doctype': 'Form Tour',
|
||||
'title': 'Test Form Tour',
|
||||
'reference_doctype': 'Contact',
|
||||
'save_on_complete': 1,
|
||||
'steps': [{
|
||||
"title": "Test Title 1",
|
||||
"description": "Test Description 1",
|
||||
"has_next_condition": 1,
|
||||
"next_step_condition": "eval: doc.first_name",
|
||||
"field": get_docfield_name({'parent': 'Contact', 'fieldname': 'first_name'}),
|
||||
"fieldname": "first_name",
|
||||
"fieldtype": "Data"
|
||||
},{
|
||||
"title": "Test Title 2",
|
||||
"description": "Test Description 2",
|
||||
"has_next_condition": 1,
|
||||
"next_step_condition": "eval: doc.last_name",
|
||||
"field": get_docfield_name({'parent': 'Contact', 'fieldname': 'last_name'}),
|
||||
"fieldname": "last_name",
|
||||
"fieldtype": "Data"
|
||||
},{
|
||||
"title": "Test Title 3",
|
||||
"description": "Test Description 3",
|
||||
"field": get_docfield_name({'parent': 'Contact', 'fieldname': 'phone_nos'}),
|
||||
"fieldname": "phone_nos",
|
||||
"fieldtype": "Table"
|
||||
},{
|
||||
"title": "Test Title 4",
|
||||
"description": "Test Description 4",
|
||||
"is_table_field": 1,
|
||||
"parent_field": get_docfield_name({'parent': 'Contact', 'fieldname': 'phone_nos'}),
|
||||
"field": get_docfield_name({'parent': 'Contact Phone', 'fieldname': 'phone'}),
|
||||
"next_step_condition": "eval: doc.phone",
|
||||
"has_next_condition": 1,
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Data"
|
||||
}]
|
||||
})
|
||||
tour.insert()
|
||||
|
|
@ -9,6 +9,7 @@ from frappe.utils import set_request
|
|||
from frappe.website.serve import get_response
|
||||
from frappe.utils import random_string
|
||||
from frappe.website.doctype.blog_post.blog_post import get_blog_list
|
||||
from frappe.website.utils import clear_website_cache
|
||||
from frappe.website.website_generator import WebsiteGenerator
|
||||
from frappe.custom.doctype.customize_form.customize_form import reset_customization
|
||||
|
||||
|
|
@ -90,6 +91,29 @@ class TestBlogPost(unittest.TestCase):
|
|||
frappe.delete_doc(blog.doctype, blog.name)
|
||||
frappe.delete_doc("Blog Category", blogs[0].blog_category)
|
||||
|
||||
def test_caching(self):
|
||||
# to enable caching
|
||||
frappe.flags.force_website_cache = True
|
||||
print(frappe.session.user)
|
||||
|
||||
clear_website_cache()
|
||||
# first response no-cache
|
||||
pages = frappe.get_all('Blog Post', fields=['name', 'route'],
|
||||
filters={'published': 1, 'title': "_Test Blog Post"}, limit=1)
|
||||
|
||||
route = pages[0].route
|
||||
set_request(path=route)
|
||||
# response = get_response()
|
||||
response = get_response()
|
||||
# TODO: enable this assert
|
||||
# self.assertIn(('X-From-Cache', 'False'), list(response.headers))
|
||||
|
||||
set_request(path=route)
|
||||
response = get_response()
|
||||
self.assertIn(('X-From-Cache', 'True'), list(response.headers))
|
||||
|
||||
frappe.flags.force_website_cache = True
|
||||
|
||||
def scrub(text):
|
||||
return WebsiteGenerator.scrub(None, text)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,15 +18,6 @@ class TestWebPage(unittest.TestCase):
|
|||
self.assertTrue(PathResolver("test-web-page-1/test-web-page-3").is_valid_path())
|
||||
self.assertFalse(PathResolver("test-web-page-1/test-web-page-Random").is_valid_path())
|
||||
|
||||
def test_base_template(self):
|
||||
content = get_response_content('/_test/_test_custom_base.html')
|
||||
|
||||
# assert the text in base template is rendered
|
||||
self.assertIn('<h1>This is for testing</h1>', content)
|
||||
|
||||
# assert template block rendered
|
||||
self.assertIn('<p>Test content</p>', content)
|
||||
|
||||
def test_content_type(self):
|
||||
web_page = frappe.get_doc(dict(
|
||||
doctype = 'Web Page',
|
||||
|
|
@ -67,48 +58,3 @@ class TestWebPage(unittest.TestCase):
|
|||
self.assertIn('<div>DocField</div>', content)
|
||||
finally:
|
||||
web_page.delete()
|
||||
|
||||
def test_custom_base_template_path(self):
|
||||
content = get_response_content('/_test/_test_folder/_test_page')
|
||||
# assert the text in base template is rendered
|
||||
self.assertIn('<h1>This is for testing</h1>', content)
|
||||
|
||||
# assert template block rendered
|
||||
self.assertIn('<p>Test content</p>', content)
|
||||
|
||||
def test_json_sidebar_data(self):
|
||||
frappe.flags.look_for_sidebar = False
|
||||
content = get_response_content('/_test/_test_folder/_test_page')
|
||||
self.assertNotIn('Test Sidebar', content)
|
||||
frappe.flags.look_for_sidebar = True
|
||||
content = get_response_content('/_test/_test_folder/_test_page')
|
||||
self.assertIn('Test Sidebar', content)
|
||||
frappe.flags.look_for_sidebar = False
|
||||
|
||||
def test_index_and_next_comment(self):
|
||||
content = get_response_content('/_test/_test_folder')
|
||||
# test if {index} was rendered
|
||||
self.assertIn('<a href="/_test/_test_folder/_test_page"> Test Page</a>', content)
|
||||
|
||||
self.assertIn('<a href="/_test/_test_folder/_test_toc">Test TOC</a>', content)
|
||||
|
||||
content = get_response_content('/_test/_test_folder/_test_page')
|
||||
# test if {next} was rendered
|
||||
self.assertIn('Next: <a class="btn-next" href="/_test/_test_folder/_test_toc">Test TOC</a>', content)
|
||||
|
||||
def test_colocated_assets(self):
|
||||
content = get_response_content('/_test/_test_folder/_test_page')
|
||||
self.assertIn("<script>console.log('test data');</script>", content)
|
||||
self.assertIn("background-color: var(--bg-color);", content)
|
||||
|
||||
def test_breadcrumbs(self):
|
||||
content = get_response_content('/_test/_test_folder/_test_page')
|
||||
self.assertIn('<span itemprop="name">Test TOC</span>', content)
|
||||
self.assertIn('<span itemprop="name"> Test Page</span>', content)
|
||||
|
||||
content = get_response_content('/_test/_test_folder/index')
|
||||
self.assertIn('<span itemprop="name"> Test</span>', content)
|
||||
self.assertIn('<span itemprop="name">Test TOC</span>', content)
|
||||
|
||||
def test_downloadable_file(self):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class BaseTemplatePage(BaseRenderer):
|
|||
def __init__(self, path, http_status_code=None):
|
||||
super().__init__(path=path, http_status_code=http_status_code)
|
||||
self.template_path = ''
|
||||
self.source = ''
|
||||
|
||||
def init_context(self):
|
||||
self.context = frappe._dict()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import frappe
|
||||
from frappe.model.document import get_controller
|
||||
from frappe.website.page_renderers.base_template_page import BaseTemplatePage
|
||||
from frappe.website.utils import build_response
|
||||
from frappe.website.utils import cache_html
|
||||
from frappe.website.router import (get_doctypes_with_web_view,
|
||||
get_page_info_from_web_page_with_dynamic_routes)
|
||||
|
||||
|
|
@ -47,14 +47,19 @@ class DocumentPage(BaseTemplatePage):
|
|||
return False
|
||||
|
||||
def render(self):
|
||||
html = self.get_html()
|
||||
html = self.add_csrf_token(html)
|
||||
|
||||
return self.build_response(html)
|
||||
|
||||
@cache_html
|
||||
def get_html(self):
|
||||
self.doc = frappe.get_doc(self.doctype, self.docname)
|
||||
self.init_context()
|
||||
self.update_context()
|
||||
self.post_process_context()
|
||||
html = frappe.get_template(self.template_path).render(self.context)
|
||||
html = self.add_csrf_token(html)
|
||||
|
||||
return build_response(self.path, html, self.http_status_code or 200, self.headers)
|
||||
return html
|
||||
|
||||
def update_context(self):
|
||||
self.context.doc = self.doc
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from frappe.website.router import get_page_info
|
|||
from frappe.website.page_renderers.base_template_page import BaseTemplatePage
|
||||
from frappe.website.router import get_base_template
|
||||
from frappe.website.utils import (extract_comment_tag, extract_title, get_next_link,
|
||||
get_toc, get_frontmatter, cache_html, get_sidebar_items, build_response)
|
||||
get_toc, get_frontmatter, cache_html, get_sidebar_items)
|
||||
|
||||
WEBPAGE_PY_MODULE_PROPERTIES = ("base_template_path", "template", "no_cache", "sitemap", "condition_field")
|
||||
|
||||
|
|
@ -58,7 +58,9 @@ class TemplatePage(BaseTemplatePage):
|
|||
return (frappe.as_unicode(f'{search_path}{d}') for d in ('', '.html', '.md', '/index.html', '/index.md'))
|
||||
|
||||
def render(self):
|
||||
return build_response(self.path, self.get_html(), self.http_status_code, self.headers)
|
||||
html = self.get_html()
|
||||
html = self.add_csrf_token(html)
|
||||
return self.build_response(html)
|
||||
|
||||
@cache_html
|
||||
def get_html(self):
|
||||
|
|
@ -67,13 +69,14 @@ class TemplatePage(BaseTemplatePage):
|
|||
self.init_context()
|
||||
|
||||
self.set_pymodule()
|
||||
self.setup_template()
|
||||
self.update_context()
|
||||
self.setup_template_source()
|
||||
self.load_colocated_files()
|
||||
self.set_properties_from_source()
|
||||
self.post_process_context()
|
||||
|
||||
html = self.render_template()
|
||||
html = self.update_toc(html)
|
||||
html = self.add_csrf_token(html)
|
||||
|
||||
return html
|
||||
|
||||
|
|
@ -115,7 +118,7 @@ class TemplatePage(BaseTemplatePage):
|
|||
if os.path.exists(os.path.join(self.app_path, self.pymodule_path)):
|
||||
self.pymodule_name = self.app + "." + self.pymodule_path.replace(os.path.sep, ".")[:-3]
|
||||
|
||||
def setup_template(self):
|
||||
def setup_template_source(self):
|
||||
'''Setup template source, frontmatter and markdown conversion'''
|
||||
self.source = self.get_raw_template()
|
||||
self.extract_frontmatter()
|
||||
|
|
@ -123,8 +126,6 @@ class TemplatePage(BaseTemplatePage):
|
|||
|
||||
def update_context(self):
|
||||
self.set_page_properties()
|
||||
self.set_properties_from_source()
|
||||
self.load_colocated_files()
|
||||
self.context.build_version = frappe.utils.get_build_version()
|
||||
|
||||
if self.pymodule_name:
|
||||
|
|
@ -148,8 +149,7 @@ class TemplatePage(BaseTemplatePage):
|
|||
|
||||
def set_page_properties(self):
|
||||
self.context.base_template = self.context.base_template \
|
||||
or get_base_template(self.path) \
|
||||
or 'templates/web.html'
|
||||
or get_base_template(self.path)
|
||||
self.context.basepath = self.basepath
|
||||
self.context.basename = self.basename
|
||||
self.context.name = self.name
|
||||
|
|
@ -185,10 +185,15 @@ class TemplatePage(BaseTemplatePage):
|
|||
click.echo(f'\n⚠️ DEPRECATION WARNING: {comment_tag} will be deprecated on 2021-12-31.')
|
||||
click.echo(f'Please remove it from {self.template_path} in {self.app}')
|
||||
|
||||
def run_pymodule_method(self, method):
|
||||
if hasattr(self.pymodule, method):
|
||||
def run_pymodule_method(self, method_name):
|
||||
if hasattr(self.pymodule, method_name):
|
||||
try:
|
||||
return getattr(self.pymodule, method)(self.context)
|
||||
import inspect
|
||||
method = getattr(self.pymodule, method_name)
|
||||
if inspect.getfullargspec(method).args:
|
||||
return method(self.context)
|
||||
else:
|
||||
return method()
|
||||
except (frappe.PermissionError, frappe.DoesNotExistError, frappe.Redirect):
|
||||
raise
|
||||
except Exception:
|
||||
|
|
@ -196,13 +201,10 @@ class TemplatePage(BaseTemplatePage):
|
|||
frappe.errprint(frappe.utils.get_traceback())
|
||||
|
||||
def render_template(self):
|
||||
if self.source:
|
||||
if self.template_path.endswith('min.js'):
|
||||
html = self.source # static
|
||||
else:
|
||||
html = frappe.render_template(self.source, self.context)
|
||||
elif self.template_path:
|
||||
if self.path.endswith('min.js'):
|
||||
html = self.get_raw_template() # static
|
||||
else:
|
||||
html = frappe.get_template(self.template_path).render(self.context)
|
||||
|
||||
return html
|
||||
|
||||
|
|
@ -212,7 +214,7 @@ class TemplatePage(BaseTemplatePage):
|
|||
or '{% extends' in self.source))
|
||||
|
||||
def get_raw_template(self):
|
||||
return frappe.get_jloader().get_source(frappe.get_jenv(), self.template_path)[0]
|
||||
return frappe.get_jloader().get_source(frappe.get_jenv(), self.context.template)[0]
|
||||
|
||||
def load_colocated_files(self):
|
||||
'''load co-located css/js files with the same name'''
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class PathResolver():
|
|||
|
||||
endpoint = resolve_path(self.path)
|
||||
custom_renderers = self.get_custom_page_renderers()
|
||||
renderers = custom_renderers + [StaticPage, WebFormPage, TemplatePage, ListPage, DocumentPage, PrintPage, NotFoundPage]
|
||||
renderers = custom_renderers + [StaticPage, WebFormPage, DocumentPage, TemplatePage, ListPage, PrintPage, NotFoundPage]
|
||||
|
||||
for renderer in renderers:
|
||||
renderer_instance = renderer(endpoint, 200)
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ def get_doctypes_with_web_view():
|
|||
doctypes_with_web_view = frappe.get_all('DocType', fields=['name', 'module'],
|
||||
filters=dict(has_web_view=1))
|
||||
module_app_map = frappe.local.module_app
|
||||
doctypes += [d.name for d in doctypes_with_web_view if module_app_map[frappe.scrub(d.module)] in installed_apps]
|
||||
doctypes += [d.name for d in doctypes_with_web_view if module_app_map.get(frappe.scrub(d.module)) in installed_apps]
|
||||
return doctypes
|
||||
|
||||
return frappe.cache().get_value('doctypes_with_web_view', _get)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ def find_first_image(html):
|
|||
return None
|
||||
|
||||
def can_cache(no_cache=False):
|
||||
if frappe.flags.force_website_cache:
|
||||
return True
|
||||
if frappe.conf.disable_website_cache or frappe.conf.developer_mode:
|
||||
return False
|
||||
if getattr(frappe.local, "no_cache", False):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{% extends base_template_path %}
|
||||
{% block content %}
|
||||
{% include "templates/includes/web_sidebar.html" %}
|
||||
<p>Test content</p>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
def get_context(context):
|
||||
context.base_template_path = 'frappe/templates/test/_test_base.html'
|
||||
context.base_template_path = 'frappe/templates/test/_test_base_breadcrumbs.html'
|
||||
context.add_breadcrumbs = 1
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
title: Test TOC
|
||||
title: Test Folder
|
||||
add_breadcrumbs: 1
|
||||
show_sidebar: 1
|
||||
base_template: templates/web.html
|
||||
---
|
||||
|
||||
# Index
|
||||
|
||||
{index}
|
||||
1
frappe/www/_test/_test_no_context.html
Normal file
1
frappe/www/_test/_test_no_context.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
{{ body }}
|
||||
7
frappe/www/_test/_test_no_context.py
Normal file
7
frappe/www/_test/_test_no_context.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import frappe
|
||||
|
||||
# no context object is accepted
|
||||
def get_context():
|
||||
context = frappe._dict()
|
||||
context.body = "Custom Content"
|
||||
return context
|
||||
0
frappe/www/_test/assets/__init__.py
Normal file
0
frappe/www/_test/assets/__init__.py
Normal file
1
frappe/www/_test/assets/css_asset.css
Normal file
1
frappe/www/_test/assets/css_asset.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
body{color:red}
|
||||
2
frappe/www/_test/assets/js_asset.min.js
vendored
Normal file
2
frappe/www/_test/assets/js_asset.min.js
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
//{% if title %} {{title}} {% endif %}
|
||||
console.log('in');
|
||||
37
yarn.lock
37
yarn.lock
|
|
@ -1399,12 +1399,12 @@ component-bind@1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
|
||||
integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=
|
||||
|
||||
component-emitter@1.2.1, component-emitter@^1.2.0:
|
||||
component-emitter@1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
|
||||
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
|
||||
|
||||
component-emitter@~1.3.0:
|
||||
component-emitter@^1.2.0, component-emitter@~1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
|
||||
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
|
||||
|
|
@ -1731,28 +1731,14 @@ debug@2.6.9:
|
|||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@^3.0.1:
|
||||
debug@^3.0.1, debug@^3.1.0, debug@^3.2.6:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^3.1.0, debug@^3.2.6:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
||||
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.1.1, debug@~4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
|
||||
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.2.0, debug@^4.3.1:
|
||||
debug@^4.1.1, debug@^4.2.0, debug@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
|
||||
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
|
||||
|
|
@ -1766,6 +1752,13 @@ debug@~3.1.0:
|
|||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@~4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
|
||||
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
decamelize@^1.0.0, decamelize@^1.1.2, decamelize@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||
|
|
@ -6448,11 +6441,11 @@ socket.io-client@2.4.0:
|
|||
to-array "0.1.4"
|
||||
|
||||
socket.io-parser@~3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f"
|
||||
integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.2.tgz#ef872009d0adcf704f2fbe830191a14752ad50b6"
|
||||
integrity sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==
|
||||
dependencies:
|
||||
component-emitter "1.2.1"
|
||||
component-emitter "~1.3.0"
|
||||
debug "~3.1.0"
|
||||
isarray "2.0.1"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue