Преглед изворни кода

Merge pull request #15321 from nextchamp-saqib/link_title_refactor

feat: Show Title Field in Link Fields
version-14
Saqib Ansari пре 3 година
committed by GitHub
родитељ
комит
04696c95b3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
20 измењених фајлова са 515 додато и 51 уклоњено
  1. +45
    -0
      cypress/integration/control_link.js
  2. +10
    -0
      frappe/boot.py
  3. +8
    -1
      frappe/core/doctype/doctype/doctype.json
  4. +8
    -1
      frappe/custom/doctype/customize_form/customize_form.json
  5. +2
    -1
      frappe/custom/doctype/customize_form/customize_form.py
  6. +1
    -0
      frappe/database/mariadb/framework_mariadb.sql
  7. +1
    -0
      frappe/database/postgres/framework_postgres.sql
  8. +56
    -1
      frappe/desk/form/load.py
  9. +50
    -6
      frappe/desk/search.py
  10. +71
    -10
      frappe/public/js/frappe/form/controls/link.js
  11. +7
    -1
      frappe/public/js/frappe/form/controls/multiselect_pills.js
  12. +5
    -2
      frappe/public/js/frappe/form/controls/table_multiselect.js
  13. +8
    -5
      frappe/public/js/frappe/form/formatters.js
  14. +30
    -21
      frappe/public/js/frappe/form/save.js
  15. +8
    -0
      frappe/public/js/frappe/request.js
  16. +11
    -1
      frappe/public/js/frappe/ui/filters/filter.js
  17. +37
    -0
      frappe/public/js/frappe/utils/utils.js
  18. +93
    -0
      frappe/tests/test_utils.py
  19. +21
    -1
      frappe/utils/formatters.py
  20. +43
    -0
      frappe/www/printview.py

+ 45
- 0
cypress/integration/control_link.js Прегледај датотеку

@@ -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]}`);


+ 10
- 0
frappe/boot.py Прегледај датотеку

@@ -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(),


+ 8
- 1
frappe/core/doctype/doctype/doctype.json Прегледај датотеку

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


+ 8
- 1
frappe/custom/doctype/customize_form/customize_form.json Прегледај датотеку

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


+ 2
- 1
frappe/custom/doctype/customize_form/customize_form.py Прегледај датотеку

@@ -516,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 = {


+ 1
- 0
frappe/database/mariadb/framework_mariadb.sql Прегледај датотеку

@@ -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;


+ 1
- 0
frappe/database/postgres/framework_postgres.sql Прегледај датотеку

@@ -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")
) ;


+ 56
- 1
frappe/desk/form/load.py Прегледај датотеку

@@ -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


+ 50
- 6
frappe/desk/search.py Прегледај датотеку

@@ -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

+ 71
- 10
frappe/public/js/frappe/form/controls/link.js Прегледај датотеку

@@ -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("");
}
});


+ 7
- 1
frappe/public/js/frappe/form/controls/multiselect_pills.js Прегледај датотеку

@@ -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();



+ 5
- 2
frappe/public/js/frappe/form/controls/table_multiselect.js Прегледај датотеку

@@ -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>
`;


+ 8
- 5
frappe/public/js/frappe/form/formatters.js Прегледај датотеку

@@ -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) {


+ 30
- 21
frappe/public/js/frappe/form/save.js Прегледај датотеку

@@ -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);
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]);
}
});
} else {
frappe._from_link.set_value(newdoc.name);
}
frappe._from_link.set_value(newdoc.name);
}

// refresh field
frappe._from_link.refresh();
// 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);
});
}
// 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;
frappe._from_link = null;
});
}
}

+ 8
- 0
frappe/public/js/frappe/request.js Прегледај датотеку

@@ -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) {


+ 11
- 1
frappe/public/js/frappe/ui/filters/filter.js Прегледај датотеку

@@ -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';


+ 37
- 0
frappe/public/js/frappe/utils/utils.js Прегледај датотеку

@@ -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);
}
}
});

+ 93
- 0
frappe/tests/test_utils.py Прегледај датотеку

@@ -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()



+ 21
- 1
frappe/utils/formatters.py Прегледај датотеку

@@ -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

+ 43
- 0
frappe/www/printview.py Прегледај датотеку

@@ -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,


Loading…
Откажи
Сачувај