Merge branch 'develop' of https://github.com/frappe/frappe into bulk-workflow-approval

This commit is contained in:
Suraj Shetty 2019-05-29 20:38:17 +05:30
commit 61cc0d7918
23 changed files with 272 additions and 117 deletions

View file

@ -108,7 +108,7 @@
"no_copy": 0,
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\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",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
@ -1710,7 +1710,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2019-04-08 12:19:53.415372",
"modified": "2019-05-28 12:19:53.415372",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -178,10 +178,11 @@ class Database(object):
frappe.errprint(("Execution time: {0} sec").format(round(time_end - time_start, 2)))
except Exception as e:
if(frappe.db.db_type == 'postgres'):
if frappe.conf.db_type == 'postgres':
self.rollback()
if frappe.db.db_type == 'mariadb' and self.is_syntax_error(e):
elif self.is_syntax_error(e):
# only for mariadb
frappe.errprint('Syntax error in query:')
frappe.errprint(query)
@ -929,12 +930,26 @@ class Database(object):
if values:
query = frappe.safe_decode(self._cursor.mogrify(query, values))
if query.strip().lower().split()[0] in ('insert', 'delete', 'update', 'alter'):
# ([`\"']?) Captures ', " or ` at the begining of the table name (if provided)
# single_word_regex is designed to match following patterns
# `tabXxx`, tabXxx and "tabXxx"
# multi_word_regex is designed to match following patterns
# `tabXxx Xxx` and "tabXxx Xxx"
# ([`"]?) Captures " or ` at the begining of the table name (if provided)
# \1 matches the first captured group (quote character) at the end of the table name
# multi word table name must have surrounding quotes.
# (tab([A-Z]\w+)( [A-Z]\w+)*) Captures table names that start with "tab"
# and are continued with multiple words that start with a captital letter
# e.g. 'tabXxx' or 'tabXxx Xxx' or 'tabXxx Xxx Xxx' and so on
# \1 matches the first captured group (quote character) at the end of the table name
tables = [groups[1] for groups in re.findall(r'([`"\']?)(tab([A-Z]\w+)( [A-Z]\w+)*)\1', query)]
single_word_regex = r'([`"]?)(tab([A-Z]\w+))\1'
multi_word_regex = r'([`"])(tab([A-Z]\w+)( [A-Z]\w+)+)\1'
tables = []
for regex in (single_word_regex, multi_word_regex):
tables += [groups[1] for groups in re.findall(regex, query)]
if frappe.flags.touched_tables is None:
frappe.flags.touched_tables = set()
frappe.flags.touched_tables.update(tables)

View file

