Merge branch 'develop' into fix_df_propagation

This commit is contained in:
Suraj Shetty 2022-02-15 19:56:21 +05:30 committed by GitHub
commit 4697cc40bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1507 additions and 723 deletions

View file

@ -48,3 +48,7 @@ pull_request_rules:
actions:
merge:
method: squash
commit_message_template: |
{{ title }} (#{{ number }})
{{ body }}

View file

@ -95,6 +95,51 @@ context('Control Link', () => {
});
});
it('show title field in link', () => {
get_dialog_with_link().as('dialog');
cy.insert_doc("Property Setter", {
"doctype": "Property Setter",
"doc_type": "ToDo",
"property": "show_title_field_in_link",
"property_type": "Check",
"doctype_or_field": "DocType",
"value": "1"
}, true);
cy.window().its('frappe').then(frappe => {
if (!frappe.boot) {
frappe.boot = {
link_title_doctypes: ['ToDo']
};
} else {
frappe.boot.link_title_doctypes = ['ToDo'];
}
});
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
cy.wait('@search_link');
cy.get('@input').type('todo for link');
cy.wait('@search_link');
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
cy.get('.frappe-control[data-fieldname=link] input').blur();
cy.get('@dialog').then(dialog => {
cy.get('@todos').then(todos => {
let field = dialog.get_field('link');
let value = field.get_value();
let label = field.get_label_value();
expect(value).to.eq(todos[0]);
expect(label).to.eq('this is a test todo for link');
cy.remove_doc("Property Setter", "ToDo-main-show_title_field_in_link");
});
});
});
it('should update dependant fields (via fetch_from)', () => {
cy.get('@todos').then(todos => {
cy.visit(`/app/todo/${todos[0]}`);

View file

@ -11,30 +11,60 @@ context('Report View', () => {
'title': 'Doc 1',
'description': 'Random Text',
'enabled': 0,
// submit document
'docstatus': 1
}, true).as('doc');
'docstatus': 1 // submit document
}, true);
return cy.window().its('frappe').then(frappe => {
return frappe.call("frappe.tests.ui_test_helpers.create_multiple_contact_records");
});
});
it('Field with enabled allow_on_submit should be editable.', () => {
cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update');
cy.visit(`/app/List/${doctype_name}/Report`);
// check status column added from docstatus
cy.get('.dt-row-0 > .dt-cell--col-3').should('contain', 'Submitted');
let cell = cy.get('.dt-row-0 > .dt-cell--col-4');
// select the cell
cell.dblclick();
cell.get('.dt-cell__edit--col-4').findByRole('checkbox').check({ force: true });
cy.wait('@value-update');
cy.get('@doc').then(doc => {
cy.call('frappe.client.get_value', {
doctype: doc.doctype,
filters: {
name: doc.name,
},
fieldname: 'enabled'
}).then(r => {
expect(r.message.enabled).to.equals(1);
});
cy.call('frappe.client.get_value', {
doctype: doctype_name,
filters: {
title: 'Doc 1',
},
fieldname: 'enabled'
}).then(r => {
expect(r.message.enabled).to.equals(1);
});
});
it('test load more with count selection buttons', () => {
cy.visit('/app/contact/view/report');
cy.clear_filters();
cy.get('.list-paging-area .list-count').should('contain.text', '20 of');
cy.get('.list-paging-area .btn-more').click();
cy.get('.list-paging-area .list-count').should('contain.text', '40 of');
cy.get('.list-paging-area .btn-more').click();
cy.get('.list-paging-area .list-count').should('contain.text', '60 of');
cy.get('.list-paging-area .btn-group .btn-paging[data-value="100"]').click();
cy.get('.list-paging-area .list-count').should('contain.text', '100 of');
cy.get('.list-paging-area .btn-more').click();
cy.get('.list-paging-area .list-count').should('contain.text', '200 of');
cy.get('.list-paging-area .btn-more').click();
cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click();
cy.get('.list-paging-area .list-count').should('contain.text', '500 of');
cy.get('.list-paging-area .btn-more').click();
cy.get('.list-paging-area .list-count').should('contain.text', '1000 of');
});
});

View file

