mirror of https://github.com/wekan/wekan
parent
1742bcd9b1
commit
a570c4a861
@ -0,0 +1,320 @@ |
||||
models / exporter.js; |
||||
const stringify = require('csv-stringify'); |
||||
|
||||
// exporter maybe is broken since Gridfs introduced, add fs and path
|
||||
export class Exporter { |
||||
constructor(boardId) { |
||||
this._boardId = boardId; |
||||
} |
||||
|
||||
build() { |
||||
const fs = Npm.require('fs'); |
||||
const os = Npm.require('os'); |
||||
const path = Npm.require('path'); |
||||
|
||||
const byBoard = { boardId: this._boardId }; |
||||
const byBoardNoLinked = { |
||||
boardId: this._boardId, |
||||
linkedId: { $in: ['', null] }, |
||||
}; |
||||
// we do not want to retrieve boardId in related elements
|
||||
const noBoardId = { |
||||
fields: { |
||||
boardId: 0, |
||||
}, |
||||
}; |
||||
const result = { |
||||
_format: 'wekan-board-1.0.0', |
||||
}; |
||||
_.extend( |
||||
result, |
||||
Boards.findOne(this._boardId, { |
||||
fields: { |
||||
stars: 0, |
||||
}, |
||||
}), |
||||
); |
||||
result.lists = Lists.find(byBoard, noBoardId).fetch(); |
||||
result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch(); |
||||
result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch(); |
||||
result.customFields = CustomFields.find( |
||||
{ boardIds: { $in: [this.boardId] } }, |
||||
{ fields: { boardId: 0 } }, |
||||
).fetch(); |
||||
result.comments = CardComments.find(byBoard, noBoardId).fetch(); |
||||
result.activities = Activities.find(byBoard, noBoardId).fetch(); |
||||
result.rules = Rules.find(byBoard, noBoardId).fetch(); |
||||
result.checklists = []; |
||||
result.checklistItems = []; |
||||
result.subtaskItems = []; |
||||
result.triggers = []; |
||||
result.actions = []; |
||||
result.cards.forEach(card => { |
||||
result.checklists.push( |
||||
...Checklists.find({ |
||||
cardId: card._id, |
||||
}).fetch(), |
||||
); |
||||
result.checklistItems.push( |
||||
...ChecklistItems.find({ |
||||
cardId: card._id, |
||||
}).fetch(), |
||||
); |
||||
result.subtaskItems.push( |
||||
...Cards.find({ |
||||
parentId: card._id, |
||||
}).fetch(), |
||||
); |
||||
}); |
||||
result.rules.forEach(rule => { |
||||
result.triggers.push( |
||||
...Triggers.find( |
||||
{ |
||||
_id: rule.triggerId, |
||||
}, |
||||
noBoardId, |
||||
).fetch(), |
||||
); |
||||
result.actions.push( |
||||
...Actions.find( |
||||
{ |
||||
_id: rule.actionId, |
||||
}, |
||||
noBoardId, |
||||
).fetch(), |
||||
); |
||||
}); |
||||
|
||||
// [Old] for attachments we only export IDs and absolute url to original doc
|
||||
// [New] Encode attachment to base64
|
||||
|
||||
const getBase64Data = function(doc, callback) { |
||||
let buffer = Buffer.allocUnsafe(0); |
||||
buffer.fill(0); |
||||
|
||||
// callback has the form function (err, res) {}
|
||||
const tmpFile = path.join( |
||||
os.tmpdir(), |
||||
`tmpexport${process.pid}${Math.random()}`, |
||||
); |
||||
const tmpWriteable = fs.createWriteStream(tmpFile); |
||||
const readStream = doc.createReadStream(); |
||||
readStream.on('data', function(chunk) { |
||||
buffer = Buffer.concat([buffer, chunk]); |
||||
}); |
||||
|
||||
readStream.on('error', function() { |
||||
callback(null, null); |
||||
}); |
||||
readStream.on('end', function() { |
||||
// done
|
||||
fs.unlink(tmpFile, () => { |
||||
//ignored
|
||||
}); |
||||
|
||||
callback(null, buffer.toString('base64')); |
||||
}); |
||||
readStream.pipe(tmpWriteable); |
||||
}; |
||||
const getBase64DataSync = Meteor.wrapAsync(getBase64Data); |
||||
result.attachments = Attachments.find(byBoard) |
||||
.fetch() |
||||
.map(attachment => { |
||||
let filebase64 = null; |
||||
filebase64 = getBase64DataSync(attachment); |
||||
|
||||
return { |
||||
_id: attachment._id, |
||||
cardId: attachment.cardId, |
||||
//url: FlowRouter.url(attachment.url()),
|
||||
file: filebase64, |
||||
name: attachment.original.name, |
||||
type: attachment.original.type, |
||||
}; |
||||
}); |
||||
|
||||
// we also have to export some user data - as the other elements only
|
||||
// include id but we have to be careful:
|
||||
// 1- only exports users that are linked somehow to that board
|
||||
// 2- do not export any sensitive information
|
||||
const users = {}; |
||||
result.members.forEach(member => { |
||||
users[member.userId] = true; |
||||
}); |
||||
result.lists.forEach(list => { |
||||
users[list.userId] = true; |
||||
}); |
||||
result.cards.forEach(card => { |
||||
users[card.userId] = true; |
||||
if (card.members) { |
||||
card.members.forEach(memberId => { |
||||
users[memberId] = true; |
||||
}); |
||||
} |
||||
}); |
||||
result.comments.forEach(comment => { |
||||
users[comment.userId] = true; |
||||
}); |
||||
result.activities.forEach(activity => { |
||||
users[activity.userId] = true; |
||||
}); |
||||
result.checklists.forEach(checklist => { |
||||
users[checklist.userId] = true; |
||||
}); |
||||
const byUserIds = { |
||||
_id: { |
||||
$in: Object.getOwnPropertyNames(users), |
||||
}, |
||||
}; |
||||
// we use whitelist to be sure we do not expose inadvertently
|
||||
// some secret fields that gets added to User later.
|
||||
const userFields = { |
||||
fields: { |
||||
_id: 1, |
||||
username: 1, |
||||
'profile.fullname': 1, |
||||
'profile.initials': 1, |
||||
'profile.avatarUrl': 1, |
||||
}, |
||||
}; |
||||
result.users = Users.find(byUserIds, userFields) |
||||
.fetch() |
||||
.map(user => { |
||||
// user avatar is stored as a relative url, we export absolute
|
||||
if ((user.profile || {}).avatarUrl) { |
||||
user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl); |
||||
} |
||||
return user; |
||||
}); |
||||
return result; |
||||
} |
||||
|
||||
buildCsv(delimiter = ',') { |
||||
const result = this.build(); |
||||
const columnHeaders = []; |
||||
const cardRows = []; |
||||
columnHeaders.push( |
||||
'Title', |
||||
'Description', |
||||
'Status', |
||||
'Swimlane', |
||||
'Owner', |
||||
'Requested by', |
||||
'Assigned by', |
||||
'Members', |
||||
'Assignees', |
||||
'Labels', |
||||
'Start at', |
||||
'Due at', |
||||
'End at', |
||||
'Over time', |
||||
'Spent time (hours)', |
||||
'Created at', |
||||
'Last modified at', |
||||
'Last activity', |
||||
'Vote', |
||||
'Archived', |
||||
); |
||||
const stringifier = stringify({ |
||||
header: true, |
||||
delimiter, |
||||
columns: columnHeaders, |
||||
}); |
||||
|
||||
stringifier.on('readable', function() { |
||||
let row; |
||||
while ((row = stringifier.read())) { |
||||
cardRows.push(row); |
||||
} |
||||
}); |
||||
|
||||
stringifier.on('error', function(err) { |
||||
// eslint-disable-next-line no-console
|
||||
console.error(err.message); |
||||
}); |
||||
|
||||
result.cards.forEach(card => { |
||||
const currentRow = []; |
||||
currentRow.push(card.title); |
||||
currentRow.push(card.description); |
||||
currentRow.push( |
||||
result.lists.find(({ _id }) => _id === card.listId).title, |
||||
); |
||||
currentRow.push( |
||||
result.swimlanes.find(({ _id }) => _id === card.swimlaneId).title, |
||||
); |
||||
currentRow.push( |
||||
result.users.find(({ _id }) => _id === card.userId).username, |
||||
); |
||||
currentRow.push(card.requestedBy ? card.requestedBy : ' '); |
||||
currentRow.push(card.assignedBy ? card.assignedBy : ' '); |
||||
let usernames = ''; |
||||
card.members.forEach(memberId => { |
||||
const user = result.users.find(({ _id }) => _id === memberId); |
||||
usernames = `${usernames + user.username} `; |
||||
}); |
||||
currentRow.push(usernames.trim()); |
||||
let assignees = ''; |
||||
card.assignees.forEach(assigneeId => { |
||||
const user = result.users.find(({ _id }) => _id === assigneeId); |
||||
assignees = `${assignees + user.username} `; |
||||
}); |
||||
currentRow.push(assignees.trim()); |
||||
let labels = ''; |
||||
card.labelIds.forEach(labelId => { |
||||
const label = result.labels.find(({ _id }) => _id === labelId); |
||||
labels = `${labels + label.name}-${label.color} `; |
||||
}); |
||||
currentRow.push(labels.trim()); |
||||
currentRow.push(card.startAt ? moment(card.startAt).format('LLLL') : ' '); |
||||
currentRow.push(card.dueAt ? moment(card.dueAt).format('LLLL') : ' '); |
||||
currentRow.push(card.endAt ? moment(card.endAt).format('LLLL') : ' '); |
||||
currentRow.push(card.isOvertime ? 'true' : 'false'); |
||||
currentRow.push(card.spentTime); |
||||
currentRow.push( |
||||
card.createdAt ? moment(card.createdAt).format('LLLL') : ' ', |
||||
); |
||||
currentRow.push( |
||||
card.modifiedAt ? moment(card.modifiedAt).format('LLLL') : ' ', |
||||
); |
||||
currentRow.push( |
||||
card.dateLastActivity |
||||
? moment(card.dateLastActivity).format('LLLL') |
||||
: ' ', |
||||
); |
||||
if (card.vote.question) { |
||||
let positiveVoters = ''; |
||||
let negativeVoters = ''; |
||||
card.vote.positive.forEach(userId => { |
||||
const user = result.users.find(({ _id }) => _id === userId); |
||||
positiveVoters = `${positiveVoters + user.username} `; |
||||
}); |
||||
card.vote.negative.forEach(userId => { |
||||
const user = result.users.find(({ _id }) => _id === userId); |
||||
negativeVoters = `${negativeVoters + user.username} `; |
||||
}); |
||||
const votingResult = `${ |
||||
card.vote.public |
||||
? `yes-${ |
||||
card.vote.positive.length |
||||
}-${positiveVoters.trimRight()}-no-${ |
||||
card.vote.negative.length |
||||
}-${negativeVoters.trimRight()}` |
||||
: `yes-${card.vote.positive.length}-no-${card.vote.negative.length}` |
||||
}`;
|
||||
currentRow.push(`${card.vote.question}-${votingResult}`); |
||||
} else { |
||||
currentRow.push(' '); |
||||
} |
||||
currentRow.push(card.archived ? 'true' : 'false'); |
||||
stringifier.write(currentRow); |
||||
}); |
||||
stringifier.end(); |
||||
return cardRows; |
||||
} |
||||
|
||||
canExport(user) { |
||||
const board = Boards.findOne(this._boardId); |
||||
return board && board.isVisibleBy(user); |
||||
} |
||||
} |
Loading…
Reference in new issue