@@ -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.19' | |||||
__version__ = '8.0.20' | |||||
__title__ = "Frappe Framework" | __title__ = "Frappe Framework" | ||||
local = Local() | 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 __future__ import unicode_literals | ||||
from frappe.utils.minify import JavascriptMinify | from frappe.utils.minify import JavascriptMinify | ||||
import subprocess | |||||
""" | """ | ||||
Build the `public` folders and setup languages | Build the `public` folders and setup languages | ||||
@@ -22,16 +23,30 @@ def setup(): | |||||
except ImportError: pass | except ImportError: pass | ||||
app_paths = [os.path.dirname(pymodule.__file__) for pymodule in pymodules] | 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""" | """concat / minify js files""" | ||||
# build js files | # build js files | ||||
setup() | setup() | ||||
make_asset_dirs(make_copy=make_copy) | 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) | build(no_compress, verbose) | ||||
def watch(no_compress): | |||||
def watch(no_compress, beta=False): | |||||
"""watch and rebuild if necessary""" | """watch and rebuild if necessary""" | ||||
if beta: | |||||
command = 'node ../apps/frappe/frappe/build.js --watch' | |||||
subprocess.Popen(command.split(' ')) | |||||
return | |||||
setup() | setup() | ||||
import time | import time | ||||
@@ -101,7 +116,6 @@ def get_build_maps(): | |||||
except ValueError, e: | except ValueError, e: | ||||
print path | print path | ||||
print 'JSON syntax error {0}'.format(str(e)) | print 'JSON syntax error {0}'.format(str(e)) | ||||
return build_maps | return build_maps | ||||
timestamps = {} | timestamps = {} | ||||
@@ -8,19 +8,21 @@ from frappe.commands import pass_context, get_site | |||||
@click.command('build') | @click.command('build') | ||||
@click.option('--make-copy', is_flag=True, default=False, help='Copy the files instead of symlinking') | @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') | @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" | "Minify + concatenate JS and CSS files, build translations" | ||||
import frappe.build | import frappe.build | ||||
import frappe | import frappe | ||||
frappe.init('') | 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') | @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" | "Watch and concatenate JS and CSS files as and when they change" | ||||
import frappe.build | import frappe.build | ||||
frappe.init('') | frappe.init('') | ||||
frappe.build.watch(True) | |||||
frappe.build.watch(True, beta=beta) | |||||
@click.command('clear-cache') | @click.command('clear-cache') | ||||
@pass_context | @pass_context | ||||
@@ -397,7 +397,7 @@ def validate_fields(meta): | |||||
def check_link_table_options(d): | def check_link_table_options(d): | ||||
if d.fieldtype in ("Link", "Table"): | if d.fieldtype in ("Link", "Table"): | ||||
if not d.options: | 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: | if d.options=="[Select]" or d.options==d.parent: | ||||
return | return | ||||
if d.options != d.parent: | if d.options != d.parent: | ||||
@@ -571,6 +571,7 @@ def validate_fields(meta): | |||||
for d in fields: | for d in fields: | ||||
if not d.permlevel: d.permlevel = 0 | if not d.permlevel: d.permlevel = 0 | ||||
if d.fieldtype != "Table": d.allow_bulk_edit = 0 | |||||
if not d.fieldname: | if not d.fieldname: | ||||
frappe.throw(_("Fieldname is required in row {0}").format(d.idx)) | frappe.throw(_("Fieldname is required in row {0}").format(d.idx)) | ||||
d.fieldname = d.fieldname.lower() | d.fieldname = d.fieldname.lower() | ||||
@@ -4,9 +4,6 @@ | |||||
frappe.provide("frappe.customize_form"); | frappe.provide("frappe.customize_form"); | ||||
frappe.ui.form.on("Customize Form", { | frappe.ui.form.on("Customize Form", { | ||||
setup: function(frm) { | |||||
frm.get_docfield("fields").allow_bulk_edit = 1; | |||||
}, | |||||
onload: function(frm) { | onload: function(frm) { | ||||
frappe.customize_form.add_fields_help(frm); | frappe.customize_form.add_fields_help(frm); | ||||
@@ -13,6 +13,7 @@ | |||||
"editable_grid": 1, | "editable_grid": 1, | ||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -42,6 +43,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -71,6 +73,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -100,6 +103,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -129,6 +133,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -158,6 +163,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -186,6 +192,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -215,6 +222,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -245,6 +253,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -275,6 +284,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -304,6 +314,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -334,6 +345,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -362,6 +374,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -392,6 +405,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -422,6 +436,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -451,6 +466,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -480,6 +496,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -508,6 +525,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -536,6 +554,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -565,6 +584,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -595,6 +615,7 @@ | |||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | { | ||||
"allow_bulk_edit": 1, | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
@@ -635,7 +656,7 @@ | |||||
"issingle": 1, | "issingle": 1, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-04-10 12:17:23.627634", | |||||
"modified": "2017-04-21 16:59:12.752428", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Custom", | "module": "Custom", | ||||
"name": "Customize Form", | "name": "Customize Form", | ||||
@@ -60,7 +60,8 @@ docfield_properties = { | |||||
'read_only': 'Check', | 'read_only': 'Check', | ||||
'length': 'Int', | 'length': 'Int', | ||||
'columns': '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'), | allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'), | ||||
@@ -146,7 +146,7 @@ def export_query(): | |||||
if row: | if row: | ||||
row_list = [] | row_list = [] | ||||
for idx in range(len(data.columns)): | 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) | result.append(row_list) | ||||
else: | else: | ||||
result = result + data.result | result = result + data.result | ||||
@@ -165,8 +165,8 @@ class FrappeClient(object): | |||||
params = {} | params = {} | ||||
if filters: | if filters: | ||||
params["filters"] = json.dumps(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, | res = self.session.get(self.url + "/api/resource/" + doctype + "/" + name, | ||||
params=params, verify=self.verify) | 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("delete from `tabDesktop Icon` where module_name='Communication'") | ||||
execute:frappe.db.sql("update `tabDesktop Icon` set type='list' where _doctype='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.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) | rebuild_for_doctype(doctype) |
@@ -6,6 +6,7 @@ | |||||
"public/css/avatar.css" | "public/css/avatar.css" | ||||
], | ], | ||||
"js/frappe-web.min.js": [ | "js/frappe-web.min.js": [ | ||||
"public/js/frappe/class.js", | |||||
"public/js/lib/bootstrap.min.js", | "public/js/lib/bootstrap.min.js", | ||||
"public/js/lib/md5.min.js", | "public/js/lib/md5.min.js", | ||||
"public/js/frappe/provide.js", | "public/js/frappe/provide.js", | ||||
@@ -16,7 +17,6 @@ | |||||
"public/js/frappe/misc/pretty_date.js", | "public/js/frappe/misc/pretty_date.js", | ||||
"public/js/lib/moment/moment.min.js", | "public/js/lib/moment/moment.min.js", | ||||
"public/js/lib/highlight.pack.js", | "public/js/lib/highlight.pack.js", | ||||
"public/js/frappe/class.js", | |||||
"public/js/lib/microtemplate.js", | "public/js/lib/microtemplate.js", | ||||
"public/js/frappe/query_string.js", | "public/js/frappe/query_string.js", | ||||
"website/js/website.js", | "website/js/website.js", | ||||
@@ -75,8 +75,8 @@ | |||||
"public/js/lib/datepicker/locale-all.js" | "public/js/lib/datepicker/locale-all.js" | ||||
], | ], | ||||
"js/desk.min.js": [ | "js/desk.min.js": [ | ||||
"public/js/frappe/provide.js", | |||||
"public/js/frappe/class.js", | "public/js/frappe/class.js", | ||||
"public/js/frappe/provide.js", | |||||
"public/js/frappe/assets.js", | "public/js/frappe/assets.js", | ||||
"public/js/frappe/format.js", | "public/js/frappe/format.js", | ||||
"public/js/frappe/form/formatters.js", | "public/js/frappe/form/formatters.js", | ||||
@@ -318,8 +318,15 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ | |||||
}, | }, | ||||
set_disp_area: function() { | 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) | 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))); | 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.get_docfield(fieldname).read_only = enable ? 0 : 1;; | ||||
this.refresh(); | this.refresh(); | ||||
}, | }, | ||||
toggle_display: function(fieldname, show) { | |||||
this.get_docfield(fieldname).hidden = show ? 0 : 1;; | |||||
this.refresh(); | |||||
}, | |||||
get_docfield: function(fieldname) { | get_docfield: function(fieldname) { | ||||
return frappe.meta.get_docfield(this.doctype, fieldname, this.frm ? this.frm.docname : null); | 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); | const $item_container = $('<div class="list-item-container">').append($item); | ||||
$list_items.append($item_container); | $list_items.append($item_container); | ||||
if (this.settings.post_render_item) { | if (this.settings.post_render_item) { | ||||
this.settings.post_render_item(this, $item_container, value); | this.settings.post_render_item(this, $item_container, value); | ||||
} | } | ||||
this.render_tags($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) | var new_button = frappe.boot.user.can_create.includes(this.doctype) | ||||
? (`<p><button class='btn btn-primary btn-sm' | ? (`<p><button class='btn btn-primary btn-sm' | ||||
list_view_doc='${this.doctype}'> | list_view_doc='${this.doctype}'> | ||||
${__('Make a new ' + __(this.doctype))} | |||||
${__('Make a new {0}', [__(this.doctype)])} | |||||
</button></p>`) | </button></p>`) | ||||
: ''; | : ''; | ||||
var no_result_message = | var no_result_message = | ||||
@@ -116,10 +116,11 @@ frappe.get_route_str = function(route) { | |||||
} | } | ||||
frappe.set_route = function() { | 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)) { | if($.isPlainObject(a)) { | ||||
frappe.route_options = a; | frappe.route_options = a; | ||||
return null; | return null; | ||||
@@ -11,6 +11,11 @@ frappe.socket = { | |||||
return; | return; | ||||
} | } | ||||
if (frappe.boot.developer_mode) { | |||||
// File watchers for development | |||||
frappe.socket.setup_file_watchers(); | |||||
} | |||||
//Enable secure option when using HTTPS | //Enable secure option when using HTTPS | ||||
if (window.location.protocol == "https:") { | if (window.location.protocol == "https:") { | ||||
frappe.socket.socket = io.connect(frappe.socket.get_host(), {secure: true}); | frappe.socket.socket = io.connect(frappe.socket.get_host(), {secure: true}); | ||||
@@ -186,7 +191,38 @@ frappe.socket = { | |||||
} | } | ||||
}, 5000); | }, 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) { | process_response: function(data, method) { | ||||
if(!data) { | if(!data) { | ||||
@@ -250,7 +250,7 @@ frappe.hide_progress = function() { | |||||
} | } | ||||
// Floating Message | // Floating Message | ||||
frappe.show_alert = function(message, seconds) { | |||||
frappe.show_alert = function(message, seconds=7) { | |||||
if(typeof message==='string') { | if(typeof message==='string') { | ||||
message = { | message = { | ||||
message: message | message: message | ||||
@@ -261,14 +261,19 @@ frappe.show_alert = function(message, seconds) { | |||||
} | } | ||||
if(message.indicator) { | 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 { | } else { | ||||
message_html = message.message; | 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") | .appendTo("#alert-container") | ||||
.fadeIn(300); | .fadeIn(300); | ||||
@@ -277,7 +282,7 @@ frappe.show_alert = function(message, seconds) { | |||||
return false; | return false; | ||||
}); | }); | ||||
div.delay(seconds ? seconds * 1000 : 7000).fadeOut(300); | |||||
div.delay(seconds * 1000).fadeOut(300); | |||||
return div; | return div; | ||||
} | } | ||||
@@ -68,8 +68,7 @@ frappe.views.ImageView = frappe.views.ListRenderer.extend({ | |||||
gallery.show(name); | gallery.show(name); | ||||
return false; | return false; | ||||
}); | }); | ||||
}, | |||||
refresh: this.render_view | |||||
} | |||||
}); | }); | ||||
frappe.views.GalleryView = Class.extend({ | frappe.views.GalleryView = Class.extend({ | ||||
@@ -178,21 +178,33 @@ frappe.provide("frappe.views"); | |||||
}); | }); | ||||
}, | }, | ||||
update_order: function(updater, order) { | 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", | method: method_prefix + "update_order", | ||||
args: { | args: { | ||||
board_name: this.board.name, | board_name: this.board.name, | ||||
order: order | 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({ | 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.cur_list = opts.cur_list; | ||||
self.board_name = opts.board_name; | 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() { | function init() { | ||||
@@ -250,8 +266,13 @@ frappe.provide("frappe.views"); | |||||
} | } | ||||
function prepare() { | 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'); | self.$filter_area = self.cur_list.$page.find('.set-filters'); | ||||
bind_events(); | bind_events(); | ||||
setup_sortable(); | setup_sortable(); | ||||
@@ -5,7 +5,7 @@ frappe.views.KanbanView = frappe.views.ListRenderer.extend({ | |||||
render_view: function(values) { | render_view: function(values) { | ||||
var board_name = this.get_board_name(); | var board_name = this.get_board_name(); | ||||
if(this.kanban && board_name === this.kanban.board_name) { | if(this.kanban && board_name === this.kanban.board_name) { | ||||
this.kanban.update_cards(values); | |||||
this.kanban.update(values); | |||||
return; | return; | ||||
} | } | ||||
@@ -39,7 +39,7 @@ var user_img = {}; | |||||
var _f = {}; | var _f = {}; | ||||
var _p = {}; | var _p = {}; | ||||
var _r = {}; | var _r = {}; | ||||
var FILTER_SEP = '\1'; | |||||
// var FILTER_SEP = '\1'; | |||||
// API globals | // API globals | ||||
var frms={}; | var frms={}; | ||||
@@ -172,6 +172,6 @@ class TestGlobalSearch(unittest.TestCase): | |||||
field_as_text = '' | field_as_text = '' | ||||
for field in doc.meta.fields: | for field in doc.meta.fields: | ||||
if field.fieldname == 'description': | 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) | self.assertEquals(case["result"], field_as_text) |
@@ -6,6 +6,7 @@ from __future__ import unicode_literals | |||||
import frappe | import frappe | ||||
import re | import re | ||||
from frappe.utils import cint, strip_html_tags | from frappe.utils import cint, strip_html_tags | ||||
from frappe.model.base_document import get_controller | |||||
def setup_global_search_table(): | def setup_global_search_table(): | ||||
'''Creates __global_seach table''' | '''Creates __global_seach table''' | ||||
@@ -27,11 +28,13 @@ def reset(): | |||||
'''Deletes all data in __global_search''' | '''Deletes all data in __global_search''' | ||||
frappe.db.sql('delete from __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''' | '''Return doctypes with global search fields''' | ||||
def _get(): | def _get(): | ||||
global_search_doctypes = [] | 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) | meta = frappe.get_meta(d.name) | ||||
if len(meta.get_global_search_fields()) > 0: | if len(meta.get_global_search_fields()) > 0: | ||||
global_search_doctypes.append(d) | 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) | 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): | def update_global_search(doc): | ||||
'''Add values marked with `in_global_search` to | '''Add values marked with `in_global_search` to | ||||
`frappe.flags.update_global_search` from given doc | `frappe.flags.update_global_search` from given doc | ||||
:param doc: Document to be added to global search''' | :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: | if frappe.flags.update_global_search==None: | ||||
frappe.flags.update_global_search = [] | frappe.flags.update_global_search = [] | ||||
content = [] | content = [] | ||||
for field in doc.meta.get_global_search_fields(): | 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: | if content: | ||||
published = 0 | published = 0 | ||||
@@ -83,12 +223,11 @@ def update_global_search(doc): | |||||
dict(doctype=doc.doctype, name=doc.name, content=' ||| '.join(content or ''), | dict(doctype=doc.doctype, name=doc.name, content=' ||| '.join(content or ''), | ||||
published=published, title=doc.get_title(), route=doc.get('route'))) | 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''' | '''Prepare field from raw data''' | ||||
from HTMLParser import HTMLParser | from HTMLParser import HTMLParser | ||||
value = doc.get(field.fieldname) | |||||
if(getattr(field, 'fieldtype', None) in ["Text", "Text Editor"]): | if(getattr(field, 'fieldtype', None) in ["Text", "Text Editor"]): | ||||
h = HTMLParser() | h = HTMLParser() | ||||
value = h.unescape(value) | value = h.unescape(value) | ||||
@@ -111,23 +250,6 @@ def sync_global_search(): | |||||
frappe.flags.update_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): | def delete_for_document(doc): | ||||
'''Delete the __global_search entry of a document that has | '''Delete the __global_search entry of a document that has | ||||
been deleted | been deleted | ||||
@@ -433,6 +433,15 @@ def column_has_value(data, fieldname): | |||||
trigger_print_script = """ | trigger_print_script = """ | ||||
<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(); | window.print(); | ||||
// close the window after print | // close the window after print | ||||
@@ -293,3 +293,5 @@ function get_conf() { | |||||
return 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"]'); | |||||
}); |