@ -159,7 +159,10 @@ def get_request_form_data():
else:
data = frappe.local.form_dict.data
return frappe.parse_json(data)
try:
return frappe.parse_json(data)
except ValueError:
return frappe.local.form_dict
def validate_auth():
@ -208,7 +211,6 @@ def validate_oauth(authorization_header):
pass
def validate_auth_via_api_keys(authorization_header):
"""
Authenticate request using API keys and set session user

View file

@ -89,6 +89,7 @@ def get_bootinfo():
bootinfo.additional_filters_config = get_additional_filters_from_hooks()
bootinfo.desk_settings = get_desk_settings()
bootinfo.app_logo_url = get_app_logo()
bootinfo.link_title_doctypes = get_link_title_doctypes()
return bootinfo
@ -324,6 +325,15 @@ def get_desk_settings():
def get_notification_settings():
return frappe.get_cached_doc('Notification Settings', frappe.session.user)
def get_link_title_doctypes():
dts = frappe.get_all("DocType", {"show_title_field_in_link": 1})
custom_dts = frappe.get_all(
"Property Setter",
{"property": "show_title_field_in_link", "value": "1"},
["doc_type as name"],
)
return [d.name for d in dts + custom_dts if d]
def set_time_zone(bootinfo):
bootinfo.time_zone = {
"system": get_time_zone(),

View file

@ -17,6 +17,7 @@
"hide_days",
"hide_seconds",
"reqd",
"is_virtual",
"search_index",
"column_break_18",
"options",
@ -534,13 +535,19 @@
"fieldname": "show_dashboard",
"fieldtype": "Check",
"label": "Show Dashboard"
},
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Virtual"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-01-03 11:56:19.812863",
"modified": "2022-01-27 21:22:20.529072",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -46,6 +46,7 @@
"allow_auto_repeat",
"view_settings",
"title_field",
"show_title_field_in_link",
"search_fields",
"default_print_format",
"sort_field",
@ -582,6 +583,12 @@
"fieldname": "document_states_section",
"fieldtype": "Section Break",
"label": "Document States"
},
{
"default": "0",
"fieldname": "show_title_field_in_link",
"fieldtype": "Check",
"label": "Show Title in Link Fields"
}
],
"icon": "fa fa-bolt",
@ -663,7 +670,7 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2021-12-09 14:53:10.717788",
"modified": "2022-01-07 16:07:06.196534",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -1078,6 +1078,9 @@ def validate_fields(meta):
field.fetch_from = field.fetch_from.strip('\n').strip()
def validate_data_field_type(docfield):
if docfield.get("is_virtual"):
return
if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"):
if docfield.options and (docfield.options not in data_field_options):
df_str = frappe.bold(_(docfield.label))
@ -1323,10 +1326,9 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
else:
raise
def check_fieldname_conflicts(doctype, fieldname):
def check_fieldname_conflicts(docfield):
"""Checks if fieldname conflicts with methods or properties"""
doc = frappe.get_doc({"doctype": doctype})
doc = frappe.get_doc({"doctype": docfield.dt})
available_objects = [x for x in dir(doc) if isinstance(x, str)]
property_list = [
x for x in available_objects if isinstance(getattr(type(doc), x, None), property)
@ -1334,9 +1336,10 @@ def check_fieldname_conflicts(doctype, fieldname):
method_list = [
x for x in available_objects if x not in property_list and callable(getattr(doc, x))
]
msg = _("Fieldname {0} conflicting with meta object").format(docfield.fieldname)
if fieldname in method_list + property_list:
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname))
if docfield.fieldname in method_list + property_list:
frappe.msgprint(msg, raise_exception=not docfield.is_virtual)
def clear_linked_doctype_cache():
frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled')

View file

@ -1,458 +1,468 @@
{
"actions": [],
"allow_import": 1,
"creation": "2013-01-10 16:34:01",
"description": "Adds a custom field to a DocType",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"dt",
"module",
"label",
"label_help",
"fieldname",
"insert_after",
"length",
"column_break_6",
"fieldtype",
"precision",
"hide_seconds",
"hide_days",
"options",
"fetch_from",
"fetch_if_empty",
"options_help",
"section_break_11",
"collapsible",
"collapsible_depends_on",
"default",
"depends_on",
"mandatory_depends_on",
"read_only_depends_on",
"properties",
"non_negative",
"reqd",
"unique",
"read_only",
"ignore_user_permissions",
"hidden",
"print_hide",
"print_hide_if_no_value",
"print_width",
"no_copy",
"allow_on_submit",
"in_list_view",
"in_standard_filter",
"in_global_search",
"in_preview",
"bold",
"report_hide",
"search_index",
"allow_in_quick_entry",
"ignore_xss_filter",
"translatable",
"hide_border",
"description",
"permlevel",
"width",
"columns"
],
"fields": [{
"bold": 1,
"fieldname": "dt",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Document",
"oldfieldname": "dt",
"oldfieldtype": "Link",
"options": "DocType",
"reqd": 1,
"search_index": 1
},
{
"bold": 1,
"fieldname": "label",
"fieldtype": "Data",
"in_filter": 1,
"label": "Label",
"no_copy": 1,
"oldfieldname": "label",
"oldfieldtype": "Data"
},
{
"fieldname": "label_help",
"fieldtype": "HTML",
"label": "Label Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname",
"no_copy": 1,
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1
},
{
"description": "Select the label after which you want to insert new field.",
"fieldname": "insert_after",
"fieldtype": "Select",
"label": "Insert After",
"no_copy": 1,
"oldfieldname": "insert_after",
"oldfieldtype": "Select"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"bold": 1,
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"label": "Field Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
"reqd": 1
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
},
{
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
},
{
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
},
{
"fieldname": "options_help",
"fieldtype": "HTML",
"label": "Options Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
},
{
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On"
},
{
"fieldname": "default",
"fieldtype": "Text",
"label": "Default Value",
"oldfieldname": "default",
"oldfieldtype": "Text"
},
{
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"length": 255
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Field Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
},
{
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"label": "Permission Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
},
{
"fieldname": "width",
"fieldtype": "Data",
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data"
},
{
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
},
{
"fieldname": "properties",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
},
{
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Mandatory Field",
"oldfieldname": "reqd",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype===\"Link\"",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden"
},
{
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
},
{
"fieldname": "print_width",
"fieldtype": "Data",
"hidden": 1,
"label": "Print Width",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"fieldname": "no_copy",
"fieldtype": "Check",
"label": "No Copy",
"oldfieldname": "no_copy",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
},
{
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
},
{
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
},
{
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
},
{
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "search_index",
"fieldtype": "Check",
"hidden": 1,
"label": "Index",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
},
{
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
},
{
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
},
{
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"length": 255
},
{
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"length": 255
},
{
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
},
{
"default": "0",
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_seconds",
"fieldtype": "Check",
"label": "Hide Seconds"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_days",
"fieldtype": "Check",
"label": "Hide Days"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
},
{
"default": "0",
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
"fieldname": "non_negative",
"fieldtype": "Check",
"label": "Non Negative"
},
{
"fieldname": "module",
"fieldtype": "Link",
"label": "Module (for export)",
"options": "Module Def"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-09-04 12:45:23.810120",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
"owner": "Administrator",
"permissions": [{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "dt,label,fieldtype,options",
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
"actions": [],
"allow_import": 1,
"creation": "2013-01-10 16:34:01",
"description": "Adds a custom field to a DocType",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"dt",
"module",
"label",
"label_help",
"fieldname",
"insert_after",
"length",
"column_break_6",
"fieldtype",
"precision",
"hide_seconds",
"hide_days",
"options",
"fetch_from",
"fetch_if_empty",
"options_help",
"section_break_11",
"collapsible",
"collapsible_depends_on",
"default",
"depends_on",
"mandatory_depends_on",
"read_only_depends_on",
"properties",
"non_negative",
"reqd",
"unique",
"is_virtual",
"read_only",
"ignore_user_permissions",
"hidden",
"print_hide",
"print_hide_if_no_value",
"print_width",
"no_copy",
"allow_on_submit",
"in_list_view",
"in_standard_filter",
"in_global_search",
"in_preview",
"bold",
"report_hide",
"search_index",
"allow_in_quick_entry",
"ignore_xss_filter",
"translatable",
"hide_border",
"description",
"permlevel",
"width",
"columns"
],
"fields": [
{
"bold": 1,
"fieldname": "dt",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Document",
"oldfieldname": "dt",
"oldfieldtype": "Link",
"options": "DocType",
"reqd": 1,
"search_index": 1
},
{
"bold": 1,
"fieldname": "label",
"fieldtype": "Data",
"in_filter": 1,
"label": "Label",
"no_copy": 1,
"oldfieldname": "label",
"oldfieldtype": "Data"
},
{
"fieldname": "label_help",
"fieldtype": "HTML",
"label": "Label Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname",
"no_copy": 1,
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1
},
{
"description": "Select the label after which you want to insert new field.",
"fieldname": "insert_after",
"fieldtype": "Select",
"label": "Insert After",
"no_copy": 1,
"oldfieldname": "insert_after",
"oldfieldtype": "Select"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"bold": 1,
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"label": "Field Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
"reqd": 1
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
},
{
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
},
{
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
},
{
"fieldname": "options_help",
"fieldtype": "HTML",
"label": "Options Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
},
{
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On"
},
{
"fieldname": "default",
"fieldtype": "Text",
"label": "Default Value",
"oldfieldname": "default",
"oldfieldtype": "Text"
},
{
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"length": 255
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Field Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
},
{
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"label": "Permission Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
},
{
"fieldname": "width",
"fieldtype": "Data",
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data"
},
{
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
},
{
"fieldname": "properties",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
},
{
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Mandatory Field",
"oldfieldname": "reqd",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Is Virtual"
},
{
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype===\"Link\"",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden"
},
{
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
},
{
"fieldname": "print_width",
"fieldtype": "Data",
"hidden": 1,
"label": "Print Width",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"fieldname": "no_copy",
"fieldtype": "Check",
"label": "No Copy",
"oldfieldname": "no_copy",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
},
{
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
},
{
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
},
{
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
},
{
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "search_index",
"fieldtype": "Check",
"hidden": 1,
"label": "Index",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
},
{
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
},
{
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
},
{
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"length": 255
},
{
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"length": 255
},
{
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
},
{
"default": "0",
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_seconds",
"fieldtype": "Check",
"label": "Hide Seconds"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_days",
"fieldtype": "Check",
"label": "Hide Days"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
},
{
"default": "0",
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
"fieldname": "non_negative",
"fieldtype": "Check",
"label": "Non Negative"
},
{
"fieldname": "module",
"fieldtype": "Link",
"label": "Module (for export)",
"options": "Module Def"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-01-27 21:47:01.065556",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "dt,label,fieldtype,options",
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View file

@ -54,7 +54,7 @@ class CustomField(Document):
old_fieldtype = self.db_get('fieldtype')
is_fieldtype_changed = (not self.is_new()) and (old_fieldtype != self.fieldtype)
if is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype):
if not self.is_virtual and is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype):
frappe.throw(_("Fieldtype cannot be changed from {0} to {1}").format(old_fieldtype, self.fieldtype))
if not self.fieldname:
@ -65,7 +65,7 @@ class CustomField(Document):
if not self.flags.ignore_validate:
from frappe.core.doctype.doctype.doctype import check_fieldname_conflicts
check_fieldname_conflicts(self.dt, self.fieldname)
check_fieldname_conflicts(self)
def on_update(self):
if not frappe.flags.in_setup_wizard:

View file

@ -27,6 +27,7 @@
"autoname",
"view_settings_section",
"title_field",
"show_title_field_in_link",
"image_field",
"default_print_format",
"column_break_29",
@ -296,6 +297,12 @@
"fieldtype": "Table",
"label": "States",
"options": "DocType State"
},
{
"default": "0",
"fieldname": "show_title_field_in_link",
"fieldtype": "Check",
"label": "Show Title in Link Fields"
}
],
"hide_toolbar": 1,
@ -304,7 +311,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-12-14 16:45:04.308690",
"modified": "2022-01-07 16:07:06.196534",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",

View file

@ -418,6 +418,9 @@ class CustomizeForm(Document):
return property_value
def validate_fieldtype_change(self, df, old_value, new_value):
if df.is_virtual:
return
allowed = self.allow_fieldtype_change(old_value, new_value)
if allowed:
old_value_length = cint(frappe.db.type_map.get(old_value)[1])
@ -430,7 +433,8 @@ class CustomizeForm(Document):
self.validate_fieldtype_length()
else:
self.flags.update_db = True
if not allowed:
else:
frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx))
def validate_fieldtype_length(self):
@ -512,7 +516,8 @@ doctype_properties = {
'email_append_to': 'Check',
'subject_field': 'Data',
'sender_field': 'Data',
'autoname': 'Data'
'autoname': 'Data',
'show_title_field_in_link': 'Check'
}
docfield_properties = {
@ -558,7 +563,8 @@ docfield_properties = {
'allow_in_quick_entry': 'Check',
'hide_border': 'Check',
'hide_days': 'Check',
'hide_seconds': 'Check'
'hide_seconds': 'Check',
'is_virtual': 'Check',
}
doctype_link_properties = {

View file

@ -14,6 +14,7 @@
"non_negative",
"reqd",
"unique",
"is_virtual",
"in_list_view",
"in_standard_filter",
"in_global_search",
@ -115,6 +116,12 @@
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Is Virtual"
},
{
"default": "0",
"fieldname": "in_list_view",
@ -436,7 +443,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-01-03 14:50:32.035768",
"modified": "2022-01-27 21:45:22.349776",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -1,4 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
@ -18,53 +18,19 @@ class PropertySetter(Document):
def validate(self):
self.validate_fieldtype_change()
if self.is_new():
delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name)
# clear cache
frappe.clear_cache(doctype = self.doc_type)
def validate_fieldtype_change(self):
if self.field_name in not_allowed_fieldtype_change and \
self.property == 'fieldtype':
frappe.throw(_("Field type cannot be changed for {0}").format(self.field_name))
def get_property_list(self, dt):
return frappe.db.get_all('DocField',
fields=['fieldname', 'label', 'fieldtype'],
filters={
'parent': dt,
'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
'fieldname': ['!=', '']
},
order_by='label asc',
as_dict=1
)
def get_setup_data(self):
return {
'doctypes': frappe.get_all("DocType", pluck="name"),
'dt_properties': self.get_property_list('DocType'),
'df_properties': self.get_property_list('DocField')
}
def get_field_ids(self):
return frappe.db.get_values(
"DocField",
filters={"parent": self.doc_type},
fieldname=["name", "fieldtype", "label", "fieldname"],
as_dict=True,
)
def get_defaults(self):
if not self.field_name:
return frappe.get_all("DocType", filters={"name": self.doc_type}, fields="*")[0]
else:
return frappe.db.get_values(
"DocField",
filters={"fieldname": self.field_name, "parent": self.doc_type},
fieldname="*",
)[0]
if (
self.property == 'fieldtype'
and self.field_name in not_allowed_fieldtype_change
):
frappe.throw(
_("Field type cannot be changed for {0}").format(self.field_name)
)
def on_update(self):
if frappe.flags.in_patch:
@ -74,6 +40,7 @@ class PropertySetter(Document):
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
validate_fields_for_doctype(self.doc_type)
def make_property_setter(doctype, fieldname, property, value, property_type, for_doctype = False,
validate_fields_for_doctype=True):
# WARNING: Ignores Permissions
@ -91,6 +58,7 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for
property_setter.insert()
return property_setter
def delete_property_setter(doc_type, property, field_name=None, row_name=None):
"""delete other property setters on this, if this is new"""
filters = dict(doc_type=doc_type, property=property)
@ -100,4 +68,3 @@ def delete_property_setter(doc_type, property, field_name=None, row_name=None):
filters["row_name"] = row_name
frappe.db.delete('Property Setter', filters)

View file

@ -177,6 +177,8 @@ class Database(object):
raise frappe.QueryTimeoutError(e)
elif frappe.conf.db_type == 'postgres':
# TODO: added temporarily
print(e)
raise
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):

View file

@ -224,6 +224,7 @@ CREATE TABLE `tabDocType` (
`email_append_to` int(1) NOT NULL DEFAULT 0,
`subject_field` varchar(255) DEFAULT NULL,
`sender_field` varchar(255) DEFAULT NULL,
`show_title_field_in_link` int(1) NOT NULL DEFAULT 0,
`migration_hash` varchar(255) DEFAULT NULL,
PRIMARY KEY (`name`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View file

@ -229,6 +229,7 @@ CREATE TABLE "tabDocType" (
"email_append_to" smallint NOT NULL DEFAULT 0,
"subject_field" varchar(255) DEFAULT NULL,
"sender_field" varchar(255) DEFAULT NULL,
"show_title_field_in_link" smallint NOT NULL DEFAULT 0,
"migration_hash" varchar(255) DEFAULT NULL,
PRIMARY KEY ("name")
) ;

View file

@ -67,7 +67,7 @@ class DBTable:
"""
get columns from docfields and custom fields
"""
fields = self.meta.get_fieldnames_with_value(True)
fields = self.meta.get_fieldnames_with_value(with_field_meta=True)
# optional fields like _comments
if not self.meta.get('istable'):
@ -85,6 +85,9 @@ class DBTable:
})
for field in fields:
if field.get("is_virtual"):
continue
self.columns[field.get('fieldname')] = DbColumn(
self,
field.get('fieldname'),

View file

@ -0,0 +1,16 @@
frappe.listview_settings['Dashboard'] = {
button: {
show(doc) {
return doc.name;
},
get_label() {
return frappe.utils.icon("dashboard-list", "sm");
},
get_description(doc) {
return __('View {0}', [`${doc.name}`]);
},
action(doc) {
frappe.set_route('dashboard-view', doc.name);
}
},
};

View file

@ -49,7 +49,7 @@ def getdoc(doctype, name, user=None):
raise
doc.add_seen()
set_link_titles(doc)
frappe.response.docs.append(doc)
@frappe.whitelist()
@ -367,6 +367,60 @@ def get_additional_timeline_content(doctype, docname):
return contents
def set_link_titles(doc):
link_titles = {}
link_titles.update(get_title_values_for_link_and_dynamic_link_fields(doc))
link_titles.update(get_title_values_for_table_and_multiselect_fields(doc))
send_link_titles(link_titles)
def get_title_values_for_link_and_dynamic_link_fields(doc, link_fields=None):
link_titles = {}
if not link_fields:
meta = frappe.get_meta(doc.doctype)
link_fields = meta.get_link_fields() + meta.get_dynamic_link_fields()
for field in link_fields:
if not doc.get(field.fieldname):
continue
doctype = field.options if field.fieldtype == "Link" else doc.get(field.options)
meta = frappe.get_meta(doctype)
if not meta or not (meta.title_field and meta.show_title_field_in_link):
continue
link_title = frappe.db.get_value(
doctype, doc.get(field.fieldname), meta.title_field, cache=True
)
link_titles.update({doctype + "::" + doc.get(field.fieldname): link_title})
return link_titles
def get_title_values_for_table_and_multiselect_fields(doc, table_fields=None):
link_titles = {}
if not table_fields:
meta = frappe.get_meta(doc.doctype)
table_fields = meta.get_table_fields()
for field in table_fields:
if not doc.get(field.fieldname):
continue
for value in doc.get(field.fieldname):
link_titles.update(get_title_values_for_link_and_dynamic_link_fields(value))
return link_titles
def send_link_titles(link_titles):
"""Append link titles dict in `frappe.local.response`."""
if "_link_titles" not in frappe.local.response:
frappe.local.response["_link_titles"] = {}
frappe.local.response["_link_titles"].update(link_titles)
def update_user_info(docinfo):
for d in docinfo.communications:
frappe.utils.add_user_info(d.sender, docinfo.user_info)
@ -387,3 +441,4 @@ def get_user_info_for_viewers(users):
frappe.utils.add_user_info(user, user_info)
return user_info

View file

@ -49,8 +49,10 @@ def sanitize_searchfield(searchfield):
# this is called by the Link Field
@frappe.whitelist()
def search_link(doctype, txt, query=None, filters=None, page_length=20, searchfield=None, reference_doctype=None, ignore_user_permissions=False):
search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
frappe.response['results'] = build_for_autosuggest(frappe.response["values"])
search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters,
reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
frappe.response["results"] = build_for_autosuggest(frappe.response["values"], doctype=doctype)
del frappe.response["values"]
# this is called by the search box
@ -138,6 +140,12 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
fields = list(set(fields + json.loads(filter_fields)))
formatted_fields = ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in fields]
title_field_query = get_title_field_query(meta)
# Insert title field query after name
if title_field_query:
formatted_fields.insert(1, title_field_query)
# find relevance as location of search term from the beginning of string `name`. used for sorting results.
formatted_fields.append("""locate({_txt}, `tab{doctype}`.`name`) as `_relevance`""".format(
_txt=frappe.db.escape((txt or "").replace("%", "").replace("@", "")), doctype=doctype))
@ -205,11 +213,38 @@ def get_std_fields_list(meta, key):
return sflist
def build_for_autosuggest(res):
def get_title_field_query(meta):
title_field = meta.title_field if meta.title_field else None
show_title_field_in_link = meta.show_title_field_in_link if meta.show_title_field_in_link else None
field = None
if title_field and show_title_field_in_link:
field = "`tab{0}`.{1} as `label`".format(meta.name, title_field)
return field
def build_for_autosuggest(res, doctype):
results = []
for r in res:
out = {"value": r[0], "description": ", ".join(unique(cstr(d) for d in r if d)[1:])}
results.append(out)
meta = frappe.get_meta(doctype)
if not (meta.title_field and meta.show_title_field_in_link):
for r in res:
r = list(r)
results.append({
"value": r[0],
"description": ", ".join(unique(cstr(d) for d in r[1:] if d))
})
else:
title_field_exists = meta.title_field and meta.show_title_field_in_link
_from = 2 if title_field_exists else 1 # to exclude title from description if title_field_exists
for r in res:
r = list(r)
results.append({
"value": r[0],
"label": r[1] if title_field_exists else None,
"description": ", ".join(unique(cstr(d) for d in r[_from:] if d))
})
return results
def scrub_custom_query(query, key, txt):
@ -272,3 +307,12 @@ def get_user_groups():
return frappe.get_all('User Group', fields=['name as id', 'name as value'], update={
'is_group': True
})
@frappe.whitelist()
def get_link_title(doctype, docname):
meta = frappe.get_meta(doctype)
if meta.title_field and meta.show_title_field_in_link:
return frappe.db.get_value(doctype, docname, meta.title_field)
return docname

View file

@ -1,16 +1,14 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import datetime
import frappe
import datetime
from frappe import _
from frappe.model import default_fields, table_fields, child_table_fields
from frappe.model import child_table_fields, default_fields, display_fieldtypes, table_fields
from frappe.model.naming import set_new_name
from frappe.model.utils.link_count import notify_link_count
from frappe.modules import load_doctype_module
from frappe.model import display_fieldtypes
from frappe.utils import (cint, flt, now, cstr, strip_html,
sanitize_html, sanitize_email, cast_fieldtype)
from frappe.utils import cast_fieldtype, cint, cstr, flt, now, sanitize_html, strip_html
from frappe.utils.html_utils import unescape_html
from frappe.model.docstatus import DocStatus
@ -75,9 +73,12 @@ def get_controller(doctype):
return site_controllers[doctype]
class BaseDocument(object):
ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")
ignore_in_setter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")
def __init__(self, d):
if d.get("doctype"):
self.doctype = d["doctype"]
self.update(d)
self.dont_update_if_missing = []
@ -146,10 +147,14 @@ class BaseDocument(object):
else:
value = self.__dict__.get(key, default)
if value is None and key not in self.ignore_in_getter \
and key in (d.fieldname for d in self.meta.get_table_fields()):
self.set(key, [])
value = self.__dict__.get(key)
if value is None and key in (
d.fieldname for d in self.meta.get_table_fields()
):
value = []
self.set(key, value)
if limit and isinstance(value, (list, tuple)) and len(value) > limit:
value = value[:limit]
return value
else:
@ -159,6 +164,9 @@ class BaseDocument(object):
return self.get(key, filters=filters, limit=1)[0]
def set(self, key, value, as_value=False):
if key in self.ignore_in_setter:
return
if isinstance(value, list) and not as_value:
self.__dict__[key] = []
self.extend(key, value)
@ -244,7 +252,7 @@ class BaseDocument(object):
return value
def get_valid_dict(self, sanitize=True, convert_dates_to_str=False, ignore_nulls = False):
def get_valid_dict(self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False):
d = frappe._dict()
for fieldname in self.meta.get_valid_columns():
d[fieldname] = self.get(fieldname)
@ -254,7 +262,26 @@ class BaseDocument(object):
continue
df = self.meta.get_field(fieldname)
if df:
if df and df.get("is_virtual"):
if ignore_virtual:
del d[fieldname]
continue
from frappe.utils.safe_exec import get_safe_globals
if d[fieldname] is None:
if df.get("options"):
d[fieldname] = frappe.safe_eval(
code=df.get("options"),
eval_globals=get_safe_globals(),
eval_locals={"doc": self},
)
else:
_val = getattr(self, fieldname, None)
if _val and not callable(_val):
d[fieldname] = _val
elif df:
if df.fieldtype=="Check":
d[fieldname] = 1 if cint(d[fieldname]) else 0
@ -328,6 +355,7 @@ class BaseDocument(object):
def as_dict(self, no_nulls=False, no_default_fields=False, convert_dates_to_str=False, no_child_table_fields=False):
doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str)
doc["doctype"] = self.doctype
for df in self.meta.get_table_fields():
children = self.get(df.fieldname) or []
doc[df.fieldname] = [
@ -375,26 +403,43 @@ class BaseDocument(object):
fieldname = [df.fieldname for df in self.meta.get_table_fields() if df.options==doctype]
return fieldname[0] if fieldname else None
def db_insert(self):
"""INSERT the document (with valid columns) in the database."""
def db_insert(self, ignore_if_duplicate=False):
"""INSERT the document (with valid columns) in the database.
args:
ignore_if_duplicate: ignore primary key collision
at database level (postgres)
in python (mariadb)
"""
if not self.name:
# name will be set by document class in most cases
set_new_name(self)
conflict_handler = ""
# On postgres we can't implcitly ignore PK collision
# So instruct pg to ignore `name` field conflicts
if ignore_if_duplicate and frappe.db.db_type == "postgres":
conflict_handler = "on conflict (name) do nothing"
if not self.creation:
self.creation = self.modified = now()
self.created_by = self.modified_by = frappe.session.user
# if doctype is "DocType", don't insert null values as we don't know who is valid yet
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE)
d = self.get_valid_dict(
convert_dates_to_str=True,
ignore_nulls=self.doctype in DOCTYPES_FOR_DOCTYPE,
ignore_virtual=True,
)
columns = list(d)
try:
frappe.db.sql("""INSERT INTO `tab{doctype}` ({columns})
VALUES ({values})""".format(
doctype = self.doctype,
columns = ", ".join("`"+c+"`" for c in columns),
values = ", ".join(["%s"] * len(columns))
VALUES ({values}) {conflict_handler}""".format(
doctype=self.doctype,
columns=", ".join("`"+c+"`" for c in columns),
values=", ".join(["%s"] * len(columns)),
conflict_handler=conflict_handler
), list(d.values()))
except Exception as e:
if frappe.db.is_primary_key_violation(e):
@ -407,8 +452,11 @@ class BaseDocument(object):
self.db_insert()
return
frappe.msgprint(_("{0} {1} already exists").format(self.doctype, frappe.bold(self.name)), title=_("Duplicate Name"), indicator="red")
raise frappe.DuplicateEntryError(self.doctype, self.name, e)
if not ignore_if_duplicate:
frappe.msgprint(_("{0} {1} already exists")
.format(self.doctype, frappe.bold(self.name)),
title=_("Duplicate Name"), indicator="red")
raise frappe.DuplicateEntryError(self.doctype, self.name, e)
elif frappe.db.is_unique_key_violation(e):
# unique constraint
@ -736,7 +784,7 @@ class BaseDocument(object):
type_map = frappe.db.type_map
for fieldname, value in self.get_valid_dict().items():
for fieldname, value in self.get_valid_dict(ignore_virtual=True).items():
df = self.meta.get_field(fieldname)
if not df or df.fieldtype == 'Check':
@ -814,7 +862,7 @@ class BaseDocument(object):
if frappe.flags.in_install:
return
for fieldname, value in self.get_valid_dict().items():
for fieldname, value in self.get_valid_dict(ignore_virtual=True).items():
if not value or not isinstance(value, str):
continue

