| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631 | if (Meteor.isServer) {  // todo XXX once we have a real API in place, move that route there  // todo XXX also  share the route definition between the client and the server  // so that we could use something like  // `ApiRoutes.path('boards/exportExcel', boardId)``  // on the client instead of copy/pasting the route path manually between the  // client and the server.  /**   * @operation exportExcel   * @tag Boards   *   * @summary This route is used to export the board Excel.   *   * @description If user is already logged-in, pass loginToken as param   * "authToken": '/api/boards/:boardId/exportExcel?authToken=:token'   *   * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/   * for detailed explanations   *   * @param {string} boardId the ID of the board we are exporting   * @param {string} authToken the loginToken   */  const Excel = require('exceljs');  Picker.route('/api/boards/:boardId/exportExcel', function(params, req, res) {    const boardId = params.boardId;    let user = null;    const loginToken = params.query.authToken;    if (loginToken) {      const hashToken = Accounts._hashLoginToken(loginToken);      user = Meteor.users.findOne({        'services.resume.loginTokens.hashedToken': hashToken,      });    } else if (!Meteor.settings.public.sandstorm) {      Authentication.checkUserId(req.userId);      user = Users.findOne({        _id: req.userId,        isAdmin: true,      });    }    const exporterExcel = new ExporterExcel(boardId);    if (exporterExcel.canExport(user)) {      exporterExcel.build(res);    } else {      res.end(TAPi18n.__('user-can-not-export-excel'));    }  });}// exporter maybe is broken since Gridfs introduced, add fs and pathexport class ExporterExcel {  constructor(boardId) {    this._boardId = boardId;  }  build(res) {    const fs = Npm.require('fs');    const os = Npm.require('os');    const path = Npm.require('path');    const byBoard = {      boardId: this._boardId,    };    const byBoardNoLinked = {      boardId: this._boardId,      linkedId: {        $in: ['', null],      },    };    // we do not want to retrieve boardId in related elements    const noBoardId = {      fields: {        boardId: 0,      },    };    const result = {      _format: 'wekan-board-1.0.0',    };    _.extend(      result,      Boards.findOne(this._boardId, {        fields: {          stars: 0,        },      }),    );    result.lists = Lists.find(byBoard, noBoardId).fetch();    result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();    result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();    result.customFields = CustomFields.find(      {        boardIds: {          $in: [this.boardId],        },      },      {        fields: {          boardId: 0,        },      },    ).fetch();    result.comments = CardComments.find(byBoard, noBoardId).fetch();    result.activities = Activities.find(byBoard, noBoardId).fetch();    result.rules = Rules.find(byBoard, noBoardId).fetch();    result.checklists = [];    result.checklistItems = [];    result.subtaskItems = [];    result.triggers = [];    result.actions = [];    result.cards.forEach(card => {      result.checklists.push(        ...Checklists.find({          cardId: card._id,        }).fetch(),      );      result.checklistItems.push(        ...ChecklistItems.find({          cardId: card._id,        }).fetch(),      );      result.subtaskItems.push(        ...Cards.find({          parentId: card._id,        }).fetch(),      );    });    result.rules.forEach(rule => {      result.triggers.push(        ...Triggers.find(          {            _id: rule.triggerId,          },          noBoardId,        ).fetch(),      );      result.actions.push(        ...Actions.find(          {            _id: rule.actionId,          },          noBoardId,        ).fetch(),      );    });    // we also have to export some user data - as the other elements only    // include id but we have to be careful:    // 1- only exports users that are linked somehow to that board    // 2- do not export any sensitive information    const users = {};    result.members.forEach(member => {      users[member.userId] = true;    });    result.lists.forEach(list => {      users[list.userId] = true;    });    result.cards.forEach(card => {      users[card.userId] = true;      if (card.members) {        card.members.forEach(memberId => {          users[memberId] = true;        });      }      if (card.assignees) {        card.assignees.forEach(memberId => {          users[memberId] = true;        });      }    });    result.comments.forEach(comment => {      users[comment.userId] = true;    });    result.activities.forEach(activity => {      users[activity.userId] = true;    });    result.checklists.forEach(checklist => {      users[checklist.userId] = true;    });    const byUserIds = {      _id: {        $in: Object.getOwnPropertyNames(users),      },    };    // we use whitelist to be sure we do not expose inadvertently    // some secret fields that gets added to User later.    const userFields = {      fields: {        _id: 1,        username: 1,        'profile.initials': 1,        'profile.avatarUrl': 1,      },    };    result.users = Users.find(byUserIds, userFields)      .fetch()      .map(user => {        // user avatar is stored as a relative url, we export absolute        if ((user.profile || {}).avatarUrl) {          user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);        }        return user;      });    //init exceljs workbook    const Excel = require('exceljs');    const workbook = new Excel.Workbook();    workbook.creator = TAPi18n.__('export-board');    workbook.lastModifiedBy = TAPi18n.__('export-board');    workbook.created = new Date();    workbook.modified = new Date();    workbook.lastPrinted = new Date();    const filename = `${result.title}.xlsx`;    //init worksheet    const worksheet = workbook.addWorksheet(result.title, {      properties: {        tabColor: {          argb: 'FFC0000',        },      },      pageSetup: {        paperSize: 9,        orientation: 'landscape',      },    });    //get worksheet    const ws = workbook.getWorksheet(result.title);    ws.properties.defaultRowHeight = 20;    //init columns    //Excel font. Western: Arial. zh-CN: 宋体    ws.columns = [      {        key: 'a',        width: 14,      },      {        key: 'b',        width: 20,      },      {        key: 'c',        width: 20,      },      {        key: 'd',        width: 20,        style: {          font: {            name: TAPi18n.__('excel-font'),            size: '10',          },          numFmt: 'yyyy/mm/dd hh:mm:ss',        },      },      {        key: 'e',        width: 20,        style: {          font: {            name: TAPi18n.__('excel-font'),            size: '10',          },          numFmt: 'yyyy/mm/dd hh:mm:ss',        },      },      {        key: 'f',        width: 20,        style: {          font: {            name: TAPi18n.__('excel-font'),            size: '10',          },          numFmt: 'yyyy/mm/dd hh:mm:ss',        },      },      {        key: 'g',        width: 20,        style: {          font: {            name: TAPi18n.__('excel-font'),            size: '10',          },          numFmt: 'yyyy/mm/dd hh:mm:ss',        },      },      {        key: 'h',        width: 20,        style: {          font: {            name: TAPi18n.__('excel-font'),            size: '10',          },          numFmt: 'yyyy/mm/dd hh:mm:ss',        },      },      {        key: 'i',        width: 20,        style: {          font: {            name: TAPi18n.__('excel-font'),            size: '10',          },          numFmt: 'yyyy/mm/dd hh:mm:ss',        },      },      {        key: 'j',        width: 20,        style: {          font: {            name: TAPi18n.__('excel-font'),            size: '10',          },          numFmt: 'yyyy/mm/dd hh:mm:ss',        },      },      {        key: 'k',        width: 20,      },      {        key: 'l',        width: 20,      },      {        key: 'm',        width: 20,      },      {        key: 'n',        width: 20,      },      {        key: 'o',        width: 20,      },      {        key: 'p',        width: 20,      },    ];    //add title line    ws.mergeCells('A1:H1');    ws.getCell('A1').value = result.title;    ws.getCell('A1').style = {      font: {        name: TAPi18n.__('excel-font'),        size: '20',      },    };    ws.getCell('A1').alignment = {      vertical: 'middle',      horizontal: 'center',    };    ws.getRow(1).height = 40;    //get member and assignee info    let jmem = '';    let jassig = '';    const jmeml = {};    const jassigl = {};    for (const i in result.users) {      jmem = `${jmem + result.users[i].username},`;      jmeml[result.users[i]._id] = result.users[i].username;    }    jmem = jmem.substr(0, jmem.length - 1);    for (const ia in result.users) {      jassig = `${jassig + result.users[ia].username},`;      jassigl[result.users[ia]._id] = result.users[ia].username;    }    jassig = jassig.substr(0, jassig.length - 1);    //get kanban list info    const jlist = {};    for (const klist in result.lists) {      jlist[result.lists[klist]._id] = result.lists[klist].title;    }    //get kanban label info    const jlabel = {};    for (const klabel in result.labels) {      jlabel[result.labels[klabel]._id] = result.labels[klabel].name;    }    //add data +8 hours    function addTZhours(jdate) {      const curdate = new Date(jdate);      const checkCorrectDate = moment(curdate);      if (checkCorrectDate.isValid()) {        return curdate;      } else {        return ' ';      }      ////Do not add 8 hours to GMT. Use GMT instead.      ////Could not yet figure out how to get localtime.      //return new Date(curdate.setHours(curdate.getHours() + 8));      //return curdate;    }    //add blank row    ws.addRow().values = ['', '', '', '', '', ''];    //add kanban info    ws.addRow().values = [      TAPi18n.__('createdAt'),      addTZhours(result.createdAt),      TAPi18n.__('modifiedAt'),      addTZhours(result.modifiedAt),      TAPi18n.__('members'),      jmem,    ];    ws.getRow(3).font = {      name: TAPi18n.__('excel-font'),      size: 10,      bold: true,    };    ws.mergeCells('F3:P3');    ws.getCell('B3').style = {      font: {        name: TAPi18n.__('excel-font'),        size: '10',        bold: true,      },      numFmt: 'yyyy/mm/dd hh:mm:ss',    };    //cell center    function cellCenter(cellno) {      ws.getCell(cellno).alignment = {        vertical: 'middle',        horizontal: 'center',        wrapText: true,      };    }    function cellLeft(cellno) {      ws.getCell(cellno).alignment = {        vertical: 'middle',        horizontal: 'left',        wrapText: true,      };    }    cellCenter('A3');    cellCenter('B3');    cellCenter('C3');    cellCenter('D3');    cellCenter('E3');    cellLeft('F3');    ws.getRow(3).height = 20;    //all border    function allBorder(cellno) {      ws.getCell(cellno).border = {        top: {          style: 'thin',        },        left: {          style: 'thin',        },        bottom: {          style: 'thin',        },        right: {          style: 'thin',        },      };    }    allBorder('A3');    allBorder('B3');    allBorder('C3');    allBorder('D3');    allBorder('E3');    allBorder('F3');    //add blank row    ws.addRow().values = [      '',      '',      '',      '',      '',      '',      '',      '',      '',      '',      '',      '',      '',      '',      '',    ];    //add card title    //ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];    ws.addRow().values = [      TAPi18n.__('number'),      TAPi18n.__('title'),      TAPi18n.__('description'),      TAPi18n.__('owner'),      TAPi18n.__('createdAt'),      TAPi18n.__('last-modified-at'),      TAPi18n.__('card-received'),      TAPi18n.__('card-start'),      TAPi18n.__('card-due'),      TAPi18n.__('card-end'),      TAPi18n.__('list'),      TAPi18n.__('assignee'),      TAPi18n.__('members'),      TAPi18n.__('labels'),      TAPi18n.__('overtime-hours'),      TAPi18n.__('spent-time-hours'),    ];    ws.getRow(5).height = 20;    allBorder('A5');    allBorder('B5');    allBorder('C5');    allBorder('D5');    allBorder('E5');    allBorder('F5');    allBorder('G5');    allBorder('H5');    allBorder('I5');    allBorder('J5');    allBorder('K5');    allBorder('L5');    allBorder('M5');    allBorder('N5');    allBorder('O5');    allBorder('P5');    cellCenter('A5');    cellCenter('B5');    cellCenter('C5');    cellCenter('D5');    cellCenter('E5');    cellCenter('F5');    cellCenter('G5');    cellCenter('H5');    cellCenter('I5');    cellCenter('J5');    cellCenter('K5');    cellCenter('L5');    cellCenter('M5');    cellCenter('N5');    cellCenter('O5');    cellCenter('P5');    ws.getRow(5).font = {      name: TAPi18n.__('excel-font'),      size: 12,      bold: true,    };    //add blank row    //add card info    for (const i in result.cards) {      const jcard = result.cards[i];      //get member info      let jcmem = '';      for (const j in jcard.members) {        jcmem += jmeml[jcard.members[j]];        jcmem += ' ';      }      //get assignee info      let jcassig = '';      for (const ja in jcard.assignees) {        jcassig += jassigl[jcard.assignees[ja]];        jcassig += ' ';      }      //get card label info      let jclabel = '';      for (const jl in jcard.labelIds) {        jclabel += jlabel[jcard.labelIds[jl]];        jclabel += ' ';      }      //add card detail      const t = Number(i) + 1;      ws.addRow().values = [        t.toString(),        jcard.title,        jcard.description,        jmeml[jcard.userId],        addTZhours(jcard.createdAt),        addTZhours(jcard.dateLastActivity),        addTZhours(jcard.receivedAt),        addTZhours(jcard.startAt),        addTZhours(jcard.dueAt),        addTZhours(jcard.endAt),        jlist[jcard.listId],        jcassig,        jcmem,        jclabel,        jcard.isOvertime ? 'true' : 'false',        jcard.spentTime,      ];      const y = Number(i) + 6;      //ws.getRow(y).height = 25;      allBorder(`A${y}`);      allBorder(`B${y}`);      allBorder(`C${y}`);      allBorder(`D${y}`);      allBorder(`E${y}`);      allBorder(`F${y}`);      allBorder(`G${y}`);      allBorder(`H${y}`);      allBorder(`I${y}`);      allBorder(`J${y}`);      allBorder(`K${y}`);      allBorder(`L${y}`);      allBorder(`M${y}`);      allBorder(`N${y}`);      allBorder(`O${y}`);      allBorder(`P${y}`);      cellCenter(`A${y}`);      ws.getCell(`B${y}`).alignment = {        wrapText: true,      };      ws.getCell(`C${y}`).alignment = {        wrapText: true,      };      ws.getCell(`L${y}`).alignment = {        wrapText: true,      };      ws.getCell(`M${y}`).alignment = {        wrapText: true,      };      ws.getCell(`N${y}`).alignment = {        wrapText: true,      };    }    workbook.xlsx.write(res).then(function() {});  }  canExport(user) {    const board = Boards.findOne(this._boardId);    return board && board.isVisibleBy(user);  }}
 |