mirror of
https://github.com/frappe/books.git
synced 2024-11-09 23:30:56 +00:00
collapse frappe folder and start desk
This commit is contained in:
parent
f89c6e778e
commit
e3a94dc997
6
app.js
Normal file
6
app.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const server = require('frappe-core/server');
|
||||||
|
|
||||||
|
server.start({
|
||||||
|
backend: 'sqllite',
|
||||||
|
connection_params: {db_path: 'test.db'}
|
||||||
|
});
|
130
backends/rest_client.js
Normal file
130
backends/rest_client.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class RESTClient {
|
||||||
|
constructor({server, protocol='http'}) {
|
||||||
|
this.server = server;
|
||||||
|
this.protocol = protocol;
|
||||||
|
|
||||||
|
this.init_type_map();
|
||||||
|
|
||||||
|
this.json_headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async insert(doctype, doc) {
|
||||||
|
doc.doctype = doctype;
|
||||||
|
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}`);
|
||||||
|
let response = await frappe.fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.json_headers,
|
||||||
|
body: JSON.stringify(doc)
|
||||||
|
});
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(doctype, name) {
|
||||||
|
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${name}`);
|
||||||
|
let response = await frappe.fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.json_headers
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_all({doctype, fields, filters, start, limit, sort_by, order}) {
|
||||||
|
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}`);
|
||||||
|
|
||||||
|
url = url + "?" + this.get_query_string({
|
||||||
|
fields: JSON.stringify(fields),
|
||||||
|
filters: JSON.stringify(filters),
|
||||||
|
start: start,
|
||||||
|
limit: limit,
|
||||||
|
sort_by: sort_by,
|
||||||
|
order: order
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = await frappe.fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.json_headers
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(doctype, doc) {
|
||||||
|
doc.doctype = doctype;
|
||||||
|
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${doc.name}`);
|
||||||
|
let response = await frappe.fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: this.json_headers,
|
||||||
|
body: JSON.stringify(doc)
|
||||||
|
});
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(doctype, name) {
|
||||||
|
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${name}`);
|
||||||
|
|
||||||
|
let response = await frappe.fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: this.json_headers
|
||||||
|
});
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
get_query_string(params) {
|
||||||
|
return Object.keys(params)
|
||||||
|
.map(k => params[k] != null ? encodeURIComponent(k) + '=' + encodeURIComponent(params[k]) : null)
|
||||||
|
.filter(v => v)
|
||||||
|
.join('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
init_type_map() {
|
||||||
|
this.type_map = {
|
||||||
|
'Currency': true
|
||||||
|
,'Int': true
|
||||||
|
,'Float': true
|
||||||
|
,'Percent': true
|
||||||
|
,'Check': true
|
||||||
|
,'Small Text': true
|
||||||
|
,'Long Text': true
|
||||||
|
,'Code': true
|
||||||
|
,'Text Editor': true
|
||||||
|
,'Date': true
|
||||||
|
,'Datetime': true
|
||||||
|
,'Time': true
|
||||||
|
,'Text': true
|
||||||
|
,'Data': true
|
||||||
|
,'Link': true
|
||||||
|
,'Dynamic Link':true
|
||||||
|
,'Password': true
|
||||||
|
,'Select': true
|
||||||
|
,'Read Only': true
|
||||||
|
,'Attach': true
|
||||||
|
,'Attach Image':true
|
||||||
|
,'Signature': true
|
||||||
|
,'Color': true
|
||||||
|
,'Barcode': true
|
||||||
|
,'Geolocation': true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Database: RESTClient
|
||||||
|
}
|
258
backends/sqlite.js
Normal file
258
backends/sqlite.js
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
|
||||||
|
class sqliteDatabase {
|
||||||
|
constructor({db_path}) {
|
||||||
|
this.db_path = db_path;
|
||||||
|
this.init_type_map();
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(db_path) {
|
||||||
|
if (db_path) {
|
||||||
|
this.db_path = db_path;
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.conn = new sqlite3.Database(this.db_path, () => {
|
||||||
|
// debug
|
||||||
|
// this.conn.on('trace', (trace) => console.log(trace));
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrate() {
|
||||||
|
for (let doctype in frappe.models.data.doctype) {
|
||||||
|
if (await this.table_exists(doctype)) {
|
||||||
|
await this.alter_table(doctype);
|
||||||
|
} else {
|
||||||
|
await this.create_table(doctype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async create_table(doctype) {
|
||||||
|
let meta = frappe.get_meta(doctype);
|
||||||
|
let columns = [];
|
||||||
|
let values = [];
|
||||||
|
|
||||||
|
for (let df of this.get_fields(meta)) {
|
||||||
|
if (this.type_map[df.fieldtype]) {
|
||||||
|
columns.push(this.get_column_definition(df));
|
||||||
|
if (df.default) {
|
||||||
|
values.push(df.default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `CREATE TABLE IF NOT EXISTS ${frappe.slug(doctype)} (
|
||||||
|
${columns.join(", ")})`;
|
||||||
|
|
||||||
|
return await this.run(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.conn.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
get_column_definition(df) {
|
||||||
|
return `${df.fieldname} ${this.type_map[df.fieldtype]} ${df.reqd ? "not null" : ""} ${df.default ? "default ?" : ""}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async alter_table(doctype) {
|
||||||
|
// get columns
|
||||||
|
let table_columns = (await this.sql(`PRAGMA table_info(${doctype})`)).map(d => d.name);
|
||||||
|
let meta = frappe.get_meta(doctype);
|
||||||
|
let values = [];
|
||||||
|
|
||||||
|
for (let df of this.get_fields(meta)) {
|
||||||
|
if (!table_columns.includes(df.fieldname) && this.type_map[df.fieldtype]) {
|
||||||
|
values = []
|
||||||
|
if (df.default) {
|
||||||
|
values.push(df.default);
|
||||||
|
}
|
||||||
|
await this.run(`ALTER TABLE ${frappe.slug(doctype)} ADD COLUMN ${this.get_column_definition(df)}`, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(doctype, name, fields='*') {
|
||||||
|
if (fields instanceof Array) {
|
||||||
|
fields = fields.join(", ");
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.conn.get(`select ${fields} from ${frappe.slug(doctype)}
|
||||||
|
where name = ?`, name,
|
||||||
|
(err, row) => {
|
||||||
|
resolve(row || {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async insert(doctype, doc) {
|
||||||
|
let placeholders = Object.keys(doc).map(d => '?').join(', ');
|
||||||
|
return await this.run(`insert into ${frappe.slug(doctype)}
|
||||||
|
(${Object.keys(doc).join(", ")})
|
||||||
|
values (${placeholders})`, this.get_formatted_values(doc));
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(doctype, doc) {
|
||||||
|
let assigns = Object.keys(doc).map(key => `${key} = ?`);
|
||||||
|
let values = this.get_formatted_values(doc);
|
||||||
|
values.push(doc.name);
|
||||||
|
|
||||||
|
return await this.run(`update ${frappe.slug(doctype)}
|
||||||
|
set ${assigns.join(", ")} where name=?`, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
get_formatted_values(doc) {
|
||||||
|
return Object.values(doc).map(value => {
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return value.toISOString();
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(doctype, name) {
|
||||||
|
return await this.run(`delete from ${frappe.slug(doctype)} where name=?`, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
get_all({doctype, fields=['name'], filters, start, limit, order_by='modified', order='desc'} = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let conditions = this.get_filter_conditions(filters);
|
||||||
|
|
||||||
|
this.conn.all(`select ${fields.join(", ")}
|
||||||
|
from ${frappe.slug(doctype)}
|
||||||
|
${conditions.conditions ? "where" : ""} ${conditions.conditions}
|
||||||
|
${order_by ? ("order by " + order_by) : ""} ${order_by ? (order || "asc") : ""}
|
||||||
|
${limit ? ("limit " + limit) : ""} ${start ? ("offset " + start) : ""}`, conditions.values,
|
||||||
|
(err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(rows);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get_filter_conditions(filters) {
|
||||||
|
// {"status": "Open"} => `status = "Open"`
|
||||||
|
// {"status": "Open", "name": ["like", "apple%"]}
|
||||||
|
// => `status="Open" and name like "apple%"
|
||||||
|
let conditions = [];
|
||||||
|
let values = [];
|
||||||
|
for (let key in filters) {
|
||||||
|
const value = filters[key];
|
||||||
|
if (value instanceof Array) {
|
||||||
|
conditions.push(`${key} ${value[0]} ?`);
|
||||||
|
values.push(value[1]);
|
||||||
|
} else {
|
||||||
|
conditions.push(`${key} = ?`);
|
||||||
|
values.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
conditions: conditions.length ? conditions.join(" and ") : "",
|
||||||
|
values: values
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
run(query, params) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.conn.run(query, params, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sql(query, params) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.conn.all(query, params, (err, rows) => {
|
||||||
|
resolve(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async commit() {
|
||||||
|
try {
|
||||||
|
await this.run('commit');
|
||||||
|
} catch (e) {
|
||||||
|
if (e.errno !== 1) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_value(doctype, filters, fieldname='name') {
|
||||||
|
if (typeof filters==='string') {
|
||||||
|
filters = {name: filters};
|
||||||
|
}
|
||||||
|
|
||||||
|
let row = await this.get_all({
|
||||||
|
doctype:doctype,
|
||||||
|
fields: [fieldname],
|
||||||
|
filters: filters,
|
||||||
|
start: 0,
|
||||||
|
limit: 1});
|
||||||
|
return row.length ? row[0][fieldname] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_fields(meta) {
|
||||||
|
// add standard fields
|
||||||
|
let fields = frappe.model.standard_fields.slice();
|
||||||
|
if (meta.istable) {
|
||||||
|
fields = fields.concat(frappe.model.child_fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add model fields
|
||||||
|
fields = fields.concat(meta.fields);
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
async table_exists(table) {
|
||||||
|
const name = await this.sql(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`);
|
||||||
|
return (name && name.length) ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
init_type_map() {
|
||||||
|
this.type_map = {
|
||||||
|
'Currency': 'real'
|
||||||
|
,'Int': 'integer'
|
||||||
|
,'Float': 'real'
|
||||||
|
,'Percent': 'real'
|
||||||
|
,'Check': 'integer'
|
||||||
|
,'Small Text': 'text'
|
||||||
|
,'Long Text': 'text'
|
||||||
|
,'Code': 'text'
|
||||||
|
,'Text Editor': 'text'
|
||||||
|
,'Date': 'text'
|
||||||
|
,'Datetime': 'text'
|
||||||
|
,'Time': 'text'
|
||||||
|
,'Text': 'text'
|
||||||
|
,'Data': 'text'
|
||||||
|
,'Link': 'text'
|
||||||
|
,'Dynamic Link':'text'
|
||||||
|
,'Password': 'text'
|
||||||
|
,'Select': 'text'
|
||||||
|
,'Read Only': 'text'
|
||||||
|
,'Attach': 'text'
|
||||||
|
,'Attach Image':'text'
|
||||||
|
,'Signature': 'text'
|
||||||
|
,'Color': 'text'
|
||||||
|
,'Barcode': 'text'
|
||||||
|
,'Geolocation': 'text'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Database: sqliteDatabase };
|
46
client/desk/index.js
Normal file
46
client/desk/index.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
const Search = require('./search');
|
||||||
|
const Router = require('./router');
|
||||||
|
|
||||||
|
module.exports = class Desk {
|
||||||
|
constructor() {
|
||||||
|
frappe.router = new Router();
|
||||||
|
|
||||||
|
this.wrapper = document.querySelector('.desk');
|
||||||
|
|
||||||
|
this.nav = frappe.ui.add('header', 'nav text-center', this.wrapper);
|
||||||
|
|
||||||
|
this.body = frappe.ui.add('div', 'desk-body two-column', this.wrapper);
|
||||||
|
this.sidebar = frappe.ui.add('div', 'sidebar', this.body);
|
||||||
|
this.main = frappe.ui.add('div', 'main', this.body);
|
||||||
|
|
||||||
|
this.sidebar_items = [];
|
||||||
|
this.list_pages = {};
|
||||||
|
this.edit_pages = {};
|
||||||
|
|
||||||
|
// this.search = new Search(this.nav);
|
||||||
|
}
|
||||||
|
|
||||||
|
init_routes() {
|
||||||
|
frappe.router.on('list/:doctype', (params) => {
|
||||||
|
|
||||||
|
})
|
||||||
|
frappe.router.on('edit/:doctype/:name', (params) => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
add_sidebar_item(label, action) {
|
||||||
|
let item = frappe.ui.add('a', '', frappe.ui.add('p', null, frappe.desk.sidebar));
|
||||||
|
item.textContent = label;
|
||||||
|
if (typeof action === 'string') {
|
||||||
|
item.href = action;
|
||||||
|
} else {
|
||||||
|
item.addEventHandler('click', () => {
|
||||||
|
action();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
82
client/desk/router.js
Normal file
82
client/desk/router.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
|
||||||
|
module.exports = class Router {
|
||||||
|
constructor() {
|
||||||
|
this.current_page = null;
|
||||||
|
this.static_routes = {};
|
||||||
|
this.dynamic_routes = {};
|
||||||
|
this.listen();
|
||||||
|
}
|
||||||
|
|
||||||
|
add(route, handler) {
|
||||||
|
let page = {handler: handler};
|
||||||
|
|
||||||
|
// '/todo/:name/:place'.match(/:([^/]+)/g);
|
||||||
|
page.param_keys = route.match(/:([^/]+)/g);
|
||||||
|
|
||||||
|
if (page.param_keys) {
|
||||||
|
// make expression
|
||||||
|
// '/todo/:name/:place'.replace(/\/:([a-z1-9]+)/g, "\/([a-z0-9]+)");
|
||||||
|
page.expression = route.replace(/\/:([a-z1-9]+)/g, "\/([a-z0-9]+)");
|
||||||
|
this.dynamic_routes[route] = page;
|
||||||
|
} else {
|
||||||
|
this.static_routes[route] = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
listen() {
|
||||||
|
window.onhashchange = this.changed.bind(this);
|
||||||
|
this.changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
async changed(event) {
|
||||||
|
if (window.location.hash.length > 0) {
|
||||||
|
const page_name = window.location.hash.substr(1);
|
||||||
|
this.show(page_name);
|
||||||
|
} else if (this.static_routes['default']) {
|
||||||
|
this.show('default');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show(route) {
|
||||||
|
if (!route) {
|
||||||
|
route = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route[0]==='#') {
|
||||||
|
route = route.substr(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let page = this.match(route);
|
||||||
|
|
||||||
|
if (page) {
|
||||||
|
if (typeof page.handler==='function') {
|
||||||
|
page.handler(page.params);
|
||||||
|
} else {
|
||||||
|
page.handler.show(page.params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match(route) {
|
||||||
|
for(let key in this.static_routes) {
|
||||||
|
if (key === route) {
|
||||||
|
return {handler: this.static_routes[key].handler};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let key in this.dynamic_routes) {
|
||||||
|
let page = this.dynamic_routes[key];
|
||||||
|
let matches = route.match(new RegExp(page.expression));
|
||||||
|
|
||||||
|
if (matches && matches.length == page.param_keys.length + 1) {
|
||||||
|
let params = {}
|
||||||
|
for (let i=0; i < page.param_keys.length; i++) {
|
||||||
|
params[page.param_keys[i].substr(1)] = matches[i + 1];
|
||||||
|
}
|
||||||
|
return {handler:page.handler, params: params};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
client/desk/search.js
Normal file
16
client/desk/search.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
|
||||||
|
module.exports = class Search {
|
||||||
|
constructor(parent) {
|
||||||
|
this.input = frappe.ui.add('input', 'form-control nav-search', parent);
|
||||||
|
this.input.addEventListener('keypress', function(event) {
|
||||||
|
if (event.keyCode===13) {
|
||||||
|
let list = frappe.router.current_page.list;
|
||||||
|
if (list) {
|
||||||
|
list.search_text = this.value;
|
||||||
|
list.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
20
client/index.js
Normal file
20
client/index.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const common = require('frappe-core/common');
|
||||||
|
const Database = require('frappe-core/backends/rest_client').Database;
|
||||||
|
const frappe = require('frappe-core');
|
||||||
|
frappe.ui = require('./ui');
|
||||||
|
const Desk = require('./desk');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async start({server, container}) {
|
||||||
|
window.frappe = frappe;
|
||||||
|
frappe.init();
|
||||||
|
common.init_libs(frappe);
|
||||||
|
|
||||||
|
frappe.fetch = window.fetch.bind();
|
||||||
|
frappe.db = await new Database({server: server});
|
||||||
|
|
||||||
|
frappe.desk = new Desk();
|
||||||
|
await frappe.login();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
89
client/ui/dropdown.js
Normal file
89
client/ui/dropdown.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
|
||||||
|
class Dropdown {
|
||||||
|
constructor({parent, label, btn_class = 'btn-secondary', items = []}) {
|
||||||
|
Object.assign(this, arguments[0]);
|
||||||
|
|
||||||
|
this.dropdown_items = [];
|
||||||
|
this.setup_background_click();
|
||||||
|
this.make();
|
||||||
|
|
||||||
|
// init items
|
||||||
|
if (this.items) {
|
||||||
|
for (item of this.items) {
|
||||||
|
this.add_item(item.label, item.action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_background_click() {
|
||||||
|
if (!document.dropdown_setup) {
|
||||||
|
frappe.dropdowns = [];
|
||||||
|
// setup hiding all dropdowns on click
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
for (let d of frappe.dropdowns) {
|
||||||
|
if (d.button !== event.target) {
|
||||||
|
d.collapse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.dropdown_setup = true;
|
||||||
|
}
|
||||||
|
frappe.dropdowns.push(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
make() {
|
||||||
|
this.dropdown = frappe.ui.add('div', 'dropdown', this.parent);
|
||||||
|
this.make_button();
|
||||||
|
this.dropdown_menu = frappe.ui.add('div', 'dropdown-menu', this.dropdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
make_button() {
|
||||||
|
this.button = frappe.ui.add('button', 'btn ' + this.btn_class,
|
||||||
|
this.dropdown);
|
||||||
|
frappe.ui.add_class(this.button, 'dropdown-toggle');
|
||||||
|
this.button.textContent = this.label;
|
||||||
|
this.button.addEventListener('click', () => {
|
||||||
|
this.toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expand() {
|
||||||
|
this.dropdown.classList.add('show');
|
||||||
|
this.dropdown_menu.classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
collapse() {
|
||||||
|
this.dropdown.classList.remove('show');
|
||||||
|
this.dropdown_menu.classList.remove('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.dropdown.classList.toggle('show');
|
||||||
|
this.dropdown_menu.classList.toggle('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
add_item(label, action) {
|
||||||
|
let item = frappe.ui.add('a', 'dropdown-item', this.dropdown_menu);
|
||||||
|
item.textContent = label;
|
||||||
|
if (typeof action === 'string') {
|
||||||
|
item.src = action;
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
this.toggle();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
item.addEventListener('click', async () => {
|
||||||
|
await action();
|
||||||
|
this.toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.dropdown_items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
float_right() {
|
||||||
|
frappe.ui.add_class(this.dropdown, 'float-right');
|
||||||
|
frappe.ui.add_class(this.dropdown_menu, 'dropdown-menu-right');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Dropdown;
|
46
client/ui/index.js
Normal file
46
client/ui/index.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
const Dropdown = require('./dropdown');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
add(tag, className, parent) {
|
||||||
|
let element = document.createElement(tag);
|
||||||
|
if (className) {
|
||||||
|
for (let c of className.split(' ')) {
|
||||||
|
this.add_class(element, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parent) {
|
||||||
|
parent.appendChild(element);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(element) {
|
||||||
|
element.parentNode.removeChild(element);
|
||||||
|
},
|
||||||
|
|
||||||
|
add_class(element, className) {
|
||||||
|
if (element.classList) {
|
||||||
|
element.classList.add(className);
|
||||||
|
} else {
|
||||||
|
element.className += " " + className;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
remove_class(element, className) {
|
||||||
|
if (element.classList) {
|
||||||
|
element.classList.remove(className);
|
||||||
|
} else {
|
||||||
|
element.className = element.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle(element, default_display = '') {
|
||||||
|
element.style.display = element.style.display === 'none' ? default_display : 'none';
|
||||||
|
},
|
||||||
|
|
||||||
|
make_dropdown(label, parent, btn_class = 'btn-secondary') {
|
||||||
|
return new Dropdown({parent: parent, label:label, btn_class:btn_class});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
101
client/view/controls/base.js
Normal file
101
client/view/controls/base.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
|
||||||
|
class BaseControl {
|
||||||
|
constructor(docfield, parent) {
|
||||||
|
Object.assign(this, docfield);
|
||||||
|
if (!this.fieldname) {
|
||||||
|
this.fieldname = frappe.slug(this.label);
|
||||||
|
}
|
||||||
|
this.parent = parent;
|
||||||
|
if (this.setup) {
|
||||||
|
this.setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bind(doc) {
|
||||||
|
this.doc = doc;
|
||||||
|
|
||||||
|
this.doc.add_handler(this.fieldname, () => {
|
||||||
|
this.set_doc_value();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.set_doc_value();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.make();
|
||||||
|
this.set_doc_value();
|
||||||
|
}
|
||||||
|
|
||||||
|
set_doc_value() {
|
||||||
|
if (this.doc) {
|
||||||
|
this.set_input_value(this.doc.get(this.fieldname));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
make() {
|
||||||
|
if (!this.form_group) {
|
||||||
|
this.make_form_group();
|
||||||
|
this.make_label();
|
||||||
|
this.make_input();
|
||||||
|
this.set_input_name();
|
||||||
|
this.make_description();
|
||||||
|
this.bind_change_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
make_form_group() {
|
||||||
|
this.form_group = frappe.ui.add('div', 'form-group', this.parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
make_label() {
|
||||||
|
this.label_element = frappe.ui.add('label', null, this.form_group);
|
||||||
|
this.label_element.textContent = this.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
make_input() {
|
||||||
|
this.input = frappe.ui.add('input', 'form-control', this.form_group);
|
||||||
|
}
|
||||||
|
|
||||||
|
set_input_name() {
|
||||||
|
this.input.setAttribute('name', this.fieldname);
|
||||||
|
}
|
||||||
|
|
||||||
|
make_description() {
|
||||||
|
if (this.description) {
|
||||||
|
this.description_element = frappe.ui.add('small', 'form-text text-muted', this.form_group);
|
||||||
|
this.description_element.textContent = this.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_input_value(value) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
value = '';
|
||||||
|
}
|
||||||
|
this.input.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_input_value() {
|
||||||
|
return await this.parse(this.input.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parse(value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bind_change_event() {
|
||||||
|
this.input.addEventListener('change', (e) => this.handle_change(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle_change(e) {
|
||||||
|
let value = await this.get_input_value();
|
||||||
|
value = await this.validate(value);
|
||||||
|
await this.doc.set(this.fieldname, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BaseControl;
|
18
client/view/controls/index.js
Normal file
18
client/view/controls/index.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const control_classes = {
|
||||||
|
Data: require('./data'),
|
||||||
|
Text: require('./text'),
|
||||||
|
Select: require('./select')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
get_control_class(fieldtype) {
|
||||||
|
return control_classes[fieldtype];
|
||||||
|
},
|
||||||
|
make_control(field, parent) {
|
||||||
|
const control_class = this.get_control_class(field.fieldtype);
|
||||||
|
let control = new control_class(field, parent);
|
||||||
|
control.make();
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
}
|
24
client/view/controls/select.js
Normal file
24
client/view/controls/select.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
const BaseControl = require('./base');
|
||||||
|
|
||||||
|
class SelectControl extends BaseControl {
|
||||||
|
make_input() {
|
||||||
|
this.input = frappe.ui.add('select', 'form-control', this.form_group);
|
||||||
|
|
||||||
|
let options = this.options;
|
||||||
|
if (typeof options==='string') {
|
||||||
|
options = options.split('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let value of options) {
|
||||||
|
let option = frappe.ui.add('option', null, this.input);
|
||||||
|
option.textContent = value;
|
||||||
|
option.setAttribute('value', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
make() {
|
||||||
|
super.make();
|
||||||
|
this.input.setAttribute('row', '3');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = SelectControl;
|
13
client/view/controls/text.js
Normal file
13
client/view/controls/text.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const BaseControl = require('./base');
|
||||||
|
|
||||||
|
class TextControl extends BaseControl {
|
||||||
|
make_input() {
|
||||||
|
this.input = frappe.ui.add('textarea', 'form-control', this.form_group);
|
||||||
|
}
|
||||||
|
make() {
|
||||||
|
super.make();
|
||||||
|
this.input.setAttribute('rows', '8');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = TextControl;
|
108
client/view/form.js
Normal file
108
client/view/form.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
const controls = require('./controls');
|
||||||
|
|
||||||
|
class Form {
|
||||||
|
constructor({doctype, parent, submit_label='Submit'}) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.doctype = doctype;
|
||||||
|
this.submit_label = submit_label;
|
||||||
|
|
||||||
|
this.controls = {};
|
||||||
|
this.controls_list = [];
|
||||||
|
|
||||||
|
this.meta = frappe.get_meta(this.doctype);
|
||||||
|
this.make();
|
||||||
|
}
|
||||||
|
|
||||||
|
make() {
|
||||||
|
if (this.body || !this.parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.body = frappe.ui.add('div', 'form-body', this.parent);
|
||||||
|
this.make_actions();
|
||||||
|
|
||||||
|
this.form = frappe.ui.add('form', null, this.body);
|
||||||
|
for(let df of this.meta.fields) {
|
||||||
|
if (controls.get_control_class(df.fieldtype)) {
|
||||||
|
let control = controls.make_control(df, this.form);
|
||||||
|
this.controls_list.push(control);
|
||||||
|
this.controls[df.fieldname] = control;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.make_submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
make_actions() {
|
||||||
|
this.toolbar = frappe.ui.add('div', 'form-toolbar', this.body);
|
||||||
|
this.actions = frappe.ui.make_dropdown('Actions', this.toolbar);
|
||||||
|
|
||||||
|
// delete
|
||||||
|
this.actions.add_item('Delete', async () => {
|
||||||
|
await this.doc.delete();
|
||||||
|
this.show_alert('Deleted', 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.actions.float_right();
|
||||||
|
}
|
||||||
|
|
||||||
|
make_submit() {
|
||||||
|
this.submit_btn = frappe.ui.add('button', 'btn btn-outline-primary',
|
||||||
|
this.body);
|
||||||
|
this.submit_btn.setAttribute('type', 'submit');
|
||||||
|
this.submit_btn.textContent = this.submit_label;
|
||||||
|
this.submit_btn.addEventListener('click', (event) => {
|
||||||
|
this.submit();
|
||||||
|
event.preventDefault();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
show_alert(message, type) {
|
||||||
|
this.clear_alert();
|
||||||
|
this.alert = frappe.ui.add('div', `alert alert-${type}`, this.body);
|
||||||
|
this.alert.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_alert() {
|
||||||
|
if (this.alert) {
|
||||||
|
frappe.ui.remove(this.alert);
|
||||||
|
this.alert = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async use(doc, is_new = false) {
|
||||||
|
if (this.doc) {
|
||||||
|
// clear handlers of outgoing doc
|
||||||
|
this.doc.clear_handlers();
|
||||||
|
}
|
||||||
|
this.clear_alert();
|
||||||
|
this.doc = doc;
|
||||||
|
this.is_new = is_new;
|
||||||
|
for (let control of this.controls_list) {
|
||||||
|
control.bind(this.doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
try {
|
||||||
|
if (this.is_new) {
|
||||||
|
await this.doc.insert();
|
||||||
|
} else {
|
||||||
|
await this.doc.update();
|
||||||
|
}
|
||||||
|
await this.refresh();
|
||||||
|
this.show_alert('Saved', 'success');
|
||||||
|
} catch (e) {
|
||||||
|
this.show_alert('Failed', 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
for(let control of this.controls_list) {
|
||||||
|
control.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {Form: Form};
|
124
client/view/list.js
Normal file
124
client/view/list.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
|
||||||
|
class ListView {
|
||||||
|
constructor({doctype, parent, fields}) {
|
||||||
|
this.doctype = doctype;
|
||||||
|
this.parent = parent;
|
||||||
|
this.fields = fields;
|
||||||
|
|
||||||
|
this.meta = frappe.get_meta(this.doctype);
|
||||||
|
|
||||||
|
this.start = 0;
|
||||||
|
this.page_length = 20;
|
||||||
|
|
||||||
|
this.body = null;
|
||||||
|
this.rows = [];
|
||||||
|
this.data = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
this.make_body();
|
||||||
|
this.set_filters();
|
||||||
|
|
||||||
|
let data = await this.meta.get_list({
|
||||||
|
filters: this.filters,
|
||||||
|
start:this.start,
|
||||||
|
limit:this.page_length + 1
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i=0; i< Math.min(this.page_length, data.length); i++) {
|
||||||
|
this.render_row(this.start + i, data[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.start > 0) {
|
||||||
|
this.data = this.data.concat(data);
|
||||||
|
} else {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clear_empty_rows();
|
||||||
|
this.update_more(data.length > this.page_length);
|
||||||
|
}
|
||||||
|
|
||||||
|
async append() {
|
||||||
|
this.start += this.page_length;
|
||||||
|
await this.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
set_filters() {
|
||||||
|
this.filters = {};
|
||||||
|
if (this.search_input.value) {
|
||||||
|
this.filters.keywords = ['like', '%' + this.search_input.value + '%'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
make_body() {
|
||||||
|
if (!this.body) {
|
||||||
|
this.make_search();
|
||||||
|
this.body = frappe.ui.add('div', 'list-body', this.parent);
|
||||||
|
this.make_more_btn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
make_search() {
|
||||||
|
this.search_input_group = frappe.ui.add('div', 'input-group list-search', this.parent);
|
||||||
|
|
||||||
|
this.search_input = frappe.ui.add('input', 'form-control', this.search_input_group);
|
||||||
|
this.search_input.addEventListener('keypress', (event) => {
|
||||||
|
if (event.keyCode===13) {
|
||||||
|
this.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.search_input_group_append = frappe.ui.add('div', 'input-group-append', this.search_input_group);
|
||||||
|
this.search_button = frappe.ui.add('button', 'btn btn-secondary', this.search_input_group_append);
|
||||||
|
this.search_button.textContent = 'Search';
|
||||||
|
this.search_button.addEventListener('click', (event) => {
|
||||||
|
this.run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
make_more_btn() {
|
||||||
|
this.more_btn = frappe.ui.add('button', 'btn btn-secondary hide', this.parent);
|
||||||
|
this.more_btn.textContent = 'More';
|
||||||
|
this.more_btn.addEventListener('click', () => {
|
||||||
|
this.append();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render_row(i, data) {
|
||||||
|
let row = this.get_row(i);
|
||||||
|
row.innerHTML = this.meta.get_row_html(data);
|
||||||
|
row.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
get_row(i) {
|
||||||
|
if (!this.rows[i]) {
|
||||||
|
this.rows[i] = frappe.ui.add('div', 'list-row', this.body);
|
||||||
|
}
|
||||||
|
return this.rows[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_empty_rows() {
|
||||||
|
if (this.rows.length > this.data.length) {
|
||||||
|
for (let i=this.data.length; i < this.rows.length; i++) {
|
||||||
|
let row = this.get_row(i);
|
||||||
|
row.innerHTML = '';
|
||||||
|
row.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_more(show) {
|
||||||
|
if (show) {
|
||||||
|
this.more_btn.classList.remove('hide');
|
||||||
|
} else {
|
||||||
|
this.more_btn.classList.add('hide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ListView: ListView
|
||||||
|
};
|
45
client/view/page.js
Normal file
45
client/view/page.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
|
||||||
|
class Page {
|
||||||
|
constructor(title) {
|
||||||
|
this.handlers = {};
|
||||||
|
this.title = title;
|
||||||
|
this.make();
|
||||||
|
}
|
||||||
|
|
||||||
|
make() {
|
||||||
|
this.body = frappe.ui.add('div', 'page hide', frappe.desk.main);
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
frappe.ui.add_class(this.body, 'hide');
|
||||||
|
|
||||||
|
this.trigger('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
show(params) {
|
||||||
|
if (frappe.router.current_page) {
|
||||||
|
frappe.router.current_page.hide();
|
||||||
|
}
|
||||||
|
frappe.ui.remove_class(this.body, 'hide');
|
||||||
|
frappe.router.current_page = this;
|
||||||
|
document.title = this.title;
|
||||||
|
|
||||||
|
this.trigger('show', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, fn) {
|
||||||
|
if (!this.handlers[event]) this.handlers.event = [];
|
||||||
|
this.handlers[event].push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger(event, params) {
|
||||||
|
if (this.handlers[event]) {
|
||||||
|
for (let handler of this.handlers[event]) {
|
||||||
|
handler(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Page: Page };
|
@ -1,6 +0,0 @@
|
|||||||
const server = require('frappe-core/frappe/server');
|
|
||||||
|
|
||||||
server.start({
|
|
||||||
backend: 'sqllite',
|
|
||||||
connection_params: {db_path: 'test.db'}
|
|
||||||
});
|
|
@ -1,130 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
class RESTClient {
|
|
||||||
constructor({server, protocol='http'}) {
|
|
||||||
this.server = server;
|
|
||||||
this.protocol = protocol;
|
|
||||||
|
|
||||||
this.init_type_map();
|
|
||||||
|
|
||||||
this.json_headers = {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async insert(doctype, doc) {
|
|
||||||
doc.doctype = doctype;
|
|
||||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}`);
|
|
||||||
let response = await frappe.fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: this.json_headers,
|
|
||||||
body: JSON.stringify(doc)
|
|
||||||
});
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(doctype, name) {
|
|
||||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${name}`);
|
|
||||||
let response = await frappe.fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: this.json_headers
|
|
||||||
});
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async get_all({doctype, fields, filters, start, limit, sort_by, order}) {
|
|
||||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}`);
|
|
||||||
|
|
||||||
url = url + "?" + this.get_query_string({
|
|
||||||
fields: JSON.stringify(fields),
|
|
||||||
filters: JSON.stringify(filters),
|
|
||||||
start: start,
|
|
||||||
limit: limit,
|
|
||||||
sort_by: sort_by,
|
|
||||||
order: order
|
|
||||||
});
|
|
||||||
|
|
||||||
let response = await frappe.fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: this.json_headers
|
|
||||||
});
|
|
||||||
return await response.json();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(doctype, doc) {
|
|
||||||
doc.doctype = doctype;
|
|
||||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${doc.name}`);
|
|
||||||
let response = await frappe.fetch(url, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: this.json_headers,
|
|
||||||
body: JSON.stringify(doc)
|
|
||||||
});
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(doctype, name) {
|
|
||||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${name}`);
|
|
||||||
|
|
||||||
let response = await frappe.fetch(url, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: this.json_headers
|
|
||||||
});
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
get_query_string(params) {
|
|
||||||
return Object.keys(params)
|
|
||||||
.map(k => params[k] != null ? encodeURIComponent(k) + '=' + encodeURIComponent(params[k]) : null)
|
|
||||||
.filter(v => v)
|
|
||||||
.join('&');
|
|
||||||
}
|
|
||||||
|
|
||||||
init_type_map() {
|
|
||||||
this.type_map = {
|
|
||||||
'Currency': true
|
|
||||||
,'Int': true
|
|
||||||
,'Float': true
|
|
||||||
,'Percent': true
|
|
||||||
,'Check': true
|
|
||||||
,'Small Text': true
|
|
||||||
,'Long Text': true
|
|
||||||
,'Code': true
|
|
||||||
,'Text Editor': true
|
|
||||||
,'Date': true
|
|
||||||
,'Datetime': true
|
|
||||||
,'Time': true
|
|
||||||
,'Text': true
|
|
||||||
,'Data': true
|
|
||||||
,'Link': true
|
|
||||||
,'Dynamic Link':true
|
|
||||||
,'Password': true
|
|
||||||
,'Select': true
|
|
||||||
,'Read Only': true
|
|
||||||
,'Attach': true
|
|
||||||
,'Attach Image':true
|
|
||||||
,'Signature': true
|
|
||||||
,'Color': true
|
|
||||||
,'Barcode': true
|
|
||||||
,'Geolocation': true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Database: RESTClient
|
|
||||||
}
|
|
@ -1,243 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
const sqlite3 = require('sqlite3').verbose();
|
|
||||||
|
|
||||||
class sqliteDatabase {
|
|
||||||
constructor({db_path}) {
|
|
||||||
this.db_path = db_path;
|
|
||||||
this.init_type_map();
|
|
||||||
}
|
|
||||||
|
|
||||||
connect(db_path) {
|
|
||||||
if (db_path) {
|
|
||||||
this.db_path = db_path;
|
|
||||||
}
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.conn = new sqlite3.Database(this.db_path, () => {
|
|
||||||
// debug
|
|
||||||
// this.conn.on('trace', (trace) => console.log(trace));
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async migrate() {
|
|
||||||
for (let doctype in frappe.models.data.doctype) {
|
|
||||||
if (await this.table_exists(doctype)) {
|
|
||||||
await this.alter_table(doctype);
|
|
||||||
} else {
|
|
||||||
await this.create_table(doctype);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await this.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
async create_table(doctype) {
|
|
||||||
let meta = frappe.get_meta(doctype);
|
|
||||||
let columns = [];
|
|
||||||
let values = [];
|
|
||||||
|
|
||||||
for (let df of this.get_fields(meta)) {
|
|
||||||
if (this.type_map[df.fieldtype]) {
|
|
||||||
columns.push(`${df.fieldname} ${this.type_map[df.fieldtype]} ${df.reqd ? "not null" : ""} ${df.default ? "default ?" : ""}`);
|
|
||||||
if (df.default) {
|
|
||||||
values.push(df.default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = `CREATE TABLE IF NOT EXISTS ${frappe.slug(doctype)} (
|
|
||||||
${columns.join(", ")})`;
|
|
||||||
|
|
||||||
return await this.run(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.conn.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
async alter_table(doctype) {
|
|
||||||
// add columns
|
|
||||||
|
|
||||||
// change columns
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
get(doctype, name, fields='*') {
|
|
||||||
if (fields instanceof Array) {
|
|
||||||
fields = fields.join(", ");
|
|
||||||
}
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.conn.get(`select ${fields} from ${frappe.slug(doctype)}
|
|
||||||
where name = ?`, name,
|
|
||||||
(err, row) => {
|
|
||||||
resolve(row || {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async insert(doctype, doc) {
|
|
||||||
let placeholders = Object.keys(doc).map(d => '?').join(', ');
|
|
||||||
return await this.run(`insert into ${frappe.slug(doctype)}
|
|
||||||
(${Object.keys(doc).join(", ")})
|
|
||||||
values (${placeholders})`, this.get_formatted_values(doc));
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(doctype, doc) {
|
|
||||||
let assigns = Object.keys(doc).map(key => `${key} = ?`);
|
|
||||||
let values = this.get_formatted_values(doc);
|
|
||||||
values.push(doc.name);
|
|
||||||
|
|
||||||
return await this.run(`update ${frappe.slug(doctype)}
|
|
||||||
set ${assigns.join(", ")} where name=?`, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
get_formatted_values(doc) {
|
|
||||||
return Object.values(doc).map(value => {
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return value.toISOString();
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(doctype, name) {
|
|
||||||
return await this.run(`delete from ${frappe.slug(doctype)} where name=?`, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
get_all({doctype, fields=['name'], filters, start, limit, order_by='modified', order='desc'} = {}) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let conditions = this.get_filter_conditions(filters);
|
|
||||||
|
|
||||||
this.conn.all(`select ${fields.join(", ")}
|
|
||||||
from ${frappe.slug(doctype)}
|
|
||||||
${conditions.conditions ? "where" : ""} ${conditions.conditions}
|
|
||||||
${order_by ? ("order by " + order_by) : ""} ${order_by ? (order || "asc") : ""}
|
|
||||||
${limit ? ("limit " + limit) : ""} ${start ? ("offset " + start) : ""}`, conditions.values,
|
|
||||||
(err, rows) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve(rows);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get_filter_conditions(filters) {
|
|
||||||
// {"status": "Open"} => `status = "Open"`
|
|
||||||
// {"status": "Open", "name": ["like", "apple%"]}
|
|
||||||
// => `status="Open" and name like "apple%"
|
|
||||||
let conditions = [];
|
|
||||||
let values = [];
|
|
||||||
for (let key in filters) {
|
|
||||||
const value = filters[key];
|
|
||||||
if (value instanceof Array) {
|
|
||||||
conditions.push(`${key} ${value[0]} ?`);
|
|
||||||
} else {
|
|
||||||
conditions.push(`${key} = ?`);
|
|
||||||
}
|
|
||||||
values.push(value);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
conditions: conditions.length ? conditions.join(" and ") : "",
|
|
||||||
values: values
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
run(query, params) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.conn.run(query, params, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.log(err);
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sql(query, params) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.conn.all(query, params, (err, rows) => {
|
|
||||||
resolve(rows);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async commit() {
|
|
||||||
try {
|
|
||||||
await this.run('commit');
|
|
||||||
} catch (e) {
|
|
||||||
if (e.errno !== 1) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async get_value(doctype, filters, fieldname='name') {
|
|
||||||
if (typeof filters==='string') {
|
|
||||||
filters = {name: filters};
|
|
||||||
}
|
|
||||||
|
|
||||||
let row = await this.get_all({
|
|
||||||
doctype:doctype,
|
|
||||||
fields: [fieldname],
|
|
||||||
filters: filters,
|
|
||||||
start: 0,
|
|
||||||
limit: 1});
|
|
||||||
return row.length ? row[0][fieldname] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get_fields(meta) {
|
|
||||||
// add standard fields
|
|
||||||
let fields = frappe.model.standard_fields.slice();
|
|
||||||
if (meta.istable) {
|
|
||||||
fields = fields.concat(frappe.model.child_fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add model fields
|
|
||||||
fields = fields.concat(meta.fields);
|
|
||||||
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
async table_exists(table) {
|
|
||||||
const name = await this.sql(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`);
|
|
||||||
return (name && name.length) ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
init_type_map() {
|
|
||||||
this.type_map = {
|
|
||||||
'Currency': 'real'
|
|
||||||
,'Int': 'integer'
|
|
||||||
,'Float': 'real'
|
|
||||||
,'Percent': 'real'
|
|
||||||
,'Check': 'integer'
|
|
||||||
,'Small Text': 'text'
|
|
||||||
,'Long Text': 'text'
|
|
||||||
,'Code': 'text'
|
|
||||||
,'Text Editor': 'text'
|
|
||||||
,'Date': 'text'
|
|
||||||
,'Datetime': 'text'
|
|
||||||
,'Time': 'text'
|
|
||||||
,'Text': 'text'
|
|
||||||
,'Data': 'text'
|
|
||||||
,'Link': 'text'
|
|
||||||
,'Dynamic Link':'text'
|
|
||||||
,'Password': 'text'
|
|
||||||
,'Select': 'text'
|
|
||||||
,'Read Only': 'text'
|
|
||||||
,'Attach': 'text'
|
|
||||||
,'Attach Image':'text'
|
|
||||||
,'Signature': 'text'
|
|
||||||
,'Color': 'text'
|
|
||||||
,'Barcode': 'text'
|
|
||||||
,'Geolocation': 'text'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { Database: sqliteDatabase };
|
|
@ -1,22 +0,0 @@
|
|||||||
const common = require('frappe-core/frappe/common');
|
|
||||||
const Database = require('frappe-core/frappe/backends/rest_client').Database;
|
|
||||||
const frappe = require('frappe-core');
|
|
||||||
frappe.ui = require('./ui');
|
|
||||||
frappe.view = require('./view');
|
|
||||||
const Router = require('./view/router').Router;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async start({server, container}) {
|
|
||||||
window.frappe = frappe;
|
|
||||||
frappe.init();
|
|
||||||
common.init_libs(frappe);
|
|
||||||
|
|
||||||
frappe.fetch = window.fetch.bind();
|
|
||||||
frappe.db = await new Database({server: server});
|
|
||||||
|
|
||||||
frappe.view.init({container: container});
|
|
||||||
frappe.router = new Router();
|
|
||||||
await frappe.login();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
|
|
||||||
class Dropdown {
|
|
||||||
constructor({parent, label, btn_class = 'btn-secondary', items = []}) {
|
|
||||||
Object.assign(this, arguments[0]);
|
|
||||||
|
|
||||||
this.dropdown_items = [];
|
|
||||||
this.setup_background_click();
|
|
||||||
this.make();
|
|
||||||
|
|
||||||
// init items
|
|
||||||
if (this.items) {
|
|
||||||
for (item of this.items) {
|
|
||||||
this.add_item(item.label, item.action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_background_click() {
|
|
||||||
if (!document.dropdown_setup) {
|
|
||||||
frappe.dropdowns = [];
|
|
||||||
// setup hiding all dropdowns on click
|
|
||||||
document.addEventListener('click', (event) => {
|
|
||||||
for (let d of frappe.dropdowns) {
|
|
||||||
if (d.button !== event.target) {
|
|
||||||
d.collapse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.dropdown_setup = true;
|
|
||||||
}
|
|
||||||
frappe.dropdowns.push(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
make() {
|
|
||||||
this.dropdown = frappe.ui.add('div', 'dropdown', this.parent);
|
|
||||||
this.make_button();
|
|
||||||
this.dropdown_menu = frappe.ui.add('div', 'dropdown-menu', this.dropdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
make_button() {
|
|
||||||
this.button = frappe.ui.add('button', 'btn ' + this.btn_class,
|
|
||||||
this.dropdown);
|
|
||||||
frappe.ui.add_class(this.button, 'dropdown-toggle');
|
|
||||||
this.button.textContent = this.label;
|
|
||||||
this.button.addEventListener('click', () => {
|
|
||||||
this.toggle();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
expand() {
|
|
||||||
this.dropdown.classList.add('show');
|
|
||||||
this.dropdown_menu.classList.add('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
collapse() {
|
|
||||||
this.dropdown.classList.remove('show');
|
|
||||||
this.dropdown_menu.classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle() {
|
|
||||||
this.dropdown.classList.toggle('show');
|
|
||||||
this.dropdown_menu.classList.toggle('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
add_item(label, action) {
|
|
||||||
let item = frappe.ui.add('a', 'dropdown-item', this.dropdown_menu);
|
|
||||||
item.textContent = label;
|
|
||||||
if (typeof action === 'string') {
|
|
||||||
item.src = action;
|
|
||||||
item.addEventListener('click', () => {
|
|
||||||
this.toggle();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
item.addEventListener('click', async () => {
|
|
||||||
await action();
|
|
||||||
this.toggle();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.dropdown_items.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
float_right() {
|
|
||||||
frappe.ui.add_class(this.dropdown, 'float-right');
|
|
||||||
frappe.ui.add_class(this.dropdown_menu, 'dropdown-menu-right');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Dropdown;
|
|
@ -1,46 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
const Dropdown = require('./dropdown');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
add(tag, className, parent) {
|
|
||||||
let element = document.createElement(tag);
|
|
||||||
if (className) {
|
|
||||||
for (let c of className.split(' ')) {
|
|
||||||
this.add_class(element, c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (parent) {
|
|
||||||
parent.appendChild(element);
|
|
||||||
}
|
|
||||||
return element;
|
|
||||||
},
|
|
||||||
|
|
||||||
remove(element) {
|
|
||||||
element.parentNode.removeChild(element);
|
|
||||||
},
|
|
||||||
|
|
||||||
add_class(element, className) {
|
|
||||||
if (element.classList) {
|
|
||||||
element.classList.add(className);
|
|
||||||
} else {
|
|
||||||
element.className += " " + className;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
remove_class(element, className) {
|
|
||||||
if (element.classList) {
|
|
||||||
element.classList.remove(className);
|
|
||||||
} else {
|
|
||||||
element.className = element.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
toggle(element, default_display = '') {
|
|
||||||
element.style.display = element.style.display === 'none' ? default_display : 'none';
|
|
||||||
},
|
|
||||||
|
|
||||||
make_dropdown(label, parent, btn_class = 'btn-secondary') {
|
|
||||||
return new Dropdown({parent: parent, label:label, btn_class:btn_class});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
|
|
||||||
class BaseControl {
|
|
||||||
constructor(docfield, parent) {
|
|
||||||
Object.assign(this, docfield);
|
|
||||||
if (!this.fieldname) {
|
|
||||||
this.fieldname = frappe.slug(this.label);
|
|
||||||
}
|
|
||||||
this.parent = parent;
|
|
||||||
if (this.setup) {
|
|
||||||
this.setup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bind(doc) {
|
|
||||||
this.doc = doc;
|
|
||||||
|
|
||||||
this.doc.add_handler(this.fieldname, () => {
|
|
||||||
this.set_doc_value();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.set_doc_value();
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
this.make();
|
|
||||||
this.set_doc_value();
|
|
||||||
}
|
|
||||||
|
|
||||||
set_doc_value() {
|
|
||||||
if (this.doc) {
|
|
||||||
this.set_input_value(this.doc.get(this.fieldname));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
make() {
|
|
||||||
if (!this.form_group) {
|
|
||||||
this.make_form_group();
|
|
||||||
this.make_label();
|
|
||||||
this.make_input();
|
|
||||||
this.set_input_name();
|
|
||||||
this.make_description();
|
|
||||||
this.bind_change_event();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
make_form_group() {
|
|
||||||
this.form_group = frappe.ui.add('div', 'form-group', this.parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
make_label() {
|
|
||||||
this.label_element = frappe.ui.add('label', null, this.form_group);
|
|
||||||
this.label_element.textContent = this.label;
|
|
||||||
}
|
|
||||||
|
|
||||||
make_input() {
|
|
||||||
this.input = frappe.ui.add('input', 'form-control', this.form_group);
|
|
||||||
}
|
|
||||||
|
|
||||||
set_input_name() {
|
|
||||||
this.input.setAttribute('name', this.fieldname);
|
|
||||||
}
|
|
||||||
|
|
||||||
make_description() {
|
|
||||||
if (this.description) {
|
|
||||||
this.description_element = frappe.ui.add('small', 'form-text text-muted', this.form_group);
|
|
||||||
this.description_element.textContent = this.description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set_input_value(value) {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
value = '';
|
|
||||||
}
|
|
||||||
this.input.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get_input_value() {
|
|
||||||
return await this.parse(this.input.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
async parse(value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async validate(value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
bind_change_event() {
|
|
||||||
this.input.addEventListener('change', (e) => this.handle_change(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
async handle_change(e) {
|
|
||||||
let value = await this.get_input_value();
|
|
||||||
value = await this.validate(value);
|
|
||||||
await this.doc.set(this.fieldname, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = BaseControl;
|
|
@ -1,18 +0,0 @@
|
|||||||
const control_classes = {
|
|
||||||
Data: require('./data'),
|
|
||||||
Text: require('./text'),
|
|
||||||
Select: require('./select')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
get_control_class(fieldtype) {
|
|
||||||
return control_classes[fieldtype];
|
|
||||||
},
|
|
||||||
make_control(field, parent) {
|
|
||||||
const control_class = this.get_control_class(field.fieldtype);
|
|
||||||
let control = new control_class(field, parent);
|
|
||||||
control.make();
|
|
||||||
return control;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
const BaseControl = require('./base');
|
|
||||||
|
|
||||||
class SelectControl extends BaseControl {
|
|
||||||
make_input() {
|
|
||||||
this.input = frappe.ui.add('select', 'form-control', this.form_group);
|
|
||||||
|
|
||||||
let options = this.options;
|
|
||||||
if (typeof options==='string') {
|
|
||||||
options = options.split('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let value of options) {
|
|
||||||
let option = frappe.ui.add('option', null, this.input);
|
|
||||||
option.textContent = value;
|
|
||||||
option.setAttribute('value', value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
make() {
|
|
||||||
super.make();
|
|
||||||
this.input.setAttribute('row', '3');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = SelectControl;
|
|
@ -1,13 +0,0 @@
|
|||||||
const BaseControl = require('./base');
|
|
||||||
|
|
||||||
class TextControl extends BaseControl {
|
|
||||||
make_input() {
|
|
||||||
this.input = frappe.ui.add('textarea', 'form-control', this.form_group);
|
|
||||||
}
|
|
||||||
make() {
|
|
||||||
super.make();
|
|
||||||
this.input.setAttribute('rows', '8');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = TextControl;
|
|
@ -1,108 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
const controls = require('./controls');
|
|
||||||
|
|
||||||
class Form {
|
|
||||||
constructor({doctype, parent, submit_label='Submit'}) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.doctype = doctype;
|
|
||||||
this.submit_label = submit_label;
|
|
||||||
|
|
||||||
this.controls = {};
|
|
||||||
this.controls_list = [];
|
|
||||||
|
|
||||||
this.meta = frappe.get_meta(this.doctype);
|
|
||||||
this.make();
|
|
||||||
}
|
|
||||||
|
|
||||||
make() {
|
|
||||||
if (this.body || !this.parent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.body = frappe.ui.add('div', 'form-body', this.parent);
|
|
||||||
this.make_actions();
|
|
||||||
|
|
||||||
this.form = frappe.ui.add('form', null, this.body);
|
|
||||||
for(let df of this.meta.fields) {
|
|
||||||
if (controls.get_control_class(df.fieldtype)) {
|
|
||||||
let control = controls.make_control(df, this.form);
|
|
||||||
this.controls_list.push(control);
|
|
||||||
this.controls[df.fieldname] = control;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.make_submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
make_actions() {
|
|
||||||
this.toolbar = frappe.ui.add('div', 'form-toolbar', this.body);
|
|
||||||
this.actions = frappe.ui.make_dropdown('Actions', this.toolbar);
|
|
||||||
|
|
||||||
// delete
|
|
||||||
this.actions.add_item('Delete', async () => {
|
|
||||||
await this.doc.delete();
|
|
||||||
this.show_alert('Deleted', 'success');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.actions.float_right();
|
|
||||||
}
|
|
||||||
|
|
||||||
make_submit() {
|
|
||||||
this.submit_btn = frappe.ui.add('button', 'btn btn-outline-primary',
|
|
||||||
this.body);
|
|
||||||
this.submit_btn.setAttribute('type', 'submit');
|
|
||||||
this.submit_btn.textContent = this.submit_label;
|
|
||||||
this.submit_btn.addEventListener('click', (event) => {
|
|
||||||
this.submit();
|
|
||||||
event.preventDefault();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
show_alert(message, type) {
|
|
||||||
this.clear_alert();
|
|
||||||
this.alert = frappe.ui.add('div', `alert alert-${type}`, this.body);
|
|
||||||
this.alert.textContent = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
clear_alert() {
|
|
||||||
if (this.alert) {
|
|
||||||
frappe.ui.remove(this.alert);
|
|
||||||
this.alert = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async use(doc, is_new = false) {
|
|
||||||
if (this.doc) {
|
|
||||||
// clear handlers of outgoing doc
|
|
||||||
this.doc.clear_handlers();
|
|
||||||
}
|
|
||||||
this.clear_alert();
|
|
||||||
this.doc = doc;
|
|
||||||
this.is_new = is_new;
|
|
||||||
for (let control of this.controls_list) {
|
|
||||||
control.bind(this.doc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async submit() {
|
|
||||||
try {
|
|
||||||
if (this.is_new) {
|
|
||||||
await this.doc.insert();
|
|
||||||
} else {
|
|
||||||
await this.doc.update();
|
|
||||||
}
|
|
||||||
await this.refresh();
|
|
||||||
this.show_alert('Saved', 'success');
|
|
||||||
} catch (e) {
|
|
||||||
this.show_alert('Failed', 'danger');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
for(let control of this.controls_list) {
|
|
||||||
control.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {Form: Form};
|
|
@ -1,20 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
init({container, main, sidebar}) {
|
|
||||||
frappe.container = container;
|
|
||||||
|
|
||||||
if (sidebar) {
|
|
||||||
frappe.sidebar = sidebar;
|
|
||||||
} else {
|
|
||||||
frappe.sidebar = frappe.ui.add('div', 'sidebar', frappe.container);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (main) {
|
|
||||||
frappe.main = main;
|
|
||||||
} else {
|
|
||||||
frappe.main = frappe.ui.add('div', 'main', frappe.container);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
|
|
||||||
class ListView {
|
|
||||||
constructor({doctype, parent, fields}) {
|
|
||||||
this.doctype = doctype;
|
|
||||||
this.parent = parent;
|
|
||||||
this.fields = fields;
|
|
||||||
|
|
||||||
this.meta = frappe.get_meta(this.doctype);
|
|
||||||
|
|
||||||
this.start = 0;
|
|
||||||
this.page_length = 20;
|
|
||||||
|
|
||||||
this.body = null;
|
|
||||||
this.rows = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async run() {
|
|
||||||
this.make_body();
|
|
||||||
let data = await this.meta.get_list({
|
|
||||||
start:this.start,
|
|
||||||
limit:this.page_length
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i=0; i< data.length; i++) {
|
|
||||||
this.render_row(this.start + i, data[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clear_empty_rows(data.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
make_body() {
|
|
||||||
if (!this.body) {
|
|
||||||
this.body = frappe.ui.add('div', 'list-body', this.parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render_row(i, data) {
|
|
||||||
let row = this.get_row(i);
|
|
||||||
row.innerHTML = this.meta.get_row_html(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
get_row(i) {
|
|
||||||
if (!this.rows[i]) {
|
|
||||||
this.rows[i] = frappe.ui.add('div', 'list-row', this.body);
|
|
||||||
}
|
|
||||||
return this.rows[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
clear_empty_rows(start) {
|
|
||||||
if (this.rows.length > start) {
|
|
||||||
for (let i=start; i < this.rows.length; i++) {
|
|
||||||
let row = this.get_row(i);
|
|
||||||
row.innerHTML = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
ListView: ListView
|
|
||||||
};
|
|
@ -1,45 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
|
|
||||||
class Page {
|
|
||||||
constructor(title) {
|
|
||||||
this.handlers = {};
|
|
||||||
this.title = title;
|
|
||||||
this.make();
|
|
||||||
}
|
|
||||||
|
|
||||||
make() {
|
|
||||||
this.body = frappe.ui.add('div', 'page hide', frappe.main);
|
|
||||||
}
|
|
||||||
|
|
||||||
hide() {
|
|
||||||
frappe.ui.add_class(this.body, 'hide');
|
|
||||||
|
|
||||||
this.trigger('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
show(params) {
|
|
||||||
if (frappe.router.current_page) {
|
|
||||||
frappe.router.current_page.hide();
|
|
||||||
}
|
|
||||||
frappe.ui.remove_class(this.body, 'hide');
|
|
||||||
frappe.router.current_page = this;
|
|
||||||
document.title = this.title;
|
|
||||||
|
|
||||||
this.trigger('show', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
on(event, fn) {
|
|
||||||
if (!this.handlers[event]) this.handlers.event = [];
|
|
||||||
this.handlers[event].push(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
trigger(event, params) {
|
|
||||||
if (this.handlers[event]) {
|
|
||||||
for (let handler of this.handlers[event]) {
|
|
||||||
handler(params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { Page: Page };
|
|
@ -1,82 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
|
|
||||||
class Router {
|
|
||||||
constructor() {
|
|
||||||
this.current_page = null;
|
|
||||||
this.routes = {};
|
|
||||||
this.listen();
|
|
||||||
}
|
|
||||||
|
|
||||||
add(route, handler) {
|
|
||||||
let page = {handler: handler};
|
|
||||||
|
|
||||||
// '/todo/:name/:place'.match(/:([^/]+)/g);
|
|
||||||
page.param_keys = route.match(/:([^/]+)/g);
|
|
||||||
|
|
||||||
if (page.param_keys) {
|
|
||||||
// make expression
|
|
||||||
// '/todo/:name/:place'.replace(/\/:([a-z1-9]+)/g, "\/([a-z0-9]+)");
|
|
||||||
page.expression = route.replace(/\/:([a-z1-9]+)/g, "\/([a-z0-9]+)");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.routes[route] = page;
|
|
||||||
}
|
|
||||||
|
|
||||||
listen() {
|
|
||||||
window.onhashchange = this.changed.bind(this);
|
|
||||||
this.changed();
|
|
||||||
}
|
|
||||||
|
|
||||||
async changed(event) {
|
|
||||||
if (window.location.hash.length > 0) {
|
|
||||||
const page_name = window.location.hash.substr(1);
|
|
||||||
this.show(page_name);
|
|
||||||
} else if (this.routes['default']) {
|
|
||||||
this.show('default');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
show(route) {
|
|
||||||
if (!route) {
|
|
||||||
route = 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route[0]==='#') {
|
|
||||||
route = route.substr(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let page = this.match(route);
|
|
||||||
|
|
||||||
if (page) {
|
|
||||||
if (typeof page.handler==='function') {
|
|
||||||
page.handler(page.params);
|
|
||||||
} else {
|
|
||||||
page.handler.show(page.params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match(route) {
|
|
||||||
for(let key in this.routes) {
|
|
||||||
let page = this.routes[key];
|
|
||||||
|
|
||||||
if (page.param_keys) {
|
|
||||||
let matches = route.match(new RegExp(page.expression));
|
|
||||||
if (matches && matches.length == page.param_keys.length + 1) {
|
|
||||||
let params = {}
|
|
||||||
for (let i=0; i < page.param_keys.length; i++) {
|
|
||||||
params[page.param_keys[i].substr(1)] = matches[i + 1];
|
|
||||||
}
|
|
||||||
return {handler:page.handler, params: params};
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if (key === route) {
|
|
||||||
return {handler:page.handler};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {Router: Router};
|
|
@ -1,69 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
async init() {
|
|
||||||
if (this._initialized) return;
|
|
||||||
this.init_config();
|
|
||||||
this.init_errors();
|
|
||||||
this.init_globals();
|
|
||||||
this._initialized = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
init_config() {
|
|
||||||
this.config = {
|
|
||||||
backend: 'sqlite',
|
|
||||||
port: 8000
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
init_errors() {
|
|
||||||
this.ValueError = class extends Error { };
|
|
||||||
},
|
|
||||||
|
|
||||||
init_globals() {
|
|
||||||
this.meta_cache = {};
|
|
||||||
},
|
|
||||||
|
|
||||||
get_meta(doctype) {
|
|
||||||
if (!this.meta_cache[doctype]) {
|
|
||||||
this.meta_cache[doctype] = new (this.models.get_meta_class(doctype))(this.models.get('DocType', doctype));
|
|
||||||
}
|
|
||||||
return this.meta_cache[doctype];
|
|
||||||
},
|
|
||||||
|
|
||||||
init_controller(doctype, module) {
|
|
||||||
doctype = this.slug(doctype);
|
|
||||||
this.models.controllers[doctype] = module[doctype];
|
|
||||||
this.models.meta_classes[doctype] = module[doctype + '_meta'];
|
|
||||||
},
|
|
||||||
|
|
||||||
async get_doc(data, name) {
|
|
||||||
if (typeof data==='string' && typeof name==='string') {
|
|
||||||
let controller_class = this.models.get_controller(data);
|
|
||||||
var doc = new controller_class({doctype:data, name: name});
|
|
||||||
await doc.load();
|
|
||||||
} else {
|
|
||||||
let controller_class = this.models.get_controller(data.doctype);
|
|
||||||
var doc = new controller_class(data);
|
|
||||||
}
|
|
||||||
return doc;
|
|
||||||
},
|
|
||||||
|
|
||||||
async insert(data) {
|
|
||||||
const doc = await this.get_doc(data);
|
|
||||||
return await doc.insert();
|
|
||||||
},
|
|
||||||
|
|
||||||
login(user='guest', user_key) {
|
|
||||||
this.session = new this._session.Session(user);
|
|
||||||
if (user && user_key) {
|
|
||||||
this.authenticate(user_key);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.db.close();
|
|
||||||
|
|
||||||
if (this.server) {
|
|
||||||
this.server.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,133 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
|
|
||||||
class Document {
|
|
||||||
constructor(data) {
|
|
||||||
this.handlers = {};
|
|
||||||
this.setup();
|
|
||||||
Object.assign(this, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
// add handlers
|
|
||||||
}
|
|
||||||
|
|
||||||
clear_handlers() {
|
|
||||||
this.handlers = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
add_handler(key, method) {
|
|
||||||
if (!this.handlers[key]) {
|
|
||||||
this.handlers[key] = [];
|
|
||||||
}
|
|
||||||
this.handlers[key].push(method || key);
|
|
||||||
}
|
|
||||||
|
|
||||||
get(key) {
|
|
||||||
return this[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
set(key, value) {
|
|
||||||
this.validate_field(key, value);
|
|
||||||
this[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
set_name() {
|
|
||||||
// assign a random name by default
|
|
||||||
// override this to set a name
|
|
||||||
if (!this.name) {
|
|
||||||
this.name = Math.random().toString(36).substr(3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get meta() {
|
|
||||||
if (!this._meta) {
|
|
||||||
this._meta = frappe.get_meta(this.doctype);
|
|
||||||
}
|
|
||||||
return this._meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
append(key, document) {
|
|
||||||
if (!this[key]) {
|
|
||||||
this[key] = [];
|
|
||||||
}
|
|
||||||
this[key].push(this.init_doc(document));
|
|
||||||
}
|
|
||||||
|
|
||||||
init_doc(data) {
|
|
||||||
if (data.prototype instanceof Document) {
|
|
||||||
return data;
|
|
||||||
} else {
|
|
||||||
return new Document(d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_field (key, value) {
|
|
||||||
let df = this.meta.get_field(key);
|
|
||||||
if (df.fieldtype=='Select') {
|
|
||||||
this.meta.validate_select(df, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get_valid_dict() {
|
|
||||||
let doc = {};
|
|
||||||
for(let df of this.meta.get_valid_fields()) {
|
|
||||||
doc[df.fieldname] = this.get(df.fieldname);
|
|
||||||
}
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
set_standard_values() {
|
|
||||||
let now = new Date();
|
|
||||||
if (this.docstatus === null || this.docstatus === undefined) {
|
|
||||||
this.docstatus = 0;
|
|
||||||
}
|
|
||||||
if (!this.owner) {
|
|
||||||
this.owner = frappe.session.user;
|
|
||||||
this.creation = now;
|
|
||||||
}
|
|
||||||
this.modified_by = frappe.session.user;
|
|
||||||
this.modified = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
Object.assign(this, await frappe.db.get(this.doctype, this.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
async insert() {
|
|
||||||
this.set_name();
|
|
||||||
this.set_standard_values();
|
|
||||||
await this.trigger('validate', 'before_insert');
|
|
||||||
await frappe.db.insert(this.doctype, this.get_valid_dict());
|
|
||||||
await this.trigger('after_insert', 'after_save');
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete() {
|
|
||||||
await this.trigger('before_delete');
|
|
||||||
await frappe.db.delete(this.doctype, this.name);
|
|
||||||
await this.trigger('after_delete');
|
|
||||||
}
|
|
||||||
|
|
||||||
async trigger() {
|
|
||||||
for(var key of arguments) {
|
|
||||||
if (this.handlers[key]) {
|
|
||||||
for (let method of this.handlers[key]) {
|
|
||||||
if (typeof method === 'string') {
|
|
||||||
await this[method]();
|
|
||||||
} else {
|
|
||||||
await method(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update() {
|
|
||||||
this.set_standard_values();
|
|
||||||
await this.trigger('validate', 'before_update');
|
|
||||||
await frappe.db.update(this.doctype, this.get_valid_dict());
|
|
||||||
await this.trigger('after_update', 'after_save');
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = { Document: Document };
|
|
@ -1,36 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
standard_fields: [
|
|
||||||
{
|
|
||||||
fieldname: 'name', fieldtype: 'Data', reqd: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'owner', fieldtype: 'Link', reqd: 1, options: 'User'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'modified_by', fieldtype: 'Link', reqd: 1, options: 'User'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'creation', fieldtype: 'Datetime', reqd: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'modified', fieldtype: 'Datetime', reqd: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'docstatus', fieldtype: 'Int', reqd: 1, default: 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
child_fields: [
|
|
||||||
{
|
|
||||||
fieldname: 'idx', fieldtype: 'Int', reqd: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'parent', fieldtype: 'Data', reqd: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'parenttype', fieldtype: 'Link', reqd: 1, options: 'DocType'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'parentfield', fieldtype: 'Data', reqd: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
@ -1,111 +0,0 @@
|
|||||||
const Document = require('./document').Document;
|
|
||||||
const frappe = require('frappe-core');
|
|
||||||
|
|
||||||
class Meta extends Document {
|
|
||||||
constructor(data) {
|
|
||||||
super(data);
|
|
||||||
this.event_handlers = {};
|
|
||||||
this.list_options = {
|
|
||||||
fields: ['name', 'modified']
|
|
||||||
};
|
|
||||||
if (this.setup_meta) {
|
|
||||||
this.setup_meta();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get_field(fieldname) {
|
|
||||||
if (!this.field_map) {
|
|
||||||
this.field_map = {};
|
|
||||||
for (let df of this.fields) {
|
|
||||||
this.field_map[df.fieldname] = df;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.field_map[fieldname];
|
|
||||||
}
|
|
||||||
|
|
||||||
on(key, fn) {
|
|
||||||
if (!this.event_handlers[key]) {
|
|
||||||
this.event_handlers[key] = [];
|
|
||||||
}
|
|
||||||
this.event_handlers[key].push(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
async set(fieldname, value) {
|
|
||||||
this[fieldname] = value;
|
|
||||||
await this.trigger(fieldname);
|
|
||||||
}
|
|
||||||
|
|
||||||
get(fieldname) {
|
|
||||||
return this[fieldname];
|
|
||||||
}
|
|
||||||
|
|
||||||
get_valid_fields() {
|
|
||||||
if (!this._valid_fields) {
|
|
||||||
this._valid_fields = [];
|
|
||||||
|
|
||||||
// standard fields
|
|
||||||
for (let df of frappe.model.standard_fields) {
|
|
||||||
this._valid_fields.push(df);
|
|
||||||
}
|
|
||||||
|
|
||||||
// parent fields
|
|
||||||
if (this.istable) {
|
|
||||||
for (let df of frappe.model.child_fields) {
|
|
||||||
this._valid_fields.push(df);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// doctype fields
|
|
||||||
for (let df of this.fields) {
|
|
||||||
if (frappe.db.type_map[df.fieldtype]) {
|
|
||||||
this._valid_fields.push(df);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._valid_fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_select(df, value) {
|
|
||||||
let options = df.options;
|
|
||||||
if (typeof options === 'string') {
|
|
||||||
// values given as string
|
|
||||||
options = df.options.split('\n');
|
|
||||||
}
|
|
||||||
if (!options.includes(value)) {
|
|
||||||
throw new frappe.ValueError(`${value} must be one of ${options.join(", ")}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async trigger(key, event = {}) {
|
|
||||||
|
|
||||||
Object.assign(event, {
|
|
||||||
doc: this,
|
|
||||||
name: key
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.event_handlers[key]) {
|
|
||||||
for (var handler of this.event_handlers[key]) {
|
|
||||||
await handler(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// collections
|
|
||||||
async get_list({start, limit=20, filters}) {
|
|
||||||
return await frappe.db.get_all({
|
|
||||||
doctype: this.name,
|
|
||||||
fields: this.list_options.fields,
|
|
||||||
filters: filters,
|
|
||||||
start: start,
|
|
||||||
limit: limit
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get_row_html(data) {
|
|
||||||
return `<a href="/view/${this.name}/${data.name}">${data.name}</a>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { Meta: Meta }
|
|
@ -1,25 +0,0 @@
|
|||||||
const process = require('process');
|
|
||||||
const frappe = require('frappe-core');
|
|
||||||
|
|
||||||
class Models {
|
|
||||||
constructor() {
|
|
||||||
this.data = {doctype: {}};
|
|
||||||
this.controllers = {};
|
|
||||||
this.meta_classes = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
get(doctype, name) {
|
|
||||||
return this.data[frappe.slug(doctype)][frappe.slug(name)];
|
|
||||||
}
|
|
||||||
|
|
||||||
get_controller(doctype) {
|
|
||||||
return this.controllers[frappe.slug(doctype)];
|
|
||||||
}
|
|
||||||
|
|
||||||
get_meta_class(doctype) {
|
|
||||||
return this.meta_classes[frappe.slug(doctype)] || frappe.meta.Meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { Models: Models }
|
|
@ -1,30 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
|
|
||||||
class todo_meta extends frappe.meta.Meta {
|
|
||||||
setup_meta() {
|
|
||||||
Object.assign(this, require('./todo.json'));
|
|
||||||
this.name = 'ToDo';
|
|
||||||
this.list_options.fields = ['name', 'subject', 'status', 'description'];
|
|
||||||
}
|
|
||||||
|
|
||||||
get_row_html(data) {
|
|
||||||
return `<a href="#edit/todo/${data.name}">${data.subject}</a>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class todo extends frappe.document.Document {
|
|
||||||
setup() {
|
|
||||||
this.add_handler('validate');
|
|
||||||
}
|
|
||||||
validate() {
|
|
||||||
if (!this.status) {
|
|
||||||
this.status = 'Open';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
todo: todo,
|
|
||||||
todo_meta: todo_meta
|
|
||||||
};
|
|
@ -1,52 +0,0 @@
|
|||||||
const backends = {};
|
|
||||||
backends.sqlite = require('frappe-core/frappe/backends/sqlite');
|
|
||||||
|
|
||||||
const express = require('express');
|
|
||||||
const app = express();
|
|
||||||
const frappe = require('frappe-core');
|
|
||||||
const rest_api = require('./rest_api')
|
|
||||||
const models = require('frappe-core/frappe/server/models');
|
|
||||||
const common = require('frappe-core/frappe/common');
|
|
||||||
const bodyParser = require('body-parser');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async init() {
|
|
||||||
await frappe.init();
|
|
||||||
common.init_libs(frappe);
|
|
||||||
await frappe.login();
|
|
||||||
|
|
||||||
// walk and find models
|
|
||||||
models.init();
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
async init_db({backend, connection_params}) {
|
|
||||||
frappe.db = await new backends[backend].Database(connection_params);
|
|
||||||
await frappe.db.connect();
|
|
||||||
await frappe.db.migrate();
|
|
||||||
},
|
|
||||||
|
|
||||||
async start({backend, connection_params, static}) {
|
|
||||||
await this.init();
|
|
||||||
await this.init_db({backend:backend, connection_params:connection_params});
|
|
||||||
// database
|
|
||||||
|
|
||||||
// app
|
|
||||||
app.use(bodyParser.json());
|
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
|
||||||
app.use(express.static('./'));
|
|
||||||
|
|
||||||
app.use(function (err, req, res, next) {
|
|
||||||
console.error(err.stack);
|
|
||||||
res.status(500).send('Something broke!');
|
|
||||||
})
|
|
||||||
// routes
|
|
||||||
rest_api.setup(app);
|
|
||||||
|
|
||||||
// listen
|
|
||||||
frappe.app = app;
|
|
||||||
frappe.server = app.listen(frappe.config.port);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
const walk = require('walk');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
init() {
|
|
||||||
const cwd = process.cwd();
|
|
||||||
|
|
||||||
const setup_model = (doctype, name, file_path) => {
|
|
||||||
// add to frappe.models.data
|
|
||||||
if (!frappe.models[doctype]) {
|
|
||||||
frappe.models[doctype] = {};
|
|
||||||
}
|
|
||||||
frappe.models.data[doctype][name] = require(file_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
const setup_controller = (doctype, file_path) => {
|
|
||||||
let _module = require(file_path);
|
|
||||||
frappe.models.controllers[doctype] = _module[doctype];
|
|
||||||
if (_module[doctype + '_meta']) {
|
|
||||||
frappe.models.meta_classes[doctype] = _module[doctype + '_meta'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// init for all apps
|
|
||||||
if (!frappe.config.apps) {
|
|
||||||
frappe.config.apps = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.config.apps.unshift('frappe-core');
|
|
||||||
|
|
||||||
// walk and sync
|
|
||||||
for (let app_name of frappe.config.apps) {
|
|
||||||
let start = path.resolve(require.resolve(app_name), '../models');
|
|
||||||
walk.walkSync(start, {
|
|
||||||
listeners: {
|
|
||||||
file: (basepath, file_data, next) => {
|
|
||||||
const doctype = path.basename(path.dirname(basepath));
|
|
||||||
const name = path.basename(basepath);
|
|
||||||
const file_path = path.resolve(basepath, file_data.name);
|
|
||||||
if (file_data.name.endsWith('.json')) {
|
|
||||||
setup_model(doctype, name, file_path)
|
|
||||||
}
|
|
||||||
if (doctype==='doctype' && file_data.name.endsWith('.js')) {
|
|
||||||
setup_controller(path.basename(basepath), file_path);
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
setup(app) {
|
|
||||||
// get list
|
|
||||||
app.get('/api/resource/:doctype', frappe.async_handler(async function(request, response) {
|
|
||||||
let fields, filters;
|
|
||||||
for (key of ['fields', 'filters']) {
|
|
||||||
if (request.query[key]) {
|
|
||||||
request.query[key] = JSON.parse(request.query[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = await frappe.db.get_all({
|
|
||||||
doctype: request.params.doctype,
|
|
||||||
fields: request.query.fields || ['name', 'subject'],
|
|
||||||
filters: request.query.filters,
|
|
||||||
start: request.query.start || 0,
|
|
||||||
limit: request.query.limit || 20,
|
|
||||||
order_by: request.query.order_by,
|
|
||||||
order: request.query.order
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.json(data);
|
|
||||||
}));
|
|
||||||
|
|
||||||
// create
|
|
||||||
app.post('/api/resource/:doctype', frappe.async_handler(async function(request, response) {
|
|
||||||
data = request.body;
|
|
||||||
data.doctype = request.params.doctype;
|
|
||||||
let doc = await frappe.get_doc(data);
|
|
||||||
await doc.insert();
|
|
||||||
await frappe.db.commit();
|
|
||||||
return response.json(doc.get_valid_dict());
|
|
||||||
}));
|
|
||||||
|
|
||||||
// update
|
|
||||||
app.put('/api/resource/:doctype/:name', frappe.async_handler(async function(request, response) {
|
|
||||||
data = request.body;
|
|
||||||
let doc = await frappe.get_doc(request.params.doctype, request.params.name);
|
|
||||||
Object.assign(doc, data);
|
|
||||||
await doc.update();
|
|
||||||
await frappe.db.commit();
|
|
||||||
return response.json(doc.get_valid_dict());
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
// get document
|
|
||||||
app.get('/api/resource/:doctype/:name', frappe.async_handler(async function(request, response) {
|
|
||||||
let doc = await frappe.get_doc(request.params.doctype, request.params.name);
|
|
||||||
return response.json(doc.get_valid_dict());
|
|
||||||
}));
|
|
||||||
|
|
||||||
// delete
|
|
||||||
app.delete('/api/resource/:doctype/:name', frappe.async_handler(async function(request, response) {
|
|
||||||
let doc = await frappe.get_doc(request.params.doctype, request.params.name)
|
|
||||||
await doc.delete();
|
|
||||||
return response.json({});
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,17 +0,0 @@
|
|||||||
const frappe = require('frappe-core');
|
|
||||||
|
|
||||||
class Session {
|
|
||||||
constructor(user, user_key) {
|
|
||||||
this.user = user || 'guest';
|
|
||||||
if (this.user !== 'guest') {
|
|
||||||
this.login(user_key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
login(user_key) {
|
|
||||||
// could be password, sessionid, otp
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { Session: Session };
|
|
@ -1,11 +0,0 @@
|
|||||||
const server = require('frappe-core/frappe/server');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async init_sqlite() {
|
|
||||||
server.init()
|
|
||||||
server.init_db({
|
|
||||||
backend: 'sqlite',
|
|
||||||
connection_params: {db_path: 'test.db'}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
const assert = require('assert');
|
|
||||||
const frappe = require('frappe-core');
|
|
||||||
const helpers = require('./helpers');
|
|
||||||
|
|
||||||
describe('Controller', () => {
|
|
||||||
before(async function() {
|
|
||||||
await helpers.init_sqlite();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call controller method', async () => {
|
|
||||||
let doc = await frappe.get_doc({
|
|
||||||
doctype:'ToDo',
|
|
||||||
subject: 'test'
|
|
||||||
});
|
|
||||||
doc.trigger('validate');
|
|
||||||
assert.equal(doc.status, 'Open');
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,49 +0,0 @@
|
|||||||
const assert = require('assert');
|
|
||||||
const frappe = require('frappe-core');
|
|
||||||
const helpers = require('./helpers');
|
|
||||||
|
|
||||||
describe('Database', () => {
|
|
||||||
before(async function() {
|
|
||||||
await helpers.init_sqlite();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should insert and get values', async () => {
|
|
||||||
await frappe.db.sql('delete from todo');
|
|
||||||
await frappe.insert({doctype:'ToDo', subject: 'testing 1'});
|
|
||||||
await frappe.insert({doctype:'ToDo', subject: 'testing 3'});
|
|
||||||
await frappe.insert({doctype:'ToDo', subject: 'testing 2'});
|
|
||||||
|
|
||||||
let subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject']})
|
|
||||||
subjects = subjects.map(d => d.subject);
|
|
||||||
|
|
||||||
assert.ok(subjects.includes('testing 1'));
|
|
||||||
assert.ok(subjects.includes('testing 2'));
|
|
||||||
assert.ok(subjects.includes('testing 3'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should filter correct values', async () => {
|
|
||||||
let subjects = null;
|
|
||||||
|
|
||||||
await frappe.db.sql('delete from todo');
|
|
||||||
await frappe.insert({doctype:'ToDo', subject: 'testing 1', status: 'Open'});
|
|
||||||
await frappe.insert({doctype:'ToDo', subject: 'testing 3', status: 'Open'});
|
|
||||||
await frappe.insert({doctype:'ToDo', subject: 'testing 2', status: 'Closed'});
|
|
||||||
|
|
||||||
subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject'],
|
|
||||||
filters:{status: 'Open'}});
|
|
||||||
subjects = subjects.map(d => d.subject);
|
|
||||||
|
|
||||||
assert.ok(subjects.includes('testing 1'));
|
|
||||||
assert.ok(subjects.includes('testing 3'));
|
|
||||||
assert.equal(subjects.includes('testing 2'), false);
|
|
||||||
|
|
||||||
subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject'],
|
|
||||||
filters:{status: 'Closed'}});
|
|
||||||
subjects = subjects.map(d => d.subject);
|
|
||||||
|
|
||||||
assert.equal(subjects.includes('testing 1'), false);
|
|
||||||
assert.equal(subjects.includes('testing 3'), false);
|
|
||||||
assert.ok(subjects.includes('testing 2'));
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,75 +0,0 @@
|
|||||||
const assert = require('assert');
|
|
||||||
const frappe = require('frappe-core');
|
|
||||||
const helpers = require('./helpers');
|
|
||||||
|
|
||||||
describe('Document', () => {
|
|
||||||
before(async function() {
|
|
||||||
await helpers.init_sqlite();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should insert a doc', async () => {
|
|
||||||
let doc1 = await test_doc();
|
|
||||||
doc1.subject = 'insert subject 1';
|
|
||||||
doc1.description = 'insert description 1';
|
|
||||||
await doc1.insert();
|
|
||||||
|
|
||||||
// get it back from the db
|
|
||||||
let doc2 = await frappe.get_doc(doc1.doctype, doc1.name);
|
|
||||||
assert.equal(doc1.subject, doc2.subject);
|
|
||||||
assert.equal(doc1.description, doc2.description);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update a doc', async () => {
|
|
||||||
let doc = await test_doc();
|
|
||||||
await doc.insert();
|
|
||||||
|
|
||||||
assert.notEqual(await frappe.db.get_value(doc.doctype, doc.name, 'subject'), 'subject 2');
|
|
||||||
|
|
||||||
doc.subject = 'subject 2'
|
|
||||||
await doc.update();
|
|
||||||
|
|
||||||
assert.equal(await frappe.db.get_value(doc.doctype, doc.name, 'subject'), 'subject 2');
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should get a value', async () => {
|
|
||||||
let doc = await test_doc();
|
|
||||||
assert.equal(doc.get('subject'), 'testing 1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set a value', async () => {
|
|
||||||
let doc = await test_doc();
|
|
||||||
doc.set('subject', 'testing 1')
|
|
||||||
assert.equal(doc.get('subject'), 'testing 1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow incorrect Select option', async () => {
|
|
||||||
let doc = await test_doc();
|
|
||||||
assert.throws(
|
|
||||||
() => {
|
|
||||||
doc.set('status', 'Illegal');
|
|
||||||
},
|
|
||||||
frappe.ValueError
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete a document', async () => {
|
|
||||||
let doc = await test_doc();
|
|
||||||
await doc.insert();
|
|
||||||
|
|
||||||
assert.equal(await frappe.db.get_value(doc.doctype, doc.name), doc.name);
|
|
||||||
|
|
||||||
await doc.delete();
|
|
||||||
|
|
||||||
assert.equal(await frappe.db.get_value(doc.doctype, doc.name), null);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
async function test_doc() {
|
|
||||||
return await frappe.get_doc({
|
|
||||||
doctype: 'ToDo',
|
|
||||||
status: 'Open',
|
|
||||||
subject: 'testing 1',
|
|
||||||
description: 'test description 1'
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
const assert = require('assert');
|
|
||||||
const frappe = require('frappe-core');
|
|
||||||
const helpers = require('./helpers');
|
|
||||||
|
|
||||||
describe('Meta', () => {
|
|
||||||
before(async function() {
|
|
||||||
await helpers.init_sqlite();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get init from json file', () => {
|
|
||||||
let todo = frappe.get_meta('ToDo');
|
|
||||||
assert.equal(todo.issingle, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get fields from meta', () => {
|
|
||||||
let todo = frappe.get_meta('ToDo');
|
|
||||||
let fields = todo.fields.map((df) => df.fieldname);
|
|
||||||
assert.ok(fields.includes('subject'));
|
|
||||||
assert.ok(fields.includes('description'));
|
|
||||||
assert.ok(fields.includes('status'));
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,14 +0,0 @@
|
|||||||
const assert = require('assert');
|
|
||||||
const frappe = require('frappe-core');
|
|
||||||
const helpers = require('./helpers');
|
|
||||||
|
|
||||||
describe('Models', () => {
|
|
||||||
before(async function() {
|
|
||||||
await helpers.init_sqlite();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get todo json', () => {
|
|
||||||
let todo = frappe.models.get('DocType', 'ToDo');
|
|
||||||
assert.equal(todo.issingle, 0);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,66 +0,0 @@
|
|||||||
const assert = require('assert');
|
|
||||||
const frappe = require('frappe-core');
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
const helpers = require('./helpers');
|
|
||||||
const { spawn } = require('child_process');
|
|
||||||
const process = require('process');
|
|
||||||
const Database = require('frappe-core/frappe/backends/rest_client').Database
|
|
||||||
|
|
||||||
// create a copy of frappe
|
|
||||||
|
|
||||||
var test_server;
|
|
||||||
|
|
||||||
describe('REST', () => {
|
|
||||||
before(async function() {
|
|
||||||
test_server = spawn('node', ['frappe/tests/test_server.js'], {
|
|
||||||
stdio: [process.stdin, process.stdout, process.stderr, 'pipe', 'pipe']
|
|
||||||
});
|
|
||||||
|
|
||||||
await frappe.init();
|
|
||||||
await frappe.login();
|
|
||||||
|
|
||||||
frappe.db = await new Database({server: 'localhost:8000'});
|
|
||||||
frappe.fetch = fetch;
|
|
||||||
|
|
||||||
// wait for server to start
|
|
||||||
await frappe.sleep(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
frappe.close();
|
|
||||||
test_server.kill();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a document', async () => {
|
|
||||||
let doc = await frappe.get_doc({doctype:'ToDo', subject:'test rest insert 1'});
|
|
||||||
await doc.insert();
|
|
||||||
|
|
||||||
let doc1 = await frappe.get_doc('ToDo', doc.name);
|
|
||||||
|
|
||||||
assert.equal(doc.subject, doc1.subject);
|
|
||||||
assert.equal(doc1.status, 'Open');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update a document', async () => {
|
|
||||||
let doc = await frappe.get_doc({doctype:'ToDo', subject:'test rest insert 1'});
|
|
||||||
await doc.insert();
|
|
||||||
|
|
||||||
doc.subject = 'subject changed';
|
|
||||||
await doc.update();
|
|
||||||
|
|
||||||
let doc1 = await frappe.get_doc('ToDo', doc.name);
|
|
||||||
assert.equal(doc.subject, doc1.subject);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get multiple documents', async () => {
|
|
||||||
await frappe.insert({doctype:'ToDo', subject:'all test 1'});
|
|
||||||
await frappe.insert({doctype:'ToDo', subject:'all test 2'});
|
|
||||||
|
|
||||||
let data = await frappe.db.get_all({doctype:'ToDo'});
|
|
||||||
let subjects = data.map(d => d.subject);
|
|
||||||
assert.ok(subjects.includes('all test 1'));
|
|
||||||
assert.ok(subjects.includes('all test 2'));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
@ -1,5 +0,0 @@
|
|||||||
const server = require('frappe-core/frappe/server');
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
server.start({backend: 'sqlite', connection_params: {db_path: 'test.db'}});
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
slug(text) {
|
|
||||||
return text.toLowerCase().replace(/ /g, '_');
|
|
||||||
},
|
|
||||||
|
|
||||||
async_handler(fn) {
|
|
||||||
return (req, res, next) => Promise.resolve(fn(req, res, next))
|
|
||||||
.catch(next);
|
|
||||||
},
|
|
||||||
|
|
||||||
async sleep(seconds) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
setTimeout(resolve, seconds * 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
69
index.js
Normal file
69
index.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
module.exports = {
|
||||||
|
async init() {
|
||||||
|
if (this._initialized) return;
|
||||||
|
this.init_config();
|
||||||
|
this.init_errors();
|
||||||
|
this.init_globals();
|
||||||
|
this._initialized = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
init_config() {
|
||||||
|
this.config = {
|
||||||
|
backend: 'sqlite',
|
||||||
|
port: 8000
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
init_errors() {
|
||||||
|
this.ValueError = class extends Error { };
|
||||||
|
},
|
||||||
|
|
||||||
|
init_globals() {
|
||||||
|
this.meta_cache = {};
|
||||||
|
},
|
||||||
|
|
||||||
|
get_meta(doctype) {
|
||||||
|
if (!this.meta_cache[doctype]) {
|
||||||
|
this.meta_cache[doctype] = new (this.models.get_meta_class(doctype))(this.models.get('DocType', doctype));
|
||||||
|
}
|
||||||
|
return this.meta_cache[doctype];
|
||||||
|
},
|
||||||
|
|
||||||
|
init_controller(doctype, module) {
|
||||||
|
doctype = this.slug(doctype);
|
||||||
|
this.models.controllers[doctype] = module[doctype];
|
||||||
|
this.models.meta_classes[doctype] = module[doctype + '_meta'];
|
||||||
|
},
|
||||||
|
|
||||||
|
async get_doc(data, name) {
|
||||||
|
if (typeof data==='string' && typeof name==='string') {
|
||||||
|
let controller_class = this.models.get_controller(data);
|
||||||
|
var doc = new controller_class({doctype:data, name: name});
|
||||||
|
await doc.load();
|
||||||
|
} else {
|
||||||
|
let controller_class = this.models.get_controller(data.doctype);
|
||||||
|
var doc = new controller_class(data);
|
||||||
|
}
|
||||||
|
return doc;
|
||||||
|
},
|
||||||
|
|
||||||
|
async insert(data) {
|
||||||
|
const doc = await this.get_doc(data);
|
||||||
|
return await doc.insert();
|
||||||
|
},
|
||||||
|
|
||||||
|
login(user='guest', user_key) {
|
||||||
|
this.session = new this._session.Session(user);
|
||||||
|
if (user && user_key) {
|
||||||
|
this.authenticate(user_key);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.db.close();
|
||||||
|
|
||||||
|
if (this.server) {
|
||||||
|
this.server.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
143
model/document.js
Normal file
143
model/document.js
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
|
||||||
|
class Document {
|
||||||
|
constructor(data) {
|
||||||
|
this.handlers = {};
|
||||||
|
this.setup();
|
||||||
|
Object.assign(this, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
// add handlers
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_handlers() {
|
||||||
|
this.handlers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
add_handler(key, method) {
|
||||||
|
if (!this.handlers[key]) {
|
||||||
|
this.handlers[key] = [];
|
||||||
|
}
|
||||||
|
this.handlers[key].push(method || key);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key) {
|
||||||
|
return this[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key, value) {
|
||||||
|
this.validate_field(key, value);
|
||||||
|
this[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_name() {
|
||||||
|
// assign a random name by default
|
||||||
|
// override this to set a name
|
||||||
|
if (!this.name) {
|
||||||
|
this.name = Math.random().toString(36).substr(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_keywords() {
|
||||||
|
let keywords = [];
|
||||||
|
for (let fieldname of this.meta.get_keyword_fields()) {
|
||||||
|
keywords.push(this[fieldname]);
|
||||||
|
}
|
||||||
|
this.keywords = keywords.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
get meta() {
|
||||||
|
if (!this._meta) {
|
||||||
|
this._meta = frappe.get_meta(this.doctype);
|
||||||
|
}
|
||||||
|
return this._meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
append(key, document) {
|
||||||
|
if (!this[key]) {
|
||||||
|
this[key] = [];
|
||||||
|
}
|
||||||
|
this[key].push(this.init_doc(document));
|
||||||
|
}
|
||||||
|
|
||||||
|
init_doc(data) {
|
||||||
|
if (data.prototype instanceof Document) {
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
return new Document(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_field (key, value) {
|
||||||
|
let df = this.meta.get_field(key);
|
||||||
|
if (df.fieldtype=='Select') {
|
||||||
|
this.meta.validate_select(df, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_valid_dict() {
|
||||||
|
let doc = {};
|
||||||
|
for(let df of this.meta.get_valid_fields()) {
|
||||||
|
doc[df.fieldname] = this.get(df.fieldname);
|
||||||
|
}
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_standard_values() {
|
||||||
|
let now = new Date();
|
||||||
|
if (this.docstatus === null || this.docstatus === undefined) {
|
||||||
|
this.docstatus = 0;
|
||||||
|
}
|
||||||
|
if (!this.owner) {
|
||||||
|
this.owner = frappe.session.user;
|
||||||
|
this.creation = now;
|
||||||
|
}
|
||||||
|
this.modified_by = frappe.session.user;
|
||||||
|
this.modified = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
Object.assign(this, await frappe.db.get(this.doctype, this.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
async insert() {
|
||||||
|
this.set_name();
|
||||||
|
this.set_standard_values();
|
||||||
|
this.set_keywords();
|
||||||
|
await this.trigger('validate', 'before_insert');
|
||||||
|
await frappe.db.insert(this.doctype, this.get_valid_dict());
|
||||||
|
await this.trigger('after_insert', 'after_save');
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete() {
|
||||||
|
await this.trigger('before_delete');
|
||||||
|
await frappe.db.delete(this.doctype, this.name);
|
||||||
|
await this.trigger('after_delete');
|
||||||
|
}
|
||||||
|
|
||||||
|
async trigger() {
|
||||||
|
for(var key of arguments) {
|
||||||
|
if (this.handlers[key]) {
|
||||||
|
for (let method of this.handlers[key]) {
|
||||||
|
if (typeof method === 'string') {
|
||||||
|
await this[method]();
|
||||||
|
} else {
|
||||||
|
await method(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update() {
|
||||||
|
this.set_standard_values();
|
||||||
|
this.set_keywords();
|
||||||
|
await this.trigger('validate', 'before_update');
|
||||||
|
await frappe.db.update(this.doctype, this.get_valid_dict());
|
||||||
|
await this.trigger('after_update', 'after_save');
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { Document: Document };
|
39
model/index.js
Normal file
39
model/index.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
module.exports = {
|
||||||
|
standard_fields: [
|
||||||
|
{
|
||||||
|
fieldname: 'name', fieldtype: 'Data', reqd: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'owner', fieldtype: 'Link', reqd: 1, options: 'User'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'modified_by', fieldtype: 'Link', reqd: 1, options: 'User'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'creation', fieldtype: 'Datetime', reqd: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'modified', fieldtype: 'Datetime', reqd: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'keywords', fieldtype: 'Text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'docstatus', fieldtype: 'Int', reqd: 1, default: 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
child_fields: [
|
||||||
|
{
|
||||||
|
fieldname: 'idx', fieldtype: 'Int', reqd: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'parent', fieldtype: 'Data', reqd: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'parenttype', fieldtype: 'Link', reqd: 1, options: 'DocType'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'parentfield', fieldtype: 'Data', reqd: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
115
model/meta.js
Normal file
115
model/meta.js
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
const Document = require('./document').Document;
|
||||||
|
const frappe = require('frappe-core');
|
||||||
|
|
||||||
|
class Meta extends Document {
|
||||||
|
constructor(data) {
|
||||||
|
super(data);
|
||||||
|
this.event_handlers = {};
|
||||||
|
this.list_options = {
|
||||||
|
fields: ['name', 'modified']
|
||||||
|
};
|
||||||
|
if (this.setup_meta) {
|
||||||
|
this.setup_meta();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_field(fieldname) {
|
||||||
|
if (!this.field_map) {
|
||||||
|
this.field_map = {};
|
||||||
|
for (let df of this.fields) {
|
||||||
|
this.field_map[df.fieldname] = df;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.field_map[fieldname];
|
||||||
|
}
|
||||||
|
|
||||||
|
on(key, fn) {
|
||||||
|
if (!this.event_handlers[key]) {
|
||||||
|
this.event_handlers[key] = [];
|
||||||
|
}
|
||||||
|
this.event_handlers[key].push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(fieldname, value) {
|
||||||
|
this[fieldname] = value;
|
||||||
|
await this.trigger(fieldname);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(fieldname) {
|
||||||
|
return this[fieldname];
|
||||||
|
}
|
||||||
|
|
||||||
|
get_valid_fields() {
|
||||||
|
if (!this._valid_fields) {
|
||||||
|
this._valid_fields = [];
|
||||||
|
|
||||||
|
// standard fields
|
||||||
|
for (let df of frappe.model.standard_fields) {
|
||||||
|
this._valid_fields.push(df);
|
||||||
|
}
|
||||||
|
|
||||||
|
// parent fields
|
||||||
|
if (this.istable) {
|
||||||
|
for (let df of frappe.model.child_fields) {
|
||||||
|
this._valid_fields.push(df);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// doctype fields
|
||||||
|
for (let df of this.fields) {
|
||||||
|
if (frappe.db.type_map[df.fieldtype]) {
|
||||||
|
this._valid_fields.push(df);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._valid_fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_keyword_fields() {
|
||||||
|
return this.keyword_fields || this.meta.fields.filter(df => df.reqd).map(df => df.fieldname);
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_select(df, value) {
|
||||||
|
let options = df.options;
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
// values given as string
|
||||||
|
options = df.options.split('\n');
|
||||||
|
}
|
||||||
|
if (!options.includes(value)) {
|
||||||
|
throw new frappe.ValueError(`${value} must be one of ${options.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async trigger(key, event = {}) {
|
||||||
|
|
||||||
|
Object.assign(event, {
|
||||||
|
doc: this,
|
||||||
|
name: key
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.event_handlers[key]) {
|
||||||
|
for (var handler of this.event_handlers[key]) {
|
||||||
|
await handler(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collections
|
||||||
|
async get_list({start, limit=20, filters}) {
|
||||||
|
return await frappe.db.get_all({
|
||||||
|
doctype: this.name,
|
||||||
|
fields: this.list_options.fields,
|
||||||
|
filters: filters,
|
||||||
|
start: start,
|
||||||
|
limit: limit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get_row_html(data) {
|
||||||
|
return `<a href="/view/${this.name}/${data.name}">${data.name}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Meta: Meta }
|
25
model/models.js
Normal file
25
model/models.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const process = require('process');
|
||||||
|
const frappe = require('frappe-core');
|
||||||
|
|
||||||
|
class Models {
|
||||||
|
constructor() {
|
||||||
|
this.data = {doctype: {}};
|
||||||
|
this.controllers = {};
|
||||||
|
this.meta_classes = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
get(doctype, name) {
|
||||||
|
return this.data[frappe.slug(doctype)][frappe.slug(name)];
|
||||||
|
}
|
||||||
|
|
||||||
|
get_controller(doctype) {
|
||||||
|
return this.controllers[frappe.slug(doctype)];
|
||||||
|
}
|
||||||
|
|
||||||
|
get_meta_class(doctype) {
|
||||||
|
return this.meta_classes[frappe.slug(doctype)] || frappe.meta.Meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Models: Models }
|
30
models/doctype/todo/todo.js
Normal file
30
models/doctype/todo/todo.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
|
||||||
|
class todo_meta extends frappe.meta.Meta {
|
||||||
|
setup_meta() {
|
||||||
|
Object.assign(this, require('./todo.json'));
|
||||||
|
this.name = 'ToDo';
|
||||||
|
this.list_options.fields = ['name', 'subject', 'status', 'description'];
|
||||||
|
}
|
||||||
|
|
||||||
|
get_row_html(data) {
|
||||||
|
return `<a href="#edit/todo/${data.name}">${data.subject}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class todo extends frappe.document.Document {
|
||||||
|
setup() {
|
||||||
|
this.add_handler('validate');
|
||||||
|
}
|
||||||
|
validate() {
|
||||||
|
if (!this.status) {
|
||||||
|
this.status = 'Open';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
todo: todo,
|
||||||
|
todo_meta: todo_meta
|
||||||
|
};
|
@ -3,6 +3,10 @@
|
|||||||
"name": "ToDo",
|
"name": "ToDo",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
|
"keyword_fields": [
|
||||||
|
"subject",
|
||||||
|
"description"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldname": "subject",
|
"fieldname": "subject",
|
@ -2,10 +2,10 @@
|
|||||||
"name": "frappe-core",
|
"name": "frappe-core",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Frappe Core",
|
"description": "Frappe Core",
|
||||||
"main": "frappe/index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha frappe/tests",
|
"test": "mocha tests",
|
||||||
"start": "nodemon frappe/app.js"
|
"start": "nodemon app.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.18.2",
|
"body-parser": "^1.18.2",
|
||||||
|
52
server/index.js
Normal file
52
server/index.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const backends = {};
|
||||||
|
backends.sqlite = require('frappe-core/backends/sqlite');
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
const frappe = require('frappe-core');
|
||||||
|
const rest_api = require('./rest_api')
|
||||||
|
const models = require('frappe-core/server/models');
|
||||||
|
const common = require('frappe-core/common');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async init() {
|
||||||
|
await frappe.init();
|
||||||
|
common.init_libs(frappe);
|
||||||
|
await frappe.login();
|
||||||
|
|
||||||
|
// walk and find models
|
||||||
|
models.init();
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
async init_db({backend, connection_params}) {
|
||||||
|
frappe.db = await new backends[backend].Database(connection_params);
|
||||||
|
await frappe.db.connect();
|
||||||
|
await frappe.db.migrate();
|
||||||
|
},
|
||||||
|
|
||||||
|
async start({backend, connection_params, static}) {
|
||||||
|
await this.init();
|
||||||
|
await this.init_db({backend:backend, connection_params:connection_params});
|
||||||
|
// database
|
||||||
|
|
||||||
|
// app
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
app.use(express.static('./'));
|
||||||
|
|
||||||
|
app.use(function (err, req, res, next) {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).send('Something broke!');
|
||||||
|
})
|
||||||
|
// routes
|
||||||
|
rest_api.setup(app);
|
||||||
|
|
||||||
|
// listen
|
||||||
|
frappe.app = app;
|
||||||
|
frappe.server = app.listen(frappe.config.port);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
54
server/models.js
Normal file
54
server/models.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
const walk = require('walk');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
init() {
|
||||||
|
const cwd = process.cwd();
|
||||||
|
|
||||||
|
const setup_model = (doctype, name, file_path) => {
|
||||||
|
// add to frappe.models.data
|
||||||
|
if (!frappe.models[doctype]) {
|
||||||
|
frappe.models[doctype] = {};
|
||||||
|
}
|
||||||
|
frappe.models.data[doctype][name] = require(file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup_controller = (doctype, file_path) => {
|
||||||
|
let _module = require(file_path);
|
||||||
|
frappe.models.controllers[doctype] = _module[doctype];
|
||||||
|
if (_module[doctype + '_meta']) {
|
||||||
|
frappe.models.meta_classes[doctype] = _module[doctype + '_meta'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// init for all apps
|
||||||
|
if (!frappe.config.apps) {
|
||||||
|
frappe.config.apps = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.config.apps.unshift('frappe-core');
|
||||||
|
|
||||||
|
// walk and sync
|
||||||
|
for (let app_name of frappe.config.apps) {
|
||||||
|
let start = path.resolve(require.resolve(app_name), '../models');
|
||||||
|
walk.walkSync(start, {
|
||||||
|
listeners: {
|
||||||
|
file: (basepath, file_data, next) => {
|
||||||
|
const doctype = path.basename(path.dirname(basepath));
|
||||||
|
const name = path.basename(basepath);
|
||||||
|
const file_path = path.resolve(basepath, file_data.name);
|
||||||
|
if (file_data.name.endsWith('.json')) {
|
||||||
|
setup_model(doctype, name, file_path)
|
||||||
|
}
|
||||||
|
if (doctype==='doctype' && file_data.name.endsWith('.js')) {
|
||||||
|
setup_controller(path.basename(basepath), file_path);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
61
server/rest_api.js
Normal file
61
server/rest_api.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
setup(app) {
|
||||||
|
// get list
|
||||||
|
app.get('/api/resource/:doctype', frappe.async_handler(async function(request, response) {
|
||||||
|
let fields, filters;
|
||||||
|
for (key of ['fields', 'filters']) {
|
||||||
|
if (request.query[key]) {
|
||||||
|
request.query[key] = JSON.parse(request.query[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = await frappe.db.get_all({
|
||||||
|
doctype: request.params.doctype,
|
||||||
|
fields: request.query.fields || ['name', 'subject'],
|
||||||
|
filters: request.query.filters,
|
||||||
|
start: request.query.start || 0,
|
||||||
|
limit: request.query.limit || 20,
|
||||||
|
order_by: request.query.order_by,
|
||||||
|
order: request.query.order
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.json(data);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// create
|
||||||
|
app.post('/api/resource/:doctype', frappe.async_handler(async function(request, response) {
|
||||||
|
data = request.body;
|
||||||
|
data.doctype = request.params.doctype;
|
||||||
|
let doc = await frappe.get_doc(data);
|
||||||
|
await doc.insert();
|
||||||
|
await frappe.db.commit();
|
||||||
|
return response.json(doc.get_valid_dict());
|
||||||
|
}));
|
||||||
|
|
||||||
|
// update
|
||||||
|
app.put('/api/resource/:doctype/:name', frappe.async_handler(async function(request, response) {
|
||||||
|
data = request.body;
|
||||||
|
let doc = await frappe.get_doc(request.params.doctype, request.params.name);
|
||||||
|
Object.assign(doc, data);
|
||||||
|
await doc.update();
|
||||||
|
await frappe.db.commit();
|
||||||
|
return response.json(doc.get_valid_dict());
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
// get document
|
||||||
|
app.get('/api/resource/:doctype/:name', frappe.async_handler(async function(request, response) {
|
||||||
|
let doc = await frappe.get_doc(request.params.doctype, request.params.name);
|
||||||
|
return response.json(doc.get_valid_dict());
|
||||||
|
}));
|
||||||
|
|
||||||
|
// delete
|
||||||
|
app.delete('/api/resource/:doctype/:name', frappe.async_handler(async function(request, response) {
|
||||||
|
let doc = await frappe.get_doc(request.params.doctype, request.params.name)
|
||||||
|
await doc.delete();
|
||||||
|
return response.json({});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
17
session.js
Normal file
17
session.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const frappe = require('frappe-core');
|
||||||
|
|
||||||
|
class Session {
|
||||||
|
constructor(user, user_key) {
|
||||||
|
this.user = user || 'guest';
|
||||||
|
if (this.user !== 'guest') {
|
||||||
|
this.login(user_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
login(user_key) {
|
||||||
|
// could be password, sessionid, otp
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Session: Session };
|
11
tests/helpers.js
Normal file
11
tests/helpers.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const server = require('frappe-core/server');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async init_sqlite() {
|
||||||
|
server.init()
|
||||||
|
server.init_db({
|
||||||
|
backend: 'sqlite',
|
||||||
|
connection_params: {db_path: 'test.db'}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
18
tests/test_controller.js
Normal file
18
tests/test_controller.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const frappe = require('frappe-core');
|
||||||
|
const helpers = require('./helpers');
|
||||||
|
|
||||||
|
describe('Controller', () => {
|
||||||
|
before(async function() {
|
||||||
|
await helpers.init_sqlite();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call controller method', async () => {
|
||||||
|
let doc = await frappe.get_doc({
|
||||||
|
doctype:'ToDo',
|
||||||
|
subject: 'test'
|
||||||
|
});
|
||||||
|
doc.trigger('validate');
|
||||||
|
assert.equal(doc.status, 'Open');
|
||||||
|
});
|
||||||
|
});
|
49
tests/test_database.js
Normal file
49
tests/test_database.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const frappe = require('frappe-core');
|
||||||
|
const helpers = require('./helpers');
|
||||||
|
|
||||||
|
describe('Database', () => {
|
||||||
|
before(async function() {
|
||||||
|
await helpers.init_sqlite();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert and get values', async () => {
|
||||||
|
await frappe.db.sql('delete from todo');
|
||||||
|
await frappe.insert({doctype:'ToDo', subject: 'testing 1'});
|
||||||
|
await frappe.insert({doctype:'ToDo', subject: 'testing 3'});
|
||||||
|
await frappe.insert({doctype:'ToDo', subject: 'testing 2'});
|
||||||
|
|
||||||
|
let subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject']})
|
||||||
|
subjects = subjects.map(d => d.subject);
|
||||||
|
|
||||||
|
assert.ok(subjects.includes('testing 1'));
|
||||||
|
assert.ok(subjects.includes('testing 2'));
|
||||||
|
assert.ok(subjects.includes('testing 3'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter correct values', async () => {
|
||||||
|
let subjects = null;
|
||||||
|
|
||||||
|
await frappe.db.sql('delete from todo');
|
||||||
|
await frappe.insert({doctype:'ToDo', subject: 'testing 1', status: 'Open'});
|
||||||
|
await frappe.insert({doctype:'ToDo', subject: 'testing 3', status: 'Open'});
|
||||||
|
await frappe.insert({doctype:'ToDo', subject: 'testing 2', status: 'Closed'});
|
||||||
|
|
||||||
|
subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject'],
|
||||||
|
filters:{status: 'Open'}});
|
||||||
|
subjects = subjects.map(d => d.subject);
|
||||||
|
|
||||||
|
assert.ok(subjects.includes('testing 1'));
|
||||||
|
assert.ok(subjects.includes('testing 3'));
|
||||||
|
assert.equal(subjects.includes('testing 2'), false);
|
||||||
|
|
||||||
|
subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject'],
|
||||||
|
filters:{status: 'Closed'}});
|
||||||
|
subjects = subjects.map(d => d.subject);
|
||||||
|
|
||||||
|
assert.equal(subjects.includes('testing 1'), false);
|
||||||
|
assert.equal(subjects.includes('testing 3'), false);
|
||||||
|
assert.ok(subjects.includes('testing 2'));
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
75
tests/test_document.js
Normal file
75
tests/test_document.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const frappe = require('frappe-core');
|
||||||
|
const helpers = require('./helpers');
|
||||||
|
|
||||||
|
describe('Document', () => {
|
||||||
|
before(async function() {
|
||||||
|
await helpers.init_sqlite();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert a doc', async () => {
|
||||||
|
let doc1 = await test_doc();
|
||||||
|
doc1.subject = 'insert subject 1';
|
||||||
|
doc1.description = 'insert description 1';
|
||||||
|
await doc1.insert();
|
||||||
|
|
||||||
|
// get it back from the db
|
||||||
|
let doc2 = await frappe.get_doc(doc1.doctype, doc1.name);
|
||||||
|
assert.equal(doc1.subject, doc2.subject);
|
||||||
|
assert.equal(doc1.description, doc2.description);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update a doc', async () => {
|
||||||
|
let doc = await test_doc();
|
||||||
|
await doc.insert();
|
||||||
|
|
||||||
|
assert.notEqual(await frappe.db.get_value(doc.doctype, doc.name, 'subject'), 'subject 2');
|
||||||
|
|
||||||
|
doc.subject = 'subject 2'
|
||||||
|
await doc.update();
|
||||||
|
|
||||||
|
assert.equal(await frappe.db.get_value(doc.doctype, doc.name, 'subject'), 'subject 2');
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get a value', async () => {
|
||||||
|
let doc = await test_doc();
|
||||||
|
assert.equal(doc.get('subject'), 'testing 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set a value', async () => {
|
||||||
|
let doc = await test_doc();
|
||||||
|
doc.set('subject', 'testing 1')
|
||||||
|
assert.equal(doc.get('subject'), 'testing 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow incorrect Select option', async () => {
|
||||||
|
let doc = await test_doc();
|
||||||
|
assert.throws(
|
||||||
|
() => {
|
||||||
|
doc.set('status', 'Illegal');
|
||||||
|
},
|
||||||
|
frappe.ValueError
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete a document', async () => {
|
||||||
|
let doc = await test_doc();
|
||||||
|
await doc.insert();
|
||||||
|
|
||||||
|
assert.equal(await frappe.db.get_value(doc.doctype, doc.name), doc.name);
|
||||||
|
|
||||||
|
await doc.delete();
|
||||||
|
|
||||||
|
assert.equal(await frappe.db.get_value(doc.doctype, doc.name), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
async function test_doc() {
|
||||||
|
return await frappe.get_doc({
|
||||||
|
doctype: 'ToDo',
|
||||||
|
status: 'Open',
|
||||||
|
subject: 'testing 1',
|
||||||
|
description: 'test description 1'
|
||||||
|
});
|
||||||
|
}
|
22
tests/test_meta.js
Normal file
22
tests/test_meta.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const frappe = require('frappe-core');
|
||||||
|
const helpers = require('./helpers');
|
||||||
|
|
||||||
|
describe('Meta', () => {
|
||||||
|
before(async function() {
|
||||||
|
await helpers.init_sqlite();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get init from json file', () => {
|
||||||
|
let todo = frappe.get_meta('ToDo');
|
||||||
|
assert.equal(todo.issingle, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get fields from meta', () => {
|
||||||
|
let todo = frappe.get_meta('ToDo');
|
||||||
|
let fields = todo.fields.map((df) => df.fieldname);
|
||||||
|
assert.ok(fields.includes('subject'));
|
||||||
|
assert.ok(fields.includes('description'));
|
||||||
|
assert.ok(fields.includes('status'));
|
||||||
|
});
|
||||||
|
});
|
14
tests/test_models.js
Normal file
14
tests/test_models.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const frappe = require('frappe-core');
|
||||||
|
const helpers = require('./helpers');
|
||||||
|
|
||||||
|
describe('Models', () => {
|
||||||
|
before(async function() {
|
||||||
|
await helpers.init_sqlite();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get todo json', () => {
|
||||||
|
let todo = frappe.models.get('DocType', 'ToDo');
|
||||||
|
assert.equal(todo.issingle, 0);
|
||||||
|
});
|
||||||
|
});
|
66
tests/test_rest_api.js
Normal file
66
tests/test_rest_api.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const frappe = require('frappe-core');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
const helpers = require('./helpers');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const process = require('process');
|
||||||
|
const Database = require('frappe-core/backends/rest_client').Database
|
||||||
|
|
||||||
|
// create a copy of frappe
|
||||||
|
|
||||||
|
var test_server;
|
||||||
|
|
||||||
|
describe('REST', () => {
|
||||||
|
before(async function() {
|
||||||
|
test_server = spawn('node', ['tests/test_server.js'], {
|
||||||
|
stdio: [process.stdin, process.stdout, process.stderr, 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
await frappe.init();
|
||||||
|
await frappe.login();
|
||||||
|
|
||||||
|
frappe.db = await new Database({server: 'localhost:8000'});
|
||||||
|
frappe.fetch = fetch;
|
||||||
|
|
||||||
|
// wait for server to start
|
||||||
|
await frappe.sleep(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
frappe.close();
|
||||||
|
test_server.kill();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a document', async () => {
|
||||||
|
let doc = await frappe.get_doc({doctype:'ToDo', subject:'test rest insert 1'});
|
||||||
|
await doc.insert();
|
||||||
|
|
||||||
|
let doc1 = await frappe.get_doc('ToDo', doc.name);
|
||||||
|
|
||||||
|
assert.equal(doc.subject, doc1.subject);
|
||||||
|
assert.equal(doc1.status, 'Open');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update a document', async () => {
|
||||||
|
let doc = await frappe.get_doc({doctype:'ToDo', subject:'test rest insert 1'});
|
||||||
|
await doc.insert();
|
||||||
|
|
||||||
|
doc.subject = 'subject changed';
|
||||||
|
await doc.update();
|
||||||
|
|
||||||
|
let doc1 = await frappe.get_doc('ToDo', doc.name);
|
||||||
|
assert.equal(doc.subject, doc1.subject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get multiple documents', async () => {
|
||||||
|
await frappe.insert({doctype:'ToDo', subject:'all test 1'});
|
||||||
|
await frappe.insert({doctype:'ToDo', subject:'all test 2'});
|
||||||
|
|
||||||
|
let data = await frappe.db.get_all({doctype:'ToDo'});
|
||||||
|
let subjects = data.map(d => d.subject);
|
||||||
|
assert.ok(subjects.includes('all test 1'));
|
||||||
|
assert.ok(subjects.includes('all test 2'));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
5
tests/test_server.js
Normal file
5
tests/test_server.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const server = require('frappe-core/server');
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
server.start({backend: 'sqlite', connection_params: {db_path: 'test.db'}});
|
||||||
|
}
|
16
utils/index.js
Normal file
16
utils/index.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
slug(text) {
|
||||||
|
return text.toLowerCase().replace(/ /g, '_');
|
||||||
|
},
|
||||||
|
|
||||||
|
async_handler(fn) {
|
||||||
|
return (req, res, next) => Promise.resolve(fn(req, res, next))
|
||||||
|
.catch(next);
|
||||||
|
},
|
||||||
|
|
||||||
|
async sleep(seconds) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(resolve, seconds * 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user