View file

@ -249,11 +249,7 @@ class Document(BaseDocument):
if getattr(self.meta, "issingle", 0):
self.update_single(self.get_valid_dict())
else:
try:
self.db_insert()
except frappe.DuplicateEntryError as e:
if not ignore_if_duplicate:
raise e
self.db_insert(ignore_if_duplicate=ignore_if_duplicate)
# children
for d in self.get_all_children():

View file

@ -444,9 +444,16 @@ class Meta(Document):
self.permissions = [Document(d) for d in custom_perms]
def get_fieldnames_with_value(self, with_field_meta=False):
return [df if with_field_meta else df.fieldname \
for df in self.fields if df.fieldtype not in no_value_fields]
def is_value_field(docfield):
return not (
docfield.get("is_virtual")
or docfield.fieldtype in no_value_fields
)
if with_field_meta:
return [df for df in self.fields if is_value_field(df)]
return [df.fieldname for df in self.fields if is_value_field(df)]
def get_fields_to_check_permissions(self, user_permission_doctypes):
fields = self.get("fields", {

View file

@ -184,6 +184,7 @@ frappe.patches.v13_0.queryreport_columns
frappe.patches.v13_0.jinja_hook
frappe.patches.v13_0.update_notification_channel_if_empty
frappe.patches.v13_0.set_first_day_of_the_week
execute:frappe.reload_doc('custom', 'doctype', 'custom_field')
frappe.patches.v14_0.update_workspace2 # 20.09.2021
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021
frappe.patches.v14_0.transform_todo_schema

View file

@ -814,6 +814,13 @@
<path d="M16.814 13.3304L17.9274 12.6875" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-dashboard-list">
<path d="M7.5 2.5H4.5C3.94772 2.5 3.5 2.94772 3.5 3.5V9.5C3.5 10.0523 3.94772 10.5 4.5 10.5H7.5C8.05228 10.5 8.5 10.0523 8.5 9.5V3.5C8.5 2.94772 8.05228 2.5 7.5 2.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 13.5H4.5C3.94772 13.5 3.5 13.9477 3.5 14.5V16.5C3.5 17.0523 3.94772 17.5 4.5 17.5H7.5C8.05228 17.5 8.5 17.0523 8.5 16.5V14.5C8.5 13.9477 8.05228 13.5 7.5 13.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.5 2.5H12.5C11.9477 2.5 11.5 2.94772 11.5 3.5V6.5C11.5 7.05228 11.9477 7.5 12.5 7.5H15.5C16.0523 7.5 16.5 7.05228 16.5 6.5V3.5C16.5 2.94772 16.0523 2.5 15.5 2.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.5 10.5H12.5C11.9477 10.5 11.5 10.9477 11.5 11.5V16.5C11.5 17.0523 11.9477 17.5 12.5 17.5H15.5C16.0523 17.5 16.5 17.0523 16.5 16.5V11.5C16.5 10.9477 16.0523 10.5 15.5 10.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-text">
<path d="M5 4V6.4H9V16H11.4V6.4H15.4V4H5Z" fill="var(--icon-stroke)" stroke="none"/>
</symbol>

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View file

@ -39,6 +39,9 @@ frappe.ui.form.Control = class BaseControl {
if (this.df.get_status) {
return this.df.get_status(this);
}
if (this.df.is_virtual) {
return "Read";
}
if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) {
// like in case of a dialog box
@ -52,7 +55,7 @@ frappe.ui.form.Control = class BaseControl {
if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console
return "None";
} else if (cint(this.df.read_only)) {
} else if (cint(this.df.read_only || this.df.is_virtual)) {
// eslint-disable-next-line
if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console
return "Read";

View file

@ -29,7 +29,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
setTimeout(function() {
if(me.$input.val() && me.get_options()) {
let doctype = me.get_options();
let name = me.$input.val();
let name = me.get_input_value();
me.$link.toggle(true);
me.$link_open.attr('href', frappe.utils.get_form_link(doctype, name));
}
@ -69,6 +69,59 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
this.$input_area.find(".link-btn").remove();
}
}
set_formatted_input(value) {
super.set_formatted_input();
if (!value) return;
if (!this.title_value_map) {
this.title_value_map = {};
}
this.set_link_title(value);
}
set_link_title(value) {
let doctype = this.get_options();
if (!doctype) return;
if (in_list(frappe.boot.link_title_doctypes, doctype)) {
let link_title = frappe.utils.get_link_title(doctype, value);
if (!link_title) {
link_title = frappe.utils
.fetch_link_title(doctype, value)
.then(link_title => {
this.set_input_value(link_title);
this.title_value_map[link_title] = value;
});
} else {
this.set_input_value(link_title);
this.title_value_map[link_title] = value;
}
} else {
this.set_input_value(value);
}
}
parse_validate_and_set_in_model(value, e, label) {
if (this.parse) value = this.parse(value, label);
if (label) {
this.label = label;
frappe.utils.add_link_title(this.df.options, value, label);
}
return this.validate_and_set_in_model(value, e);
}
get_input_value() {
if (this.$input) {
const input_value = this.$input.val();
return this.title_value_map?.[input_value] || input_value;
}
return null;
}
get_label_value() {
return this.$input ? this.$input.val() : "";
}
set_input_value(value) {
this.$input && this.$input.val(value);
}
open_advanced_search() {
var doctype = this.get_options();
if(!doctype) return;
@ -98,7 +151,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
}
// partially entered name field
frappe.route_options.name_field = this.get_value();
frappe.route_options.name_field = this.get_label_value();
// reference to calling link
frappe._from_link = this;
@ -120,6 +173,11 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
maxItems: 99,
autoFirst: true,
list: [],
replace: function (suggestion) {
// Override Awesomeplete replace function as it is used to set the input value
// https://github.com/LeaVerou/awesomplete/issues/17104#issuecomment-359185403
this.input.value = suggestion.label || suggestion.value;
},
data: function (item) {
return {
label: item.label || item.value,
@ -236,9 +294,11 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
me.selected = false;
return;
}
var value = me.get_input_value();
if(value!==me.last_value) {
me.parse_validate_and_set_in_model(value);
let value = me.get_input_value();
let label = me.get_label_value();
if (value !== me.last_value || me.label !== label) {
me.parse_validate_and_set_in_model(value, null, label);
}
});
@ -258,14 +318,15 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
// prevent selection on tab
var TABKEY = 9;
if(e.keyCode === TABKEY) {
if (e.keyCode === TABKEY) {
e.preventDefault();
me.awesomplete.close();
return false;
}
if(item.action) {
if (item.action) {
item.value = "";
item.label = "";
item.action.apply(me);
}
@ -277,12 +338,12 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
frappe.boot.user.last_selected_values[me.df.options] = item.value;
}
me.parse_validate_and_set_in_model(item.value);
me.parse_validate_and_set_in_model(item.value, null, item.label);
});
this.$input.on("awesomplete-selectcomplete", function(e) {
var o = e.originalEvent;
if(o.text.value.indexOf("__link_option") !== -1) {
let o = e.originalEvent;
if (o.text.value.indexOf("__link_option") !== -1) {
me.$input.val("");
}
});

View file

@ -83,15 +83,21 @@ frappe.ui.form.ControlMultiSelectPills = class ControlMultiSelectPills extends f
}
get_pill_html(value) {
const label = this.get_label(value);
const encoded_value = encodeURIComponent(value);
return `
<button class="data-pill btn tb-selected-value" data-value="${encoded_value}">
<span class="btn-link-to-form">${__(value)}</span>
<span class="btn-link-to-form">${__(label || value)}</span>
<span class="btn-remove">${frappe.utils.icon('close')}</span>
</button>
`;
}
get_label(value) {
const item = this._data?.find(d => d.value === value);
return item ? item.label || item.value : null;
}
get_awesomplete_settings() {
const settings = super.get_awesomplete_settings();

View file

@ -49,7 +49,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f
setup_buttons() {
this.$input_area.find('.link-btn').remove();
}
parse(value) {
parse(value, label) {
const link_field = this.get_link_field();
if (value) {
@ -62,6 +62,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f
[link_field.fieldname]: value
});
}
frappe.utils.add_link_title(link_field.options, value, label);
}
this._rows_list = this.rows.map(row => row[link_field.fieldname]);
return this.rows;
@ -126,10 +127,12 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f
this.$input_area.prepend(html);
}
get_pill_html(value) {
const link_field = this.get_link_field();
const encoded_value = encodeURIComponent(value);
const pill_name = frappe.utils.get_link_title(link_field.options, value) || value;
return `
<button class="data-pill btn tb-selected-value" data-value="${encoded_value}">
<span class="btn-link-to-form">${__(value)}</span>
<span class="btn-link-to-form">${__(pill_name)}</span>
<span class="btn-remove">${frappe.utils.icon('close')}</span>
</button>
`;

View file

@ -110,12 +110,14 @@ frappe.form.formatters = {
Link: function(value, docfield, options, doc) {
var doctype = docfield._options || docfield.options;
var original_value = value;
let link_title = frappe.utils.get_link_title(doctype, value);
if(value && value.match && value.match(/^['"].*['"]$/)) {
value.replace(/^.(.*).$/, "$1");
}
if(options && (options.for_print || options.only_value)) {
return value;
return link_title || value;
}
if(frappe.form.link_formatters[doctype]) {
@ -139,13 +141,14 @@ frappe.form.formatters = {
return `<a
href="/app/${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(original_value)}"
data-doctype="${doctype}"
data-name="${original_value}">
${__(options && options.label || value)}</a>`;
data-name="${original_value}"
data-value="${original_value}">
${__(options && options.label || link_title || value)}</a>`;
} else {
return value;
return link_title || value;
}
} else {
return value;
return link_title || value;
}
},
Date: function(value) {

View file

@ -55,7 +55,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
// prepare a list of mandatory, bold and allow in quick entry fields
this.mandatory = fields.filter(df => {
return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only);
return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only && !df.is_virtual);
});
}

View file

@ -249,30 +249,39 @@ frappe.ui.form.update_calling_link = (newdoc) => {
};
if (is_valid_doctype()) {
// set value
if (doc && doc.parentfield) {
//update values for child table
$.each(frappe._from_link.frm.fields_dict[doc.parentfield].grid.grid_rows, function (index, field) {
if (field.doc && field.doc.name === frappe._from_link.docname) {
frappe._from_link.set_value(newdoc.name);
}
});
} else {
frappe._from_link.set_value(newdoc.name);
}
// refresh field
frappe._from_link.refresh();
// if from form, switch
if (frappe._from_link.frm) {
frappe.set_route("Form",
frappe._from_link.frm.doctype, frappe._from_link.frm.docname)
.then(() => {
frappe.utils.scroll_to(frappe._from_link_scrollY);
frappe.model.with_doctype(newdoc.doctype, () => {
let meta = frappe.get_meta(newdoc.doctype);
// set value
if (doc && doc.parentfield) {
//update values for child table
$.each(frappe._from_link.frm.fields_dict[doc.parentfield].grid.grid_rows, function (index, field) {
if (field.doc && field.doc.name === frappe._from_link.docname) {
if (meta.title_field && meta.show_title_field_in_link) {
frappe.utils.add_link_title(newdoc.doctype, newdoc.name, newdoc[meta.title_field]);
}
frappe._from_link.set_value(newdoc.name);
}
});
}
} else {
if (meta.title_field && meta.show_title_field_in_link) {
frappe.utils.add_link_title(newdoc.doctype, newdoc.name, newdoc[meta.title_field]);
}
frappe._from_link.set_value(newdoc.name);
}
frappe._from_link = null;
// refresh field
frappe._from_link.refresh();
// if from form, switch
if (frappe._from_link.frm) {
frappe.set_route("Form",
frappe._from_link.frm.doctype, frappe._from_link.frm.docname)
.then(() => {
frappe.utils.scroll_to(frappe._from_link_scrollY);
});
}
frappe._from_link = null;
});
}
}

View file

@ -192,9 +192,18 @@ frappe.ui.form.ScriptManager = class ScriptManager {
}
function setup_add_fetch(df) {
if ((['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Attach Image',
'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype) || df.read_only==1)
&& df.fetch_from && df.fetch_from.indexOf(".")!=-1) {
let is_read_only_field = (
['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Text Editor', 'Attach Image',
'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype)
|| df.read_only == 1
|| df.is_virtual == 1
)
if (
is_read_only_field
&& df.fetch_from
&& df.fetch_from.indexOf(".") != -1
) {
var parts = df.fetch_from.split(".");
me.frm.add_fetch(parts[0], parts[1], df.fieldname, df.parent);
}

View file

@ -394,7 +394,6 @@ frappe.views.BaseList = class BaseList {
this.page_length = $this.data().value;
} else if ($this.is(".btn-more")) {
this.start = this.start + this.page_length;
this.page_length = 20;
}
this.refresh();
});
@ -475,7 +474,6 @@ frappe.views.BaseList = class BaseList {
this.render();
this.after_render();
this.freeze(false);
this.reset_defaults();
if (this.settings.refresh) {
this.settings.refresh(this);
}
@ -502,11 +500,6 @@ frappe.views.BaseList = class BaseList {
this.data = this.data.uniqBy((d) => d.name);
}
reset_defaults() {
this.page_length = this.page_length + this.start;
this.start = 0;
}
freeze() {
// show a freeze message while data is loading
}

View file

@ -1672,7 +1672,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
frappe.model.is_value_type(field_doc) &&
field_doc.fieldtype !== "Read Only" &&
!field_doc.hidden &&
!field_doc.read_only
!field_doc.read_only &&
!field_doc.is_virtual
);
};

View file

@ -260,6 +260,14 @@ frappe.request.call = function(opts) {
$.extend(frappe._messages, data.__messages);
}
// sync link titles
if (data._link_titles) {
if (!frappe._link_titles) {
frappe._link_titles = {};
}
$.extend(frappe._link_titles, data._link_titles);
}
// callbacks
var status_code_handler = statusCode[xhr.statusCode().status];
if (status_code_handler) {

View file

@ -314,6 +314,10 @@ frappe.ui.Filter = class {
return this.utils.get_selected_value(this.field, this.get_condition());
}
get_selected_label() {
return this.utils.get_selected_label(this.field);
}
get_condition() {
return this.filter_edit_area.find('.condition').val();
}
@ -361,7 +365,7 @@ frappe.ui.Filter = class {
get_filter_button_text() {
let value = this.utils.get_formatted_value(
this.field,
this.get_selected_value()
this.get_selected_label() || this.get_selected_value()
);
return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(
value
@ -449,6 +453,12 @@ frappe.ui.filter_utils = {
return val;
},
get_selected_label(field) {
if (in_list(["Link", "Dynamic Link"], field.df.fieldtype)) {
return field.get_label_value();
}
},
get_default_condition(df) {
if (df.fieldtype == 'Data') {
return 'like';

View file

@ -259,8 +259,16 @@ frappe.utils.xss_sanitise = function (string, options) {
'/': '&#x2F;'
};
const REGEX_SCRIPT = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi; // used in jQuery 1.7.2 src/ajax.js Line 14
const REGEX_ALERT = /confirm\(.*\)|alert\(.*\)|prompt\(.*\)/gi; // captures alert, confirm, prompt
options = Object.assign({}, DEFAULT_OPTIONS, options); // don't deep copy, immutable beauty.
// Rule 3 - TODO: Check event handlers?
// script and alert should be checked first or else it will be escaped
if (options.strategies.includes('js')) {
sanitised = sanitised.replace(REGEX_SCRIPT, "");
sanitised = sanitised.replace(REGEX_ALERT, "");
}
// Rule 1
if (options.strategies.includes('html')) {
for (let char in HTML_ESCAPE_MAP) {
@ -270,11 +278,6 @@ frappe.utils.xss_sanitise = function (string, options) {
}
}
// Rule 3 - TODO: Check event handlers?
if (options.strategies.includes('js')) {
sanitised = sanitised.replace(REGEX_SCRIPT, "");
}
return sanitised;
}

View file

@ -1416,5 +1416,42 @@ Object.assign(frappe.utils, {
arr.push(i);
}
return arr;
},
get_link_title(doctype, name) {
if (!doctype || !name || !frappe._link_titles) {
return;
}
return frappe._link_titles[doctype + "::" + name];
},
add_link_title(doctype, name, value) {
if (!doctype || !name) {
return;
}
if (!frappe._link_titles) {
// for link titles
frappe._link_titles = {};
}
frappe._link_titles[doctype + "::" + name] = value;
},
fetch_link_title(doctype, name) {
try {
return frappe.xcall("frappe.desk.search.get_link_title", {
"doctype": doctype,
"docname": name
}).then(title => {
frappe.utils.add_link_title(doctype, name, title);
return title;
});
} catch (error) {
console.log('Error while fetching link title.'); // eslint-disable-line
console.log(error); // eslint-disable-line
return Promise.resolve(name);
}
}
});

View file

@ -648,6 +648,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
// not a cancelled doc
&& data.docstatus !== 2
&& !df.read_only
&& !df.is_virtual
&& !df.hidden
// not a standard field i.e., owner, modified_by, etc.
&& !frappe.model.std_fields_list.includes(df.fieldname))
@ -1029,7 +1030,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
title += ` (${__(doctype)})`;
}
const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only;
const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only && !docfield.is_virtual;
const align = (() => {
const is_numeric = frappe.model.is_numeric_field(docfield);

View file

@ -343,7 +343,7 @@ frappe.views.TreeView = class TreeView {
this.ignore_fields = this.opts.ignore_fields || [];
var mandatory_fields = $.map(me.opts.meta.fields, function(d) {
return (d.reqd || d.bold && !d.read_only) ? d : null });
return (d.reqd || d.bold && !d.read_only && !!d.is_virtual) ? d : null });
var opts_field_names = this.fields.map(function(d) {
return d.fieldname

View file

@ -136,6 +136,8 @@
--shadow-md: 0px 8px 14px rgba(25, 39, 52, 0.08), 0px 2px 6px rgba(25, 39, 52, 0.04);
--shadow-lg: 0px 18px 22px rgba(25, 39, 52, 0.1), 0px 1px 10px rgba(0, 0, 0, 0.06), 0px 0.5px 5px rgba(25, 39, 52, 0.04);
--drop-shadow: 0px 0.5px 0px rgba(0, 0, 0, 0.05), 0px 0px 0px rgba(0, 0, 0, 0), 0px 2px 4px rgba(0, 0, 0, 0.05);
--modal-shadow: var(--shadow-md);
--card-shadow: var(--shadow-sm);
--btn-shadow: var(--shadow-xs);

View file

@ -187,7 +187,31 @@ $level-margin-right: 8px;
}
.list-paging-area, .footnote-area {
border-top: 1px sol var(--border-color);
border-top: 1px solid var(--border-color);
.btn-group {
box-shadow: var(--drop-shadow);
border-radius: var(--border-radius-md);
&> .btn:nth-child(2) {
border-left: none;
border-right: none;
}
.btn-paging {
box-shadow: none;
margin-left: 0px !important;
border: 1px solid var(--dark-border-color);
&.btn-info {
background-color: var(--gray-400);
border-color: var(--gray-400);
color: var(--white);
font-weight: var(--text-bold);
}
}
}
}
.frappe-card {

View file

@ -104,10 +104,10 @@ body[data-route^="Module"] .main-menu {
}
.sidebar-image-section {
width: min(100%, 170px);
cursor: pointer;
.sidebar-image {
width: min(100%, 170px);
height: auto;
max-height: 170px;
object-fit: cover;

View file

@ -1,77 +1,126 @@
import sys
import unittest
from contextlib import contextmanager
from random import choice
from threading import Thread
from typing import Dict, Optional, Tuple
from unittest.mock import patch
import requests
from semantic_version import Version
from werkzeug.test import TestResponse
import frappe
from frappe.utils import get_site_url
from frappe.utils import get_site_url, get_test_client
try:
_site = frappe.local.site
except Exception:
_site = None
authorization_token = None
@contextmanager
def suppress_stdout():
"""Supress stdout for tests which expectedly make noise
but that you don't need in tests"""
sys.stdout = None
try:
yield
finally:
sys.stdout = sys.__stdout__
def maintain_state(f):
def wrapper(*args, **kwargs):
frappe.db.rollback()
r = f(*args, **kwargs)
frappe.db.commit()
return r
return wrapper
def make_request(target: str, args: Optional[Tuple] = None, kwargs: Optional[Dict] = None) -> TestResponse:
t = ThreadWithReturnValue(target=target, args=args, kwargs=kwargs)
t.start()
t.join()
return t._return
class TestResourceAPI(unittest.TestCase):
SITE_URL = get_site_url(frappe.local.site)
def patch_request_header(key, *args, **kwargs):
if key == "Authorization":
return f"token {authorization_token}"
class ThreadWithReturnValue(Thread):
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
Thread.__init__(self, group, target, name, args, kwargs)
self._return = None
def run(self):
if self._target is not None:
with patch("frappe.app.get_site_name", return_value=_site):
header_patch = patch("frappe.get_request_header", new=patch_request_header)
if authorization_token:
header_patch.start()
self._return = self._target(*self._args, **self._kwargs)
if authorization_token:
header_patch.stop()
def join(self, *args):
Thread.join(self, *args)
return self._return
class FrappeAPITestCase(unittest.TestCase):
SITE = frappe.local.site
SITE_URL = get_site_url(SITE)
RESOURCE_URL = f"{SITE_URL}/api/resource"
TEST_CLIENT = get_test_client()
@property
def sid(self) -> str:
if not getattr(self, "_sid", None):
r = self.post("/api/method/login", {
"usr": "Administrator",
"pwd": frappe.conf.admin_password or "admin",
})
self._sid = r.headers[2][1].split(";")[0].lstrip("sid=")
return self._sid
def get(self, path: str, params: Optional[Dict] = None) -> TestResponse:
return make_request(target=self.TEST_CLIENT.get, args=(path, ), kwargs={"data": params})
def post(self, path, data) -> TestResponse:
return make_request(target=self.TEST_CLIENT.post, args=(path, ), kwargs={"data": data})
def put(self, path, data) -> TestResponse:
return make_request(target=self.TEST_CLIENT.put, args=(path, ), kwargs={"data": data})
def delete(self, path) -> TestResponse:
return make_request(target=self.TEST_CLIENT.delete, args=(path, ))
class TestResourceAPI(FrappeAPITestCase):
DOCTYPE = "ToDo"
GENERATED_DOCUMENTS = []
@classmethod
@maintain_state
def setUpClass(self):
def setUpClass(cls):
for _ in range(10):
doc = frappe.get_doc(
{"doctype": "ToDo", "description": frappe.mock("paragraph")}
).insert()
self.GENERATED_DOCUMENTS.append(doc.name)
cls.GENERATED_DOCUMENTS.append(doc.name)
frappe.db.commit()
@classmethod
@maintain_state
def tearDownClass(self):
for name in self.GENERATED_DOCUMENTS:
frappe.delete_doc_if_exists(self.DOCTYPE, name)
def tearDownClass(cls):
for name in cls.GENERATED_DOCUMENTS:
frappe.delete_doc_if_exists(cls.DOCTYPE, name)
frappe.db.commit()
def setUp(self):
# commit to ensure consistency in session (postgres CI randomly fails)
if frappe.conf.db_type == "postgres":
frappe.db.commit()
@property
def sid(self):
if not getattr(self, "_sid", None):
self._sid = requests.post(
f"{self.SITE_URL}/api/method/login",
data={
"usr": "Administrator",
"pwd": frappe.conf.admin_password or "admin",
},
).cookies.get("sid")
return self._sid
def get(self, path, params=""):
return requests.get(f"{self.RESOURCE_URL}/{path}?sid={self.sid}{params}")
def post(self, path, data):
return requests.post(
f"{self.RESOURCE_URL}/{path}?sid={self.sid}", data=frappe.as_json(data)
)
def put(self, path, data):
return requests.put(
f"{self.RESOURCE_URL}/{path}?sid={self.sid}", data=frappe.as_json(data)
)
def delete(self, path):
return requests.delete(f"{self.RESOURCE_URL}/{path}?sid={self.sid}")
if self._testMethodName == "test_auth_cycle":
from frappe.core.doctype.user.user import generate_keys
generate_keys("Administrator")
frappe.db.commit()
def test_unauthorized_call(self):
# test 1: fetch documents without auth
@ -80,88 +129,107 @@ class TestResourceAPI(unittest.TestCase):
def test_get_list(self):
# test 2: fetch documents without params
response = self.get(self.DOCTYPE)
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid})
self.assertEqual(response.status_code, 200)
self.assertIsInstance(response.json(), dict)
self.assertIn("data", response.json())
self.assertIsInstance(response.json, dict)
self.assertIn("data", response.json)
def test_get_list_limit(self):
# test 3: fetch data with limit
response = self.get(self.DOCTYPE, "&limit=2")
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "limit": 2})
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()["data"]), 2)
self.assertEqual(len(response.json["data"]), 2)
def test_get_list_dict(self):
# test 4: fetch response as (not) dict
response = self.get(self.DOCTYPE, "&as_dict=True")
json = frappe._dict(response.json())
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "as_dict": True})
json = frappe._dict(response.json)
self.assertEqual(response.status_code, 200)
self.assertIsInstance(json.data, list)
self.assertIsInstance(json.data[0], dict)
response = self.get(self.DOCTYPE, "&as_dict=False")
json = frappe._dict(response.json())
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "as_dict": False})
json = frappe._dict(response.json)
self.assertEqual(response.status_code, 200)
self.assertIsInstance(json.data, list)
self.assertIsInstance(json.data[0], list)
def test_get_list_debug(self):
# test 5: fetch response with debug
response = self.get(self.DOCTYPE, "&debug=true")
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "debug": True})
self.assertEqual(response.status_code, 200)
self.assertIn("exc", response.json())
self.assertIsInstance(response.json()["exc"], str)
self.assertIsInstance(eval(response.json()["exc"]), list)
self.assertIn("exc", response.json)
self.assertIsInstance(response.json["exc"], str)
self.assertIsInstance(eval(response.json["exc"]), list)
def test_get_list_fields(self):
# test 6: fetch response with fields
response = self.get(self.DOCTYPE, r'&fields=["description"]')
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "fields": '["description"]'})
self.assertEqual(response.status_code, 200)
json = frappe._dict(response.json())
json = frappe._dict(response.json)
self.assertIn("description", json.data[0])
def test_create_document(self):
# test 7: POST method on /api/resource to create doc
data = {"description": frappe.mock("paragraph")}
response = self.post(self.DOCTYPE, data)
data = {"description": frappe.mock("paragraph"), "sid": self.sid}
response = self.post(f"/api/resource/{self.DOCTYPE}", data)
self.assertEqual(response.status_code, 200)
docname = response.json()["data"]["name"]
docname = response.json["data"]["name"]
self.assertIsInstance(docname, str)
self.GENERATED_DOCUMENTS.append(docname)
def test_update_document(self):
# test 8: PUT method on /api/resource to update doc
generated_desc = frappe.mock("paragraph")
data = {"description": generated_desc}
data = {"description": generated_desc, "sid": self.sid}
random_doc = choice(self.GENERATED_DOCUMENTS)
desc_before_update = frappe.db.get_value(self.DOCTYPE, random_doc, "description")
response = self.put(f"{self.DOCTYPE}/{random_doc}", data=data)
response = self.put(f"/api/resource/{self.DOCTYPE}/{random_doc}", data=data)
self.assertEqual(response.status_code, 200)
self.assertNotEqual(response.json()["data"]["description"], desc_before_update)
self.assertEqual(response.json()["data"]["description"], generated_desc)
self.assertNotEqual(response.json["data"]["description"], desc_before_update)
self.assertEqual(response.json["data"]["description"], generated_desc)
def test_delete_document(self):
# test 9: DELETE method on /api/resource
doc_to_delete = choice(self.GENERATED_DOCUMENTS)
response = self.delete(f"{self.DOCTYPE}/{doc_to_delete}")
response = self.delete(f"/api/resource/{self.DOCTYPE}/{doc_to_delete}")
self.assertEqual(response.status_code, 202)
self.assertDictEqual(response.json(), {"message": "ok"})
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}")
with suppress_stdout():
response = self.delete(f"/api/resource/{self.DOCTYPE}/{non_existent_doc}")
self.assertEqual(response.status_code, 404)
self.assertDictEqual(response.json(), {})
self.assertDictEqual(response.json, {})
def test_run_doc_method(self):
# test 10: Run whitelisted method on doc via /api/resource
# status_code is 403 if no other tests are run before this - it's not logged in
self.post("/api/resource/Website Theme/Standard", {"run_method": "get_apps"})
response = self.get("/api/resource/Website Theme/Standard", {"run_method": "get_apps"})
self.assertIn(response.status_code, (403, 200))
if response.status_code == 403:
self.assertTrue(set(response.json.keys()) == {'exc_type', 'exception', 'exc', '_server_messages'})
self.assertEqual(response.json.get('exc_type'), 'PermissionError')
self.assertEqual(response.json.get('exception'), 'frappe.exceptions.PermissionError: Not permitted')
self.assertIsInstance(response.json.get('exc'), str)
elif response.status_code == 200:
data = response.json.get("data")
self.assertIsInstance(data, list)
self.assertIsInstance(data[0], dict)
class TestMethodAPI(unittest.TestCase):
METHOD_URL = f"{get_site_url(frappe.local.site)}/api/method"
class TestMethodAPI(FrappeAPITestCase):
METHOD_PATH = "/api/method"
def test_version(self):
# test 1: test for /api/method/version
response = requests.get(f"{self.METHOD_URL}/version")
json = frappe._dict(response.json())
response = self.get(f"{self.METHOD_PATH}/version")
json = frappe._dict(response.json)
self.assertEqual(response.status_code, 200)
self.assertIsInstance(json, dict)
@ -170,7 +238,27 @@ class TestMethodAPI(unittest.TestCase):
def test_ping(self):
# test 2: test for /api/method/ping
response = requests.get(f"{self.METHOD_URL}/ping")
response = self.get(f"{self.METHOD_PATH}/ping")
self.assertEqual(response.status_code, 200)
self.assertIsInstance(response.json(), dict)
self.assertEqual(response.json()['message'], "pong")
self.assertIsInstance(response.json, dict)
self.assertEqual(response.json["message"], "pong")
def test_get_user_info(self):
# test 3: test for /api/method/frappe.realtime.get_user_info
response = self.get(f"{self.METHOD_PATH}/frappe.realtime.get_user_info")
self.assertEqual(response.status_code, 200)
self.assertIsInstance(response.json, dict)
self.assertIn(response.json.get("message").get("user"), ("Administrator", "Guest"))
def test_auth_cycle(self):
# test 4: Pass authorization token in request
global authorization_token
user = frappe.get_doc("User", "Administrator")
api_key, api_secret = user.api_key, user.get_password("api_secret")
authorization_token = f"{api_key}:{api_secret}"
response = self.get("/api/method/frappe.auth.get_logged_user")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json["message"], "Administrator")
authorization_token = None

View file

@ -291,6 +291,16 @@ class TestDB(unittest.TestCase):
frappe.db.MAX_WRITES_PER_TRANSACTION = Database.MAX_WRITES_PER_TRANSACTION
def test_pk_collision_ignoring(self):
# note has `name` generated from title
for _ in range(3):
frappe.get_doc(doctype="Note", title="duplicate name").insert(ignore_if_duplicate=True)
with savepoint():
self.assertRaises(frappe.DuplicateEntryError, frappe.get_doc(doctype="Note", title="duplicate name").insert)
# recover transaction to continue other tests
raise Exception
@run_only_if(db_type_is.MARIADB)
class TestDDLCommandsMaria(unittest.TestCase):

View file

@ -1,11 +1,20 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import os
import unittest
from contextlib import contextmanager
from datetime import timedelta
from unittest.mock import patch
import frappe
from frappe.utils import cint
from frappe.model.naming import revert_series_if_last, make_autoname, parse_naming_series
from frappe.desk.doctype.note.note import Note
from frappe.model.naming import make_autoname, parse_naming_series, revert_series_if_last
from frappe.utils import cint, now_datetime
class CustomTestNote(Note):
@property
def age(self):
return now_datetime() - self.creation
class TestDocument(unittest.TestCase):
@ -255,5 +264,58 @@ class TestDocument(unittest.TestCase):
def test_limit_for_get(self):
doc = frappe.get_doc("DocType", "DocType")
# assuming DocType has more that 3 Data fields
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)
# assuming DocType has more than 3 Data fields
self.assertEquals(len(doc.get("fields", limit=3)), 3)
# limit with filters
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)
def test_virtual_fields(self):
"""Virtual fields are accessible via API and Form views, whenever .as_dict is invoked
"""
frappe.db.delete("Custom Field", {"dt": "Note", "fieldname":"age"})
note = frappe.new_doc("Note")
note.content = "some content"
note.title = frappe.generate_hash(length=20)
note.insert()
def patch_note():
return patch("frappe.controllers", new={frappe.local.site: {'Note': CustomTestNote}})
@contextmanager
def customize_note(with_options=False):
options = "frappe.utils.now_datetime() - doc.creation" if with_options else ""
custom_field = frappe.get_doc({
"doctype": "Custom Field",
"dt": "Note",
"fieldname": "age",
"fieldtype": "Data",
"read_only": True,
"is_virtual": True,
"options": options,
})
try:
yield custom_field.insert(ignore_if_duplicate=True)
finally:
custom_field.delete()
with patch_note():
doc = frappe.get_last_doc("Note")
self.assertIsInstance(doc, CustomTestNote)
self.assertIsInstance(doc.age, timedelta)
self.assertIsNone(doc.as_dict().get("age"))
self.assertIsNone(doc.get_valid_dict().get("age"))
with customize_note(), patch_note():
doc = frappe.get_last_doc("Note")
self.assertIsInstance(doc, CustomTestNote)
self.assertIsInstance(doc.age, timedelta)
self.assertIsInstance(doc.as_dict().get("age"), timedelta)
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta)
with customize_note(with_options=True):
doc = frappe.get_last_doc("Note")
self.assertIsInstance(doc, Note)
self.assertIsInstance(doc.as_dict().get("age"), timedelta)
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta)

View file

@ -10,7 +10,8 @@ import requests
import base64
class TestFrappeClient(unittest.TestCase):
PASSWORD = "admin"
PASSWORD = frappe.conf.admin_password or "admin"
def test_insert_many(self):
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
frappe.db.delete("Note", {"title": ("in", ('Sing','a','song','of','sixpence'))})
@ -169,7 +170,6 @@ class TestFrappeClient(unittest.TestCase):
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header)
self.assertEqual(res.status_code, 403)
# random api key and api secret
api_key = "@3djdk3kld"
api_secret = "ksk&93nxoe3os"

View file

@ -419,3 +419,96 @@ class TestXlsxUtils(unittest.TestCase):
val = handle_html("<p>html data &gt;</p>")
self.assertIn("html data >", val)
self.assertEqual("abc", handle_html("abc"))
class TestLinkTitle(unittest.TestCase):
def test_link_title_doctypes_in_boot_info(self):
"""
Test that doctypes are added to link_title_map in boot_info
"""
custom_doctype = frappe.get_doc(
{
"doctype": "DocType",
"module": "Core",
"custom": 1,
"fields": [
{
"label": "Test Field",
"fieldname": "test_title_field",
"fieldtype": "Data",
}
],
"show_title_field_in_link": 1,
"title_field": "test_title_field",
"permissions": [{"role": "System Manager", "read": 1}],
"name": "Test Custom Doctype for Link Title",
}
)
custom_doctype.insert()
prop_setter = frappe.get_doc(
{
"doctype": "Property Setter",
"doc_type": "User",
"property": "show_title_field_in_link",
"property_type": "Check",
"doctype_or_field": "DocType",
"value": "1",
}
).insert()
from frappe.boot import get_link_title_doctypes
link_title_doctypes = get_link_title_doctypes()
self.assertTrue("User" in link_title_doctypes)
self.assertTrue("Test Custom Doctype for Link Title" in link_title_doctypes)
prop_setter.delete()
custom_doctype.delete()
def test_link_titles_on_getdoc(self):
"""
Test that link titles are added to the doctype on getdoc
"""
prop_setter = frappe.get_doc(
{
"doctype": "Property Setter",
"doc_type": "User",
"property": "show_title_field_in_link",
"property_type": "Check",
"doctype_or_field": "DocType",
"value": "1",
}
).insert()
user = frappe.get_doc(
{
"doctype": "User",
"user_type": "Website User",
"email": "test_user_for_link_title@example.com",
"send_welcome_email": 0,
"first_name": "Test User for Link Title",
}
).insert(ignore_permissions=True)
todo = frappe.get_doc(
{
"doctype": "ToDo",
"description": "test-link-title-on-getdoc",
"allocated_to": user.name,
}
).insert()
from frappe.desk.form.load import getdoc
getdoc("ToDo", todo.name)
link_titles = frappe.local.response["_link_titles"]
self.assertTrue(f"{user.doctype}::{user.name}" in link_titles)
self.assertEqual(link_titles[f"{user.doctype}::{user.name}"], user.full_name)
todo.delete()
user.delete()
prop_setter.delete()

View file

@ -134,6 +134,12 @@ def create_contact_records():
insert_contact('Test Form Contact 2', '54321')
insert_contact('Test Form Contact 3', '12345')
@frappe.whitelist()
def create_multiple_contact_records():
if frappe.db.get_all('Contact', {'first_name': 'Multiple Contact 1'}):
return
for index in range(1001):
insert_contact('Multiple Contact {}'.format(index+1), '12345{}'.format(index+1))
def insert_contact(first_name, phone_number):
doc = frappe.get_doc({

View file

@ -148,6 +148,8 @@ More Information,Mehr Informationen,
More...,Mehr...,
Move,Bewegen,
My Account,Mein Konto,
My Profile,Mein Profil,
My Settings,Meine Einstellungen,
New Address,Neue Adresse,
New Contact,Neuer Kontakt,
Next,Weiter,
@ -406,7 +408,7 @@ Allow Self Approval,Erlaube Selbstgenehmigung,
Allow approval for creator of the document,Genehmigung für den Ersteller des Dokuments zulassen,
Allow events in timeline,Ereignisse in der Zeitleiste zulassen,
Allow in Quick Entry,In Schnelleingabe zulassen,
Allow on Submit,Beim Übertragen zulassen,
Allow on Submit,Änderungen zulassen wenn gebucht,
Allow only one session per user,Nur eine Sitzung pro Benutzer zulassen,
Allow page break inside tables,Seitenumbruch innerhalb von Tabellen erlauben,
Allow saving if mandatory fields are not filled,Speichern trotz leerer Pflichtfelder zulassen,

1 A4 A4
148 More... Mehr...
149 Move Bewegen
150 My Account Mein Konto
151 My Profile Mein Profil
152 My Settings Meine Einstellungen
153 New Address Neue Adresse
154 New Contact Neuer Kontakt
155 Next Weiter
408 Allow approval for creator of the document Genehmigung für den Ersteller des Dokuments zulassen
409 Allow events in timeline Ereignisse in der Zeitleiste zulassen
410 Allow in Quick Entry In Schnelleingabe zulassen
411 Allow on Submit Beim Übertragen zulassen Änderungen zulassen wenn gebucht
412 Allow only one session per user Nur eine Sitzung pro Benutzer zulassen
413 Allow page break inside tables Seitenumbruch innerhalb von Tabellen erlauben
414 Allow saving if mandatory fields are not filled Speichern trotz leerer Pflichtfelder zulassen

View file

@ -438,7 +438,8 @@ def touch_file(path):
os.utime(path, None)
return path
def get_test_client():
def get_test_client() -> Client:
"""Returns an test instance of the Frappe WSGI"""
from frappe.app import application
return Client(application)

View file

@ -88,9 +88,14 @@ def format_value(value, df=None, doc=None, currency=None, translated=False, form
return frappe.utils.markdown(value)
elif df.get("fieldtype") == "Table MultiSelect":
values = []
meta = frappe.get_meta(df.options)
link_field = [df for df in meta.fields if df.fieldtype == 'Link'][0]
values = [v.get(link_field.fieldname, 'asdf') for v in value]
for v in value:
v.update({'__link_titles': doc.get('__link_titles')})
formatted_value = frappe.format_value(v.get(link_field.fieldname, ''), link_field, v)
values.append(formatted_value)
return ', '.join(values)
elif df.get("fieldtype") == "Duration":
@ -100,4 +105,19 @@ def format_value(value, df=None, doc=None, currency=None, translated=False, form
elif df.get("fieldtype") == "Text Editor":
return "<div class='ql-snow'>{}</div>".format(value)
elif df.get("fieldtype") in ["Link", "Dynamic Link"]:
if not doc or not doc.get("__link_titles") or not df.options:
return value
doctype = df.options
if df.get("fieldtype") == "Dynamic Link":
if not df.parent:
return value
meta = frappe.get_meta(df.parent)
_field = meta.get_field(df.options)
doctype = _field.options
return doc.__link_titles.get("{0}::{1}".format(doctype, value), value)
return value

View file

@ -60,7 +60,7 @@ frappe.ui.form.on("Web Form", {
options: field.options,
reqd: field.reqd,
default: field.default,
read_only: field.read_only,
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,

View file

@ -169,6 +169,48 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None,
return html
def set_link_titles(doc):
# Adds name with title of link field doctype to __link_titles
if not doc.get("__link_titles"):
setattr(doc, "__link_titles", {})
meta = frappe.get_meta(doc.doctype)
set_title_values_for_link_and_dynamic_link_fields(meta, doc)
set_title_values_for_table_and_multiselect_fields(meta, doc)
def set_title_values_for_link_and_dynamic_link_fields(meta, doc, parent_doc=None):
if parent_doc and not parent_doc.get("__link_titles"):
setattr(parent_doc, "__link_titles", {})
elif doc and not doc.get("__link_titles"):
setattr(doc, "__link_titles", {})
for field in meta.get_link_fields() + meta.get_dynamic_link_fields():
if not doc.get(field.fieldname):
continue
# If link field, then get doctype from options
# If dynamic link field, then get doctype from dependent field
doctype = field.options if field.fieldtype == "Link" else doc.get(field.options)
meta = frappe.get_meta(doctype)
if not meta or not (meta.title_field and meta.show_title_field_in_link):
continue
link_title = frappe.get_cached_value(doctype, doc.get(field.fieldname), meta.title_field)
if parent_doc:
parent_doc.__link_titles["{0}::{1}".format(doctype, doc.get(field.fieldname))] = link_title
elif doc:
doc.__link_titles["{0}::{1}".format(doctype, doc.get(field.fieldname))] = link_title
def set_title_values_for_table_and_multiselect_fields(meta, doc):
for field in meta.get_table_fields():
if not doc.get(field.fieldname):
continue
_meta = frappe.get_meta(field.options)
for value in doc.get(field.fieldname):
set_title_values_for_link_and_dynamic_link_fields(_meta, value, doc)
def convert_markdown(doc, meta):
'''Convert text field values to markdown if necessary'''
for field in meta.fields:
@ -190,6 +232,7 @@ def get_html_and_style(doc, name=None, print_format=None, meta=None,
doc = frappe.get_doc(json.loads(doc))
print_format = get_print_format_doc(print_format, meta=meta or frappe.get_meta(doc.doctype))
set_link_titles(doc)
try:
html = get_rendered_template(doc, name=name, print_format=print_format, meta=meta,