exporter.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. // const stringify = require('csv-stringify');
  2. // exporter maybe is broken since Gridfs introduced, add fs and path
  3. export class Exporter {
  4. /*
  5. constructor(boardId) {
  6. this._boardId = boardId;
  7. }
  8. build() {
  9. const fs = Npm.require('fs');
  10. const os = Npm.require('os');
  11. const path = Npm.require('path');
  12. const byBoard = { boardId: this._boardId };
  13. const byBoardNoLinked = {
  14. boardId: this._boardId,
  15. linkedId: { $in: ['', null] },
  16. };
  17. // we do not want to retrieve boardId in related elements
  18. const noBoardId = {
  19. fields: {
  20. boardId: 0,
  21. },
  22. };
  23. const result = {
  24. _format: 'wekan-board-1.0.0',
  25. };
  26. _.extend(
  27. result,
  28. Boards.findOne(this._boardId, {
  29. fields: {
  30. stars: 0,
  31. },
  32. }),
  33. );
  34. result.lists = Lists.find(byBoard, noBoardId).fetch();
  35. result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
  36. result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
  37. result.customFields = CustomFields.find(
  38. { boardIds: this._boardId },
  39. { fields: { boardIds: 0 } },
  40. ).fetch();
  41. result.comments = CardComments.find(byBoard, noBoardId).fetch();
  42. result.activities = Activities.find(byBoard, noBoardId).fetch();
  43. result.rules = Rules.find(byBoard, noBoardId).fetch();
  44. result.checklists = [];
  45. result.checklistItems = [];
  46. result.subtaskItems = [];
  47. result.triggers = [];
  48. result.actions = [];
  49. result.cards.forEach(card => {
  50. result.checklists.push(
  51. ...Checklists.find({
  52. cardId: card._id,
  53. }).fetch(),
  54. );
  55. result.checklistItems.push(
  56. ...ChecklistItems.find({
  57. cardId: card._id,
  58. }).fetch(),
  59. );
  60. result.subtaskItems.push(
  61. ...Cards.find({
  62. parentId: card._id,
  63. }).fetch(),
  64. );
  65. });
  66. result.rules.forEach(rule => {
  67. result.triggers.push(
  68. ...Triggers.find(
  69. {
  70. _id: rule.triggerId,
  71. },
  72. noBoardId,
  73. ).fetch(),
  74. );
  75. result.actions.push(
  76. ...Actions.find(
  77. {
  78. _id: rule.actionId,
  79. },
  80. noBoardId,
  81. ).fetch(),
  82. );
  83. });
  84. // [Old] for attachments we only export IDs and absolute url to original doc
  85. // [New] Encode attachment to base64
  86. const getBase64Data = function(doc, callback) {
  87. let buffer = Buffer.allocUnsafe(0);
  88. buffer.fill(0);
  89. // callback has the form function (err, res) {}
  90. const tmpFile = path.join(
  91. os.tmpdir(),
  92. `tmpexport${process.pid}${Math.random()}`,
  93. );
  94. const tmpWriteable = fs.createWriteStream(tmpFile);
  95. const readStream = doc.createReadStream();
  96. readStream.on('data', function(chunk) {
  97. buffer = Buffer.concat([buffer, chunk]);
  98. });
  99. readStream.on('error', function() {
  100. callback(null, null);
  101. });
  102. readStream.on('end', function() {
  103. // done
  104. fs.unlink(tmpFile, () => {
  105. //ignored
  106. });
  107. callback(null, buffer.toString('base64'));
  108. });
  109. readStream.pipe(tmpWriteable);
  110. };
  111. const getBase64DataSync = Meteor.wrapAsync(getBase64Data);
  112. result.attachments = Attachments.find(byBoard)
  113. .fetch()
  114. .map(attachment => {
  115. let filebase64 = null;
  116. filebase64 = getBase64DataSync(attachment);
  117. return {
  118. _id: attachment._id,
  119. cardId: attachment.cardId,
  120. //url: FlowRouter.url(attachment.url()),
  121. file: filebase64,
  122. name: attachment.original.name,
  123. type: attachment.original.type,
  124. };
  125. });
  126. // we also have to export some user data - as the other elements only
  127. // include id but we have to be careful:
  128. // 1- only exports users that are linked somehow to that board
  129. // 2- do not export any sensitive information
  130. const users = {};
  131. result.members.forEach(member => {
  132. users[member.userId] = true;
  133. });
  134. result.lists.forEach(list => {
  135. users[list.userId] = true;
  136. });
  137. result.cards.forEach(card => {
  138. users[card.userId] = true;
  139. if (card.members) {
  140. card.members.forEach(memberId => {
  141. users[memberId] = true;
  142. });
  143. }
  144. });
  145. result.comments.forEach(comment => {
  146. users[comment.userId] = true;
  147. });
  148. result.activities.forEach(activity => {
  149. users[activity.userId] = true;
  150. });
  151. result.checklists.forEach(checklist => {
  152. users[checklist.userId] = true;
  153. });
  154. const byUserIds = {
  155. _id: {
  156. $in: Object.getOwnPropertyNames(users),
  157. },
  158. };
  159. // we use whitelist to be sure we do not expose inadvertently
  160. // some secret fields that gets added to User later.
  161. const userFields = {
  162. fields: {
  163. _id: 1,
  164. username: 1,
  165. 'profile.fullname': 1,
  166. 'profile.initials': 1,
  167. 'profile.avatarUrl': 1,
  168. },
  169. };
  170. result.users = Users.find(byUserIds, userFields)
  171. .fetch()
  172. .map(user => {
  173. // user avatar is stored as a relative url, we export absolute
  174. if ((user.profile || {}).avatarUrl) {
  175. user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
  176. }
  177. return user;
  178. });
  179. return result;
  180. }
  181. buildCsv(delimiter = ',') {
  182. const result = this.build();
  183. const columnHeaders = [];
  184. const cardRows = [];
  185. columnHeaders.push(
  186. 'Title',
  187. 'Description',
  188. 'Status',
  189. 'Swimlane',
  190. 'Owner',
  191. 'Requested by',
  192. 'Assigned by',
  193. 'Members',
  194. 'Assignees',
  195. 'Labels',
  196. 'Start at',
  197. 'Due at',
  198. 'End at',
  199. 'Over time',
  200. 'Spent time (hours)',
  201. 'Created at',
  202. 'Last modified at',
  203. 'Last activity',
  204. 'Vote',
  205. 'Archived',
  206. );
  207. const customFieldMap = {};
  208. let i = 0;
  209. result.customFields.forEach(customField => {
  210. customFieldMap[customField._id] = {
  211. position: i,
  212. type: customField.type,
  213. };
  214. if (customField.type === 'dropdown') {
  215. let options = '';
  216. customField.settings.dropdownItems.forEach(item => {
  217. options = options === '' ? item.name : `${`${options}/${item.name}`}`;
  218. });
  219. columnHeaders.push(
  220. `CustomField-${customField.name}-${customField.type}-${options}`,
  221. );
  222. } else if (customField.type === 'currency') {
  223. columnHeaders.push(
  224. `CustomField-${customField.name}-${customField.type}-${customField.settings.currencyCode}`,
  225. );
  226. } else {
  227. columnHeaders.push(
  228. `CustomField-${customField.name}-${customField.type}`,
  229. );
  230. }
  231. i++;
  232. });
  233. // TODO: Try to get translations working.
  234. // These currently only bring English translations.
  235. // TAPi18n.__('title'),
  236. // TAPi18n.__('description'),
  237. // TAPi18n.__('status'),
  238. // TAPi18n.__('swimlane'),
  239. // TAPi18n.__('owner'),
  240. // TAPi18n.__('requested-by'),
  241. // TAPi18n.__('assigned-by'),
  242. // TAPi18n.__('members'),
  243. // TAPi18n.__('assignee'),
  244. // TAPi18n.__('labels'),
  245. // TAPi18n.__('card-start'),
  246. // TAPi18n.__('card-due'),
  247. // TAPi18n.__('card-end'),
  248. // TAPi18n.__('overtime-hours'),
  249. // TAPi18n.__('spent-time-hours'),
  250. // TAPi18n.__('createdAt'),
  251. // TAPi18n.__('last-modified-at'),
  252. // TAPi18n.__('last-activity'),
  253. // TAPi18n.__('voting'),
  254. // TAPi18n.__('archived'),
  255. const stringifier = stringify({
  256. header: true,
  257. delimiter,
  258. columns: columnHeaders,
  259. });
  260. stringifier.on('readable', function() {
  261. let row;
  262. while ((row = stringifier.read())) {
  263. cardRows.push(row);
  264. }
  265. });
  266. stringifier.on('error', function(err) {
  267. // eslint-disable-next-line no-console
  268. console.error(err.message);
  269. });
  270. result.cards.forEach(card => {
  271. const currentRow = [];
  272. currentRow.push(card.title);
  273. currentRow.push(card.description);
  274. currentRow.push(
  275. result.lists.find(({ _id }) => _id === card.listId).title,
  276. );
  277. currentRow.push(
  278. result.swimlanes.find(({ _id }) => _id === card.swimlaneId).title,
  279. );
  280. currentRow.push(
  281. result.users.find(({ _id }) => _id === card.userId).username,
  282. );
  283. currentRow.push(card.requestedBy ? card.requestedBy : ' ');
  284. currentRow.push(card.assignedBy ? card.assignedBy : ' ');
  285. let usernames = '';
  286. card.members.forEach(memberId => {
  287. const user = result.users.find(({ _id }) => _id === memberId);
  288. usernames = `${usernames + user.username} `;
  289. });
  290. currentRow.push(usernames.trim());
  291. let assignees = '';
  292. card.assignees.forEach(assigneeId => {
  293. const user = result.users.find(({ _id }) => _id === assigneeId);
  294. assignees = `${assignees + user.username} `;
  295. });
  296. currentRow.push(assignees.trim());
  297. let labels = '';
  298. card.labelIds.forEach(labelId => {
  299. const label = result.labels.find(({ _id }) => _id === labelId);
  300. labels = `${labels + label.name}-${label.color} `;
  301. });
  302. currentRow.push(labels.trim());
  303. currentRow.push(card.startAt ? moment(card.startAt).format() : ' ');
  304. currentRow.push(card.dueAt ? moment(card.dueAt).format() : ' ');
  305. currentRow.push(card.endAt ? moment(card.endAt).format() : ' ');
  306. currentRow.push(card.isOvertime ? 'true' : 'false');
  307. currentRow.push(card.spentTime);
  308. currentRow.push(card.createdAt ? moment(card.createdAt).format() : ' ');
  309. currentRow.push(card.modifiedAt ? moment(card.modifiedAt).format() : ' ');
  310. currentRow.push(
  311. card.dateLastActivity ? moment(card.dateLastActivity).format() : ' ',
  312. );
  313. if (card.vote && card.vote.question !== '') {
  314. let positiveVoters = '';
  315. let negativeVoters = '';
  316. card.vote.positive.forEach(userId => {
  317. const user = result.users.find(({ _id }) => _id === userId);
  318. positiveVoters = `${positiveVoters + user.username} `;
  319. });
  320. card.vote.negative.forEach(userId => {
  321. const user = result.users.find(({ _id }) => _id === userId);
  322. negativeVoters = `${negativeVoters + user.username} `;
  323. });
  324. const votingResult = `${
  325. card.vote.public
  326. ? `yes-${
  327. card.vote.positive.length
  328. }-${positiveVoters.trimRight()}-no-${
  329. card.vote.negative.length
  330. }-${negativeVoters.trimRight()}`
  331. : `yes-${card.vote.positive.length}-no-${card.vote.negative.length}`
  332. }`;
  333. currentRow.push(`${card.vote.question}-${votingResult}`);
  334. } else {
  335. currentRow.push(' ');
  336. }
  337. currentRow.push(card.archived ? 'true' : 'false');
  338. //Custom fields
  339. const customFieldValuesToPush = new Array(result.customFields.length);
  340. card.customFields.forEach(field => {
  341. if (field.value !== null) {
  342. if (customFieldMap[field._id].type === 'date') {
  343. customFieldValuesToPush[
  344. customFieldMap[field._id].position
  345. ] = moment(field.value).format();
  346. } else if (customFieldMap[field._id].type === 'dropdown') {
  347. const dropdownOptions = result.customFields.find(
  348. ({ _id }) => _id === field._id,
  349. ).settings.dropdownItems;
  350. const fieldValue = dropdownOptions.find(
  351. ({ _id }) => _id === field.value,
  352. ).name;
  353. customFieldValuesToPush[
  354. customFieldMap[field._id].position
  355. ] = fieldValue;
  356. } else {
  357. customFieldValuesToPush[customFieldMap[field._id].position] =
  358. field.value;
  359. }
  360. }
  361. });
  362. for (
  363. let valueIndex = 0;
  364. valueIndex < customFieldValuesToPush.length;
  365. valueIndex++
  366. ) {
  367. if (!(valueIndex in customFieldValuesToPush)) {
  368. currentRow.push(' ');
  369. } else {
  370. currentRow.push(customFieldValuesToPush[valueIndex]);
  371. }
  372. }
  373. stringifier.write(currentRow);
  374. });
  375. stringifier.end();
  376. return cardRows[0];
  377. }
  378. canExport(user) {
  379. const board = Boards.findOne(this._boardId);
  380. return board && board.isVisibleBy(user);
  381. }
  382. */
  383. canExport(user) {
  384. return false;
  385. }
  386. }