exportExcel.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. if (Meteor.isServer) {
  2. // todo XXX once we have a real API in place, move that route there
  3. // todo XXX also share the route definition between the client and the server
  4. // so that we could use something like
  5. // `ApiRoutes.path('boards/exportExcel', boardId)``
  6. // on the client instead of copy/pasting the route path manually between the
  7. // client and the server.
  8. /**
  9. * @operation exportExcel
  10. * @tag Boards
  11. *
  12. * @summary This route is used to export the board Excel.
  13. *
  14. * @description If user is already logged-in, pass loginToken as param
  15. * "authToken": '/api/boards/:boardId/exportExcel?authToken=:token'
  16. *
  17. * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
  18. * for detailed explanations
  19. *
  20. * @param {string} boardId the ID of the board we are exporting
  21. * @param {string} authToken the loginToken
  22. */
  23. const Excel = require('exceljs');
  24. Picker.route('/api/boards/:boardId/exportExcel', function(params, req, res) {
  25. const boardId = params.boardId;
  26. let user = null;
  27. const loginToken = params.query.authToken;
  28. if (loginToken) {
  29. const hashToken = Accounts._hashLoginToken(loginToken);
  30. user = Meteor.users.findOne({
  31. 'services.resume.loginTokens.hashedToken': hashToken,
  32. });
  33. } else if (!Meteor.settings.public.sandstorm) {
  34. Authentication.checkUserId(req.userId);
  35. user = Users.findOne({
  36. _id: req.userId,
  37. isAdmin: true,
  38. });
  39. }
  40. const exporterExcel = new ExporterExcel(boardId);
  41. if (exporterExcel.canExport(user)) {
  42. exporterExcel.build(res);
  43. } else {
  44. res.end(TAPi18n.__('user-can-not-export-excel'));
  45. }
  46. });
  47. }
  48. // exporter maybe is broken since Gridfs introduced, add fs and path
  49. export class ExporterExcel {
  50. constructor(boardId) {
  51. this._boardId = boardId;
  52. }
  53. build(res) {
  54. const fs = Npm.require('fs');
  55. const os = Npm.require('os');
  56. const path = Npm.require('path');
  57. const byBoard = {
  58. boardId: this._boardId,
  59. };
  60. const byBoardNoLinked = {
  61. boardId: this._boardId,
  62. linkedId: {
  63. $in: ['', null],
  64. },
  65. };
  66. // we do not want to retrieve boardId in related elements
  67. const noBoardId = {
  68. fields: {
  69. boardId: 0,
  70. },
  71. };
  72. const result = {
  73. _format: 'wekan-board-1.0.0',
  74. };
  75. _.extend(
  76. result,
  77. Boards.findOne(this._boardId, {
  78. fields: {
  79. stars: 0,
  80. },
  81. }),
  82. );
  83. result.lists = Lists.find(byBoard, noBoardId).fetch();
  84. result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
  85. result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
  86. result.customFields = CustomFields.find(
  87. {
  88. boardIds: {
  89. $in: [this.boardId],
  90. },
  91. },
  92. {
  93. fields: {
  94. boardId: 0,
  95. },
  96. },
  97. ).fetch();
  98. result.comments = CardComments.find(byBoard, noBoardId).fetch();
  99. result.activities = Activities.find(byBoard, noBoardId).fetch();
  100. result.rules = Rules.find(byBoard, noBoardId).fetch();
  101. result.checklists = [];
  102. result.checklistItems = [];
  103. result.subtaskItems = [];
  104. result.triggers = [];
  105. result.actions = [];
  106. result.cards.forEach(card => {
  107. result.checklists.push(
  108. ...Checklists.find({
  109. cardId: card._id,
  110. }).fetch(),
  111. );
  112. result.checklistItems.push(
  113. ...ChecklistItems.find({
  114. cardId: card._id,
  115. }).fetch(),
  116. );
  117. result.subtaskItems.push(
  118. ...Cards.find({
  119. parentId: card._id,
  120. }).fetch(),
  121. );
  122. });
  123. result.rules.forEach(rule => {
  124. result.triggers.push(
  125. ...Triggers.find(
  126. {
  127. _id: rule.triggerId,
  128. },
  129. noBoardId,
  130. ).fetch(),
  131. );
  132. result.actions.push(
  133. ...Actions.find(
  134. {
  135. _id: rule.actionId,
  136. },
  137. noBoardId,
  138. ).fetch(),
  139. );
  140. });
  141. // we also have to export some user data - as the other elements only
  142. // include id but we have to be careful:
  143. // 1- only exports users that are linked somehow to that board
  144. // 2- do not export any sensitive information
  145. const users = {};
  146. result.members.forEach(member => {
  147. users[member.userId] = true;
  148. });
  149. result.lists.forEach(list => {
  150. users[list.userId] = true;
  151. });
  152. result.cards.forEach(card => {
  153. users[card.userId] = true;
  154. if (card.members) {
  155. card.members.forEach(memberId => {
  156. users[memberId] = true;
  157. });
  158. }
  159. if (card.assignees) {
  160. card.assignees.forEach(memberId => {
  161. users[memberId] = true;
  162. });
  163. }
  164. });
  165. result.comments.forEach(comment => {
  166. users[comment.userId] = true;
  167. });
  168. result.activities.forEach(activity => {
  169. users[activity.userId] = true;
  170. });
  171. result.checklists.forEach(checklist => {
  172. users[checklist.userId] = true;
  173. });
  174. const byUserIds = {
  175. _id: {
  176. $in: Object.getOwnPropertyNames(users),
  177. },
  178. };
  179. // we use whitelist to be sure we do not expose inadvertently
  180. // some secret fields that gets added to User later.
  181. const userFields = {
  182. fields: {
  183. _id: 1,
  184. username: 1,
  185. 'profile.initials': 1,
  186. 'profile.avatarUrl': 1,
  187. },
  188. };
  189. result.users = Users.find(byUserIds, userFields)
  190. .fetch()
  191. .map(user => {
  192. // user avatar is stored as a relative url, we export absolute
  193. if ((user.profile || {}).avatarUrl) {
  194. user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
  195. }
  196. return user;
  197. });
  198. //init exceljs workbook
  199. const Excel = require('exceljs');
  200. const workbook = new Excel.Workbook();
  201. workbook.creator = TAPi18n.__('export-board');
  202. workbook.lastModifiedBy = TAPi18n.__('export-board');
  203. workbook.created = new Date();
  204. workbook.modified = new Date();
  205. workbook.lastPrinted = new Date();
  206. const filename = `${result.title}.xlsx`;
  207. //init worksheet
  208. const worksheet = workbook.addWorksheet(result.title, {
  209. properties: {
  210. tabColor: {
  211. argb: 'FFC0000',
  212. },
  213. },
  214. pageSetup: {
  215. paperSize: 9,
  216. orientation: 'landscape',
  217. },
  218. });
  219. //get worksheet
  220. const ws = workbook.getWorksheet(result.title);
  221. ws.properties.defaultRowHeight = 20;
  222. //init columns
  223. //Excel font. Western: Arial. zh-CN: 宋体
  224. ws.columns = [
  225. {
  226. key: 'a',
  227. width: 14,
  228. },
  229. {
  230. key: 'b',
  231. width: 20,
  232. },
  233. {
  234. key: 'c',
  235. width: 60,
  236. },
  237. {
  238. key: 'd',
  239. width: 20,
  240. style: {
  241. font: {
  242. name: TAPi18n.__('excel-font'),
  243. size: '10',
  244. },
  245. numFmt: 'yyyy/mm/dd hh:mm:ss',
  246. },
  247. },
  248. {
  249. key: 'e',
  250. width: 20,
  251. style: {
  252. font: {
  253. name: TAPi18n.__('excel-font'),
  254. size: '10',
  255. },
  256. numFmt: 'yyyy/mm/dd hh:mm:ss',
  257. },
  258. },
  259. {
  260. key: 'f',
  261. width: 20,
  262. style: {
  263. font: {
  264. name: TAPi18n.__('excel-font'),
  265. size: '10',
  266. },
  267. numFmt: 'yyyy/mm/dd hh:mm:ss',
  268. },
  269. },
  270. {
  271. key: 'g',
  272. width: 20,
  273. style: {
  274. font: {
  275. name: TAPi18n.__('excel-font'),
  276. size: '10',
  277. },
  278. numFmt: 'yyyy/mm/dd hh:mm:ss',
  279. },
  280. },
  281. {
  282. key: 'h',
  283. width: 20,
  284. style: {
  285. font: {
  286. name: TAPi18n.__('excel-font'),
  287. size: '10',
  288. },
  289. numFmt: 'yyyy/mm/dd hh:mm:ss',
  290. },
  291. },
  292. {
  293. key: 'i',
  294. width: 20,
  295. style: {
  296. font: {
  297. name: TAPi18n.__('excel-font'),
  298. size: '10',
  299. },
  300. numFmt: 'yyyy/mm/dd hh:mm:ss',
  301. },
  302. },
  303. {
  304. key: 'j',
  305. width: 20,
  306. style: {
  307. font: {
  308. name: TAPi18n.__('excel-font'),
  309. size: '10',
  310. },
  311. numFmt: 'yyyy/mm/dd hh:mm:ss',
  312. },
  313. },
  314. {
  315. key: 'k',
  316. width: 20,
  317. },
  318. {
  319. key: 'l',
  320. width: 20,
  321. },
  322. {
  323. key: 'm',
  324. width: 20,
  325. },
  326. {
  327. key: 'n',
  328. width: 20,
  329. },
  330. {
  331. key: 'o',
  332. width: 20,
  333. },
  334. {
  335. key: 'p',
  336. width: 20,
  337. },
  338. {
  339. key: 'q',
  340. width: 20,
  341. },
  342. ];
  343. //add title line
  344. ws.mergeCells('A1:H1');
  345. ws.getCell('A1').value = result.title;
  346. ws.getCell('A1').style = {
  347. font: {
  348. name: TAPi18n.__('excel-font'),
  349. size: '20',
  350. },
  351. };
  352. ws.getCell('A1').alignment = {
  353. vertical: 'middle',
  354. horizontal: 'center',
  355. };
  356. ws.getRow(1).height = 40;
  357. //get member and assignee info
  358. let jmem = '';
  359. let jassig = '';
  360. const jmeml = {};
  361. const jassigl = {};
  362. for (const i in result.users) {
  363. jmem = `${jmem + result.users[i].username},`;
  364. jmeml[result.users[i]._id] = result.users[i].username;
  365. }
  366. jmem = jmem.substr(0, jmem.length - 1);
  367. for (const ia in result.users) {
  368. jassig = `${jassig + result.users[ia].username},`;
  369. jassigl[result.users[ia]._id] = result.users[ia].username;
  370. }
  371. jassig = jassig.substr(0, jassig.length - 1);
  372. //get kanban list info
  373. const jlist = {};
  374. for (const klist in result.lists) {
  375. jlist[result.lists[klist]._id] = result.lists[klist].title;
  376. }
  377. //get kanban swimlanes info
  378. const jswimlane = {};
  379. for (const kswimlane in result.swimlanes) {
  380. jswimlane[result.swimlanes[kswimlane]._id] =
  381. result.swimlanes[kswimlane].title;
  382. }
  383. //get kanban label info
  384. const jlabel = {};
  385. var isFirst = 1;
  386. for (const klabel in result.labels) {
  387. console.log(klabel);
  388. if (isFirst == 0) {
  389. jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`;
  390. } else {
  391. isFirst = 0;
  392. jlabel[result.labels[klabel]._id] = result.labels[klabel].name;
  393. }
  394. }
  395. //add data +8 hours
  396. function addTZhours(jdate) {
  397. const curdate = new Date(jdate);
  398. const checkCorrectDate = moment(curdate);
  399. if (checkCorrectDate.isValid()) {
  400. return curdate;
  401. } else {
  402. return ' ';
  403. }
  404. ////Do not add 8 hours to GMT. Use GMT instead.
  405. ////Could not yet figure out how to get localtime.
  406. //return new Date(curdate.setHours(curdate.getHours() + 8));
  407. //return curdate;
  408. }
  409. //add blank row
  410. ws.addRow().values = ['', '', '', '', '', ''];
  411. //add kanban info
  412. ws.addRow().values = [
  413. TAPi18n.__('createdAt'),
  414. addTZhours(result.createdAt),
  415. TAPi18n.__('modifiedAt'),
  416. addTZhours(result.modifiedAt),
  417. TAPi18n.__('members'),
  418. jmem,
  419. ];
  420. ws.getRow(3).font = {
  421. name: TAPi18n.__('excel-font'),
  422. size: 10,
  423. bold: true,
  424. };
  425. ws.mergeCells('F3:Q3');
  426. ws.getCell('B3').style = {
  427. font: {
  428. name: TAPi18n.__('excel-font'),
  429. size: '10',
  430. bold: true,
  431. },
  432. numFmt: 'yyyy/mm/dd hh:mm:ss',
  433. };
  434. //cell center
  435. function cellCenter(cellno) {
  436. ws.getCell(cellno).alignment = {
  437. vertical: 'middle',
  438. horizontal: 'center',
  439. wrapText: true,
  440. };
  441. }
  442. function cellLeft(cellno) {
  443. ws.getCell(cellno).alignment = {
  444. vertical: 'middle',
  445. horizontal: 'left',
  446. wrapText: true,
  447. };
  448. }
  449. cellCenter('A3');
  450. cellCenter('B3');
  451. cellCenter('C3');
  452. cellCenter('D3');
  453. cellCenter('E3');
  454. cellLeft('F3');
  455. ws.getRow(3).height = 20;
  456. //all border
  457. function allBorder(cellno) {
  458. ws.getCell(cellno).border = {
  459. top: {
  460. style: 'thin',
  461. },
  462. left: {
  463. style: 'thin',
  464. },
  465. bottom: {
  466. style: 'thin',
  467. },
  468. right: {
  469. style: 'thin',
  470. },
  471. };
  472. }
  473. allBorder('A3');
  474. allBorder('B3');
  475. allBorder('C3');
  476. allBorder('D3');
  477. allBorder('E3');
  478. allBorder('F3');
  479. //add blank row
  480. ws.addRow().values = [
  481. '',
  482. '',
  483. '',
  484. '',
  485. '',
  486. '',
  487. '',
  488. '',
  489. '',
  490. '',
  491. '',
  492. '',
  493. '',
  494. '',
  495. '',
  496. ];
  497. //add card title
  498. //ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
  499. //this is where order in which the excel file generates
  500. ws.addRow().values = [
  501. TAPi18n.__('number'),
  502. TAPi18n.__('title'),
  503. TAPi18n.__('description'),
  504. TAPi18n.__('owner'),
  505. TAPi18n.__('createdAt'),
  506. TAPi18n.__('last-modified-at'),
  507. TAPi18n.__('card-received'),
  508. TAPi18n.__('card-start'),
  509. TAPi18n.__('card-due'),
  510. TAPi18n.__('card-end'),
  511. TAPi18n.__('list'),
  512. TAPi18n.__('swimlane'),
  513. TAPi18n.__('assignee'),
  514. TAPi18n.__('members'),
  515. TAPi18n.__('labels'),
  516. TAPi18n.__('overtime-hours'),
  517. TAPi18n.__('spent-time-hours'),
  518. ];
  519. ws.getRow(5).height = 20;
  520. allBorder('A5');
  521. allBorder('B5');
  522. allBorder('C5');
  523. allBorder('D5');
  524. allBorder('E5');
  525. allBorder('F5');
  526. allBorder('G5');
  527. allBorder('H5');
  528. allBorder('I5');
  529. allBorder('J5');
  530. allBorder('K5');
  531. allBorder('L5');
  532. allBorder('M5');
  533. allBorder('N5');
  534. allBorder('O5');
  535. allBorder('P5');
  536. allBorder('Q5');
  537. cellCenter('A5');
  538. cellCenter('B5');
  539. cellCenter('C5');
  540. cellCenter('D5');
  541. cellCenter('E5');
  542. cellCenter('F5');
  543. cellCenter('G5');
  544. cellCenter('H5');
  545. cellCenter('I5');
  546. cellCenter('J5');
  547. cellCenter('K5');
  548. cellCenter('L5');
  549. cellCenter('M5');
  550. cellCenter('N5');
  551. cellCenter('O5');
  552. cellCenter('P5');
  553. cellCenter('Q5');
  554. ws.getRow(5).font = {
  555. name: TAPi18n.__('excel-font'),
  556. size: 12,
  557. bold: true,
  558. };
  559. //add blank row
  560. //add card info
  561. for (const i in result.cards) {
  562. const jcard = result.cards[i];
  563. //get member info
  564. let jcmem = '';
  565. for (const j in jcard.members) {
  566. jcmem += jmeml[jcard.members[j]];
  567. jcmem += ' ';
  568. }
  569. //get assignee info
  570. let jcassig = '';
  571. for (const ja in jcard.assignees) {
  572. jcassig += jassigl[jcard.assignees[ja]];
  573. jcassig += ' ';
  574. }
  575. //get card label info
  576. let jclabel = '';
  577. for (const jl in jcard.labelIds) {
  578. jclabel += jlabel[jcard.labelIds[jl]];
  579. jclabel += ' ';
  580. }
  581. //add card detail
  582. const t = Number(i) + 1;
  583. ws.addRow().values = [
  584. t.toString(),
  585. jcard.title,
  586. jcard.description,
  587. jmeml[jcard.userId],
  588. addTZhours(jcard.createdAt),
  589. addTZhours(jcard.dateLastActivity),
  590. addTZhours(jcard.receivedAt),
  591. addTZhours(jcard.startAt),
  592. addTZhours(jcard.dueAt),
  593. addTZhours(jcard.endAt),
  594. jlist[jcard.listId],
  595. jswimlane[jcard.swimlaneId],
  596. jcassig,
  597. jcmem,
  598. jclabel,
  599. jcard.isOvertime ? 'true' : 'false',
  600. jcard.spentTime,
  601. ];
  602. const y = Number(i) + 6;
  603. //ws.getRow(y).height = 25;
  604. allBorder(`A${y}`);
  605. allBorder(`B${y}`);
  606. allBorder(`C${y}`);
  607. allBorder(`D${y}`);
  608. allBorder(`E${y}`);
  609. allBorder(`F${y}`);
  610. allBorder(`G${y}`);
  611. allBorder(`H${y}`);
  612. allBorder(`I${y}`);
  613. allBorder(`J${y}`);
  614. allBorder(`K${y}`);
  615. allBorder(`L${y}`);
  616. allBorder(`M${y}`);
  617. allBorder(`N${y}`);
  618. allBorder(`O${y}`);
  619. allBorder(`P${y}`);
  620. allBorder(`Q${y}`);
  621. cellCenter(`A${y}`);
  622. ws.getCell(`B${y}`).alignment = {
  623. wrapText: true,
  624. };
  625. ws.getCell(`C${y}`).alignment = {
  626. wrapText: true,
  627. };
  628. ws.getCell(`L${y}`).alignment = {
  629. wrapText: true,
  630. };
  631. ws.getCell(`M${y}`).alignment = {
  632. wrapText: true,
  633. };
  634. ws.getCell(`N${y}`).alignment = {
  635. wrapText: true,
  636. };
  637. }
  638. workbook.xlsx.write(res).then(function() {});
  639. }
  640. canExport(user) {
  641. const board = Boards.findOne(this._boardId);
  642. return board && board.isVisibleBy(user);
  643. }
  644. }