exportExcel.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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. //console.log('Excel');
  28. const loginToken = params.query.authToken;
  29. if (loginToken) {
  30. const hashToken = Accounts._hashLoginToken(loginToken);
  31. user = Meteor.users.findOne({
  32. 'services.resume.loginTokens.hashedToken': hashToken,
  33. });
  34. } else if (!Meteor.settings.public.sandstorm) {
  35. Authentication.checkUserId(req.userId);
  36. user = Users.findOne({
  37. _id: req.userId,
  38. isAdmin: true,
  39. });
  40. }
  41. const exporterExcel = new ExporterExcel(boardId);
  42. if (exporterExcel.canExport(user)) {
  43. exporterExcel.build(res);
  44. } else {
  45. res.end(TAPi18n.__('user-can-not-export-excel'));
  46. }
  47. });
  48. }
  49. // exporter maybe is broken since Gridfs introduced, add fs and path
  50. export class ExporterExcel {
  51. constructor(boardId) {
  52. this._boardId = boardId;
  53. }
  54. build(res) {
  55. const fs = Npm.require('fs');
  56. const os = Npm.require('os');
  57. const path = Npm.require('path');
  58. const byBoard = {
  59. boardId: this._boardId,
  60. };
  61. const byBoardNoLinked = {
  62. boardId: this._boardId,
  63. linkedId: {
  64. $in: ['', null],
  65. },
  66. };
  67. // we do not want to retrieve boardId in related elements
  68. const noBoardId = {
  69. fields: {
  70. boardId: 0,
  71. },
  72. };
  73. const result = {
  74. _format: 'wekan-board-1.0.0',
  75. };
  76. _.extend(
  77. result,
  78. Boards.findOne(this._boardId, {
  79. fields: {
  80. stars: 0,
  81. },
  82. }),
  83. );
  84. result.lists = Lists.find(byBoard, noBoardId).fetch();
  85. result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
  86. result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
  87. result.customFields = CustomFields.find(
  88. {
  89. boardIds: {
  90. $in: [this.boardId],
  91. },
  92. },
  93. {
  94. fields: {
  95. boardId: 0,
  96. },
  97. },
  98. ).fetch();
  99. result.comments = CardComments.find(byBoard, noBoardId).fetch();
  100. result.activities = Activities.find(byBoard, noBoardId).fetch();
  101. result.rules = Rules.find(byBoard, noBoardId).fetch();
  102. result.checklists = [];
  103. result.checklistItems = [];
  104. result.subtaskItems = [];
  105. result.triggers = [];
  106. result.actions = [];
  107. result.cards.forEach(card => {
  108. result.checklists.push(
  109. ...Checklists.find({
  110. cardId: card._id,
  111. }).fetch(),
  112. );
  113. result.checklistItems.push(
  114. ...ChecklistItems.find({
  115. cardId: card._id,
  116. }).fetch(),
  117. );
  118. result.subtaskItems.push(
  119. ...Cards.find({
  120. parentId: card._id,
  121. }).fetch(),
  122. );
  123. });
  124. result.rules.forEach(rule => {
  125. result.triggers.push(
  126. ...Triggers.find(
  127. {
  128. _id: rule.triggerId,
  129. },
  130. noBoardId,
  131. ).fetch(),
  132. );
  133. result.actions.push(
  134. ...Actions.find(
  135. {
  136. _id: rule.actionId,
  137. },
  138. noBoardId,
  139. ).fetch(),
  140. );
  141. });
  142. // we also have to export some user data - as the other elements only
  143. // include id but we have to be careful:
  144. // 1- only exports users that are linked somehow to that board
  145. // 2- do not export any sensitive information
  146. const users = {};
  147. result.members.forEach(member => {
  148. users[member.userId] = true;
  149. });
  150. result.lists.forEach(list => {
  151. users[list.userId] = true;
  152. });
  153. result.cards.forEach(card => {
  154. users[card.userId] = true;
  155. if (card.members) {
  156. card.members.forEach(memberId => {
  157. users[memberId] = true;
  158. });
  159. }
  160. });
  161. result.comments.forEach(comment => {
  162. users[comment.userId] = true;
  163. });
  164. result.activities.forEach(activity => {
  165. users[activity.userId] = true;
  166. });
  167. result.checklists.forEach(checklist => {
  168. users[checklist.userId] = true;
  169. });
  170. const byUserIds = {
  171. _id: {
  172. $in: Object.getOwnPropertyNames(users),
  173. },
  174. };
  175. // we use whitelist to be sure we do not expose inadvertently
  176. // some secret fields that gets added to User later.
  177. const userFields = {
  178. fields: {
  179. _id: 1,
  180. username: 1,
  181. 'profile.fullname': 1,
  182. 'profile.initials': 1,
  183. 'profile.avatarUrl': 1,
  184. },
  185. };
  186. result.users = Users.find(byUserIds, userFields)
  187. .fetch()
  188. .map(user => {
  189. // user avatar is stored as a relative url, we export absolute
  190. if ((user.profile || {}).avatarUrl) {
  191. user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
  192. }
  193. return user;
  194. });
  195. const jdata = result;
  196. //init exceljs workbook
  197. const Excel = require('exceljs');
  198. const workbook = new Excel.Workbook();
  199. workbook.creator = TAPi18n.__('export-board');
  200. workbook.lastModifiedBy = TAPi18n.__('export-board');
  201. workbook.created = new Date();
  202. workbook.modified = new Date();
  203. workbook.lastPrinted = new Date();
  204. const filename = `${jdata.title}.xlsx`;
  205. //init worksheet
  206. const worksheet = workbook.addWorksheet(jdata.title, {
  207. properties: {
  208. tabColor: {
  209. argb: 'FFC0000',
  210. },
  211. },
  212. pageSetup: {
  213. paperSize: 9,
  214. orientation: 'landscape',
  215. },
  216. });
  217. //get worksheet
  218. const ws = workbook.getWorksheet(jdata.title);
  219. ws.properties.defaultRowHeight = 20;
  220. //init columns
  221. //Excel font. Western: Arial. zh-CN: 宋体
  222. ws.columns = [
  223. {
  224. key: 'a',
  225. width: 7,
  226. },
  227. {
  228. key: 'b',
  229. width: 16,
  230. },
  231. {
  232. key: 'c',
  233. width: 7,
  234. },
  235. {
  236. key: 'd',
  237. width: 14,
  238. style: {
  239. font: {
  240. name: TAPi18n.__('excel-font'),
  241. size: '10',
  242. },
  243. numFmt: 'yyyy/mm/dd hh:mm:ss',
  244. },
  245. },
  246. {
  247. key: 'e',
  248. width: 14,
  249. style: {
  250. font: {
  251. name: TAPi18n.__('excel-font'),
  252. size: '10',
  253. },
  254. numFmt: 'yyyy/mm/dd hh:mm:ss',
  255. },
  256. },
  257. {
  258. key: 'f',
  259. width: 10,
  260. },
  261. {
  262. key: 'g',
  263. width: 10,
  264. },
  265. {
  266. key: 'h',
  267. width: 18,
  268. },
  269. ];
  270. //add title line
  271. ws.mergeCells('A1:H1');
  272. ws.getCell('A1').value = jdata.title;
  273. ws.getCell('A1').style = {
  274. font: {
  275. name: TAPi18n.__('excel-font'),
  276. size: '20',
  277. },
  278. };
  279. ws.getCell('A1').alignment = {
  280. vertical: 'middle',
  281. horizontal: 'center',
  282. };
  283. ws.getRow(1).height = 40;
  284. //get member info
  285. let jmem = '';
  286. const jmeml = {};
  287. for (const i in jdata.users) {
  288. jmem = `${jmem + jdata.users[i].profile.fullname},`;
  289. jmeml[jdata.users[i]._id] = jdata.users[i].profile.fullname;
  290. }
  291. jmem = jmem.substr(0, jmem.length - 1);
  292. //get kanban list info
  293. const jlist = {};
  294. for (const klist in jdata.lists) {
  295. jlist[jdata.lists[klist]._id] = jdata.lists[klist].title;
  296. }
  297. //get kanban label info
  298. const jlabel = {};
  299. for (const klabel in jdata.labels) {
  300. jlabel[jdata.labels[klabel]._id] = jdata.labels[klabel].name;
  301. }
  302. //add data +8 hours
  303. function add8hours(jdate) {
  304. const curdate = new Date(jdate);
  305. return new Date(curdate.setHours(curdate.getHours() + 8));
  306. }
  307. //add blank row
  308. ws.addRow().values = ['', '', '', '', '', '', '', ''];
  309. //add kanban info
  310. ws.addRow().values = [
  311. TAPi18n.__('createdAt'),
  312. add8hours(jdata.createdAt),
  313. TAPi18n.__('modifiedAt'),
  314. add8hours(jdata.modifiedAt),
  315. TAPi18n.__('r-member'),
  316. jmem,
  317. ];
  318. ws.getRow(3).font = {
  319. name: TAPi18n.__('excel-font'),
  320. size: 10,
  321. bold: true,
  322. };
  323. ws.getCell('B3').style = {
  324. font: {
  325. name: TAPi18n.__('excel-font'),
  326. size: '10',
  327. bold: true,
  328. },
  329. numFmt: 'yyyy/mm/dd hh:mm:ss',
  330. };
  331. //cell center
  332. function cellCenter(cellno) {
  333. ws.getCell(cellno).alignment = {
  334. vertical: 'middle',
  335. horizontal: 'center',
  336. wrapText: true,
  337. };
  338. }
  339. cellCenter('A3');
  340. cellCenter('B3');
  341. cellCenter('C3');
  342. cellCenter('D3');
  343. cellCenter('E3');
  344. cellCenter('F3');
  345. ws.getRow(3).height = 20;
  346. //all border
  347. function allBorder(cellno) {
  348. ws.getCell(cellno).border = {
  349. top: {
  350. style: 'thin',
  351. },
  352. left: {
  353. style: 'thin',
  354. },
  355. bottom: {
  356. style: 'thin',
  357. },
  358. right: {
  359. style: 'thin',
  360. },
  361. };
  362. }
  363. allBorder('A3');
  364. allBorder('B3');
  365. allBorder('C3');
  366. allBorder('D3');
  367. allBorder('E3');
  368. allBorder('F3');
  369. //add blank row
  370. ws.addRow().values = ['', '', '', '', '', '', '', '', ''];
  371. //add card title
  372. //ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
  373. ws.addRow().values = [
  374. TAPi18n.__('number'),
  375. TAPi18n.__('title'),
  376. TAPi18n.__('owner'),
  377. TAPi18n.__('createdAt'),
  378. TAPi18n.__('last-modified-at'),
  379. TAPi18n.__('list'),
  380. TAPi18n.__('description'),
  381. TAPi18n.__('status'),
  382. ];
  383. ws.getRow(5).height = 20;
  384. allBorder('A5');
  385. allBorder('B5');
  386. allBorder('C5');
  387. allBorder('D5');
  388. allBorder('E5');
  389. allBorder('F5');
  390. allBorder('G5');
  391. allBorder('H5');
  392. allBorder('I5');
  393. cellCenter('A5');
  394. cellCenter('B5');
  395. cellCenter('C5');
  396. cellCenter('D5');
  397. cellCenter('E5');
  398. cellCenter('F5');
  399. cellCenter('G5');
  400. cellCenter('H5');
  401. cellCenter('I5');
  402. ws.getRow(5).font = {
  403. name: TAPi18n.__('excel-font'),
  404. size: 12,
  405. bold: true,
  406. };
  407. //add blank row
  408. //add card info
  409. for (const i in jdata.cards) {
  410. const jcard = jdata.cards[i];
  411. //get member info
  412. let jcmem = '';
  413. for (const j in jcard.members) {
  414. jcmem += jmeml[jcard.members[j]];
  415. jcmem += ' ';
  416. }
  417. //get card label info
  418. let jclabel = '';
  419. for (const jl in jcard.labelIds) {
  420. jclabel += jlabel[jcard.labelIds[jl]];
  421. jclabel += ' ';
  422. }
  423. // console.log(jclabel);
  424. //add card detail
  425. const t = Number(i) + 1;
  426. ws.addRow().values = [
  427. t.toString(),
  428. jcard.title,
  429. jmeml[jcard.userId],
  430. add8hours(jcard.createdAt),
  431. add8hours(jcard.dateLastActivity),
  432. jlist[jcard.listId],
  433. jcmem,
  434. jcard.description,
  435. jclabel,
  436. ];
  437. const y = Number(i) + 6;
  438. //ws.getRow(y).height = 25;
  439. allBorder(`A${y}`);
  440. allBorder(`B${y}`);
  441. allBorder(`C${y}`);
  442. allBorder(`D${y}`);
  443. allBorder(`E${y}`);
  444. allBorder(`F${y}`);
  445. allBorder(`G${y}`);
  446. allBorder(`H${y}`);
  447. allBorder(`I${y}`);
  448. cellCenter(`A${y}`);
  449. ws.getCell(`B${y}`).alignment = {
  450. wrapText: true,
  451. };
  452. ws.getCell(`H${y}`).alignment = {
  453. wrapText: true,
  454. };
  455. ws.getCell(`I${y}`).alignment = {
  456. wrapText: true,
  457. };
  458. }
  459. // var exporte=new Stream;
  460. workbook.xlsx.write(res).then(function() {});
  461. // return exporte;
  462. }
  463. canExport(user) {
  464. const board = Boards.findOne(this._boardId);
  465. return board && board.isVisibleBy(user);
  466. }
  467. }