@@ -24,6 +24,7 @@ install: | |||||
- cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ | - cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ | ||||
script: | script: | ||||
- set -e | |||||
- bench --verbose run-tests | - bench --verbose run-tests | ||||
- bench reinstall --yes | - bench reinstall --yes | ||||
- testcafe chrome apps/frappe/frappe/tests/testcafe/ | - testcafe chrome apps/frappe/frappe/tests/testcafe/ | ||||
@@ -13,7 +13,7 @@ import os, sys, importlib, inspect, json | |||||
from .exceptions import * | from .exceptions import * | ||||
from .utils.jinja import get_jenv, get_template, render_template | from .utils.jinja import get_jenv, get_template, render_template | ||||
__version__ = '8.0.43' | |||||
__version__ = '8.0.44' | |||||
__title__ = "Frappe Framework" | __title__ = "Frappe Framework" | ||||
local = Local() | local = Local() | ||||
@@ -7,6 +7,11 @@ def get_data(): | |||||
"label": _("Payments"), | "label": _("Payments"), | ||||
"icon": "fa fa-star", | "icon": "fa fa-star", | ||||
"items": [ | "items": [ | ||||
{ | |||||
"type": "doctype", | |||||
"name": "Stripe Settings", | |||||
"description": _("Stripe payment gateway settings"), | |||||
}, | |||||
{ | { | ||||
"type": "doctype", | "type": "doctype", | ||||
"name": "PayPal Settings", | "name": "PayPal Settings", | ||||
@@ -902,7 +902,7 @@ | |||||
"issingle": 1, | "issingle": 1, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-05-01 15:27:11.079447", | |||||
"modified": "2017-05-11 15:27:11.079447", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "System Settings", | "name": "System Settings", | ||||
@@ -13,7 +13,9 @@ frappe.ui.form.on('Custom Field', { | |||||
if(user!=="Administrator") { | if(user!=="Administrator") { | ||||
filters.push(['DocType', 'module', '!=', 'Core']) | filters.push(['DocType', 'module', '!=', 'Core']) | ||||
} | } | ||||
return filters | |||||
return { | |||||
"filters": filters | |||||
} | |||||
}); | }); | ||||
}, | }, | ||||
refresh: function(frm) { | refresh: function(frm) { | ||||
@@ -118,10 +118,13 @@ def update_order(board_name, order): | |||||
def quick_kanban_board(doctype, board_name, field_name): | def quick_kanban_board(doctype, board_name, field_name): | ||||
'''Create new KanbanBoard quickly with default options''' | '''Create new KanbanBoard quickly with default options''' | ||||
doc = frappe.new_doc('Kanban Board') | doc = frappe.new_doc('Kanban Board') | ||||
options = frappe.get_value('DocField', dict( | |||||
parent=doctype, | |||||
fieldname=field_name | |||||
), 'options') | |||||
meta = frappe.get_meta(doctype) | |||||
options = '' | |||||
for field in meta.fields: | |||||
if field.fieldname == field_name: | |||||
options = field.options | |||||
columns = [] | columns = [] | ||||
if options: | if options: | ||||
@@ -198,4 +201,4 @@ def set_indicator(board_name, column_name, indicator): | |||||
def save_filters(board_name, filters): | def save_filters(board_name, filters): | ||||
'''Save filters silently''' | '''Save filters silently''' | ||||
frappe.db.set_value('Kanban Board', board_name, 'filters', | frappe.db.set_value('Kanban Board', board_name, 'filters', | ||||
filters, update_modified=False) | |||||
filters, update_modified=False) |
@@ -3,7 +3,7 @@ | |||||
# Search | # Search | ||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | |||||
import frappe, json | |||||
from frappe.utils import cstr, unique | from frappe.utils import cstr, unique | ||||
# this is called by the Link Field | # this is called by the Link Field | ||||
@@ -16,7 +16,7 @@ def search_link(doctype, txt, query=None, filters=None, page_len=20, searchfield | |||||
# this is called by the search box | # this is called by the search box | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def search_widget(doctype, txt, query=None, searchfield=None, start=0, | def search_widget(doctype, txt, query=None, searchfield=None, start=0, | ||||
page_len=10, filters=None, as_dict=False): | |||||
page_len=10, filters=None, filter_fields=None, as_dict=False): | |||||
if isinstance(filters, basestring): | if isinstance(filters, basestring): | ||||
import json | import json | ||||
filters = json.loads(filters) | filters = json.loads(filters) | ||||
@@ -76,20 +76,24 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, | |||||
if meta.get("fields", {"fieldname":"disabled", "fieldtype":"Check"}): | if meta.get("fields", {"fieldname":"disabled", "fieldtype":"Check"}): | ||||
filters.append([doctype, "disabled", "!=", 1]) | filters.append([doctype, "disabled", "!=", 1]) | ||||
# format a list of fields combining search fields and filter fields | |||||
fields = get_std_fields_list(meta, searchfield or "name") | fields = get_std_fields_list(meta, searchfield or "name") | ||||
if filter_fields: | |||||
fields = list(set(fields + json.loads(filter_fields))) | |||||
formatted_fields = ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in fields] | |||||
# find relevance as location of search term from the beginning of string `name`. used for sorting results. | # find relevance as location of search term from the beginning of string `name`. used for sorting results. | ||||
fields.append("""locate("{_txt}", `tab{doctype}`.`name`) as `_relevance`""".format( | |||||
formatted_fields.append("""locate("{_txt}", `tab{doctype}`.`name`) as `_relevance`""".format( | |||||
_txt=frappe.db.escape((txt or "").replace("%", "")), doctype=frappe.db.escape(doctype))) | _txt=frappe.db.escape((txt or "").replace("%", "")), doctype=frappe.db.escape(doctype))) | ||||
# In order_by, `idx` gets second priority, because it stores link count | # In order_by, `idx` gets second priority, because it stores link count | ||||
from frappe.model.db_query import get_order_by | from frappe.model.db_query import get_order_by | ||||
order_by_based_on_meta = get_order_by(doctype, meta) | order_by_based_on_meta = get_order_by(doctype, meta) | ||||
order_by = "if(_relevance, _relevance, 99999), idx desc, {0}".format(order_by_based_on_meta) | order_by = "if(_relevance, _relevance, 99999), idx desc, {0}".format(order_by_based_on_meta) | ||||
values = frappe.get_list(doctype, | values = frappe.get_list(doctype, | ||||
filters=filters, fields=fields, | |||||
filters=filters, fields=formatted_fields, | |||||
or_filters = or_filters, limit_start = start, | or_filters = or_filters, limit_start = start, | ||||
limit_page_length=page_len, | limit_page_length=page_len, | ||||
order_by=order_by, | order_by=order_by, | ||||
@@ -97,6 +101,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, | |||||
as_list=not as_dict) | as_list=not as_dict) | ||||
# remove _relevance from results | # remove _relevance from results | ||||
frappe.response["fields"] = fields | |||||
frappe.response["values"] = [r[:-1] for r in values] | frappe.response["values"] = [r[:-1] for r in values] | ||||
def get_std_fields_list(meta, key): | def get_std_fields_list(meta, key): | ||||
@@ -107,7 +112,7 @@ def get_std_fields_list(meta, key): | |||||
if not key in sflist: | if not key in sflist: | ||||
sflist = sflist + [key] | sflist = sflist + [key] | ||||
return ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in sflist] | |||||
return sflist | |||||
def build_for_autosuggest(res): | def build_for_autosuggest(res): | ||||
results = [] | results = [] | ||||
@@ -217,7 +217,14 @@ def evaluate_alert(doc, alert, event): | |||||
return | return | ||||
if event=="Value Change" and not doc.is_new(): | if event=="Value Change" and not doc.is_new(): | ||||
db_value = frappe.db.get_value(doc.doctype, doc.name, alert.value_changed) | |||||
try: | |||||
db_value = frappe.db.get_value(doc.doctype, doc.name, alert.value_changed) | |||||
except frappe.DatabaseOperationalError as e: | |||||
if e.args[0]==1054: | |||||
alert.db_set('enabled', 0) | |||||
frappe.log_error('Email Alert {0} has been disabled due to missing field'.format(alert.name)) | |||||
return | |||||
db_value = parse_val(db_value) | db_value = parse_val(db_value) | ||||
if (doc.get(alert.value_changed) == db_value) or \ | if (doc.get(alert.value_changed) == db_value) or \ | ||||
(not db_value and not doc.get(alert.value_changed)): | (not db_value and not doc.get(alert.value_changed)): | ||||
@@ -87,6 +87,36 @@ class TestEmailAlert(unittest.TestCase): | |||||
self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", | self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", | ||||
"reference_name": event.name, "status":"Not Sent"})) | "reference_name": event.name, "status":"Not Sent"})) | ||||
def test_alert_disabled_on_wrong_field(self): | |||||
frappe.set_user('Administrator') | |||||
email_alert = frappe.get_doc({ | |||||
"doctype": "Email Alert", | |||||
"subject":"_Test Email Alert for wrong field", | |||||
"document_type": "Event", | |||||
"event": "Value Change", | |||||
"attach_print": 0, | |||||
"value_changed": "description1", | |||||
"message": "Description changed", | |||||
"recipients": [ | |||||
{ "email_by_document_field": "owner" } | |||||
] | |||||
}).insert() | |||||
event = frappe.new_doc("Event") | |||||
event.subject = "test-2", | |||||
event.event_type = "Private" | |||||
event.starts_on = "2014-06-06 12:00:00" | |||||
event.insert() | |||||
event.subject = "test 1" | |||||
event.save() | |||||
# verify that email_alert is disabled | |||||
email_alert.reload() | |||||
self.assertEqual(email_alert.enabled, 0) | |||||
email_alert.delete() | |||||
event.delete() | |||||
def test_date_changed(self): | def test_date_changed(self): | ||||
event = frappe.new_doc("Event") | event = frappe.new_doc("Event") | ||||
event.subject = "test", | event.subject = "test", | ||||
@@ -7,6 +7,7 @@ from __future__ import unicode_literals | |||||
from werkzeug.exceptions import NotFound | from werkzeug.exceptions import NotFound | ||||
from MySQLdb import ProgrammingError as SQLError, Error | from MySQLdb import ProgrammingError as SQLError, Error | ||||
from MySQLdb import OperationalError as DatabaseOperationalError | |||||
class ValidationError(Exception): | class ValidationError(Exception): | ||||
@@ -25,16 +25,21 @@ def make_mapped_doc(method, source_name, selected_children=None): | |||||
return method(source_name) | return method(source_name) | ||||
@frappe.whitelist() | |||||
def map_docs(method, source_names, target_doc): | |||||
'''Returns the mapped document calling the given mapper method | |||||
with each of the given source docs on the target doc''' | |||||
method = frappe.get_attr(method) | |||||
if method not in frappe.whitelisted: | |||||
raise frappe.PermissionError | |||||
for src in json.loads(source_names): | |||||
target_doc = method(src, target_doc) | |||||
return target_doc | |||||
def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None, | def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None, | ||||
postprocess=None, ignore_permissions=False, ignore_child_tables=False): | postprocess=None, ignore_permissions=False, ignore_child_tables=False): | ||||
source_doc = frappe.get_doc(from_doctype, from_docname) | |||||
if not ignore_permissions: | |||||
if not source_doc.has_permission("read"): | |||||
source_doc.raise_no_permission_to("read") | |||||
# main | # main | ||||
if not target_doc: | if not target_doc: | ||||
target_doc = frappe.new_doc(table_maps[from_doctype]["doctype"]) | target_doc = frappe.new_doc(table_maps[from_doctype]["doctype"]) | ||||
@@ -44,6 +49,12 @@ def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None, | |||||
if not ignore_permissions and not target_doc.has_permission("create"): | if not ignore_permissions and not target_doc.has_permission("create"): | ||||
target_doc.raise_no_permission_to("create") | target_doc.raise_no_permission_to("create") | ||||
source_doc = frappe.get_doc(from_doctype, from_docname) | |||||
if not ignore_permissions: | |||||
if not source_doc.has_permission("read"): | |||||
source_doc.raise_no_permission_to("read") | |||||
map_doc(source_doc, target_doc, table_maps[source_doc.doctype]) | map_doc(source_doc, target_doc, table_maps[source_doc.doctype]) | ||||
row_exists_for_parentfield = {} | row_exists_for_parentfield = {} | ||||
@@ -94,12 +94,15 @@ def make_autoname(key='', doctype='', doc=''): | |||||
elif not "." in key: | elif not "." in key: | ||||
frappe.throw(_("Invalid naming series (. missing)") + (_(" for {0}").format(doctype) if doctype else "")) | frappe.throw(_("Invalid naming series (. missing)") + (_(" for {0}").format(doctype) if doctype else "")) | ||||
parts = key.split('.') | |||||
n = parse_naming_series(parts, doctype, doc) | |||||
return n | |||||
def parse_naming_series(parts, doctype= '', doc = ''): | |||||
n = '' | n = '' | ||||
l = key.split('.') | |||||
series_set = False | series_set = False | ||||
today = now_datetime() | today = now_datetime() | ||||
for e in l: | |||||
for e in parts: | |||||
part = '' | part = '' | ||||
if e.startswith('#'): | if e.startswith('#'): | ||||
if not series_set: | if not series_set: | ||||
@@ -120,6 +123,7 @@ def make_autoname(key='', doctype='', doc=''): | |||||
if isinstance(part, basestring): | if isinstance(part, basestring): | ||||
n+=part | n+=part | ||||
return n | return n | ||||
def getseries(key, digits, doctype=''): | def getseries(key, digits, doctype=''): | ||||
@@ -12,7 +12,7 @@ frappe.patches.v8_0.drop_in_dialog | |||||
execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2017-03-03 | execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2017-03-03 | ||||
execute:frappe.reload_doc('core', 'doctype', 'docperm') #2017-03-03 | execute:frappe.reload_doc('core', 'doctype', 'docperm') #2017-03-03 | ||||
frappe.patches.v8_0.drop_is_custom_from_docperm | frappe.patches.v8_0.drop_is_custom_from_docperm | ||||
frappe.patches.v8_0.update_records_in_global_search | |||||
frappe.patches.v8_0.update_records_in_global_search #11-05-2017 | |||||
frappe.patches.v8_0.update_published_in_global_search | frappe.patches.v8_0.update_published_in_global_search | ||||
execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') | execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') | ||||
execute:frappe.reload_doc('core', 'doctype', 'deleted_document') | execute:frappe.reload_doc('core', 'doctype', 'deleted_document') | ||||
@@ -1,5 +1,7 @@ | |||||
import frappe | |||||
from frappe.utils.global_search import get_doctypes_with_global_search, rebuild_for_doctype | from frappe.utils.global_search import get_doctypes_with_global_search, rebuild_for_doctype | ||||
def execute(): | def execute(): | ||||
frappe.cache().delete_value('doctypes_with_global_search') | |||||
for doctype in get_doctypes_with_global_search(with_child_tables=False): | for doctype in get_doctypes_with_global_search(with_child_tables=False): | ||||
rebuild_for_doctype(doctype) | rebuild_for_doctype(doctype) |
@@ -330,7 +330,8 @@ def get_all_perms(role): | |||||
'''Returns valid permissions for a given role''' | '''Returns valid permissions for a given role''' | ||||
perms = frappe.get_all('DocPerm', fields='*', filters=dict(role=role)) | perms = frappe.get_all('DocPerm', fields='*', filters=dict(role=role)) | ||||
custom_perms = frappe.get_all('Custom DocPerm', fields='*', filters=dict(role=role)) | custom_perms = frappe.get_all('Custom DocPerm', fields='*', filters=dict(role=role)) | ||||
doctypes_with_custom_perms = list(set(p.parent for p in custom_perms)) | |||||
doctypes_with_custom_perms = frappe.db.sql_list("""select distinct parent | |||||
from `tabCustom DocPerm`""") | |||||
for p in perms: | for p in perms: | ||||
if p.parent not in doctypes_with_custom_perms: | if p.parent not in doctypes_with_custom_perms: | ||||
@@ -30,6 +30,7 @@ | |||||
"public/js/frappe/ui/field_group.js", | "public/js/frappe/ui/field_group.js", | ||||
"public/js/frappe/form/control.js", | "public/js/frappe/form/control.js", | ||||
"public/js/frappe/form/link_selector.js", | "public/js/frappe/form/link_selector.js", | ||||
"public/js/frappe/form/multi_select_dialog.js", | |||||
"public/js/frappe/ui/dialog.js" | "public/js/frappe/ui/dialog.js" | ||||
], | ], | ||||
"css/desk.min.css": [ | "css/desk.min.css": [ | ||||
@@ -104,6 +105,7 @@ | |||||
"public/js/frappe/ui/field_group.js", | "public/js/frappe/ui/field_group.js", | ||||
"public/js/frappe/form/control.js", | "public/js/frappe/form/control.js", | ||||
"public/js/frappe/form/link_selector.js", | "public/js/frappe/form/link_selector.js", | ||||
"public/js/frappe/form/multi_select_dialog.js", | |||||
"public/js/frappe/ui/dialog.js", | "public/js/frappe/ui/dialog.js", | ||||
"public/js/frappe/ui/app_icon.js", | "public/js/frappe/ui/app_icon.js", | ||||
@@ -978,6 +978,13 @@ input[type="checkbox"]:checked:before { | |||||
font-size: 13px; | font-size: 13px; | ||||
color: #3b99fc; | color: #3b99fc; | ||||
} | } | ||||
.multiselect-empty-state { | |||||
min-height: 300px; | |||||
display: flex; | |||||
align-items: center; | |||||
justify-content: center; | |||||
height: 100%; | |||||
} | |||||
@-moz-document url-prefix() { | @-moz-document url-prefix() { | ||||
input[type="checkbox"] { | input[type="checkbox"] { | ||||
visibility: visible; | visibility: visible; | ||||
@@ -447,8 +447,9 @@ frappe.ui.form.Timeline = Class.extend({ | |||||
var parts = [], count = 0; | var parts = [], count = 0; | ||||
data.row_changed.every(function(row) { | data.row_changed.every(function(row) { | ||||
row[3].every(function(p) { | row[3].every(function(p) { | ||||
var df = frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype, | |||||
p[0], me.frm.docname); | |||||
var df = me.frm.fields_dict[row[0]] && | |||||
frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype, | |||||
p[0], me.frm.docname); | |||||
if(df && !df.hidden) { | if(df && !df.hidden) { | ||||
field_display_status = frappe.perm.get_field_display_status(df, | field_display_status = frappe.perm.get_field_display_status(df, | ||||
@@ -0,0 +1,212 @@ | |||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||||
// MIT License. See license.txt | |||||
frappe.ui.form.MultiSelectDialog = Class.extend({ | |||||
init: function(opts) { | |||||
/* Options: doctype, target, setters, get_query, action */ | |||||
$.extend(this, opts); | |||||
var me = this; | |||||
if(this.doctype!="[Select]") { | |||||
frappe.model.with_doctype(this.doctype, function(r) { | |||||
me.make(); | |||||
}); | |||||
} else { | |||||
this.make(); | |||||
} | |||||
}, | |||||
make: function() { | |||||
let me = this; | |||||
let fields = []; | |||||
let count = 0; | |||||
if(!this.date_field) { | |||||
this.date_field = "transaction_date"; | |||||
} | |||||
Object.keys(this.setters).forEach(function(setter) { | |||||
fields.push({ | |||||
fieldtype: me.target.fields_dict[setter].df.fieldtype, | |||||
label: me.target.fields_dict[setter].df.label, | |||||
fieldname: setter, | |||||
options: me.target.fields_dict[setter].df.options, | |||||
default: me.setters[setter] | |||||
}); | |||||
if (count++ < Object.keys(me.setters).length - 1) { | |||||
fields.push({fieldtype: "Column Break"}); | |||||
} | |||||
}); | |||||
fields = fields.concat([ | |||||
{ fieldtype: "Section Break" }, | |||||
{ fieldtype: "HTML", fieldname: "results_area" }, | |||||
{ fieldtype: "Button", fieldname: "make_new", label: __("Make a new " + me.doctype) } | |||||
]); | |||||
let doctype_plural = !this.doctype.endsWith('y') ? this.doctype + 's' | |||||
: this.doctype.slice(0, -1) + 'ies'; | |||||
this.dialog = new frappe.ui.Dialog({ | |||||
title: __("Select {0}", [(this.doctype=='[Select]') ? __("value") : __(doctype_plural)]), | |||||
fields: fields, | |||||
primary_action_label: __("Get Items"), | |||||
primary_action: function() { | |||||
me.action(me.get_checked_values(), me.args); | |||||
} | |||||
}); | |||||
this.$parent = $(this.dialog.body); | |||||
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results" | |||||
style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`); | |||||
this.$results = this.$wrapper.find('.results'); | |||||
this.$make_new_btn = this.dialog.fields_dict.make_new.$wrapper; | |||||
this.$placeholder = $(`<div class="multiselect-empty-state"> | |||||
<span class="text-center" style="margin-top: -40px;"> | |||||
<i class="fa fa-2x fa-tags text-extra-muted"></i> | |||||
<p class="text-extra-muted">No ${this.doctype} found</p> | |||||
<button class="btn btn-default btn-xs text-muted" data-fieldtype="Button" | |||||
data-fieldname="make_new" placeholder="" value="">Make a new ${this.doctype}</button> | |||||
</span> | |||||
</div>`); | |||||
this.args = {}; | |||||
this.bind_events(); | |||||
this.get_results(); | |||||
this.dialog.show(); | |||||
}, | |||||
bind_events: function() { | |||||
let me = this; | |||||
this.$results.on('click', '.list-item-container', function (e) { | |||||
if (!$(e.target).is(':checkbox') && !$(e.target).is('a')) { | |||||
$(this).find(':checkbox').trigger('click'); | |||||
} | |||||
}); | |||||
this.$results.on('click', '.list-item--head :checkbox', (e) => { | |||||
this.$results.find('.list-item-container .list-row-check') | |||||
.prop("checked", ($(e.target).is(':checked'))); | |||||
}); | |||||
this.$parent.find('.input-with-feedback').on('change', (e) => { | |||||
this.get_results(); | |||||
}); | |||||
this.$parent.on('click', '.btn[data-fieldname="make_new"]', (e) => { | |||||
frappe.route_options = {}; | |||||
Object.keys(this.setters).forEach(function(setter) { | |||||
frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined; | |||||
}); | |||||
frappe.new_doc(this.doctype, true); | |||||
}); | |||||
}, | |||||
get_checked_values: function() { | |||||
return this.$results.find('.list-item-container').map(function() { | |||||
if ($(this).find('.list-row-check:checkbox:checked').length > 0 ) { | |||||
return $(this).attr('data-item-name'); | |||||
} | |||||
}).get(); | |||||
}, | |||||
make_list_row: function(result={}) { | |||||
var me = this; | |||||
// Make a head row by default (if result not passed) | |||||
let head = Object.keys(result).length === 0; | |||||
let contents = ``; | |||||
let columns = (["name"].concat(Object.keys(this.setters))).concat("Date"); | |||||
columns.forEach(function(column) { | |||||
contents += `<div class="list-item__content ellipsis"> | |||||
${ | |||||
head ? __(frappe.model.unscrub(column)) | |||||
: (column !== "name" ? __(result[column]) | |||||
: `<a href="${"#Form/"+ me.doctype + "/" + result[column]}" class="list-id"> | |||||
${__(result[column])}</a>`) | |||||
} | |||||
</div>`; | |||||
}) | |||||
let $row = $(`<div class="list-item"> | |||||
<div class="list-item__content ellipsis" style="flex: 0 0 10px;"> | |||||
<input type="checkbox" class="list-row-check" ${result.checked ? 'checked' : ''}> | |||||
</div> | |||||
${contents} | |||||
</div>`); | |||||
head ? $row.addClass('list-item--head') | |||||
: $row = $(`<div class="list-item-container" data-item-name="${result.name}"></div>`).append($row); | |||||
return $row; | |||||
}, | |||||
render_result_list: function(results) { | |||||
var me = this; | |||||
this.$results.empty(); | |||||
if(results.length === 0) { | |||||
this.$make_new_btn.addClass('hide'); | |||||
this.$results.append(me.$placeholder); | |||||
return; | |||||
} | |||||
this.$make_new_btn.removeClass('hide'); | |||||
this.$results.append(this.make_list_row()); | |||||
results.forEach((result) => { | |||||
me.$results.append(me.make_list_row(result)); | |||||
}) | |||||
}, | |||||
get_results: function() { | |||||
let me = this; | |||||
let filters = this.get_query().filters; | |||||
Object.keys(this.setters).forEach(function(setter) { | |||||
filters[setter] = me.dialog.fields_dict[setter].get_value() || undefined; | |||||
me.args[setter] = filters[setter]; | |||||
}); | |||||
let args = { | |||||
doctype: me.doctype, | |||||
txt: '', | |||||
filters: filters, | |||||
filter_fields: Object.keys(me.setters).concat([me.date_field]) | |||||
} | |||||
frappe.call({ | |||||
type: "GET", | |||||
method:'frappe.desk.search.search_widget', | |||||
no_spinner: true, | |||||
args: args, | |||||
callback: function(r) { | |||||
if(r.values) { | |||||
let results = []; | |||||
r.values.forEach(function(value_list) { | |||||
let result = {}; | |||||
value_list.forEach(function(value, index){ | |||||
if(r.fields[index] === me.date_field) { | |||||
result["Date"] = value; | |||||
} else { | |||||
result[r.fields[index]] = value; | |||||
} | |||||
}); | |||||
result.checked = 0; | |||||
result.parsed_date = Date.parse(result["Date"]); | |||||
results.push(result); | |||||
}); | |||||
results.map( (result) => { | |||||
result["Date"] = frappe.format(result["Date"], {"fieldtype":"Date"}); | |||||
}) | |||||
results.sort((a, b) => { | |||||
return a.parsed_date - b.parsed_date; | |||||
}); | |||||
// Preselect oldest entry | |||||
results[0].checked = 1 | |||||
me.render_result_list(results); | |||||
} | |||||
} | |||||
}); | |||||
}, | |||||
}); |
@@ -31,7 +31,7 @@ frappe.ui.form.quick_entry = function(doctype, success) { | |||||
} | } | ||||
var dialog = new frappe.ui.Dialog({ | var dialog = new frappe.ui.Dialog({ | ||||
title: __("New {0}", [doctype]), | |||||
title: __("New {0}", [__(doctype)]), | |||||
fields: mandatory, | fields: mandatory, | ||||
}); | }); | ||||
@@ -144,7 +144,7 @@ frappe.views.ListRenderer = Class.extend({ | |||||
} | } | ||||
// kanban column fields | // kanban column fields | ||||
if (me.meta.__kanban_column_fields) { | if (me.meta.__kanban_column_fields) { | ||||
me.fields = me.fields.concat(me.meta.__kanban_column_fields); | |||||
me.meta.__kanban_column_fields.map(add_field); | |||||
} | } | ||||
}, | }, | ||||
set_columns: function () { | set_columns: function () { | ||||
@@ -187,6 +187,11 @@ $.extend(frappe.model, { | |||||
return txt.replace(/ /g, "_").toLowerCase(); | return txt.replace(/ /g, "_").toLowerCase(); | ||||
}, | }, | ||||
unscrub: function(txt) { | |||||
return __(txt || '').replace(/-|_/g, " ").replace(/\w*/g, | |||||
function(keywords){return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase();}); | |||||
}, | |||||
can_create: function(doctype) { | can_create: function(doctype) { | ||||
return frappe.boot.user.can_create.indexOf(doctype)!==-1; | return frappe.boot.user.can_create.indexOf(doctype)!==-1; | ||||
}, | }, | ||||
@@ -67,7 +67,6 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({ | |||||
var f = this.fields_dict[key]; | var f = this.fields_dict[key]; | ||||
if(f.get_parsed_value) { | if(f.get_parsed_value) { | ||||
var v = f.get_parsed_value(); | var v = f.get_parsed_value(); | ||||
if(f.df.reqd && is_null(v)) | if(f.df.reqd && is_null(v)) | ||||
errors.push(__(f.df.label)); | errors.push(__(f.df.label)); | ||||
@@ -8,7 +8,6 @@ frappe.ui.keys.setup = function() { | |||||
for(var i=0, l = frappe.ui.keys.handlers[key].length; i<l; i++) { | for(var i=0, l = frappe.ui.keys.handlers[key].length; i<l; i++) { | ||||
var handler = frappe.ui.keys.handlers[key][i]; | var handler = frappe.ui.keys.handlers[key][i]; | ||||
var _out = handler.apply(this, [e]); | var _out = handler.apply(this, [e]); | ||||
if(_out===false) { | if(_out===false) { | ||||
out = _out; | out = _out; | ||||
} | } | ||||
@@ -23,10 +22,9 @@ frappe.ui.keys.get_key = function(e) { | |||||
//safari doesn't have key property | //safari doesn't have key property | ||||
if(!key) { | if(!key) { | ||||
var keycode = e.keyCode || e.which; | var keycode = e.keyCode || e.which; | ||||
key = frappe.ui.keys.key_map[keycode] || | |||||
String.fromCharCode(keycode); | |||||
key = frappe.ui.keys.key_map[keycode] || String.fromCharCode(keycode); | |||||
} | } | ||||
if(key.substr(0, 5)==='Arrow') { | |||||
if(key.substr(0, 5) === 'Arrow') { | |||||
// ArrowDown -> down | // ArrowDown -> down | ||||
key = key.substr(5).toLowerCase(); | key = key.substr(5).toLowerCase(); | ||||
} | } | ||||
@@ -69,22 +67,13 @@ frappe.ui.keys.on('ctrl+b', function(e) { | |||||
} | } | ||||
}); | }); | ||||
frappe.ui.keys.on('Escape', function(e) { | |||||
// close open grid row | |||||
var open_row = $(".grid-row-open"); | |||||
if(open_row.length) { | |||||
var grid_row = open_row.data("grid_row"); | |||||
grid_row.toggle_view(false); | |||||
return false; | |||||
} | |||||
// close open dialog | |||||
if(cur_dialog && !cur_dialog.no_cancel_flag) { | |||||
cur_dialog.cancel(); | |||||
return false; | |||||
} | |||||
frappe.ui.keys.on('escape', function(e) { | |||||
close_grid_and_dialog(); | |||||
}); | }); | ||||
frappe.ui.keys.on('esc', function(e) { | |||||
close_grid_and_dialog(); | |||||
}); | |||||
frappe.ui.keys.on('Enter', function(e) { | frappe.ui.keys.on('Enter', function(e) { | ||||
if(cur_dialog && cur_dialog.confirm_dialog) { | if(cur_dialog && cur_dialog.confirm_dialog) { | ||||
@@ -127,4 +116,20 @@ frappe.ui.keyCode = { | |||||
ENTER: 13, | ENTER: 13, | ||||
TAB: 9, | TAB: 9, | ||||
SPACE: 32 | SPACE: 32 | ||||
} | |||||
function close_grid_and_dialog() { | |||||
// close open grid row | |||||
var open_row = $(".grid-row-open"); | |||||
if (open_row.length) { | |||||
var grid_row = open_row.data("grid_row"); | |||||
grid_row.toggle_view(false); | |||||
return false; | |||||
} | |||||
// close open dialog | |||||
if (cur_dialog && !cur_dialog.no_cancel_flag) { | |||||
cur_dialog.cancel(); | |||||
return false; | |||||
} | |||||
} | } |
@@ -91,16 +91,12 @@ frappe.ui.SortSelector = Class.extend({ | |||||
var me = this; | var me = this; | ||||
var meta = frappe.get_meta(this.doctype); | var meta = frappe.get_meta(this.doctype); | ||||
var { meta_sort_field, meta_sort_order } = this.get_meta_sort_field(); | |||||
if(!this.args.sort_by) { | if(!this.args.sort_by) { | ||||
if(meta.sort_field) { | |||||
if(meta.sort_field.indexOf(',')!==-1) { | |||||
parts = meta.sort_field.split(',')[0].split(' '); | |||||
this.args.sort_by = parts[0]; | |||||
this.args.sort_order = parts[1]; | |||||
} else { | |||||
this.args.sort_by = meta.sort_field; | |||||
this.args.sort_order = meta.sort_order.toLowerCase(); | |||||
} | |||||
if(meta_sort_field) { | |||||
this.args.sort_by = meta_sort_field; | |||||
this.args.sort_order = meta_sort_order; | |||||
} else { | } else { | ||||
// default | // default | ||||
this.args.sort_by = 'modified'; | this.args.sort_by = 'modified'; | ||||
@@ -115,7 +111,7 @@ frappe.ui.SortSelector = Class.extend({ | |||||
if(!this.args.options) { | if(!this.args.options) { | ||||
// default options | // default options | ||||
var _options = [ | var _options = [ | ||||
{'fieldname': 'modified'}, | |||||
{'fieldname': 'modified'} | |||||
] | ] | ||||
// title field | // title field | ||||
@@ -130,9 +126,15 @@ frappe.ui.SortSelector = Class.extend({ | |||||
} | } | ||||
}); | }); | ||||
_options.push({'fieldname': 'name'}); | |||||
_options.push({'fieldname': 'creation'}); | |||||
_options.push({'fieldname': 'idx'}); | |||||
// meta sort field | |||||
if(meta_sort_field) _options.push({ 'fieldname': meta_sort_field }); | |||||
// more default options | |||||
_options.push( | |||||
{'fieldname': 'name'}, | |||||
{'fieldname': 'creation'}, | |||||
{'fieldname': 'idx'} | |||||
) | |||||
// de-duplicate | // de-duplicate | ||||
this.args.options = _options.uniqBy(function(obj) { | this.args.options = _options.uniqBy(function(obj) { | ||||
@@ -151,6 +153,21 @@ frappe.ui.SortSelector = Class.extend({ | |||||
this.sort_by = this.args.sort_by; | this.sort_by = this.args.sort_by; | ||||
this.sort_order = this.args.sort_order; | this.sort_order = this.args.sort_order; | ||||
}, | }, | ||||
get_meta_sort_field: function() { | |||||
var meta = frappe.get_meta(this.doctype); | |||||
if(meta.sort_field && meta.sort_field.includes(',')) { | |||||
var parts = meta.sort_field.split(',')[0].split(' '); | |||||
return { | |||||
meta_sort_field: parts[0], | |||||
meta_sort_order: parts[1] | |||||
} | |||||
} else { | |||||
return { | |||||
meta_sort_field: meta.sort_field, | |||||
meta_sort_order: meta.sort_order.toLowerCase() | |||||
} | |||||
} | |||||
}, | |||||
get_label: function(fieldname) { | get_label: function(fieldname) { | ||||
if(fieldname==='idx') { | if(fieldname==='idx') { | ||||
return __("Most Used"); | return __("Most Used"); | ||||
@@ -575,10 +575,6 @@ frappe.search.utils = { | |||||
return rendered; | return rendered; | ||||
} | } | ||||
}, | |||||
} | |||||
unscrub_and_titlecase: function(str) { | |||||
return __(str || '').replace(/-|_/g, " ").replace(/\w*/g, | |||||
function(keywords){return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase();}); | |||||
}, | |||||
} | } |
@@ -41,6 +41,12 @@ frappe.provide("frappe.views"); | |||||
columns: columns, | columns: columns, | ||||
cur_list: opts.cur_list | cur_list: opts.cur_list | ||||
}); | }); | ||||
}) | |||||
.fail(function() { | |||||
// redirect back to List | |||||
setTimeout(() => { | |||||
frappe.set_route('List', opts.doctype, 'List'); | |||||
}, 2000); | |||||
}); | }); | ||||
}, | }, | ||||
update_cards: function (updater, cards) { | update_cards: function (updater, cards) { | ||||
@@ -1038,6 +1044,9 @@ frappe.provide("frappe.views"); | |||||
function is_filters_modified(board, cur_list) { | function is_filters_modified(board, cur_list) { | ||||
return new Promise(function(resolve, reject) { | return new Promise(function(resolve, reject) { | ||||
setTimeout(function() { | setTimeout(function() { | ||||
// sometimes the filter_list is not initiated, so early return | |||||
if(!cur_list.filter_list) resolve(false); | |||||
var list_filters = JSON.stringify(cur_list.filter_list.get_filters()); | var list_filters = JSON.stringify(cur_list.filter_list.get_filters()); | ||||
resolve(list_filters !== board.filters); | resolve(list_filters !== board.filters); | ||||
}, 2000); | }, 2000); | ||||
@@ -227,7 +227,7 @@ frappe.views.ReportView = frappe.ui.BaseList.extend({ | |||||
set_route_filters: function(first_load) { | set_route_filters: function(first_load) { | ||||
var me = this; | var me = this; | ||||
if(frappe.route_options && !this.user_settings.filters) { | |||||
if(frappe.route_options) { | |||||
this.set_filters_from_route_options(); | this.set_filters_from_route_options(); | ||||
return true; | return true; | ||||
} else if(this.user_settings | } else if(this.user_settings | ||||
@@ -548,9 +548,12 @@ frappe.views.ReportView = frappe.ui.BaseList.extend({ | |||||
$.each(frappe.model.get_all_docs(doc), function(i, d) { | $.each(frappe.model.get_all_docs(doc), function(i, d) { | ||||
// find the document of the current updated record | // find the document of the current updated record | ||||
// from locals (which is synced in the response) | // from locals (which is synced in the response) | ||||
if(item[d.doctype + ":name"]===d.name) { | |||||
for(k in d) { | |||||
v = d[k]; | |||||
var name = item[d.doctype + ":name"]; | |||||
if(!name) name = item.name; | |||||
if(name===d.name) { | |||||
for(var k in d) { | |||||
var v = d[k]; | |||||
if(frappe.model.std_fields_list.indexOf(k)===-1 | if(frappe.model.std_fields_list.indexOf(k)===-1 | ||||
&& item[k]!==undefined) { | && item[k]!==undefined) { | ||||
new_item[k] = v; | new_item[k] = v; | ||||
@@ -431,7 +431,7 @@ textarea.form-control { | |||||
flex: 0 0 36px; | flex: 0 0 36px; | ||||
order: -1; | order: -1; | ||||
justify-content: flex-end; | justify-content: flex-end; | ||||
input[type="checkbox"] { | input[type="checkbox"] { | ||||
margin-right: 0; | margin-right: 0; | ||||
} | } | ||||
@@ -894,8 +894,17 @@ input[type="checkbox"] { | |||||
} | } | ||||
} | } | ||||
// Will not be required after commonifying lists with empty state | |||||
.multiselect-empty-state{ | |||||
min-height: 300px; | |||||
display: flex; | |||||
align-items: center; | |||||
justify-content: center; | |||||
height: 100%; | |||||
} | |||||
// mozilla doesn't support pseudo elements on checkbox | |||||
// mozilla doesn't support | |||||
// pseudo elements on checkbox | |||||
@-moz-document url-prefix() { | @-moz-document url-prefix() { | ||||
input[type="checkbox"] { | input[type="checkbox"] { | ||||
visibility: visible; | visibility: visible; | ||||
@@ -54,7 +54,7 @@ def rebuild_for_doctype(doctype): | |||||
:param doctype: Doctype ''' | :param doctype: Doctype ''' | ||||
def _get_filters(): | def _get_filters(): | ||||
filters = frappe._dict({ "docstatus": ["!=", 1] }) | |||||
filters = frappe._dict({ "docstatus": ["!=", 2] }) | |||||
if meta.has_field("enabled"): | if meta.has_field("enabled"): | ||||
filters.enabled = 1 | filters.enabled = 1 | ||||
if meta.has_field("disabled"): | if meta.has_field("disabled"): | ||||
@@ -105,7 +105,7 @@ def import_country_and_currency(): | |||||
country = frappe._dict(data[name]) | country = frappe._dict(data[name]) | ||||
add_country_and_currency(name, country) | add_country_and_currency(name, country) | ||||
print() | |||||
# enable frequently used currencies | # enable frequently used currencies | ||||
for currency in ("INR", "USD", "GBP", "EUR", "AED", "AUD", "JPY", "CNY", "CHF"): | for currency in ("INR", "USD", "GBP", "EUR", "AED", "AUD", "JPY", "CNY", "CHF"): | ||||
@@ -9,7 +9,6 @@ import openpyxl | |||||
from cStringIO import StringIO | from cStringIO import StringIO | ||||
from openpyxl.styles import Font | from openpyxl.styles import Font | ||||
import html2text | |||||
# return xlsx file object | # return xlsx file object | ||||
def make_xlsx(data, sheet_name): | def make_xlsx(data, sheet_name): | ||||
@@ -24,19 +23,33 @@ def make_xlsx(data, sheet_name): | |||||
clean_row = [] | clean_row = [] | ||||
for item in row: | for item in row: | ||||
if isinstance(item, basestring): | if isinstance(item, basestring): | ||||
obj = html2text.HTML2Text() | |||||
obj.ignore_links = True | |||||
obj.body_width = 0 | |||||
obj = obj.handle(unicode(item or "")) | |||||
obj = obj.rsplit('\n', 1) | |||||
value = obj[0] | |||||
value = handle_html(item) | |||||
else: | else: | ||||
value = item | value = item | ||||
clean_row.append(value) | clean_row.append(value) | ||||
ws.append(clean_row) | ws.append(clean_row) | ||||
xlsx_file = StringIO() | xlsx_file = StringIO() | ||||
wb.save(xlsx_file) | wb.save(xlsx_file) | ||||
return xlsx_file | |||||
return xlsx_file | |||||
def handle_html(data): | |||||
# import html2text | |||||
from html2text import unescape, HTML2Text | |||||
h = HTML2Text() | |||||
h.unicode_snob = True | |||||
h = h.unescape(data or "") | |||||
obj = HTML2Text() | |||||
obj.ignore_links = True | |||||
obj.body_width = 0 | |||||
value = obj.handle(h) | |||||
value = value.split('\n', 1) | |||||
value = value[0].split('# ',1) | |||||
if len(value) < 2: | |||||
return value[0] | |||||
else: | |||||
return value[1] |