@ -23,7 +23,7 @@ def get_contact_list(txt, page_length=20):
out = frappe.db.sql("""select email_id as value,
concat(first_name, ifnull(concat(' ',last_name), '' )) as description
from tabContact
where name like %(txt)s
where name like %(txt)s or email_id like %(txt)s
%(condition)s
limit %(page_length)s""", {
'txt': '%' + txt + '%',

View file

@ -242,6 +242,7 @@ frappe.patches.v12_0.replace_null_values_in_tables
frappe.patches.v12_0.reset_home_settings
frappe.patches.v12_0.update_print_format_type
frappe.patches.v11_0.remove_doctype_user_permissions_for_page_and_report #2019-05-01
frappe.patches.v11_0.apply_customization_to_custom_doctype
frappe.patches.v12_0.remove_feedback_rating
frappe.patches.v12_0.move_form_attachments_to_attachments_folder
frappe.patches.v12_0.move_timeline_links_to_dynamic_links

View file

@ -0,0 +1,55 @@
import frappe
from frappe.utils import cint
# This patch aims to apply & delete all the customization
# on custom doctypes done through customize form
# This is required because customize form in now blocked
# for custom doctypes and user may not be able to
# see previous customization
def execute():
custom_doctypes = frappe.get_all('DocType', filters={
'custom': 1
})
for doctype in custom_doctypes:
property_setters = frappe.get_all('Property Setter', filters={
'doc_type': doctype.name,
'doctype_or_field': 'DocField'
}, fields=['name', 'property', 'value', 'property_type', 'field_name'])
custom_fields = frappe.get_all('Custom Field',
filters={'dt': doctype.name},
fields=['*']
)
property_setter_map = {}
for prop in property_setters:
property_setter_map[prop.field_name] = prop
frappe.db.sql('DELETE FROM `tabProperty Setter` WHERE `name`=%s', prop.name)
meta = frappe.get_doc('DocType', doctype.name)
for df in meta.fields:
ps = property_setter_map.get(df.fieldname, None)
if ps:
value = cint(ps.value) if ps.property_type == 'Int' else ps.value
df.set(ps.property, value)
for cf in custom_fields:
cf.pop('parenttype')
cf.pop('parentfield')
cf.pop('parent')
cf.pop('name')
field = meta.get_field(cf.fieldname)
if field:
field.update(cf)
else:
df = frappe.new_doc('DocField', meta, 'fields')
df.update(cf)
meta.fields.append(df)
frappe.db.sql('DELETE FROM `tabCustom Field` WHERE name=%s', cf.name)
meta.save()

View file

@ -290,28 +290,31 @@ frappe.get_modal = function(title, content) {
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div class="row">
<div class="col-xs-7">
<div class="flex justify-between">
<div class="fill-width">
<span class="indicator hidden"></span>
<h4 class="modal-title" style="font-weight: bold;">${title}</h4>
</div>
<div class="col-xs-5">
<div class="text-right buttons">
<button type="button" class="btn btn-default btn-sm btn-modal-close"
data-dismiss="modal">
<h4 class="modal-title" style="font-weight: bold;">${title}</h4>
</div>
<div>
<div class="text-right buttons">
<button type="button" class="btn btn-default btn-sm btn-modal-minimize hide">
<i class="octicon octicon-chevron-down" style="padding: 1px 0px;"></i>
</button>
<button type="button" class="btn btn-default btn-sm btn-modal-close" data-dismiss="modal">
<i class="octicon octicon-x visible-xs" style="padding: 1px 0px;"></i>
<span class="hidden-xs">${__("Close")}</span></button>
<button type="button" class="btn btn-primary btn-sm hide">
${__("Confirm")}</button>
</div>
</div>
</div>
</div>
<div class="modal-body ui-front">${content}
<span class="hidden-xs">${__("Close")}</span>
</button>
<button type="button" class="btn btn-primary btn-sm hide">
${__("Confirm")}
</button>
</div>
</div>
</div>
</div>
<div class="modal-body ui-front">${content}</div>
</div>
</div>
</div>`)
</div>`);
};
frappe.is_online = function() {

View file

@ -19,6 +19,7 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
let me = this;
this.page_length = 20;
this.start = 0;
let fields = [
{
@ -55,7 +56,13 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
},
{ fieldtype: "Section Break" },
{ fieldtype: "HTML", fieldname: "results_area" },
{ fieldtype: "Button", fieldname: "make_new", label: __('Create a new ' + me.doctype) }
{ fieldtype: "Button", fieldname: "more_btn", label: __("More"),
click: function(){
me.start += 20;
frappe.flags.auto_scroll = true;
me.get_results();
}
}
]);
let doctype_plural = !this.doctype.endsWith('y') ? this.doctype + 's'
@ -65,25 +72,30 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
title: __("Select {0}", [(this.doctype=='[Select]') ? __("value") : __(doctype_plural)]),
fields: fields,
primary_action_label: __("Get Items"),
secondary_action_label: __("Make {0}", [me.doctype]),
primary_action: function() {
me.action(me.get_checked_values(), me.args);
},
secondary_action: function(e) {
// If user wants to close the modal
if (e) {
frappe.route_options = {};
Object.keys(me.setters).forEach(function(setter) {
frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined;
});
frappe.new_doc(me.doctype, true);
}
}
});
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.$results = this.$wrapper.find('.results');
this.$results.append(this.make_list_row());
this.args = {};
@ -94,6 +106,7 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
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');
@ -105,10 +118,12 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
});
this.$parent.find('.input-with-feedback').on('change', (e) => {
frappe.flags.auto_scroll = false;
this.get_results();
});
this.$parent.find('[data-fieldname="date_range"]').on('blur', (e) => {
frappe.flags.auto_scroll = false;
this.get_results();
});
@ -116,17 +131,10 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
var $this = $(this);
clearTimeout($this.data('timeout'));
$this.data('timeout', setTimeout(function() {
frappe.flags.auto_scroll = false;
me.get_results();
}, 300));
});
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() {
@ -170,22 +178,28 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
render_result_list: function(results, more = 0) {
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());
var more_btn = me.dialog.fields_dict.more_btn.$wrapper;
// Make empty result set if filter is set
if (!frappe.flags.auto_scroll) {
this.$results.empty();
}
if(results.length === 0) {
this.$results.empty();
more_btn.hide();
return;
} else if(more) {
more_btn.show();
}
results.forEach((result) => {
me.$results.append(me.make_list_row(result));
})
if (more) {
let message = __("Only {0} entries shown. Please filter for more specific results.", [this.page_length]);
me.$results.append($(`<div class="text-muted small" style="text-align: center;
margin: 10px;">${message}</div>`));
});
if (frappe.flags.auto_scroll) {
this.$results.animate({scrollTop: me.$results.prop('scrollHeight')}, 500);
}
},
@ -208,6 +222,7 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
txt: me.dialog.fields_dict["search_term"].get_value(),
filters: filters,
filter_fields: Object.keys(me.setters).concat([me.date_field]),
start: this.start,
page_length: this.page_length + 1,
query: this.get_query ? this.get_query().query : '',
as_dict: 1
@ -219,8 +234,8 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
args: args,
callback: function(r) {
let results = [], more = 0;
if(r.values.length) {
if(r.values.length > me.page_length){
if (r.values.length) {
if (r.values.length > me.page_length) {
r.values.pop();
more = 1;
}
@ -241,7 +256,9 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
});
// Preselect oldest entry
results[0].checked = 1
if (me.start < 1) {
results[0].checked = 1;
}
}
me.render_result_list(results, more);
}

