summaryrefslogtreecommitdiffstats
path: root/models/exporter.js
diff options
context:
space:
mode:
Diffstat (limited to 'models/exporter.js')
-rw-r--r--models/exporter.js344
1 files changed, 344 insertions, 0 deletions
diff --git a/models/exporter.js b/models/exporter.js
new file mode 100644
index 00000000..3fa8ca3a
--- /dev/null
+++ b/models/exporter.js
@@ -0,0 +1,344 @@
+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',
+ );
+
+ /* TODO: Try to get translations working.
+ These currently only bring English translations.
+ TAPi18n.__('title'),
+ TAPi18n.__('description'),
+ TAPi18n.__('status'),
+ TAPi18n.__('swimlane'),
+ TAPi18n.__('owner'),
+ TAPi18n.__('requested-by'),
+ TAPi18n.__('assigned-by'),
+ TAPi18n.__('members'),
+ TAPi18n.__('assignee'),
+ TAPi18n.__('labels'),
+ TAPi18n.__('card-start'),
+ TAPi18n.__('card-due'),
+ TAPi18n.__('card-end'),
+ TAPi18n.__('overtime-hours'),
+ TAPi18n.__('spent-time-hours'),
+ TAPi18n.__('createdAt'),
+ TAPi18n.__('last-modified-at'),
+ TAPi18n.__('last-activity'),
+ TAPi18n.__('voting'),
+ TAPi18n.__('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);
+ }
+}