summaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
authorLauri Ojansivu <x@xet7.org>2020-05-13 03:28:02 +0300
committerLauri Ojansivu <x@xet7.org>2020-05-13 03:28:02 +0300
commit851bfa53e684a6b9d8685bea01701af78549f0d2 (patch)
tree277243481c3c1a38324638f5684f4ee75b6e50d6 /models
parentba01ebe05d80cb468c22f9d780855af1cf14124a (diff)
parenta570c4a86157ce4b60e056a4f0583ebc0fe009cf (diff)
downloadwekan-851bfa53e684a6b9d8685bea01701af78549f0d2.tar.gz
wekan-851bfa53e684a6b9d8685bea01701af78549f0d2.tar.bz2
wekan-851bfa53e684a6b9d8685bea01701af78549f0d2.zip
Merge branch 'add-more-import-export-options' of https://github.com/brymut/wekan into brymut-add-more-import-export-options
Diffstat (limited to 'models')
-rw-r--r--models/csvCreator.js313
-rw-r--r--models/export.js240
-rw-r--r--models/exporter.js320
-rw-r--r--models/import.js10
4 files changed, 688 insertions, 195 deletions
diff --git a/models/csvCreator.js b/models/csvCreator.js
new file mode 100644
index 00000000..025a3179
--- /dev/null
+++ b/models/csvCreator.js
@@ -0,0 +1,313 @@
+import Boards from './boards';
+
+export class CsvCreator {
+ constructor(data) {
+ // date to be used for timestamps during import
+ this._nowDate = new Date();
+ // index to help keep track of what information a column stores
+ // each row represents a card
+ this.fieldIndex = {};
+ this.lists = {};
+ // Map of members using username => wekanid
+ this.members = data.membersMapping ? data.membersMapping : {};
+ this.swimlane = null;
+ }
+
+ /**
+ * If dateString is provided,
+ * return the Date it represents.
+ * If not, will return the date when it was first called.
+ * This is useful for us, as we want all import operations to
+ * have the exact same date for easier later retrieval.
+ *
+ * @param {String} dateString a properly formatted Date
+ */
+ _now(dateString) {
+ if (dateString) {
+ return new Date(dateString);
+ }
+ if (!this._nowDate) {
+ this._nowDate = new Date();
+ }
+ return this._nowDate;
+ }
+
+ _user(wekanUserId) {
+ if (wekanUserId && this.members[wekanUserId]) {
+ return this.members[wekanUserId];
+ }
+ return Meteor.userId();
+ }
+
+ /**
+ * Map the header row titles to an index to help assign proper values to the cards' fields
+ * Valid headers (name of card fields):
+ * title, description, status, owner, member, label, due date, start date, finish date, created at, updated at
+ * Some header aliases can also be accepted.
+ * Headers are NOT case-sensitive.
+ *
+ * @param {Array} headerRow array from row of headers of imported CSV/TSV for cards
+ */
+ mapHeadertoCardFieldIndex(headerRow) {
+ const index = {};
+ for (let i = 0; i < headerRow.length; i++) {
+ switch (headerRow[i].trim().toLowerCase()) {
+ case 'title':
+ index.title = i;
+ break;
+ case 'description':
+ index.description = i;
+ break;
+ case 'stage':
+ case 'status':
+ case 'state':
+ index.stage = i;
+ break;
+ case 'owner':
+ index.owner = i;
+ break;
+ case 'members':
+ case 'member':
+ index.members = i;
+ break;
+ case 'labels':
+ case 'label':
+ index.labels = i;
+ break;
+ case 'due date':
+ case 'deadline':
+ case 'due at':
+ index.dueAt = i;
+ break;
+ case 'start date':
+ case 'start at':
+ index.startAt = i;
+ break;
+ case 'finish date':
+ case 'end at':
+ index.endAt = i;
+ break;
+ case 'creation date':
+ case 'created at':
+ index.createdAt = i;
+ break;
+ case 'update date':
+ case 'updated at':
+ case 'modified at':
+ case 'modified on':
+ index.modifiedAt = i;
+ break;
+ }
+ }
+ this.fieldIndex = index;
+ }
+
+ createBoard(csvData) {
+ const boardToCreate = {
+ archived: false,
+ color: 'belize',
+ createdAt: this._now(),
+ labels: [],
+ members: [
+ {
+ userId: Meteor.userId(),
+ wekanId: Meteor.userId(),
+ isActive: true,
+ isAdmin: true,
+ isNoComments: false,
+ isCommentOnly: false,
+ swimlaneId: false,
+ },
+ ],
+ modifiedAt: this._now(),
+ //default is private, should inform user.
+ permission: 'private',
+ slug: 'board',
+ stars: 0,
+ title: `Imported Board ${this._now()}`,
+ };
+
+ // create labels
+ const labelsToCreate = new Set();
+ for (let i = 1; i < csvData.length; i++) {
+ if (csvData[i][this.fieldIndex.labels]) {
+ for (const importedLabel of csvData[i][this.fieldIndex.labels].split(
+ ' ',
+ )) {
+ if (importedLabel && importedLabel.length > 0) {
+ labelsToCreate.add(importedLabel);
+ }
+ }
+ }
+ }
+ for (const label of labelsToCreate) {
+ let labelName, labelColor;
+ if (label.indexOf('-') > -1) {
+ labelName = label.split('-')[0];
+ labelColor = label.split('-')[1];
+ } else {
+ labelName = label;
+ }
+ const labelToCreate = {
+ _id: Random.id(6),
+ color: labelColor ? labelColor : 'black',
+ name: labelName,
+ };
+ boardToCreate.labels.push(labelToCreate);
+ }
+
+ const boardId = Boards.direct.insert(boardToCreate);
+ Boards.direct.update(boardId, {
+ $set: {
+ modifiedAt: this._now(),
+ },
+ });
+ // log activity
+ Activities.direct.insert({
+ activityType: 'importBoard',
+ boardId,
+ createdAt: this._now(),
+ source: {
+ id: boardId,
+ system: 'CSV/TSV',
+ },
+ // We attribute the import to current user,
+ // not the author from the original object.
+ userId: this._user(),
+ });
+ return boardId;
+ }
+
+ createSwimlanes(boardId) {
+ const swimlaneToCreate = {
+ archived: false,
+ boardId,
+ createdAt: this._now(),
+ title: 'Default',
+ sort: 1,
+ };
+ const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate);
+ Swimlanes.direct.update(swimlaneId, { $set: { updatedAt: this._now() } });
+ this.swimlane = swimlaneId;
+ }
+
+ createLists(csvData, boardId) {
+ let numOfCreatedLists = 0;
+ for (let i = 1; i < csvData.length; i++) {
+ const listToCreate = {
+ archived: false,
+ boardId,
+ createdAt: this._now(),
+ };
+ if (csvData[i][this.fieldIndex.stage]) {
+ const existingList = Lists.find({
+ title: csvData[i][this.fieldIndex.stage],
+ boardId,
+ }).fetch();
+ if (existingList.length > 0) {
+ continue;
+ } else {
+ listToCreate.title = csvData[i][this.fieldIndex.stage];
+ }
+ } else listToCreate.title = `Imported List ${this._now()}`;
+
+ const listId = Lists.direct.insert(listToCreate);
+ this.lists[csvData[i][this.fieldIndex.stage]] = listId;
+ numOfCreatedLists++;
+ Lists.direct.update(listId, {
+ $set: {
+ updatedAt: this._now(),
+ sort: numOfCreatedLists,
+ },
+ });
+ }
+ }
+
+ createCards(csvData, boardId) {
+ for (let i = 1; i < csvData.length; i++) {
+ const cardToCreate = {
+ archived: false,
+ boardId,
+ createdAt: csvData[i][this.fieldIndex.createdAt]
+ ? this._now(new Date(csvData[i][this.fieldIndex.createdAt]))
+ : null,
+ dateLastActivity: this._now(),
+ description: csvData[i][this.fieldIndex.description],
+ listId: this.lists[csvData[i][this.fieldIndex.stage]],
+ swimlaneId: this.swimlane,
+ sort: -1,
+ title: csvData[i][this.fieldIndex.title],
+ userId: this._user(),
+ startAt: csvData[i][this.fieldIndex.startAt]
+ ? this._now(new Date(csvData[i][this.fieldIndex.startAt]))
+ : null,
+ dueAt: csvData[i][this.fieldIndex.dueAt]
+ ? this._now(new Date(csvData[i][this.fieldIndex.dueAt]))
+ : null,
+ endAt: csvData[i][this.fieldIndex.endAt]
+ ? this._now(new Date(csvData[i][this.fieldIndex.endAt]))
+ : null,
+ spentTime: null,
+ labelIds: [],
+ modifiedAt: csvData[i][this.fieldIndex.modifiedAt]
+ ? this._now(new Date(csvData[i][this.fieldIndex.modifiedAt]))
+ : null,
+ };
+ // add the labels
+ if (csvData[i][this.fieldIndex.labels]) {
+ const board = Boards.findOne(boardId);
+ for (const importedLabel of csvData[i][this.fieldIndex.labels].split(
+ ' ',
+ )) {
+ if (importedLabel && importedLabel.length > 0) {
+ let labelToApply;
+ if (importedLabel.indexOf('-') === -1) {
+ labelToApply = board.getLabel(importedLabel, 'black');
+ } else {
+ labelToApply = board.getLabel(
+ importedLabel.split('-')[0],
+ importedLabel.split('-')[1],
+ );
+ }
+ cardToCreate.labelIds.push(labelToApply._id);
+ }
+ }
+ }
+ // add the members
+ if (csvData[i][this.fieldIndex.members]) {
+ const wekanMembers = [];
+ for (const importedMember of csvData[i][this.fieldIndex.members].split(
+ ' ',
+ )) {
+ if (this.members[importedMember]) {
+ const wekanId = this.members[importedMember];
+ if (!wekanMembers.find(wId => wId === wekanId)) {
+ wekanMembers.push(wekanId);
+ }
+ }
+ }
+ if (wekanMembers.length > 0) {
+ cardToCreate.members = wekanMembers;
+ }
+ }
+ Cards.direct.insert(cardToCreate);
+ }
+ }
+
+ create(board, currentBoardId) {
+ const isSandstorm =
+ Meteor.settings &&
+ Meteor.settings.public &&
+ Meteor.settings.public.sandstorm;
+ if (isSandstorm && currentBoardId) {
+ const currentBoard = Boards.findOne(currentBoardId);
+ currentBoard.archive();
+ }
+ this.mapHeadertoCardFieldIndex(board[0]);
+ const boardId = this.createBoard(board);
+ this.createLists(board, boardId);
+ this.createSwimlanes(boardId);
+ this.createCards(board, boardId);
+ return boardId;
+ }
+}
diff --git a/models/export.js b/models/export.js
index 339123c8..c3783679 100644
--- a/models/export.js
+++ b/models/export.js
@@ -1,3 +1,4 @@
+import { Exporter } from './exporter';
/* global JsonRoutes */
if (Meteor.isServer) {
// todo XXX once we have a real API in place, move that route there
@@ -7,10 +8,10 @@ if (Meteor.isServer) {
// on the client instead of copy/pasting the route path manually between the
// client and the server.
/**
- * @operation export
+ * @operation exportJson
* @tag Boards
*
- * @summary This route is used to export the board.
+ * @summary This route is used to export the board to a json file format.
*
* @description If user is already logged-in, pass loginToken as param
* "authToken": '/api/boards/:boardId/export?authToken=:token'
@@ -46,199 +47,52 @@ if (Meteor.isServer) {
JsonRoutes.sendResult(res, 403);
}
});
-}
-
-// 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(err) {
- callback(null, null);
- });
- readStream.on('end', function() {
- // done
- fs.unlink(tmpFile, () => {
- //ignored
- });
- callback(null, buffer.toString('base64'));
+ /**
+ * @operation exportCSV/TSV
+ * @tag Boards
+ *
+ * @summary This route is used to export the board to a CSV or TSV file format.
+ *
+ * @description If user is already logged-in, pass loginToken as param
+ *
+ * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
+ * for detailed explanations
+ *
+ * @param {string} boardId the ID of the board we are exporting
+ * @param {string} authToken the loginToken
+ * @param {string} delimiter delimiter to use while building export. Default is comma ','
+ */
+ Picker.route('/api/boards/:boardId/export/csv', function(params, req, res) {
+ const boardId = params.boardId;
+ let user = null;
+ const loginToken = params.query.authToken;
+ if (loginToken) {
+ const hashToken = Accounts._hashLoginToken(loginToken);
+ user = Meteor.users.findOne({
+ 'services.resume.loginTokens.hashedToken': hashToken,
});
- 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,
- };
+ } else if (!Meteor.settings.public.sandstorm) {
+ Authentication.checkUserId(req.userId);
+ user = Users.findOne({
+ _id: req.userId,
+ isAdmin: true,
});
-
- // 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;
+ }
+ const exporter = new Exporter(boardId);
+ if (exporter.canExport(user)) {
+ body = params.query.delimiter
+ ? exporter.buildCsv(params.query.delimiter)
+ : exporter.buildCsv();
+ res.writeHead(200, {
+ 'Content-Length': body[0].length,
+ 'Content-Type': params.query.delimiter ? 'text/csv' : 'text/tsv',
});
- return result;
- }
-
- canExport(user) {
- const board = Boards.findOne(this._boardId);
- return board && board.isVisibleBy(user);
- }
+ res.write(body[0]);
+ res.end();
+ } else {
+ res.writeHead(403);
+ res.end('Permission Error');
+ }
+ });
}
diff --git a/models/exporter.js b/models/exporter.js
new file mode 100644
index 00000000..910ca9a1
--- /dev/null
+++ b/models/exporter.js
@@ -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);
+ }
+}
diff --git a/models/import.js b/models/import.js
index fbfb1483..f3cbaa9b 100644
--- a/models/import.js
+++ b/models/import.js
@@ -1,22 +1,28 @@
import { TrelloCreator } from './trelloCreator';
import { WekanCreator } from './wekanCreator';
-import { Exporter } from './export';
+import { CsvCreator } from './csvCreator';
+import { Exporter } from './exporter';
import wekanMembersMapper from './wekanmapper';
Meteor.methods({
importBoard(board, data, importSource, currentBoard) {
- check(board, Object);
check(data, Object);
check(importSource, String);
check(currentBoard, Match.Maybe(String));
let creator;
switch (importSource) {
case 'trello':
+ check(board, Object);
creator = new TrelloCreator(data);
break;
case 'wekan':
+ check(board, Object);
creator = new WekanCreator(data);
break;
+ case 'csv':
+ check(board, Array);
+ creator = new CsvCreator(data);
+ break;
}
// 1. check all parameters are ok from a syntax point of view