exportExcel.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  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 workbook = new Excel.Workbook();
  198. workbook.creator = TAPi18n.__('export-board');
  199. workbook.lastModifiedBy = TAPi18n.__('export-board');
  200. workbook.created = new Date();
  201. workbook.modified = new Date();
  202. workbook.lastPrinted = new Date();
  203. const filename = `${jdata.title}.xlsx`;
  204. //init worksheet
  205. const worksheet = workbook.addWorksheet(jdata.title, {
  206. properties: {
  207. tabColor: {
  208. argb: 'FFC0000',
  209. },
  210. },
  211. pageSetup: {
  212. paperSize: 9,
  213. orientation: 'landscape',
  214. },
  215. });
  216. //get worksheet
  217. const ws = workbook.getWorksheet(jdata.title);
  218. ws.properties.defaultRowHeight = 20;
  219. //init columns
  220. //Excel font. Western: Arial. zh-CN: 宋体
  221. ws.columns = [
  222. {
  223. key: 'a',
  224. width: 7,
  225. },
  226. {
  227. key: 'b',
  228. width: 16,
  229. },
  230. {
  231. key: 'c',
  232. width: 7,
  233. },
  234. {
  235. key: 'd',
  236. width: 14,
  237. style: {
  238. font: {
  239. name: TAPi18n.__('excel-font'),
  240. size: '10',
  241. },
  242. numFmt: 'yyyy/mm/dd hh:mm:ss',
  243. },
  244. },
  245. {
  246. key: 'e',
  247. width: 14,
  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: 'f',
  258. width: 10,
  259. },
  260. {
  261. key: 'g',
  262. width: 10,
  263. },
  264. {
  265. key: 'h',
  266. width: 18,
  267. },
  268. ];
  269. //add title line
  270. ws.mergeCells('A1:H1');
  271. ws.getCell('A1').value = jdata.title;
  272. ws.getCell('A1').style = {
  273. font: {
  274. name: TAPi18n.__('excel-font'),
  275. size: '20',
  276. },
  277. };
  278. ws.getCell('A1').alignment = {
  279. vertical: 'middle',
  280. horizontal: 'center',
  281. };
  282. ws.getRow(1).height = 40;
  283. //get member info
  284. let jmem = '';
  285. const jmeml = {};
  286. for (const i in jdata.users) {
  287. jmem = `${jmem + jdata.users[i].profile.fullname},`;
  288. jmeml[jdata.users[i]._id] = jdata.users[i].profile.fullname;
  289. }
  290. jmem = jmem.substr(0, jmem.length - 1);
  291. //get kanban list info
  292. const jlist = {};
  293. for (const klist in jdata.lists) {
  294. jlist[jdata.lists[k]._id] = jdata.lists[klist].title;
  295. }
  296. //get kanban label info
  297. const jlabel = {};
  298. for (const klabel in jdata.labels) {
  299. jlabel[jdata.labels[k]._id] = jdata.labels[klabel].name;
  300. }
  301. //add data +8 hours
  302. function add8hours(jdate) {
  303. const curdate = new Date(jdate);
  304. return new Date(curdate.setHours(curdate.getHours() + 8));
  305. }
  306. //add blank row
  307. ws.addRow().values = ['', '', '', '', '', '', '', ''];
  308. //add kanban info
  309. ws.addRow().values = [
  310. TAPi18n.__('createdAt'),
  311. add8hours(jdata.createdAt),
  312. TAPi18n.__('modifiedAt'),
  313. add8hours(jdata.modifiedAt),
  314. TAPi18n.__('r-member'),
  315. jmem,
  316. ];
  317. ws.getRow(3).font = {
  318. name: TAPi18n.__('excel-font'),
  319. size: 10,
  320. bold: true,
  321. };
  322. ws.getCell('B3').style = {
  323. font: {
  324. name: TAPi18n.__('excel-font'),
  325. size: '10',
  326. bold: true,
  327. },
  328. numFmt: 'yyyy/mm/dd hh:mm:ss',
  329. };
  330. //cell center
  331. function cellCenter(cellno) {
  332. ws.getCell(cellno).alignment = {
  333. vertical: 'middle',
  334. horizontal: 'center',
  335. wrapText: true,
  336. };
  337. }
  338. cellCenter('A3');
  339. cellCenter('B3');
  340. cellCenter('C3');
  341. cellCenter('D3');
  342. cellCenter('E3');
  343. cellCenter('F3');
  344. ws.getRow(3).height = 20;
  345. //all border
  346. function allBorder(cellno) {
  347. ws.getCell(cellno).border = {
  348. top: {
  349. style: 'thin',
  350. },
  351. left: {
  352. style: 'thin',
  353. },
  354. bottom: {
  355. style: 'thin',
  356. },
  357. right: {
  358. style: 'thin',
  359. },
  360. };
  361. }
  362. allBorder('A3');
  363. allBorder('B3');
  364. allBorder('C3');
  365. allBorder('D3');
  366. allBorder('E3');
  367. allBorder('F3');
  368. //add blank row
  369. ws.addRow().values = ['', '', '', '', '', '', '', '', ''];
  370. //add card title
  371. //ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
  372. ws.addRow().values = [
  373. TAPi18n.__('number'),
  374. TAPi18n.__('title'),
  375. TAPi18n.__('owner'),
  376. TAPi18n.__('createdAt'),
  377. TAPi18n.__('last-modified-at'),
  378. TAPi18n.__('list'),
  379. TAPi18n.__('description'),
  380. TAPi18n.__('status'),
  381. ];
  382. ws.getRow(5).height = 20;
  383. allBorder('A5');
  384. allBorder('B5');
  385. allBorder('C5');
  386. allBorder('D5');
  387. allBorder('E5');
  388. allBorder('F5');
  389. allBorder('G5');
  390. allBorder('H5');
  391. allBorder('I5');
  392. cellCenter('A5');
  393. cellCenter('B5');
  394. cellCenter('C5');
  395. cellCenter('D5');
  396. cellCenter('E5');
  397. cellCenter('F5');
  398. cellCenter('G5');
  399. cellCenter('H5');
  400. cellCenter('I5');
  401. ws.getRow(5).font = {
  402. name: TAPi18n.__('excel-font'),
  403. size: 12,
  404. bold: true,
  405. };
  406. //add blank row
  407. //add card info
  408. for (const i in jdata.cards) {
  409. const jcard = jdata.cards[i];
  410. //get member info
  411. let jcmem = '';
  412. for (const j in jcard.members) {
  413. jcmem += jmeml[jcard.members[j]];
  414. jcmem += ' ';
  415. }
  416. //get card label info
  417. let jclabel = '';
  418. for (const jl in jcard.labelIds) {
  419. jclabel += jlabel[jcard.labelIds[jl]];
  420. jclabel += ' ';
  421. }
  422. // console.log(jclabel);
  423. //add card detail
  424. const t = Number(i) + 1;
  425. ws.addRow().values = [
  426. t.toString(),
  427. jcard.title,
  428. jmeml[jcard.userId],
  429. add8hours(jcard.createdAt),
  430. add8hours(jcard.dateLastActivity),
  431. jlist[jcard.listId],
  432. jcmem,
  433. jcard.description,
  434. jclabel,
  435. ];
  436. const y = Number(i) + 6;
  437. //ws.getRow(y).height = 25;
  438. allBorder(`A${y}`);
  439. allBorder(`B${y}`);
  440. allBorder(`C${y}`);
  441. allBorder(`D${y}`);
  442. allBorder(`E${y}`);
  443. allBorder(`F${y}`);
  444. allBorder(`G${y}`);
  445. allBorder(`H${y}`);
  446. allBorder(`I${y}`);
  447. cellCenter(`A${y}`);
  448. ws.getCell(`B${y}`).alignment = {
  449. wrapText: true,
  450. };
  451. ws.getCell(`H${y}`).alignment = {
  452. wrapText: true,
  453. };
  454. ws.getCell(`I${y}`).alignment = {
  455. wrapText: true,
  456. };
  457. }
  458. // var exporte=new Stream;
  459. workbook.xlsx.write(res).then(function() {});
  460. // return exporte;
  461. }
  462. canExport(user) {
  463. const board = Boards.findOne(this._boardId);
  464. return board && board.isVisibleBy(user);
  465. }
  466. }