View file

@ -185,7 +185,10 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
throw "saving";
}
frappe.ui.form.remove_old_form_route();
// ensure we remove new docs routes ONLY
if ( frm.is_new() ) {
frappe.ui.form.remove_old_form_route();
}
frappe.ui.form.is_saving = true;
return frappe.call({
@ -224,14 +227,9 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
}
frappe.ui.form.remove_old_form_route = () => {
let index = -1;
let current_route = frappe.get_route();
frappe.route_history.map((arr, i) => {
if (arr.join("/") === current_route.join("/")) {
index = i;
}
});
frappe.route_history.splice(index, 1);
let current_route = frappe.get_route().join("/");
frappe.route_history = frappe.route_history
.filter((route) => route.join("/") !== current_route);
}
frappe.ui.form.update_calling_link = (newdoc) => {

View file

@ -195,7 +195,11 @@ $(window).on('hashchange', function() {
// hide open dialog
if(window.cur_dialog && cur_dialog.hide_on_page_refresh) {
cur_dialog.hide();
if (!cur_dialog.minimizable) {
cur_dialog.hide();
} else if (!cur_dialog.is_minimized) {
cur_dialog.toggle_minimize();
}
}
frappe.route();

View file

@ -61,6 +61,10 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
this.get_close_btn().html(this.secondary_action_label || this.action.secondary.label);
}
if (this.minimizable) {
this.get_minimize_btn().removeClass('hide').on('click', () => this.toggle_minimize());
}
var me = this;
this.$wrapper
.on("hide.bs.modal", function() {
@ -101,6 +105,10 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
return this.$wrapper.find(".modal-header .btn-primary");
}
get_minimize_btn() {
return this.$wrapper.find(".modal-header .btn-modal-minimize");
}
set_message(text) {
this.$message.removeClass('hide');
this.$body.addClass('hide');
@ -179,6 +187,13 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
cancel() {
this.get_close_btn().trigger("click");
}
toggle_minimize() {
let modal = this.$wrapper.closest('.modal').toggleClass('modal-minimize');
modal.attr('tabindex') ? modal.removeAttr('tabindex') : modal.attr('tabindex', -1);
this.get_minimize_btn().find('i').toggleClass('octicon-chevron-down').toggleClass('octicon-chevron-up');
this.is_minimized = !this.is_minimized;
this.on_minimize_toggle && this.on_minimize_toggle(this.is_minimized);
}
};

View file

@ -175,9 +175,10 @@ frappe.ui.LinkPreview = class {
animation: false,
});
if(!this.is_link) {
this.element.data('bs.popover').tip().addClass('control-field-popover');
}
const $popover = this.element.data('bs.popover').tip();
$popover.addClass('link-preview-popover');
$popover.toggleClass('control-field-popover', this.is_link);
this.$links.push(this.element);

View file

@ -32,7 +32,6 @@ Object.assign(frappe.energy_points, {
},
get_history_log_message(log) {
const owner_name = frappe.user.full_name(log.owner).bold();
const user = frappe.user.full_name(log.user).bold();
const ref_doc = log.reference_name;
if (log.type === 'Appreciation') {
@ -42,7 +41,7 @@ Object.assign(frappe.energy_points, {
return __('{0} criticized on {1}', [owner_name, ref_doc]);
}
if (log.type === 'Revert') {
return __('{0} reverted {1}', [user, log.revert_of]);
return __('{0} reverted {1}', [owner_name, log.revert_of]);
}
return __('via automatic rule {0} on {1}', [log.rule.bold(), ref_doc]);
},
@ -57,8 +56,7 @@ Object.assign(frappe.energy_points, {
return __('{0} criticized {1}', [owner_name, user]);
}
if (log.type === 'Revert') {
return __('{0} reverted {1}', [user,
frappe.utils.get_form_link('Energy Point Log', log.revert_of, true)]);
return __('{0} reverted {1}', [owner_name, log.revert_of]);
}
return __('gained by {0} via automatic rule {1}', [user, log.rule.bold()]);
},

View file

@ -60,7 +60,11 @@ frappe.views.Container = Class.extend({
// hide dialog
if(window.cur_dialog && cur_dialog.display && !cur_dialog.keep_open) {
cur_dialog.hide();
if (!cur_dialog.minimizable) {
cur_dialog.hide();
} else if (!cur_dialog.is_minimized) {
cur_dialog.toggle_minimize();
}
}
// hide current

View file

@ -464,27 +464,6 @@ li.user-progress {
border-radius: 0px;
}
// like pop-over
.liked-by-popover {
.popover-content {
padding: 0px;
overflow: scroll;
max-height: 150px;
}
min-width: 100px;
ul {
margin: 0px;
li {
padding: 10px;
cursor: pointer;
&:hover {
background: @btn-bg;
}
}
}
}
.screenshot {
border: 1px solid @border-color;
box-shadow: 1px 1px 7px rgba(0,0,0,0.15);
@ -1074,6 +1053,27 @@ img.img-loading:after {
}
}
// like pop-over
.liked-by-popover {
.popover-content {
padding: 0px;
overflow: scroll;
max-height: 150px;
}
min-width: 100px;
ul {
margin: 0px;
li {
padding: 10px;
cursor: pointer;
&:hover {
background: @btn-bg;
}
}
}
}
body.full-width {
@media (min-width: @screen-md) {
.container {
@ -1087,3 +1087,21 @@ body.full-width {
.whitespace-nowrap {
white-space: nowrap;
}
.modal-minimize {
position: initial;
.modal-backdrop {
display: none;
}
.modal-dialog {
z-index: 101;
position: fixed;
right: 0;
bottom: 0;
margin: 0;
max-width: 500px;
}
.modal-body {
display: none;
}
}

View file

@ -80,3 +80,11 @@
margin-top: 7.5px;
margin-right: 3px;
}
.indicator.blink {
animation: blink 1s linear infinite;
}
@keyframes blink {
50% { opacity: 0.5; }
}

View file

@ -1,4 +1,4 @@
.popover {
.link-preview-popover {
border-radius: 0;
max-width: 100%;
.popover-content {

View file

@ -131,26 +131,35 @@ def get_energy_points(user):
@frappe.whitelist()
def get_user_energy_and_review_points(user=None, from_date=None, as_dict=True):
conditions = ''
values = []
given_points_condition = ''
values = frappe._dict()
if user:
conditions = 'WHERE `user` = %s'
values.append(user)
conditions = 'WHERE `user` = %(user)s'
values.user = user
if from_date:
conditions += 'WHERE' if not conditions else 'AND'
conditions += ' `creation` >= %s'
values.append(from_date)
given_points_condition += "AND `creation` >= %(from_date)s"
conditions += " `creation` >= %(from_date)s OR `type`='Review'"
values.from_date = from_date
points_list = frappe.db.sql("""
SELECT
SUM(CASE WHEN `type`!= 'Review' THEN `points` ELSE 0 END) as energy_points,
SUM(CASE WHEN `type`='Review' THEN `points` ELSE 0 END) as review_points,
SUM(CASE WHEN `type`='Review' and `points` < 0 THEN ABS(`points`) ELSE 0 END) as given_points,
SUM(CASE WHEN `type` != 'Review' THEN `points` ELSE 0 END) AS energy_points,
SUM(CASE WHEN `type` = 'Review' THEN `points` ELSE 0 END) AS review_points,
SUM(CASE
WHEN `type`='Review' AND `points` < 0 {given_points_condition}
THEN ABS(`points`)
ELSE 0
END) as given_points,
`user`
FROM `tabEnergy Point Log`
{conditions}
GROUP BY `user`
ORDER BY `energy_points` DESC
""".format(conditions=conditions), values=tuple(values), as_dict=1)
""".format(
conditions=conditions,
given_points_condition=given_points_condition
), values=values, as_dict=1)
if not as_dict:
return points_list

View file

@ -48,6 +48,12 @@ class TestDB(unittest.TestCase):
todo.save()
self.assertIn('tabToDo', frappe.flags.touched_tables)
if frappe.db.db_type != "postgres":
frappe.flags.touched_tables = set()
frappe.db.sql("UPDATE tabToDo SET description = 'Updated Description'")
self.assertNotIn('tabToDo SET', frappe.flags.touched_tables)
self.assertIn('tabToDo', frappe.flags.touched_tables)
frappe.flags.touched_tables = set()
todo.delete()
self.assertIn('tabToDo', frappe.flags.touched_tables)

View file

@ -507,7 +507,7 @@ def extract_messages_from_code(code, is_py=False):
:param is_py: include messages in triple quotes e.g. `_('''message''')`"""
try:
code = frappe.as_unicode(render_include(code))
except (TemplateError, ImportError, InvalidIncludePath):
except (TemplateError, ImportError, InvalidIncludePath, IOError):
# Exception will occur when it encounters John Resig's microtemplating code
pass

View file

@ -99,7 +99,10 @@ def get_page_info_from_doctypes(path=None):
values = []
controller = get_controller(doctype)
meta = frappe.get_meta(doctype)
condition_field = meta.is_published_field or controller.website.condition_field
condition_field = (meta.is_published_field or
# custom doctypes dont have controllers and no website attribute
(controller.website.condition_field if not meta.custom else None))
if condition_field:
condition ="where {0}=1".format(condition_field)

View file

@ -24,7 +24,7 @@
"cookie": "^0.3.1",
"express": "^4.16.2",
"fast-deep-equal": "^2.0.1",
"frappe-datatable": "^1.13.1",
"frappe-datatable": "^1.13.2",
"frappe-gantt": "^0.1.0",
"fuse.js": "^3.2.0",
"highlight.js": "^9.12.0",

View file

@ -15,7 +15,7 @@ rauth>=0.6.2
requests
redis==2.10.6
selenium
babel
babel==2.6.0
ipython
html2text==2016.9.19
email_reply_parser

View file

@ -1753,10 +1753,10 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
frappe-datatable@^1.13.1:
version "1.13.1"
resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.13.1.tgz#dbfa27fe735832cea54a0b35e3d3bfb839778c8d"
integrity sha512-FOC8dpsOSI+KnF5sBrYja9b2Y+3qUvYy/H6108QchKSvXYvuWVr/uuLk2N5xlz38PWU3d7n5lK0dkx+zXTKJ0w==
frappe-datatable@^1.13.2:
version "1.13.2"
resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.13.2.tgz#8b36c7cfc0ea660fc72eea8b1ae3c5dcc2a7d67d"
integrity sha512-4PyPDX22K4e4S3WGlLQx3oyxIW+ENsbGiN9L6aUpmjU+fOCC7J/FfSwGKdua2f+4yD+2ObpkyJYazBl3inAeCA==
dependencies:
hyperlist "^1.0.0-beta"
lodash "^4.17.5"