exportExcel.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  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: 40,
  232. },
  233. {
  234. key: 'c',
  235. width: 60,
  236. },
  237. {
  238. key: 'd',
  239. width: 40,
  240. },
  241. {
  242. key: 'e',
  243. width: 20,
  244. },
  245. {
  246. key: 'f',
  247. width: 20,
  248. style: {
  249. font: {
  250. name: TAPi18n.__('excel-font'),
  251. size: '10',
  252. },
  253. numFmt: 'yyyy/mm/dd hh:mm:ss',
  254. },
  255. },
  256. {
  257. key: 'g',
  258. width: 20,
  259. style: {
  260. font: {
  261. name: TAPi18n.__('excel-font'),
  262. size: '10',
  263. },
  264. numFmt: 'yyyy/mm/dd hh:mm:ss',
  265. },
  266. },
  267. {
  268. key: 'h',
  269. width: 20,
  270. style: {
  271. font: {
  272. name: TAPi18n.__('excel-font'),
  273. size: '10',
  274. },
  275. numFmt: 'yyyy/mm/dd hh:mm:ss',
  276. },
  277. },
  278. {
  279. key: 'i',
  280. width: 20,
  281. style: {
  282. font: {
  283. name: TAPi18n.__('excel-font'),
  284. size: '10',
  285. },
  286. numFmt: 'yyyy/mm/dd hh:mm:ss',
  287. },
  288. },
  289. {
  290. key: 'j',
  291. width: 20,
  292. style: {
  293. font: {
  294. name: TAPi18n.__('excel-font'),
  295. size: '10',
  296. },
  297. numFmt: 'yyyy/mm/dd hh:mm:ss',
  298. },
  299. },
  300. {
  301. key: 'k',
  302. width: 20,
  303. style: {
  304. font: {
  305. name: TAPi18n.__('excel-font'),
  306. size: '10',
  307. },
  308. numFmt: 'yyyy/mm/dd hh:mm:ss',
  309. },
  310. },
  311. {
  312. key: 'l',
  313. width: 20,
  314. },
  315. {
  316. key: 'm',
  317. width: 20,
  318. },
  319. {
  320. key: 'n',
  321. width: 20,
  322. },
  323. {
  324. key: 'o',
  325. width: 20,
  326. },
  327. {
  328. key: 'p',
  329. width: 20,
  330. },
  331. {
  332. key: 'q',
  333. width: 20,
  334. },
  335. {
  336. key: 'r',
  337. width: 20,
  338. },
  339. ];
  340. //add title line
  341. ws.mergeCells('A1:H1');
  342. ws.getCell('A1').value = result.title;
  343. ws.getCell('A1').style = {
  344. font: {
  345. name: TAPi18n.__('excel-font'),
  346. size: '20',
  347. },
  348. };
  349. ws.getCell('A1').alignment = {
  350. vertical: 'middle',
  351. horizontal: 'center',
  352. };
  353. ws.getRow(1).height = 40;
  354. //get member and assignee info
  355. let jmem = '';
  356. let jassig = '';
  357. const jmeml = {};
  358. const jassigl = {};
  359. for (const i in result.users) {
  360. jmem = `${jmem + result.users[i].username},`;
  361. jmeml[result.users[i]._id] = result.users[i].username;
  362. }
  363. jmem = jmem.substr(0, jmem.length - 1);
  364. for (const ia in result.users) {
  365. jassig = `${jassig + result.users[ia].username},`;
  366. jassigl[result.users[ia]._id] = result.users[ia].username;
  367. }
  368. jassig = jassig.substr(0, jassig.length - 1);
  369. //get kanban list info
  370. const jlist = {};
  371. for (const klist in result.lists) {
  372. jlist[result.lists[klist]._id] = result.lists[klist].title;
  373. }
  374. //get kanban swimlanes info
  375. const jswimlane = {};
  376. for (const kswimlane in result.swimlanes) {
  377. jswimlane[result.swimlanes[kswimlane]._id] =
  378. result.swimlanes[kswimlane].title;
  379. }
  380. //get kanban label info
  381. const jlabel = {};
  382. var isFirst = 1;
  383. for (const klabel in result.labels) {
  384. console.log(klabel);
  385. if (isFirst == 0) {
  386. jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`;
  387. } else {
  388. isFirst = 0;
  389. jlabel[result.labels[klabel]._id] = result.labels[klabel].name;
  390. }
  391. }
  392. //add data +8 hours
  393. function addTZhours(jdate) {
  394. const curdate = new Date(jdate);
  395. const checkCorrectDate = moment(curdate);
  396. if (checkCorrectDate.isValid()) {
  397. return curdate;
  398. } else {
  399. return ' ';
  400. }
  401. ////Do not add 8 hours to GMT. Use GMT instead.
  402. ////Could not yet figure out how to get localtime.
  403. //return new Date(curdate.setHours(curdate.getHours() + 8));
  404. //return curdate;
  405. }
  406. //add blank row
  407. ws.addRow().values = ['', '', '', '', '', ''];
  408. //add kanban info
  409. ws.addRow().values = [
  410. TAPi18n.__('createdAt'),
  411. addTZhours(result.createdAt),
  412. TAPi18n.__('modifiedAt'),
  413. addTZhours(result.modifiedAt),
  414. TAPi18n.__('members'),
  415. jmem,
  416. ];
  417. ws.getRow(3).font = {
  418. name: TAPi18n.__('excel-font'),
  419. size: 10,
  420. bold: true,
  421. };
  422. ws.mergeCells('F3:R3');
  423. ws.getCell('B3').style = {
  424. font: {
  425. name: TAPi18n.__('excel-font'),
  426. size: '10',
  427. bold: true,
  428. },
  429. numFmt: 'yyyy/mm/dd hh:mm:ss',
  430. };
  431. //cell center
  432. function cellCenter(cellno) {
  433. ws.getCell(cellno).alignment = {
  434. vertical: 'middle',
  435. horizontal: 'center',
  436. wrapText: true,
  437. };
  438. }
  439. function cellLeft(cellno) {
  440. ws.getCell(cellno).alignment = {
  441. vertical: 'middle',
  442. horizontal: 'left',
  443. wrapText: true,
  444. };
  445. }
  446. cellCenter('A3');
  447. cellCenter('B3');
  448. cellCenter('C3');
  449. cellCenter('D3');
  450. cellCenter('E3');
  451. cellLeft('F3');
  452. ws.getRow(3).height = 20;
  453. //all border
  454. function allBorder(cellno) {
  455. ws.getCell(cellno).border = {
  456. top: {
  457. style: 'thin',
  458. },
  459. left: {
  460. style: 'thin',
  461. },
  462. bottom: {
  463. style: 'thin',
  464. },
  465. right: {
  466. style: 'thin',
  467. },
  468. };
  469. }
  470. allBorder('A3');
  471. allBorder('B3');
  472. allBorder('C3');
  473. allBorder('D3');
  474. allBorder('E3');
  475. allBorder('F3');
  476. //add blank row
  477. ws.addRow().values = [
  478. '',
  479. '',
  480. '',
  481. '',
  482. '',
  483. '',
  484. '',
  485. '',
  486. '',
  487. '',
  488. '',
  489. '',
  490. '',
  491. '',
  492. '',
  493. ];
  494. //add card title
  495. //ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
  496. //this is where order in which the excel file generates
  497. ws.addRow().values = [
  498. TAPi18n.__('number'),
  499. TAPi18n.__('title'),
  500. TAPi18n.__('description'),
  501. TAPi18n.__('parent-card'),
  502. TAPi18n.__('owner'),
  503. TAPi18n.__('createdAt'),
  504. TAPi18n.__('last-modified-at'),
  505. TAPi18n.__('card-received'),
  506. TAPi18n.__('card-start'),
  507. TAPi18n.__('card-due'),
  508. TAPi18n.__('card-end'),
  509. TAPi18n.__('list'),
  510. TAPi18n.__('swimlane'),
  511. TAPi18n.__('assignee'),
  512. TAPi18n.__('members'),
  513. TAPi18n.__('labels'),
  514. TAPi18n.__('overtime-hours'),
  515. TAPi18n.__('spent-time-hours'),
  516. ];
  517. ws.getRow(5).height = 20;
  518. allBorder('A5');
  519. allBorder('B5');
  520. allBorder('C5');
  521. allBorder('D5');
  522. allBorder('E5');
  523. allBorder('F5');
  524. allBorder('G5');
  525. allBorder('H5');
  526. allBorder('I5');
  527. allBorder('J5');
  528. allBorder('K5');
  529. allBorder('L5');
  530. allBorder('M5');
  531. allBorder('N5');
  532. allBorder('O5');
  533. allBorder('P5');
  534. allBorder('Q5');
  535. allBorder('R5');
  536. cellCenter('A5');
  537. cellCenter('B5');
  538. cellCenter('C5');
  539. cellCenter('D5');
  540. cellCenter('E5');
  541. cellCenter('F5');
  542. cellCenter('G5');
  543. cellCenter('H5');
  544. cellCenter('I5');
  545. cellCenter('J5');
  546. cellCenter('K5');
  547. cellCenter('L5');
  548. cellCenter('M5');
  549. cellCenter('N5');
  550. cellCenter('O5');
  551. cellCenter('P5');
  552. cellCenter('Q5');
  553. cellCenter('R5');
  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. //get parent name
  582. if (jcard.parentId) {
  583. const parentCard = result.cards.find(
  584. card => card._id === jcard.parentId,
  585. );
  586. jcard.parentCardTitle = parentCard ? parentCard.title : '';
  587. }
  588. //add card detail
  589. const t = Number(i) + 1;
  590. ws.addRow().values = [
  591. t.toString(),
  592. jcard.title,
  593. jcard.description,
  594. jcard.parentCardTitle,
  595. jmeml[jcard.userId],
  596. addTZhours(jcard.createdAt),
  597. addTZhours(jcard.dateLastActivity),
  598. addTZhours(jcard.receivedAt),
  599. addTZhours(jcard.startAt),
  600. addTZhours(jcard.dueAt),
  601. addTZhours(jcard.endAt),
  602. jlist[jcard.listId],
  603. jswimlane[jcard.swimlaneId],
  604. jcassig,
  605. jcmem,
  606. jclabel,
  607. jcard.isOvertime ? 'true' : 'false',
  608. jcard.spentTime,
  609. ];
  610. const y = Number(i) + 6;
  611. //ws.getRow(y).height = 25;
  612. allBorder(`A${y}`);
  613. allBorder(`B${y}`);
  614. allBorder(`C${y}`);
  615. allBorder(`D${y}`);
  616. allBorder(`E${y}`);
  617. allBorder(`F${y}`);
  618. allBorder(`G${y}`);
  619. allBorder(`H${y}`);
  620. allBorder(`I${y}`);
  621. allBorder(`J${y}`);
  622. allBorder(`K${y}`);
  623. allBorder(`L${y}`);
  624. allBorder(`M${y}`);
  625. allBorder(`N${y}`);
  626. allBorder(`O${y}`);
  627. allBorder(`P${y}`);
  628. allBorder(`Q${y}`);
  629. allBorder(`R${y}`);
  630. cellCenter(`A${y}`);
  631. ws.getCell(`B${y}`).alignment = {
  632. wrapText: true,
  633. };
  634. ws.getCell(`C${y}`).alignment = {
  635. wrapText: true,
  636. };
  637. ws.getCell(`M${y}`).alignment = {
  638. wrapText: true,
  639. };
  640. ws.getCell(`N${y}`).alignment = {
  641. wrapText: true,
  642. };
  643. ws.getCell(`O${y}`).alignment = {
  644. wrapText: true,
  645. };
  646. }
  647. workbook.xlsx.write(res).then(function() {});
  648. }
  649. canExport(user) {
  650. const board = Boards.findOne(this._boardId);
  651. return board && board.isVisibleBy(user);
  652. }
  653. }