2
0
mirror of https://github.com/frappe/books.git synced 2024-12-24 11:55:46 +00:00
File doctype

- Upload files using multer
- uploadFiles API in http.js
- Link File to doctype with name and field
- Refactor File component for fullpaths
- frappe.db.setValue(s) API
This commit is contained in:
Faris Ansari 2018-08-18 21:24:17 +05:30 committed by GitHub
parent 1aabf7ef40
commit 375325d917
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1166 additions and 906 deletions

View File

@ -30,7 +30,7 @@ module.exports = class Database extends Observable {
await this.commit(); await this.commit();
} }
async createTable(doctype, newName=null) { async createTable(doctype, newName = null) {
let meta = frappe.getMeta(doctype); let meta = frappe.getMeta(doctype);
let columns = []; let columns = [];
let indexes = []; let indexes = [];
@ -112,8 +112,8 @@ module.exports = class Database extends Observable {
let foreignKeys = await this.getForeignKeys(doctype); let foreignKeys = await this.getForeignKeys(doctype);
let newForeignKeys = []; let newForeignKeys = [];
let meta = frappe.getMeta(doctype); let meta = frappe.getMeta(doctype);
for (let field of meta.getValidFields({ withChildren: false})) { for (let field of meta.getValidFields({ withChildren: false })) {
if (field.fieldtype==='Link' && !foreignKeys.includes(field.fieldname)) { if (field.fieldtype === 'Link' && !foreignKeys.includes(field.fieldname)) {
newForeignKeys.push(field); newForeignKeys.push(field);
} }
} }
@ -138,7 +138,7 @@ module.exports = class Database extends Observable {
// alter table {doctype} add column ({column_def}); // alter table {doctype} add column ({column_def});
} }
async get(doctype, name=null, fields = '*') { async get(doctype, name = null, fields = '*') {
let meta = frappe.getMeta(doctype); let meta = frappe.getMeta(doctype);
let doc; let doc;
if (meta.isSingle) { if (meta.isSingle) {
@ -195,8 +195,8 @@ module.exports = class Database extends Observable {
} }
triggerChange(doctype, name) { triggerChange(doctype, name) {
this.trigger(`change:${doctype}`, {name:name}, 500); this.trigger(`change:${doctype}`, { name: name }, 500);
this.trigger(`change`, {doctype:name, name:name}, 500); this.trigger(`change`, { doctype: name, name: name }, 500);
} }
async insert(doctype, doc) { async insert(doctype, doc) {
@ -281,7 +281,7 @@ module.exports = class Database extends Observable {
async updateSingle(meta, doc, doctype) { async updateSingle(meta, doc, doctype) {
await this.deleteSingleValues(); await this.deleteSingleValues();
for (let field of meta.getValidFields({withChildren: false})) { for (let field of meta.getValidFields({ withChildren: false })) {
let value = doc[field.fieldname]; let value = doc[field.fieldname];
if (value) { if (value) {
let singleValue = frappe.newDoc({ let singleValue = frappe.newDoc({
@ -316,8 +316,14 @@ module.exports = class Database extends Observable {
getFormattedValues(fields, doc) { getFormattedValues(fields, doc) {
let values = fields.map(field => { let values = fields.map(field => {
let value = doc[field.fieldname]; let value = doc[field.fieldname];
return this.getFormattedValue(field, value);
});
return values;
}
getFormattedValue(field, value) {
if (value instanceof Date) { if (value instanceof Date) {
if (field.fieldtype==='Date') { if (field.fieldtype === 'Date') {
// date // date
return value.toISOString().substr(0, 10); return value.toISOString().substr(0, 10);
} else { } else {
@ -331,8 +337,6 @@ module.exports = class Database extends Observable {
} else { } else {
return value; return value;
} }
});
return values;
} }
async deleteMany(doctype, names) { async deleteMany(doctype, names) {
@ -382,6 +386,16 @@ module.exports = class Database extends Observable {
return row.length ? row[0][fieldname] : null; return row.length ? row[0][fieldname] : null;
} }
async setValue(doctype, name, fieldname, value) {
return await this.setValues(doctype, name, {
[fieldname]: value
});
}
async setValues(doctype, name, fieldValuePair) {
//
}
getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', order = 'desc' } = {}) { getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', order = 'desc' } = {}) {
// select {fields} from {doctype} where {filters} order by {orderBy} {order} limit {start} {limit} // select {fields} from {doctype} where {filters} order by {orderBy} {order} limit {start} {limit}
} }

View File

@ -21,14 +21,21 @@ module.exports = class HTTPClient extends Observable {
async insert(doctype, doc) { async insert(doctype, doc) {
doc.doctype = doctype; doc.doctype = doctype;
let filesToUpload = this.getFilesToUpload(doc);
let url = this.getURL('/api/resource', doctype); let url = this.getURL('/api/resource', doctype);
return await this.fetch(url, {
const responseDoc = await this.fetch(url, {
method: 'POST', method: 'POST',
body: JSON.stringify(doc) body: JSON.stringify(doc)
}) });
await this.uploadFilesAndUpdateDoc(filesToUpload, doctype, responseDoc);
return responseDoc;
} }
async get(doctype, name) { async get(doctype, name) {
name = encodeURIComponent(name);
let url = this.getURL('/api/resource', doctype, name); let url = this.getURL('/api/resource', doctype, name);
return await this.fetch(url, { return await this.fetch(url, {
method: 'GET', method: 'GET',
@ -36,15 +43,15 @@ module.exports = class HTTPClient extends Observable {
}) })
} }
async getAll({ doctype, fields, filters, start, limit, sort_by, order }) { async getAll({ doctype, fields, filters, start, limit, sortBy, order }) {
let url = this.getURL('/api/resource', doctype); let url = this.getURL('/api/resource', doctype);
url = url + "?" + frappe.getQueryString({ url = url + '?' + frappe.getQueryString({
fields: JSON.stringify(fields), fields: JSON.stringify(fields),
filters: JSON.stringify(filters), filters: JSON.stringify(filters),
start: start, start: start,
limit: limit, limit: limit,
sort_by: sort_by, sortBy: sortBy,
order: order order: order
}); });
@ -55,12 +62,17 @@ module.exports = class HTTPClient extends Observable {
async update(doctype, doc) { async update(doctype, doc) {
doc.doctype = doctype; doc.doctype = doctype;
let filesToUpload = this.getFilesToUpload(doc);
let url = this.getURL('/api/resource', doctype, doc.name); let url = this.getURL('/api/resource', doctype, doc.name);
return await this.fetch(url, { const responseDoc = await this.fetch(url, {
method: 'PUT', method: 'PUT',
body: JSON.stringify(doc) body: JSON.stringify(doc)
}); });
await this.uploadFilesAndUpdateDoc(filesToUpload, doctype, responseDoc);
return responseDoc;
} }
async delete(doctype, name) { async delete(doctype, name) {
@ -104,6 +116,57 @@ module.exports = class HTTPClient extends Observable {
return data; return data;
} }
getFilesToUpload(doc) {
const meta = frappe.getMeta(doc.doctype);
const fileFields = meta.getFieldsWith({ fieldtype: 'File' });
const filesToUpload = [];
if (fileFields.length > 0) {
fileFields.forEach(df => {
const files = doc[df.fieldname] || [];
if (files.length) {
filesToUpload.push({
fieldname: df.fieldname,
files: files
})
}
delete doc[df.fieldname];
});
}
return filesToUpload;
}
async uploadFilesAndUpdateDoc(filesToUpload, doctype, doc) {
if (filesToUpload.length > 0) {
// upload files
for (const fileToUpload of filesToUpload) {
const files = await this.uploadFiles(fileToUpload.files, doctype, doc.name, fileToUpload.fieldname);
doc[fileToUpload.fieldname] = files[0].name;
}
}
}
async uploadFiles(fileList, doctype, name, fieldname) {
let url = this.getURL('/api/upload', doctype, name, fieldname);
let formData = new FormData();
for (const file of fileList) {
formData.append('files', file, file.name);
}
let response = await frappe.fetch(url, {
method: 'POST',
body: formData
});
const data = await response.json();
if (response.status !== 200) {
throw Error(data.error);
}
return data;
}
getURL(...parts) { getURL(...parts) {
return this.protocol + '://' + this.server + (parts || []).join('/'); return this.protocol + '://' + this.server + (parts || []).join('/');
} }

View File

@ -68,7 +68,7 @@ module.exports = class sqliteDatabase extends Database {
columns.push(def); columns.push(def);
if (field.fieldtype==='Link' && field.target) { if (field.fieldtype === 'Link' && field.target) {
indexes.push(`FOREIGN KEY (${field.fieldname}) REFERENCES ${field.target} ON UPDATE CASCADE ON DELETE RESTRICT`); indexes.push(`FOREIGN KEY (${field.fieldname}) REFERENCES ${field.target} ON UPDATE CASCADE ON DELETE RESTRICT`);
} }
} }
@ -154,6 +154,30 @@ module.exports = class sqliteDatabase extends Database {
await frappe.db.run('delete from SingleValue where parent=?', name) await frappe.db.run('delete from SingleValue where parent=?', name)
} }
async setValues(doctype, name, fieldValuePair) {
const meta = frappe.getMeta(doctype);
const validFields = this.getKeys(doctype);
const validFieldnames = validFields.map(df => df.fieldname);
const fieldsToUpdate = Object.keys(fieldValuePair)
.filter(fieldname => validFieldnames.includes(fieldname))
// assignment part of query
const assigns = fieldsToUpdate.map(fieldname => `${fieldname} = ?`);
// values
const values = fieldsToUpdate.map(fieldname => {
const field = meta.getField(fieldname);
const value = fieldValuePair[fieldname];
return this.getFormattedValue(field, value);
});
// additional name for where clause
values.push(name);
return await this.run(`update ${doctype}
set ${assigns.join(', ')} where name=?`, values);
}
getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', groupBy, order = 'desc' } = {}) { getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', groupBy, order = 'desc' } = {}) {
if (!fields) { if (!fields) {
fields = frappe.getMeta(doctype).getKeywordFields(); fields = frappe.getMeta(doctype).getKeywordFields();

View File

@ -29,6 +29,25 @@ module.exports = class BaseMeta extends BaseDocument {
return this._field_map[fieldname]; return this._field_map[fieldname];
} }
/**
* Get fields filtered by filters
* @param {Object} filters
*
* Usage:
* meta = frappe.getMeta('ToDo')
* dataFields = meta.getFieldsWith({ fieldtype: 'Data' })
*/
getFieldsWith(filters) {
return this.fields.filter(df => {
let match = true;
for (const key in filters) {
const value = filters[key];
match = df[key] === value;
}
return match;
});
}
getLabel(fieldname) { getLabel(fieldname) {
return this.getField(fieldname).label; return this.getField(fieldname).label;
} }

View File

@ -0,0 +1,67 @@
module.exports = {
name: 'File',
doctype: 'DocType',
isSingle: 0,
keywordFields: [
'name',
'filename'
],
fields: [
{
fieldname: 'name',
label: 'File Path',
fieldtype: 'Data',
required: 1,
},
{
fieldname: 'filename',
label: 'File Name',
fieldtype: 'Data',
required: 1,
},
{
fieldname: 'mimetype',
label: 'MIME Type',
fieldtype: 'Data',
},
{
fieldname: 'size',
label: 'File Size',
fieldtype: 'Int',
},
{
fieldname: 'referenceDoctype',
label: 'Reference DocType',
fieldtype: 'Data',
},
{
fieldname: 'referenceName',
label: 'Reference Name',
fieldtype: 'Data',
},
{
fieldname: 'referenceField',
label: 'Reference Field',
fieldtype: 'Data',
},
],
layout: [
{
columns: [
{ fields: ['filename'] },
]
},
{
columns: [
{ fields: ['mimetype'] },
{ fields: ['size'] },
]
},
{
columns: [
{ fields: ['referenceDoctype'] },
{ fields: ['referenceName'] },
]
},
]
}

View File

@ -1,47 +1,47 @@
const indicatorColor = require('frappejs/ui/constants/indicators'); const { BLUE, GREEN } = require('frappejs/ui/constants/indicators');
module.exports = { module.exports = {
name: "ToDo", name: 'ToDo',
label: "To Do", label: 'To Do',
naming: "autoincrement", naming: 'autoincrement',
pageSettings: { pageSettings: {
hideTitle: true hideTitle: true
}, },
"isSingle": 0, isSingle: 0,
"keywordFields": [ keywordFields: [
"subject", 'subject',
"description" 'description'
], ],
titleField: 'subject', titleField: 'subject',
indicators: { indicators: {
key: 'status', key: 'status',
colors: { colors: {
Open: indicatorColor.BLUE, Open: BLUE,
Closed: indicatorColor.GREEN Closed: GREEN
} }
}, },
"fields": [ fields: [
{ {
"fieldname": "subject", fieldname: 'subject',
"label": "Subject", label: 'Subject',
"fieldtype": "Data", fieldtype: 'Data',
"required": 1 required: 1
}, },
{ {
"fieldname": "status", fieldname: 'status',
"label": "Status", label: 'Status',
"fieldtype": "Select", fieldtype: 'Select',
"options": [ options: [
"Open", 'Open',
"Closed" 'Closed'
], ],
"default": "Open", default: 'Open',
"required": 1 required: 1
}, },
{ {
"fieldname": "description", fieldname: 'description',
"label": "Description", label: 'Description',
"fieldtype": "Text" fieldtype: 'Text'
} }
], ],

View File

@ -11,6 +11,7 @@ module.exports = {
SystemSettings: require('./doctype/SystemSettings/SystemSettings.js'), SystemSettings: require('./doctype/SystemSettings/SystemSettings.js'),
ToDo: require('./doctype/ToDo/ToDo.js'), ToDo: require('./doctype/ToDo/ToDo.js'),
User: require('./doctype/User/User.js'), User: require('./doctype/User/User.js'),
UserRole: require('./doctype/UserRole/UserRole.js') UserRole: require('./doctype/UserRole/UserRole.js'),
File: require('./doctype/File/File.js'),
} }
} }

View File

@ -37,6 +37,7 @@
"luxon": "^1.0.0", "luxon": "^1.0.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"morgan": "^1.9.0", "morgan": "^1.9.0",
"multer": "^1.3.1",
"mysql": "^2.15.0", "mysql": "^2.15.0",
"node-fetch": "^1.7.3", "node-fetch": "^1.7.3",
"node-sass": "^4.7.2", "node-sass": "^4.7.2",

View File

@ -18,12 +18,15 @@ const auth = require('./../auth/auth')();
const morgan = require('morgan'); const morgan = require('morgan');
const { addWebpackMiddleware } = require('../webpack/serve'); const { addWebpackMiddleware } = require('../webpack/serve');
const { getAppConfig } = require('../webpack/utils'); const { getAppConfig } = require('../webpack/utils');
const appConfig = getAppConfig();
frappe.conf = getAppConfig();
require.extensions['.html'] = function (module, filename) { require.extensions['.html'] = function (module, filename) {
module.exports = fs.readFileSync(filename, 'utf8'); module.exports = fs.readFileSync(filename, 'utf8');
}; };
process.env.NODE_ENV = 'development';
module.exports = { module.exports = {
async start({backend, connectionParams, models, authConfig=null}) { async start({backend, connectionParams, models, authConfig=null}) {
await this.init(); await this.init();
@ -39,9 +42,8 @@ module.exports = {
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
for (let staticPath of [appConfig.distPath, appConfig.staticPath]) { app.use(express.static(frappe.conf.distPath));
app.use(express.static(staticPath)); app.use('/static', express.static(frappe.conf.staticPath))
}
app.use(morgan('tiny')); app.use(morgan('tiny'));
@ -65,7 +67,7 @@ module.exports = {
addWebpackMiddleware(app); addWebpackMiddleware(app);
} }
frappe.config.port = appConfig.dev.devServerPort frappe.config.port = frappe.conf.dev.devServerPort;
// listen // listen
server.listen(frappe.config.port, () => { server.listen(frappe.config.port, () => {

View File

@ -1,4 +1,6 @@
const frappe = require('frappejs'); const frappe = require('frappejs');
const path = require('path');
const multer = require('multer');
module.exports = { module.exports = {
setup(app) { setup(app) {
@ -43,6 +45,45 @@ module.exports = {
return response.json(doc.getValidDict()); return response.json(doc.getValidDict());
})); }));
const upload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => {
cb(null, frappe.conf.staticPath)
},
filename: (req, file, cb) => {
const filename = file.originalname.split('.')[0];
const extension = path.extname(file.originalname);
const now = Date.now();
cb(null, filename + '-' + now + extension);
}
})
});
app.post('/api/upload/:doctype/:name/:fieldname', upload.array('files', 10), frappe.asyncHandler(async function(request, response) {
const files = request.files;
const { doctype, name, fieldname } = request.params;
let fileDocs = [];
for (let file of files) {
const doc = frappe.newDoc({
doctype: 'File',
name: path.join('/', file.path),
filename: file.originalname,
mimetype: file.mimetype,
size: file.size,
referenceDoctype: doctype,
referenceName: name,
referenceFieldname: fieldname
});
await doc.insert();
await frappe.db.setValue(doctype, name, fieldname, doc.name);
fileDocs.push(doc.getValidDict());
}
return response.json(fileDocs);
}));
// get document // get document
app.get('/api/resource/:doctype/:name', frappe.asyncHandler(async function(request, response) { app.get('/api/resource/:doctype/:name', frappe.asyncHandler(async function(request, response) {

View File

@ -1,6 +1,6 @@
<template> <template>
<form :class="['frappe-form-layout', { 'was-validated': invalid }]"> <form :class="['frappe-form-layout', { 'was-validated': invalid }]">
<div class="row" v-if="layoutConfig" <div class="form-row" v-if="layoutConfig"
v-for="(section, i) in layoutConfig.sections" :key="i" v-for="(section, i) in layoutConfig.sections" :key="i"
v-show="showSection(i)" v-show="showSection(i)"
> >

View File

@ -3,7 +3,7 @@
<list-actions <list-actions
:doctype="doctype" :doctype="doctype"
:showDelete="checkList.length" :showDelete="checkList.length"
@new="newDoc" @new="$emit('newDoc')"
@delete="deleteCheckedItems" @delete="deleteCheckedItems"
/> />
<ul class="list-group"> <ul class="list-group">
@ -60,18 +60,18 @@ export default {
this.updateList(); this.updateList();
}, },
methods: { methods: {
async newDoc() { async updateList(query = null) {
let doc = await frappe.getNewDoc(this.doctype); let filters = null;
this.$router.push(`/edit/${this.doctype}/${doc.name}`);
},
async updateList(query=null) {
let filters = null
if (query) { if (query) {
filters = { filters = {
keywords : ['like', query] keywords: ['like', query]
};
} }
}
const indicatorField = this.hasIndicator ? this.meta.indicators.key : null; const indicatorField = this.hasIndicator
? this.meta.indicators.key
: null;
const fields = [ const fields = [
'name', 'name',
indicatorField, indicatorField,
@ -89,7 +89,7 @@ export default {
}, },
openForm(name) { openForm(name) {
this.activeItem = name; this.activeItem = name;
this.$router.push(`/edit/${this.doctype}/${name}`); this.$emit('openForm', name);
}, },
async deleteCheckedItems() { async deleteCheckedItems() {
await frappe.db.deleteMany(this.doctype, this.checkList); await frappe.db.deleteMany(this.doctype, this.checkList);
@ -112,7 +112,7 @@ export default {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../styles/variables"; @import '../../styles/variables';
.list-group-item { .list-group-item {
border-left: none; border-left: none;

View File

@ -3,38 +3,55 @@ import Base from './Base';
export default { export default {
extends: Base, extends: Base,
computed: {
inputClass() {
return ['d-none'];
}
},
methods: { methods: {
getWrapperElement(h) { getWrapperElement(h) {
let fileName = this.docfield.placeholder || this._('Choose a file..'); let fileName = this.docfield.placeholder || this._('Choose a file..');
let filePath = null;
if (this.$refs.input && this.$refs.input.files.length) { if (this.value && typeof this.value === 'string') {
filePath = this.value;
}
else if (this.$refs.input && this.$refs.input.files.length) {
fileName = this.$refs.input.files[0].name; fileName = this.$refs.input.files[0].name;
} }
const fileButton = h('button', { const fileLink = h('a', {
class: ['btn btn-outline-secondary btn-block'],
domProps: {
textContent: fileName
},
attrs: { attrs: {
type: 'button' href: filePath,
target: '_blank'
}, },
on: { domProps: {
click: () => this.$refs.input.click() textContent: this._('View File')
} }
}); });
return h('div', { const helpText = h('small', {
class: 'form-text text-muted'
}, [fileLink]);
const fileNameLabel = h('label', {
class: ['custom-file-label'],
domProps: {
textContent: filePath || fileName
}
});
const fileInputWrapper = h('div', {
class: ['custom-file']
},
[this.getInputElement(h), fileNameLabel, filePath ? helpText : null]
);
return h(
'div',
{
class: ['form-group', ...this.wrapperClass], class: ['form-group', ...this.wrapperClass],
attrs: { attrs: {
'data-fieldname': this.docfield.fieldname 'data-fieldname': this.docfield.fieldname
} }
}, [this.getLabelElement(h), this.getInputElement(h), fileButton]); },
[this.getLabelElement(h), fileInputWrapper]
);
}, },
getInputAttrs() { getInputAttrs() {
return { return {
@ -48,6 +65,9 @@ export default {
accept: (this.docfield.filetypes || []).join(',') accept: (this.docfield.filetypes || []).join(',')
}; };
}, },
getInputClass() {
return 'custom-file-input';
},
getInputListeners() { getInputListeners() {
return { return {
change: e => { change: e => {

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="frappe-list-form row no-gutters"> <div class="frappe-list-form row no-gutters">
<div class="col-4 border-right"> <div class="col-4 border-right">
<frappe-list :doctype="doctype" :key="doctype" /> <frappe-list :doctype="doctype" :key="doctype" @newDoc="openNewDoc" @openForm="openForm" />
</div> </div>
<div class="col-8"> <div class="col-8">
<frappe-form v-if="name" :key="doctype + name" :doctype="doctype" :name="name" @save="onSave" /> <frappe-form v-if="name" :key="doctype + name" :doctype="doctype" :name="name" @save="onSave" />
@ -23,9 +23,17 @@ export default {
if (doc.name !== this.$route.params.name) { if (doc.name !== this.$route.params.name) {
this.$router.push(`/edit/${doc.doctype}/${doc.name}`); this.$router.push(`/edit/${doc.doctype}/${doc.name}`);
} }
},
openForm(name) {
name = encodeURIComponent(name);
this.$router.push(`/edit/${this.doctype}/${name}`);
},
async openNewDoc() {
let doc = await frappe.getNewDoc(this.doctype);
this.$router.push(`/edit/${this.doctype}/${doc.name}`);
} }
} }
} };
</script> </script>
<style> <style>
.frappe-list-form { .frappe-list-form {