@@ -13,7 +13,7 @@ import os, sys, importlib, inspect, json | |||
from .exceptions import * | |||
from .utils.jinja import get_jenv, get_template, render_template | |||
__version__ = '8.0.19' | |||
__version__ = '8.0.20' | |||
__title__ = "Frappe Framework" | |||
local = Local() | |||
@@ -0,0 +1,301 @@ | |||
const path = require('path'); | |||
const fs = require('fs'); | |||
const babel = require('babel-core'); | |||
const less = require('less'); | |||
const chokidar = require('chokidar'); | |||
// for file watcher | |||
const app = require('express')(); | |||
const http = require('http').Server(app); | |||
const io = require('socket.io')(http); | |||
const file_watcher_port = 6787; | |||
const p = path.resolve; | |||
// basic setup | |||
const sites_path = p(__dirname, '..', '..', '..', 'sites'); | |||
const apps_path = p(__dirname, '..', '..', '..', 'apps'); // the apps folder | |||
const apps_contents = fs.readFileSync(p(sites_path, 'apps.txt'), 'utf8'); | |||
const apps = apps_contents.split('\n'); | |||
const app_paths = apps.map(app => p(apps_path, app, app)) // base_path of each app | |||
const assets_path = p(sites_path, 'assets'); | |||
const build_map = make_build_map(); | |||
// command line args | |||
const action = process.argv[2] || '--build'; | |||
if (!['--build', '--watch'].includes(action)) { | |||
console.log('Invalid argument: ', action); | |||
return; | |||
} | |||
if (action === '--build') { | |||
const minify = process.argv[3] === '--minify' ? true : false; | |||
build({ minify }); | |||
} | |||
if (action === '--watch') { | |||
watch(); | |||
} | |||
function build({ minify=false } = {}) { | |||
for (const output_path in build_map) { | |||
pack(output_path, build_map[output_path], minify); | |||
} | |||
} | |||
let socket_connection = false; | |||
function watch() { | |||
http.listen(file_watcher_port, function () { | |||
console.log('file watching on *:', file_watcher_port); | |||
}); | |||
compile_less().then(() => { | |||
build(); | |||
watch_less(function (filename) { | |||
if(socket_connection) { | |||
io.emit('reload_css', filename); | |||
} | |||
}); | |||
watch_js(function (filename) { | |||
if(socket_connection) { | |||
io.emit('reload_js', filename); | |||
} | |||
}); | |||
}); | |||
io.on('connection', function (socket) { | |||
socket_connection = true; | |||
socket.on('disconnect', function() { | |||
socket_connection = false; | |||
}) | |||
}); | |||
} | |||
function pack(output_path, inputs, minify) { | |||
const output_type = output_path.split('.').pop(); | |||
let output_txt = ''; | |||
for (const file of inputs) { | |||
if (!fs.existsSync(file)) { | |||
console.log('File not found: ', file); | |||
continue; | |||
} | |||
let file_content = fs.readFileSync(file, 'utf-8'); | |||
if (file.endsWith('.html') && output_type === 'js') { | |||
file_content = html_to_js_template(file, file_content); | |||
} | |||
if(file.endsWith('class.js')) { | |||
file_content = minify_js(file_content, file); | |||
} | |||
if (file.endsWith('.js') && !file.includes('/lib/') && output_type === 'js' && !file.endsWith('class.js')) { | |||
file_content = babelify(file_content, file, minify); | |||
} | |||
if(!minify) { | |||
output_txt += `\n/*\n *\t${file}\n */\n` | |||
} | |||
output_txt += file_content; | |||
} | |||
const target = p(assets_path, output_path); | |||
try { | |||
fs.writeFileSync(target, output_txt); | |||
console.log(`Wrote ${output_path} - ${get_file_size(target)}`); | |||
return target; | |||
} catch (e) { | |||
console.log('Error writing to file', output_path); | |||
console.log(e); | |||
} | |||
} | |||
function babelify(content, path, minify) { | |||
let presets = ['es2015', 'es2016']; | |||
if(minify) { | |||
presets.push('babili'); // new babel minifier | |||
} | |||
try { | |||
return babel.transform(content, { | |||
presets: presets, | |||
comments: false | |||
}).code; | |||
} catch (e) { | |||
console.log('Cannot babelify', path); | |||
console.log(e); | |||
return content; | |||
} | |||
} | |||
function minify_js(content, path) { | |||
try { | |||
return babel.transform(content, { | |||
comments: false | |||
}).code; | |||
} catch (e) { | |||
console.log('Cannot minify', path); | |||
console.log(e); | |||
return content; | |||
} | |||
} | |||
function make_build_map() { | |||
const build_map = {}; | |||
for (const app_path of app_paths) { | |||
const build_json_path = p(app_path, 'public', 'build.json'); | |||
if (!fs.existsSync(build_json_path)) continue; | |||
let build_json = fs.readFileSync(build_json_path); | |||
try { | |||
build_json = JSON.parse(build_json); | |||
} catch (e) { | |||
console.log(e); | |||
continue; | |||
} | |||
for (const target in build_json) { | |||
const sources = build_json[target]; | |||
const new_sources = []; | |||
for (const source of sources) { | |||
const s = p(app_path, source); | |||
new_sources.push(s); | |||
} | |||
if (new_sources.length) | |||
build_json[target] = new_sources; | |||
else | |||
delete build_json[target]; | |||
} | |||
Object.assign(build_map, build_json); | |||
} | |||
return build_map; | |||
} | |||
function compile_less() { | |||
return new Promise(function (resolve) { | |||
const promises = []; | |||
for (const app_path of app_paths) { | |||
const public_path = p(app_path, 'public'); | |||
const less_path = p(public_path, 'less'); | |||
if (!fs.existsSync(less_path)) continue; | |||
const files = fs.readdirSync(less_path); | |||
for (const file of files) { | |||
if(file.includes('variables.less')) continue; | |||
promises.push(compile_less_file(file, less_path, public_path)) | |||
} | |||
} | |||
Promise.all(promises).then(() => { | |||
console.log('Less files compiled'); | |||
resolve(); | |||
}); | |||
}); | |||
} | |||
function compile_less_file(file, less_path, public_path) { | |||
const file_content = fs.readFileSync(p(less_path, file), 'utf8'); | |||
const output_file = file.split('.')[0] + '.css'; | |||
console.log('compiling', file); | |||
return less.render(file_content, { | |||
paths: [less_path], | |||
filename: file, | |||
sourceMap: false | |||
}).then(output => { | |||
const out_css = p(public_path, 'css', output_file); | |||
fs.writeFileSync(out_css, output.css); | |||
return out_css; | |||
}).catch(e => { | |||
console.log('Error compiling ', file); | |||
console.log(e); | |||
}); | |||
} | |||
function watch_less(ondirty) { | |||
const less_paths = app_paths.map(path => p(path, 'public', 'less')); | |||
const to_watch = []; | |||
for (const less_path of less_paths) { | |||
if (!fs.existsSync(less_path)) continue; | |||
to_watch.push(less_path); | |||
} | |||
chokidar.watch(to_watch).on('change', (filename, stats) => { | |||
console.log(filename, 'dirty'); | |||
var last_index = filename.lastIndexOf('/'); | |||
const less_path = filename.slice(0, last_index); | |||
const public_path = p(less_path, '..'); | |||
filename = filename.split('/').pop(); | |||
compile_less_file(filename, less_path, public_path) | |||
.then(css_file_path => { | |||
// build the target css file for which this css file is input | |||
for (const target in build_map) { | |||
const sources = build_map[target]; | |||
if (sources.includes(css_file_path)) { | |||
pack(target, sources); | |||
ondirty && ondirty(target); | |||
break; | |||
} | |||
} | |||
}) | |||
}); | |||
} | |||
function watch_js(ondirty) { | |||
const js_paths = app_paths.map(path => p(path, 'public', 'js')); | |||
const to_watch = []; | |||
for (const js_path of js_paths) { | |||
if (!fs.existsSync(js_path)) continue; | |||
to_watch.push(js_path); | |||
} | |||
chokidar.watch(to_watch).on('change', (filename, stats) => { | |||
console.log(filename, 'dirty'); | |||
var last_index = filename.lastIndexOf('/'); | |||
const js_path = filename.slice(0, last_index); | |||
const public_path = p(js_path, '..'); | |||
// build the target js file for which this js/html file is input | |||
for (const target in build_map) { | |||
const sources = build_map[target]; | |||
if (sources.includes(filename)) { | |||
pack(target, sources); | |||
ondirty && ondirty(target); | |||
break; | |||
} | |||
} | |||
}); | |||
} | |||
function html_to_js_template(path, content) { | |||
let key = path.split('/'); | |||
key = key[key.length - 1]; | |||
key = key.split('.')[0]; | |||
content = scrub_html_template(content); | |||
return `frappe.templates['${key}'] = '${content}';\n`; | |||
} | |||
function scrub_html_template(content) { | |||
content = content.replace(/\s/g, ' '); | |||
content = content.replace(/(<!--.*?-->)/g, ''); | |||
return content.replace("'", "\'"); | |||
} | |||
function get_file_size(filepath) { | |||
const stats = fs.statSync(filepath); | |||
const size = stats.size; | |||
// convert it to humanly readable format. | |||
const i = Math.floor(Math.log(size) / Math.log(1024)); | |||
return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i]; | |||
} |
@@ -3,6 +3,7 @@ | |||
from __future__ import unicode_literals | |||
from frappe.utils.minify import JavascriptMinify | |||
import subprocess | |||
""" | |||
Build the `public` folders and setup languages | |||
@@ -22,16 +23,30 @@ def setup(): | |||
except ImportError: pass | |||
app_paths = [os.path.dirname(pymodule.__file__) for pymodule in pymodules] | |||
def bundle(no_compress, make_copy=False, verbose=False): | |||
def bundle(no_compress, make_copy=False, verbose=False, beta=False): | |||
"""concat / minify js files""" | |||
# build js files | |||
setup() | |||
make_asset_dirs(make_copy=make_copy) | |||
if beta: | |||
command = 'node ../apps/frappe/frappe/build.js --build' | |||
if not no_compress: | |||
command += ' --minify' | |||
subprocess.call(command.split(' ')) | |||
return | |||
build(no_compress, verbose) | |||
def watch(no_compress): | |||
def watch(no_compress, beta=False): | |||
"""watch and rebuild if necessary""" | |||
if beta: | |||
command = 'node ../apps/frappe/frappe/build.js --watch' | |||
subprocess.Popen(command.split(' ')) | |||
return | |||
setup() | |||
import time | |||
@@ -101,7 +116,6 @@ def get_build_maps(): | |||
except ValueError, e: | |||
print path | |||
print 'JSON syntax error {0}'.format(str(e)) | |||
return build_maps | |||
timestamps = {} | |||
@@ -8,19 +8,21 @@ from frappe.commands import pass_context, get_site | |||
@click.command('build') | |||
@click.option('--make-copy', is_flag=True, default=False, help='Copy the files instead of symlinking') | |||
@click.option('--verbose', is_flag=True, default=False, help='Verbose') | |||
def build(make_copy=False, verbose=False): | |||
@click.option('--beta', is_flag=True, default=False, help='Use the new NodeJS build system') | |||
def build(make_copy=False, verbose=False, beta=False): | |||
"Minify + concatenate JS and CSS files, build translations" | |||
import frappe.build | |||
import frappe | |||
frappe.init('') | |||
frappe.build.bundle(False, make_copy=make_copy, verbose=verbose) | |||
frappe.build.bundle(False, make_copy=make_copy, verbose=verbose, beta=beta) | |||
@click.command('watch') | |||
def watch(): | |||
@click.option('--beta', is_flag=True, default=False, help='Use the new NodeJS build system') | |||
def watch(beta=False): | |||
"Watch and concatenate JS and CSS files as and when they change" | |||
import frappe.build | |||
frappe.init('') | |||
frappe.build.watch(True) | |||
frappe.build.watch(True, beta=beta) | |||
@click.command('clear-cache') | |||
@pass_context | |||
@@ -397,7 +397,7 @@ def validate_fields(meta): | |||
def check_link_table_options(d): | |||
if d.fieldtype in ("Link", "Table"): | |||
if not d.options: | |||
frappe.throw(_("Options requried for Link or Table type field {0} in row {1}").format(d.label, d.idx)) | |||
frappe.throw(_("Options required for Link or Table type field {0} in row {1}").format(d.label, d.idx)) | |||
if d.options=="[Select]" or d.options==d.parent: | |||
return | |||
if d.options != d.parent: | |||
@@ -571,6 +571,7 @@ def validate_fields(meta): | |||
for d in fields: | |||
if not d.permlevel: d.permlevel = 0 | |||
if d.fieldtype != "Table": d.allow_bulk_edit = 0 | |||
if not d.fieldname: | |||
frappe.throw(_("Fieldname is required in row {0}").format(d.idx)) | |||
d.fieldname = d.fieldname.lower() | |||
@@ -4,9 +4,6 @@ | |||
frappe.provide("frappe.customize_form"); | |||
frappe.ui.form.on("Customize Form", { | |||
setup: function(frm) { | |||
frm.get_docfield("fields").allow_bulk_edit = 1; | |||
}, | |||
onload: function(frm) { | |||
frappe.customize_form.add_fields_help(frm); | |||
@@ -13,6 +13,7 @@ | |||
"editable_grid": 1, | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -42,6 +43,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -71,6 +73,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -100,6 +103,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -129,6 +133,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -158,6 +163,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -186,6 +192,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -215,6 +222,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -245,6 +253,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -275,6 +284,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -304,6 +314,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -334,6 +345,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -362,6 +374,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -392,6 +405,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -422,6 +436,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -451,6 +466,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -480,6 +496,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -508,6 +525,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -536,6 +554,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -565,6 +584,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -595,6 +615,7 @@ | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 1, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
@@ -635,7 +656,7 @@ | |||
"issingle": 1, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-04-10 12:17:23.627634", | |||
"modified": "2017-04-21 16:59:12.752428", | |||
"modified_by": "Administrator", | |||
"module": "Custom", | |||
"name": "Customize Form", | |||
@@ -60,7 +60,8 @@ docfield_properties = { | |||
'read_only': 'Check', | |||
'length': 'Int', | |||
'columns': 'Int', | |||
'remember_last_selected_value': 'Check' | |||
'remember_last_selected_value': 'Check', | |||
'allow_bulk_edit': 'Check', | |||
} | |||
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'), | |||
@@ -146,7 +146,7 @@ def export_query(): | |||
if row: | |||
row_list = [] | |||
for idx in range(len(data.columns)): | |||
row_list.append(row[columns[idx]["fieldname"]]) | |||
row_list.append(row.get(columns[idx]["fieldname"],"")) | |||
result.append(row_list) | |||
else: | |||
result = result + data.result | |||
@@ -165,8 +165,8 @@ class FrappeClient(object): | |||
params = {} | |||
if filters: | |||
params["filters"] = json.dumps(filters) | |||
if fields: | |||
params["fields"] = json.dumps(fields) | |||
if fields: | |||
params["fields"] = json.dumps(fields) | |||
res = self.session.get(self.url + "/api/resource/" + doctype + "/" + name, | |||
params=params, verify=self.verify) | |||
@@ -178,4 +178,5 @@ frappe.patches.v8_0.newsletter_childtable_migrate | |||
execute:frappe.db.sql("delete from `tabDesktop Icon` where module_name='Communication'") | |||
execute:frappe.db.sql("update `tabDesktop Icon` set type='list' where _doctype='Communication'") | |||
frappe.patches.v8_0.fix_non_english_desktop_icons # 2017-04-12 | |||
frappe.patches.v8_0.set_doctype_values_in_custom_role | |||
frappe.patches.v8_0.set_doctype_values_in_custom_role | |||
frappe.patches.v8_0.install_new_build_system_requirements |
@@ -0,0 +1,12 @@ | |||
import subprocess | |||
def execute(): | |||
subprocess.call([ | |||
'npm', 'install', | |||
'babel-core', | |||
'chokidar', | |||
'babel-preset-es2015', | |||
'babel-preset-es2016', | |||
'babel-preset-es2017', | |||
'babel-preset-babili' | |||
]) |
@@ -1,5 +1,5 @@ | |||
def execute(): | |||
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 | |||
for doctype in get_doctypes_with_global_search(): | |||
def execute(): | |||
for doctype in get_doctypes_with_global_search(with_child_tables=False): | |||
rebuild_for_doctype(doctype) |
@@ -6,6 +6,7 @@ | |||
"public/css/avatar.css" | |||
], | |||
"js/frappe-web.min.js": [ | |||
"public/js/frappe/class.js", | |||
"public/js/lib/bootstrap.min.js", | |||
"public/js/lib/md5.min.js", | |||
"public/js/frappe/provide.js", | |||
@@ -16,7 +17,6 @@ | |||
"public/js/frappe/misc/pretty_date.js", | |||
"public/js/lib/moment/moment.min.js", | |||
"public/js/lib/highlight.pack.js", | |||
"public/js/frappe/class.js", | |||
"public/js/lib/microtemplate.js", | |||
"public/js/frappe/query_string.js", | |||
"website/js/website.js", | |||
@@ -75,8 +75,8 @@ | |||
"public/js/lib/datepicker/locale-all.js" | |||
], | |||
"js/desk.min.js": [ | |||
"public/js/frappe/provide.js", | |||
"public/js/frappe/class.js", | |||
"public/js/frappe/provide.js", | |||
"public/js/frappe/assets.js", | |||
"public/js/frappe/format.js", | |||
"public/js/frappe/form/formatters.js", | |||
@@ -318,8 +318,15 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ | |||
}, | |||
set_disp_area: function() { | |||
let value = this.get_value(); | |||
if(inList(["Currency", "Int", "Float"], this.df.fieldtype) && (this.value === 0 || value === 0)) { | |||
// to set the 0 value in readonly for currency, int, float field | |||
value = 0; | |||
} else { | |||
value = this.value || value; | |||
} | |||
this.disp_area && $(this.disp_area) | |||
.html(frappe.format(this.value || this.get_value(), this.df, {no_icon:true, inline:true}, | |||
.html(frappe.format(value, this.df, {no_icon:true, inline:true}, | |||
this.doc || (this.frm && this.frm.doc))); | |||
}, | |||
@@ -348,6 +348,10 @@ frappe.ui.form.Grid = Class.extend({ | |||
this.get_docfield(fieldname).read_only = enable ? 0 : 1;; | |||
this.refresh(); | |||
}, | |||
toggle_display: function(fieldname, show) { | |||
this.get_docfield(fieldname).hidden = show ? 0 : 1;; | |||
this.refresh(); | |||
}, | |||
get_docfield: function(fieldname) { | |||
return frappe.meta.get_docfield(this.doctype, fieldname, this.frm ? this.frm.docname : null); | |||
}, | |||
@@ -320,11 +320,11 @@ frappe.views.ListRenderer = Class.extend({ | |||
const $item_container = $('<div class="list-item-container">').append($item); | |||
$list_items.append($item_container); | |||
if (this.settings.post_render_item) { | |||
this.settings.post_render_item(this, $item_container, value); | |||
} | |||
this.render_tags($item_container, value); | |||
}); | |||
@@ -541,7 +541,7 @@ frappe.views.ListRenderer = Class.extend({ | |||
var new_button = frappe.boot.user.can_create.includes(this.doctype) | |||
? (`<p><button class='btn btn-primary btn-sm' | |||
list_view_doc='${this.doctype}'> | |||
${__('Make a new ' + __(this.doctype))} | |||
${__('Make a new {0}', [__(this.doctype)])} | |||
</button></p>`) | |||
: ''; | |||
var no_result_message = | |||
@@ -116,10 +116,11 @@ frappe.get_route_str = function(route) { | |||
} | |||
frappe.set_route = function() { | |||
if(arguments.length===1 && $.isArray(arguments[0])) { | |||
arguments = arguments[0]; | |||
var params = arguments; | |||
if(params.length===1 && $.isArray(params[0])) { | |||
params = params[0]; | |||
} | |||
route = $.map(arguments, function(a) { | |||
route = $.map(params, function(a) { | |||
if($.isPlainObject(a)) { | |||
frappe.route_options = a; | |||
return null; | |||
@@ -11,6 +11,11 @@ frappe.socket = { | |||
return; | |||
} | |||
if (frappe.boot.developer_mode) { | |||
// File watchers for development | |||
frappe.socket.setup_file_watchers(); | |||
} | |||
//Enable secure option when using HTTPS | |||
if (window.location.protocol == "https:") { | |||
frappe.socket.socket = io.connect(frappe.socket.get_host(), {secure: true}); | |||
@@ -186,7 +191,38 @@ frappe.socket = { | |||
} | |||
}, 5000); | |||
}); | |||
}, | |||
setup_file_watchers: function() { | |||
var host = window.location.origin; | |||
var port = '6787'; | |||
// remove the port number from string | |||
host = host.split(':').slice(0, -1).join(":"); | |||
host = host + ':' + port; | |||
frappe.socket.file_watcher = io.connect(host); | |||
// css files auto reload | |||
frappe.socket.file_watcher.on('reload_css', function(filename) { | |||
let abs_file_path = "assets/" + filename; | |||
const link = $(`link[href*="${abs_file_path}"]`); | |||
abs_file_path = abs_file_path.split('?')[0] + '?v='+ moment(); | |||
link.attr('href', abs_file_path); | |||
frappe.show_alert({ | |||
indicator: 'orange', | |||
message: filename + ' reloaded' | |||
}, 5); | |||
}); | |||
// js files show alert | |||
frappe.socket.file_watcher.on('reload_js', function(filename) { | |||
filename = "assets/" + filename; | |||
var msg = $(` | |||
<span>${filename} changed <a data-action="reload">Click to Reload</a></span> | |||
`) | |||
msg.find('a').click(frappe.ui.toolbar.clear_cache); | |||
frappe.show_alert({ | |||
indicator: 'orange', | |||
message: msg | |||
}, 5); | |||
}); | |||
}, | |||
process_response: function(data, method) { | |||
if(!data) { | |||
@@ -250,7 +250,7 @@ frappe.hide_progress = function() { | |||
} | |||
// Floating Message | |||
frappe.show_alert = function(message, seconds) { | |||
frappe.show_alert = function(message, seconds=7) { | |||
if(typeof message==='string') { | |||
message = { | |||
message: message | |||
@@ -261,14 +261,19 @@ frappe.show_alert = function(message, seconds) { | |||
} | |||
if(message.indicator) { | |||
message_html = '<span class="indicator ' + message.indicator + '">' + message.message + '</span>'; | |||
message_html = $('<span class="indicator ' + message.indicator + '"></span>').append(message.message); | |||
} else { | |||
message_html = message.message; | |||
} | |||
var div = $(repl('<div class="alert desk-alert" style="display: none;">' | |||
+ '<span class="alert-message">%(txt)s</span><a class="close">×</a>' | |||
+ '</div>', {txt: message_html})) | |||
var div = $(` | |||
<div class="alert desk-alert"> | |||
<span class="alert-message"></span><a class="close">×</a> | |||
</div>`); | |||
div.find('.alert-message').append(message_html); | |||
div.hide() | |||
.appendTo("#alert-container") | |||
.fadeIn(300); | |||
@@ -277,7 +282,7 @@ frappe.show_alert = function(message, seconds) { | |||
return false; | |||
}); | |||
div.delay(seconds ? seconds * 1000 : 7000).fadeOut(300); | |||
div.delay(seconds * 1000).fadeOut(300); | |||
return div; | |||
} | |||
@@ -68,8 +68,7 @@ frappe.views.ImageView = frappe.views.ListRenderer.extend({ | |||
gallery.show(name); | |||
return false; | |||
}); | |||
}, | |||
refresh: this.render_view | |||
} | |||
}); | |||
frappe.views.GalleryView = Class.extend({ | |||
@@ -178,21 +178,33 @@ frappe.provide("frappe.views"); | |||
}); | |||
}, | |||
update_order: function(updater, order) { | |||
return frappe.call({ | |||
// cache original order | |||
const _cards = this.cards.slice(); | |||
const _columns = this.columns.slice(); | |||
frappe.call({ | |||
method: method_prefix + "update_order", | |||
args: { | |||
board_name: this.board.name, | |||
order: order | |||
}, | |||
callback: (r) => { | |||
var state = this; | |||
var board = r.message[0]; | |||
var updated_cards = r.message[1]; | |||
var cards = update_cards_column(updated_cards); | |||
var columns = prepare_columns(board.columns); | |||
updater.set({ | |||
cards: cards, | |||
columns: columns | |||
}); | |||
} | |||
}).then(function(r) { | |||
var state = this; | |||
var board = r.message[0]; | |||
var updated_cards = r.message[1]; | |||
var cards = update_cards_column(updated_cards); | |||
var columns = prepare_columns(board.columns); | |||
}) | |||
.fail(function(e) { | |||
// revert original order | |||
updater.set({ | |||
cards: cards, | |||
columns: columns | |||
cards: _cards, | |||
columns: _columns | |||
}); | |||
}); | |||
}, | |||
@@ -237,8 +249,12 @@ frappe.provide("frappe.views"); | |||
self.cur_list = opts.cur_list; | |||
self.board_name = opts.board_name; | |||
self.update_cards = function(cards) { | |||
fluxify.doAction('update_cards', cards); | |||
self.update = function(cards) { | |||
if(self.wrapper.find('.kanban').length > 0) { | |||
fluxify.doAction('update_cards', cards); | |||
} else { | |||
init(); | |||
} | |||
} | |||
function init() { | |||
@@ -250,8 +266,13 @@ frappe.provide("frappe.views"); | |||
} | |||
function prepare() { | |||
self.$kanban_board = $(frappe.render_template("kanban_board")); | |||
self.$kanban_board.appendTo(self.wrapper); | |||
self.$kanban_board = self.wrapper.find('.kanban'); | |||
if(self.$kanban_board.length === 0) { | |||
self.$kanban_board = $(frappe.render_template("kanban_board")); | |||
self.$kanban_board.appendTo(self.wrapper); | |||
} | |||
self.$filter_area = self.cur_list.$page.find('.set-filters'); | |||
bind_events(); | |||
setup_sortable(); | |||
@@ -5,7 +5,7 @@ frappe.views.KanbanView = frappe.views.ListRenderer.extend({ | |||
render_view: function(values) { | |||
var board_name = this.get_board_name(); | |||
if(this.kanban && board_name === this.kanban.board_name) { | |||
this.kanban.update_cards(values); | |||
this.kanban.update(values); | |||
return; | |||
} | |||
@@ -39,7 +39,7 @@ var user_img = {}; | |||
var _f = {}; | |||
var _p = {}; | |||
var _r = {}; | |||
var FILTER_SEP = '\1'; | |||
// var FILTER_SEP = '\1'; | |||
// API globals | |||
var frms={}; | |||
@@ -172,6 +172,6 @@ class TestGlobalSearch(unittest.TestCase): | |||
field_as_text = '' | |||
for field in doc.meta.fields: | |||
if field.fieldname == 'description': | |||
field_as_text = global_search.get_field_value(doc, field) | |||
field_as_text = global_search.get_formatted_value(doc.description, field) | |||
self.assertEquals(case["result"], field_as_text) |
@@ -6,6 +6,7 @@ from __future__ import unicode_literals | |||
import frappe | |||
import re | |||
from frappe.utils import cint, strip_html_tags | |||
from frappe.model.base_document import get_controller | |||
def setup_global_search_table(): | |||
'''Creates __global_seach table''' | |||
@@ -27,11 +28,13 @@ def reset(): | |||
'''Deletes all data in __global_search''' | |||
frappe.db.sql('delete from __global_search') | |||
def get_doctypes_with_global_search(): | |||
def get_doctypes_with_global_search(with_child_tables=True): | |||
'''Return doctypes with global search fields''' | |||
def _get(): | |||
global_search_doctypes = [] | |||
for d in frappe.get_all('DocType', 'name, module'): | |||
if not with_child_tables: | |||
filters = {"istable": ["!=", 1]} | |||
for d in frappe.get_all('DocType', fields=['name', 'module'], filters=filters): | |||
meta = frappe.get_meta(d.name) | |||
if len(meta.get_global_search_fields()) > 0: | |||
global_search_doctypes.append(d) | |||
@@ -44,35 +47,172 @@ def get_doctypes_with_global_search(): | |||
return frappe.cache().get_value('doctypes_with_global_search', _get) | |||
def rebuild_for_doctype(doctype): | |||
'''Rebuild entries of doctype's documents in __global_search on change of | |||
searchable fields | |||
:param doctype: Doctype ''' | |||
def _get_filters(): | |||
filters = frappe._dict({ "docstatus": ["!=", 1] }) | |||
if meta.has_field("enabled"): | |||
filters.enabled = 1 | |||
if meta.has_field("disabled"): | |||
filters.disabled = 0 | |||
return filters | |||
meta = frappe.get_meta(doctype) | |||
if cint(meta.istable) == 1: | |||
parent_doctypes = frappe.get_all("DocField", fields="parent", filters={ | |||
"fieldtype": "Table", | |||
"options": doctype | |||
}) | |||
for p in parent_doctypes: | |||
rebuild_for_doctype(p.parent) | |||
return | |||
# Delete records | |||
delete_global_search_records_for_doctype(doctype) | |||
parent_search_fields = meta.get_global_search_fields() | |||
fieldnames = get_selected_fields(meta, parent_search_fields) | |||
# Get all records from parent doctype table | |||
all_records = frappe.get_all(doctype, fields=fieldnames, filters=_get_filters()) | |||
# Children data | |||
all_children, child_search_fields = get_children_data(doctype, meta) | |||
all_contents = [] | |||
for doc in all_records: | |||
content = [] | |||
for field in parent_search_fields: | |||
value = doc.get(field.fieldname) | |||
if value: | |||
content.append(get_formatted_value(value, field)) | |||
# get children data | |||
for child_doctype, records in all_children.get(doc.name, {}).items(): | |||
for field in child_search_fields.get(child_doctype): | |||
for r in records: | |||
if r.get(field.fieldname): | |||
content.append(get_formatted_value(r.get(field.fieldname), field)) | |||
if content: | |||
# if doctype published in website, push title, route etc. | |||
published = 0 | |||
title, route = "", "" | |||
if hasattr(get_controller(doctype), "is_website_published") and meta.allow_guest_to_view: | |||
d = frappe.get_doc(doctype, doc.name) | |||
published = 1 if d.is_website_published() else 0 | |||
title = d.get_title() | |||
route = d.get("route") | |||
all_contents.append({ | |||
"doctype": doctype, | |||
"name": frappe.db.escape(doc.name), | |||
"content": frappe.db.escape(' ||| '.join(content or '')), | |||
"published": published, | |||
"title": frappe.db.escape(title or ''), | |||
"route": frappe.db.escape(route or '') | |||
}) | |||
if all_contents: | |||
insert_values_for_multiple_docs(all_contents) | |||
def delete_global_search_records_for_doctype(doctype): | |||
frappe.db.sql(''' | |||
delete | |||
from __global_search | |||
where | |||
doctype = %s''', doctype, as_dict=True) | |||
def get_selected_fields(meta, global_search_fields): | |||
fieldnames = [df.fieldname for df in global_search_fields] | |||
if meta.istable==1: | |||
fieldnames.append("parent") | |||
elif "name" not in fieldnames: | |||
fieldnames.append("name") | |||
if meta.has_field("is_website_published"): | |||
fieldnames.append("is_website_published") | |||
return fieldnames | |||
def get_children_data(doctype, meta): | |||
""" | |||
Get all records from all the child tables of a doctype | |||
all_children = { | |||
"parent1": { | |||
"child_doctype1": [ | |||
{ | |||
"field1": val1, | |||
"field2": val2 | |||
} | |||
] | |||
} | |||
} | |||
""" | |||
all_children = frappe._dict() | |||
child_search_fields = frappe._dict() | |||
for child in meta.get_table_fields(): | |||
child_meta = frappe.get_meta(child.options) | |||
search_fields = child_meta.get_global_search_fields() | |||
if search_fields: | |||
child_search_fields.setdefault(child.options, search_fields) | |||
child_fieldnames = get_selected_fields(child_meta, search_fields) | |||
child_records = frappe.get_all(child.options, fields=child_fieldnames, filters={ | |||
"docstatus": ["!=", 1], | |||
"parenttype": doctype | |||
}) | |||
for record in child_records: | |||
all_children.setdefault(record.parent, frappe._dict())\ | |||
.setdefault(child.options, []).append(record) | |||
return all_children, child_search_fields | |||
def insert_values_for_multiple_docs(all_contents): | |||
values = [] | |||
for content in all_contents: | |||
values.append("( '{doctype}', '{name}', '{content}', '{published}', '{title}', '{route}')" | |||
.format(**content)) | |||
frappe.db.sql(''' | |||
insert into __global_search | |||
(doctype, name, content, published, title, route) | |||
values | |||
{0} | |||
'''.format(", ".join(values))) | |||
def update_global_search(doc): | |||
'''Add values marked with `in_global_search` to | |||
`frappe.flags.update_global_search` from given doc | |||
:param doc: Document to be added to global search''' | |||
if cint(doc.meta.istable) == 1 and frappe.db.exists("DocType", doc.parenttype): | |||
d = frappe.get_doc(doc.parenttype, doc.parent) | |||
update_global_search(d) | |||
return | |||
if doc.docstatus > 1: | |||
return | |||
if doc.docstatus > 1 or (doc.meta.has_field("enabled") and not doc.get("enabled")) \ | |||
or doc.get("disabled"): | |||
return | |||
if frappe.flags.update_global_search==None: | |||
frappe.flags.update_global_search = [] | |||
content = [] | |||
for field in doc.meta.get_global_search_fields(): | |||
if doc.get(field.fieldname): | |||
if getattr(field, 'fieldtype', None) == "Table": | |||
# Get children | |||
for d in doc.get(field.fieldname): | |||
if d.parent == doc.name: | |||
for field in d.meta.get_global_search_fields(): | |||
if d.get(field.fieldname): | |||
content.append(get_field_value(d, field)) | |||
else: | |||
content.append(get_field_value(doc, field)) | |||
if doc.get(field.fieldname) and field.fieldtype != "Table": | |||
content.append(get_formatted_value(doc.get(field.fieldname), field)) | |||
# Get children | |||
for child in doc.meta.get_table_fields(): | |||
for d in doc.get(child.fieldname): | |||
if d.parent == doc.name: | |||
for field in d.meta.get_global_search_fields(): | |||
if d.get(field.fieldname): | |||
content.append(get_formatted_value(d.get(field.fieldname), field)) | |||
if content: | |||
published = 0 | |||
@@ -83,12 +223,11 @@ def update_global_search(doc): | |||
dict(doctype=doc.doctype, name=doc.name, content=' ||| '.join(content or ''), | |||
published=published, title=doc.get_title(), route=doc.get('route'))) | |||
def get_field_value(doc, field): | |||
def get_formatted_value(value, field): | |||
'''Prepare field from raw data''' | |||
from HTMLParser import HTMLParser | |||
value = doc.get(field.fieldname) | |||
if(getattr(field, 'fieldtype', None) in ["Text", "Text Editor"]): | |||
h = HTMLParser() | |||
value = h.unescape(value) | |||
@@ -111,23 +250,6 @@ def sync_global_search(): | |||
frappe.flags.update_global_search = [] | |||
def rebuild_for_doctype(doctype): | |||
'''Rebuild entries of doctype's documents in __global_search on change of | |||
searchable fields | |||
:param doctype: Doctype ''' | |||
frappe.flags.update_global_search = [] | |||
frappe.db.sql(''' | |||
delete | |||
from __global_search | |||
where | |||
doctype = %s''', doctype, as_dict=True) | |||
for d in frappe.get_all(doctype): | |||
update_global_search(frappe.get_doc(doctype, d.name)) | |||
sync_global_search() | |||
def delete_for_document(doc): | |||
'''Delete the __global_search entry of a document that has | |||
been deleted | |||
@@ -433,6 +433,15 @@ def column_has_value(data, fieldname): | |||
trigger_print_script = """ | |||
<script> | |||
//allow wrapping of long tr | |||
var elements = document.getElementsByTagName("tr"); | |||
var i = elements.length; | |||
while (i--) { | |||
if(elements[i].clientHeight>300){ | |||
elements[i].setAttribute("style", "page-break-inside: auto;"); | |||
} | |||
} | |||
window.print(); | |||
// close the window after print | |||
@@ -293,3 +293,5 @@ function get_conf() { | |||
return conf; | |||
} | |||
@@ -0,0 +1,11 @@ | |||
fixture `Example page` | |||
.page `http:localhost:8000/login`; | |||
test('Successful Login', async t => { | |||
await t | |||
.typeText('#login_email', 'Administrator') | |||
.click('#login_password', 'admin') | |||
.click('.btn-login') | |||
.click('[data-link="modules"]'); | |||
}); |