This commit is contained in:
John Supplee 2021-07-20 19:24:40 -04:00
commit 0ce2f9ea43
204 changed files with 13619 additions and 2379 deletions

View file

@ -109,6 +109,8 @@ Boards.attachSchema(
* List of labels attached to a board
*/
type: [Object],
optional: true,
/* Commented out, so does not create labels to new boards.
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
@ -122,6 +124,7 @@ Boards.attachSchema(
}));
}
},
*/
},
'labels.$._id': {
/**
@ -224,6 +227,56 @@ Boards.attachSchema(
type: String,
allowedValues: ['public', 'private'],
},
orgs: {
/**
* the list of organizations that a board belongs to
*/
type: [Object],
optional: true,
},
'orgs.$.orgId':{
/**
* The uniq ID of the organization
*/
type: String,
},
'orgs.$.orgDisplayName':{
/**
* The display name of the organization
*/
type: String,
},
'orgs.$.isActive': {
/**
* Is the organization active?
*/
type: Boolean,
},
teams: {
/**
* the list of teams that a board belongs to
*/
type: [Object],
optional: true,
},
'teams.$.teamId':{
/**
* The uniq ID of the team
*/
type: String,
},
'teams.$.teamDisplayName':{
/**
* The display name of the team
*/
type: String,
},
'teams.$.isActive': {
/**
* Is the team active?
*/
type: Boolean,
},
color: {
/**
* The color of the board.
@ -370,6 +423,14 @@ Boards.attachSchema(
defaultValue: true,
},
allowsCardSortingByNumber: {
/**
* Does the board allows card sorting by number?
*/
type: Boolean,
defaultValue: true,
},
allowsAssignedBy: {
/**
* Does the board allows requested by?
@ -684,6 +745,23 @@ Boards.helpers({
return _.where(this.members, { isActive: true });
},
activeOrgs() {
return _.where(this.orgs, { isActive: true });
},
// hasNotAnyOrg(){
// return this.orgs === undefined || this.orgs.length <= 0;
// },
activeTeams() {
return _.where(this.teams, { isActive: true });
},
// hasNotAnyTeam(){
// return this.teams === undefined || this.teams.length <= 0;
// },
activeAdmins() {
return _.where(this.members, { isActive: true, isAdmin: true });
},
@ -1187,6 +1265,10 @@ Boards.mutations({
return { $set: { allowsRequestedBy } };
},
setAllowsCardSortingByNumber(allowsCardSortingByNumber) {
return { $set: { allowsCardSortingByNumber } };
},
setAllowsAttachments(allowsAttachments) {
return { $set: { allowsAttachments } };
},
@ -1417,13 +1499,18 @@ if (Meteor.isServer) {
myLabelNames() {
let names = [];
Boards.userBoards(Meteor.userId()).forEach(board => {
names = names.concat(
board.labels
.filter(label => !!label.name)
.map(label => {
return label.name;
}),
);
// Only return labels when they exist.
if (board.labels !== undefined) {
names = names.concat(
board.labels
.filter(label => !!label.name)
.map(label => {
return label.name;
}),
);
} else {
return [];
}
});
return _.uniq(names).sort();
},
@ -1449,6 +1536,24 @@ if (Meteor.isServer) {
} else throw new Meteor.Error('error-board-notAMember');
} else throw new Meteor.Error('error-board-doesNotExist');
},
setBoardOrgs(boardOrgsArray, currBoardId){
check(boardOrgsArray, Array);
check(currBoardId, String);
Boards.update(currBoardId, {
$set: {
orgs: boardOrgsArray,
},
});
},
setBoardTeams(boardTeamsArray, currBoardId){
check(boardTeamsArray, Array);
check(currBoardId, String);
Boards.update(currBoardId, {
$set: {
teams: boardTeamsArray,
},
});
},
});
}
@ -1673,7 +1778,8 @@ if (Meteor.isServer) {
*/
JsonRoutes.add('GET', '/api/boards', function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: Boards.find(
@ -1847,7 +1953,8 @@ if (Meteor.isServer) {
* @return_type string
*/
JsonRoutes.add('PUT', '/api/boards/:boardId/labels', function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const id = req.params.boardId;
try {
if (req.body.hasOwnProperty('label')) {

View file

@ -192,8 +192,8 @@ if (Meteor.isServer) {
res,
) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramCardId = req.params.cardId;
JsonRoutes.sendResult(res, {
code: 200,
@ -230,8 +230,8 @@ if (Meteor.isServer) {
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramCommentId = req.params.commentId;
const paramCardId = req.params.cardId;
JsonRoutes.sendResult(res, {
@ -266,8 +266,8 @@ if (Meteor.isServer) {
'/api/boards/:boardId/cards/:cardId/comments',
function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramCardId = req.params.cardId;
const id = CardComments.direct.insert({
userId: req.body.authorId,
@ -312,8 +312,8 @@ if (Meteor.isServer) {
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramCommentId = req.params.commentId;
const paramCardId = req.params.cardId;
CardComments.remove({

View file

@ -338,6 +338,138 @@ Cards.attachSchema(
type: Boolean,
defaultValue: false,
},
poker: {
/**
* poker object, see below
*/
type: Object,
optional: true,
},
'poker.question': {
type: Boolean,
defaultValue: false,
},
'poker.one': {
/**
* poker card one
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.two': {
/**
* poker card two
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.three': {
/**
* poker card three
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.five': {
/**
* poker card five
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.eight': {
/**
* poker card eight
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.thirteen': {
/**
* poker card thirteen
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.twenty': {
/**
* poker card twenty
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.forty': {
/**
* poker card forty
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.oneHundred': {
/**
* poker card oneHundred
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.unsure': {
/**
* poker card unsure
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.end': {
type: Date,
optional: true,
defaultValue: null,
},
'poker.allowNonBoardMembers': {
type: Boolean,
defaultValue: false,
},
'poker.estimation': {
/**
* poker estimation value
*/
type: Number,
optional: true,
},
targetId_gantt: {
/**
* ID of card which is the child link in gantt view
*/
type: [String],
optional: true,
defaultValue: [],
},
linkType_gantt: {
/**
* ID of card which is the parent link in gantt view
*/
type: [Number],
decimal: false,
optional: true,
defaultValue: [],
},
linkId_gantt: {
/**
* ID of card which is the parent link in gantt view
*/
type: [String],
optional: true,
defaultValue: [],
},
}),
);
@ -361,6 +493,27 @@ Cards.allow({
});
Cards.helpers({
// Gantt https://github.com/wekan/wekan/issues/2870#issuecomment-857171127
setGanttTargetId(sourceId, targetId, linkType, linkId){
return Cards.update({ _id: sourceId}, {
$push: {
targetId_gantt: targetId,
linkType_gantt : linkType,
linkId_gantt: linkId
}
});
},
removeGanttTargetId(sourceId, targetId, linkType, linkId){
return Cards.update({ _id: sourceId}, {
$pull: {
targetId_gantt: targetId,
linkType_gantt : linkType,
linkId_gantt: linkId
}
});
},
mapCustomFieldsToBoard(boardId) {
// Map custom fields to new board
return this.customFields.map(cf => {
@ -1279,6 +1432,191 @@ Cards.helpers({
return null;
},
getPokerQuestion() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else if (card && card.poker) {
return card.poker.question;
} else {
return null;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else if (board && board.poker) {
return board.poker.question;
} else {
return null;
}
} else if (this.poker) {
return this.poker.question;
} else {
return null;
}
},
getPokerEstimation() {
if (this.poker) {
return this.poker.estimation;
} else {
return null;
}
},
getPokerEnd() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else if (card && card.poker) {
return card.poker.end;
} else {
return null;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else if (board && board.poker) {
return board.poker.end;
} else {
return null;
}
} else if (this.poker) {
return this.poker.end;
} else {
return null;
}
},
expiredPoker() {
let end = this.getPokerEnd();
if (end) {
end = moment(end);
return end.isBefore(new Date());
}
return false;
},
pokerMemberOne() {
if (this.poker && this.poker.one)
return Users.find({ _id: { $in: this.poker.one } });
return [];
},
pokerMemberTwo() {
if (this.poker && this.poker.two)
return Users.find({ _id: { $in: this.poker.two } });
return [];
},
pokerMemberThree() {
if (this.poker && this.poker.three)
return Users.find({ _id: { $in: this.poker.three } });
return [];
},
pokerMemberFive() {
if (this.poker && this.poker.five)
return Users.find({ _id: { $in: this.poker.five } });
return [];
},
pokerMemberEight() {
if (this.poker && this.poker.eight)
return Users.find({ _id: { $in: this.poker.eight } });
return [];
},
pokerMemberThirteen() {
if (this.poker && this.poker.thirteen)
return Users.find({ _id: { $in: this.poker.thirteen } });
return [];
},
pokerMemberTwenty() {
if (this.poker && this.poker.twenty)
return Users.find({ _id: { $in: this.poker.twenty } });
return [];
},
pokerMemberForty() {
if (this.poker && this.poker.forty)
return Users.find({ _id: { $in: this.poker.forty } });
return [];
},
pokerMemberOneHundred() {
if (this.poker && this.poker.oneHundred)
return Users.find({ _id: { $in: this.poker.oneHundred } });
return [];
},
pokerMemberUnsure() {
if (this.poker && this.poker.unsure)
return Users.find({ _id: { $in: this.poker.unsure } });
return [];
},
pokerState() {
const userId = Meteor.userId();
let state;
if (this.poker) {
if (this.poker.one) {
state = _.contains(this.poker.one, userId);
if (state === true) {
return 'one';
}
}
if (this.poker.two) {
state = _.contains(this.poker.two, userId);
if (state === true) {
return 'two';
}
}
if (this.poker.three) {
state = _.contains(this.poker.three, userId);
if (state === true) {
return 'three';
}
}
if (this.poker.five) {
state = _.contains(this.poker.five, userId);
if (state === true) {
return 'five';
}
}
if (this.poker.eight) {
state = _.contains(this.poker.eight, userId);
if (state === true) {
return 'eight';
}
}
if (this.poker.thirteen) {
state = _.contains(this.poker.thirteen, userId);
if (state === true) {
return 'thirteen';
}
}
if (this.poker.twenty) {
state = _.contains(this.poker.twenty, userId);
if (state === true) {
return 'twenty';
}
}
if (this.poker.forty) {
state = _.contains(this.poker.forty, userId);
if (state === true) {
return 'forty';
}
}
if (this.poker.oneHundred) {
state = _.contains(this.poker.oneHundred, userId);
if (state === true) {
return 'oneHundred';
}
}
if (this.poker.unsure) {
state = _.contains(this.poker.unsure, userId);
if (state === true) {
return 'unsure';
}
}
}
return null;
},
getId() {
if (this.isLinked()) {
return this.linkedId;
@ -1433,6 +1771,101 @@ Cards.helpers({
voteCount() {
return this.voteCountPositive() + this.voteCountNegative();
},
pokerAllowNonBoardMembers() {
if (this.poker) return this.poker.allowNonBoardMembers;
return null;
},
pokerCountOne() {
if (this.poker && this.poker.one) return this.poker.one.length;
return null;
},
pokerCountTwo() {
if (this.poker && this.poker.two) return this.poker.two.length;
return null;
},
pokerCountThree() {
if (this.poker && this.poker.three) return this.poker.three.length;
return null;
},
pokerCountFive() {
if (this.poker && this.poker.five) return this.poker.five.length;
return null;
},
pokerCountEight() {
if (this.poker && this.poker.eight) return this.poker.eight.length;
return null;
},
pokerCountThirteen() {
if (this.poker && this.poker.thirteen) return this.poker.thirteen.length;
return null;
},
pokerCountTwenty() {
if (this.poker && this.poker.twenty) return this.poker.twenty.length;
return null;
},
pokerCountForty() {
if (this.poker && this.poker.forty) return this.poker.forty.length;
return null;
},
pokerCountOneHundred() {
if (this.poker && this.poker.oneHundred) return this.poker.oneHundred.length;
return null;
},
pokerCountUnsure() {
if (this.poker && this.poker.unsure) return this.poker.unsure.length;
return null;
},
pokerCount() {
return (
this.pokerCountOne() +
this.pokerCountTwo() +
this.pokerCountThree() +
this.pokerCountFive() +
this.pokerCountEight() +
this.pokerCountThirteen() +
this.pokerCountTwenty() +
this.pokerCountForty() +
this.pokerCountOneHundred() +
this.pokerCountUnsure()
);
},
pokerWinner() {
const pokerListMaps = [];
let pokerWinnersListMap = [];
if (this.expiredPoker()) {
const one = { count: this.pokerCountOne(), pokerCard: 1 };
const two = { count: this.pokerCountTwo(), pokerCard: 2 };
const three = { count: this.pokerCountThree(), pokerCard: 3 };
const five = { count: this.pokerCountFive(), pokerCard: 5 };
const eight = { count: this.pokerCountEight(), pokerCard: 8 };
const thirteen = { count: this.pokerCountThirteen(), pokerCard: 13 };
const twenty = { count: this.pokerCountTwenty(), pokerCard: 20 };
const forty = { count: this.pokerCountForty(), pokerCard: 40 };
const oneHundred = { count: this.pokerCountOneHundred(), pokerCard: 100 };
const unsure = { count: this.pokerCountUnsure(), pokerCard: 'Unsure' };
pokerListMaps.push(one);
pokerListMaps.push(two);
pokerListMaps.push(three);
pokerListMaps.push(five);
pokerListMaps.push(eight);
pokerListMaps.push(thirteen);
pokerListMaps.push(twenty);
pokerListMaps.push(forty);
pokerListMaps.push(oneHundred);
pokerListMaps.push(unsure);
pokerListMaps.sort(function(a, b) {
return b.count - a.count;
});
const max = pokerListMaps[0].count;
pokerWinnersListMap = pokerListMaps.filter(task => task.count >= max);
pokerWinnersListMap.sort(function(a, b) {
return b.pokerCard - a.pokerCard;
});
}
return pokerWinnersListMap[0].pokerCard;
},
});
Cards.mutations({
@ -1870,6 +2303,279 @@ Cards.mutations({
};
}
},
setPokerQuestion(question, allowNonBoardMembers) {
return {
$set: {
poker: {
question,
allowNonBoardMembers,
one: [],
two: [],
three: [],
five: [],
eight: [],
thirteen: [],
twenty: [],
forty: [],
oneHundred: [],
unsure: [],
},
},
};
},
setPokerEstimation(estimation) {
return {
$set: { 'poker.estimation': estimation },
};
},
unsetPokerEstimation() {
return {
$unset: { 'poker.estimation': '' },
};
},
unsetPoker() {
return {
$unset: {
poker: '',
},
};
},
setPokerEnd(end) {
return {
$set: { 'poker.end': end },
};
},
unsetPokerEnd() {
return {
$unset: { 'poker.end': '' },
};
},
setPoker(userId, state) {
switch (state) {
case 'one':
// poker one
return {
$pull: {
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.one': userId,
},
};
case 'two':
// poker two
return {
$pull: {
'poker.one': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.two': userId,
},
};
case 'three':
// poker three
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.three': userId,
},
};
case 'five':
// poker five
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.five': userId,
},
};
case 'eight':
// poker eight
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.eight': userId,
},
};
case 'thirteen':
// poker thirteen
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.thirteen': userId,
},
};
case 'twenty':
// poker twenty
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.twenty': userId,
},
};
case 'forty':
// poker forty
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.forty': userId,
},
};
case 'oneHundred':
// poker one hundred
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.oneHundred': userId,
},
};
case 'unsure':
// poker unsure
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
},
$addToSet: {
'poker.unsure': userId,
},
};
default:
// Remove pokers
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
};
}
},
replayPoker() {
return {
$set: {
'poker.one': [],
'poker.two': [],
'poker.three': [],
'poker.five': [],
'poker.eight': [],
'poker.thirteen': [],
'poker.twelve': [],
'poker.forty': [],
'poker.oneHundred': [],
'poker.unsure': [],
},
};
},
});
//FUNCTIONS FOR creation of Activities
@ -2593,14 +3299,17 @@ if (Meteor.isServer) {
* @param {string} vote.question the vote question
* @param {boolean} vote.public show who voted what
* @param {boolean} vote.allowNonBoardMembers allow all logged in users to vote?
* @param {Object} [poker] the poker object
* @param {string} poker.question the vote question
* @param {boolean} poker.allowNonBoardMembers allow all logged in users to vote?
* @return_type {_id: string}
*/
JsonRoutes.add(
'PUT',
'/api/boards/:boardId/lists/:listId/cards/:cardId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramCardId = req.params.cardId;
const paramListId = req.params.listId;
@ -2698,6 +3407,31 @@ if (Meteor.isServer) {
{ $set: { vote: newVote } },
);
}
if (req.body.hasOwnProperty('poker')) {
const newPoker = req.body.poker;
newPoker.one = [];
newPoker.two = [];
newPoker.three = [];
newPoker.five = [];
newPoker.eight = [];
newPoker.thirteen = [];
newPoker.twenty = [];
newPoker.forty = [];
newPoker.oneHundred = [];
newPoker.unsure = [];
if (!newPoker.hasOwnProperty('allowNonBoardMembers'))
newPoker.allowNonBoardMembers = false;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { poker: newPoker } },
);
}
if (req.body.hasOwnProperty('labelIds')) {
let newlabelIds = req.body.labelIds;
if (_.isString(newlabelIds)) {
@ -2932,8 +3666,8 @@ if (Meteor.isServer) {
'DELETE',
'/api/boards/:boardId/lists/:listId/cards/:cardId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramListId = req.params.listId;
const paramCardId = req.params.cardId;

View file

@ -265,7 +265,8 @@ if (Meteor.isServer) {
'GET',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramItemId = req.params.itemId;
const checklistItem = ChecklistItems.findOne({ _id: paramItemId });
if (checklistItem) {
@ -298,7 +299,8 @@ if (Meteor.isServer) {
'PUT',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramItemId = req.params.itemId;
@ -349,7 +351,8 @@ if (Meteor.isServer) {
'DELETE',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramItemId = req.params.itemId;
ChecklistItems.direct.remove({ _id: paramItemId });
JsonRoutes.sendResult(res, {

View file

@ -204,7 +204,8 @@ if (Meteor.isServer) {
'GET',
'/api/boards/:boardId/cards/:cardId/checklists',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramCardId = req.params.cardId;
const checklists = Checklists.find({ cardId: paramCardId }).map(function(
doc,
@ -247,7 +248,8 @@ if (Meteor.isServer) {
'GET',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramChecklistId = req.params.checklistId;
const paramCardId = req.params.cardId;
const checklist = Checklists.findOne({
@ -351,7 +353,8 @@ if (Meteor.isServer) {
'DELETE',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramChecklistId = req.params.checklistId;
Checklists.remove({ _id: paramChecklistId });
JsonRoutes.sendResult(res, {

View file

@ -294,8 +294,8 @@ if (Meteor.isServer) {
req,
res,
) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: CustomFields.find({ boardIds: { $in: [paramBoardId] } }).map(
@ -323,8 +323,8 @@ if (Meteor.isServer) {
'GET',
'/api/boards/:boardId/custom-fields/:customFieldId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramCustomFieldId = req.params.customFieldId;
JsonRoutes.sendResult(res, {
code: 200,
@ -353,8 +353,8 @@ if (Meteor.isServer) {
req,
res,
) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const board = Boards.findOne({ _id: paramBoardId });
const id = CustomFields.direct.insert({
name: req.body.name,
@ -396,7 +396,8 @@ if (Meteor.isServer) {
'PUT',
'/api/boards/:boardId/custom-fields/:customFieldId',
(req, res) => {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramFieldId = req.params.customFieldId;
@ -461,7 +462,8 @@ if (Meteor.isServer) {
'POST',
'/api/boards/:boardId/custom-fields/:customFieldId/dropdown-items',
(req, res) => {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramCustomFieldId = req.params.customFieldId;
const paramItems = req.body.items;
@ -504,7 +506,8 @@ if (Meteor.isServer) {
'PUT',
'/api/boards/:boardId/custom-fields/:customFieldId/dropdown-items/:dropdownItemId',
(req, res) => {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramDropdownItemId = req.params.dropdownItemId;
const paramCustomFieldId = req.params.customFieldId;
@ -545,7 +548,8 @@ if (Meteor.isServer) {
'DELETE',
'/api/boards/:boardId/custom-fields/:customFieldId/dropdown-items/:dropdownItemId',
(req, res) => {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
paramCustomFieldId = req.params.customFieldId;
paramDropdownItemId = req.params.dropdownItemId;
@ -580,8 +584,8 @@ if (Meteor.isServer) {
'DELETE',
'/api/boards/:boardId/custom-fields/:customFieldId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const id = req.params.customFieldId;
CustomFields.remove({ _id: id, boardIds: { $in: [paramBoardId] } });
JsonRoutes.sendResult(res, {

View file

@ -1,4 +1,11 @@
if (Meteor.isServer) {
import { runOnServer } from './runOnServer';
runOnServer(function() {
// the ExporterExcel class is only available on server and in order to import
// it here we use runOnServer to have it inside a function instead of an
// if (Meteor.isServer) block
import { ExporterExcel } from './server/ExporterExcel';
// 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
@ -20,7 +27,6 @@ if (Meteor.isServer) {
* @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;
@ -43,6 +49,7 @@ if (Meteor.isServer) {
isAdmin: true,
});
}
const exporterExcel = new ExporterExcel(boardId);
if (exporterExcel.canExport(user) || impersonateDone) {
if (impersonateDone) {
@ -57,619 +64,4 @@ if (Meteor.isServer) {
res.end(TAPi18n.__('user-can-not-export-excel'));
}
});
}
// exporter maybe is broken since Gridfs introduced, add fs and path
export 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: 40,
},
{
key: 'c',
width: 60,
},
{
key: 'd',
width: 40,
},
{
key: 'e',
width: 20,
},
{
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,
style: {
font: {
name: TAPi18n.__('excel-font'),
size: '10',
},
numFmt: 'yyyy/mm/dd hh:mm:ss',
},
},
{
key: 'l',
width: 20,
},
{
key: 'm',
width: 20,
},
{
key: 'n',
width: 20,
},
{
key: 'o',
width: 20,
},
{
key: 'p',
width: 20,
},
{
key: 'q',
width: 20,
},
{
key: 'r',
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 swimlanes info
const jswimlane = {};
for (const kswimlane in result.swimlanes) {
jswimlane[result.swimlanes[kswimlane]._id] =
result.swimlanes[kswimlane].title;
}
//get kanban label info
const jlabel = {};
var isFirst = 1;
for (const klabel in result.labels) {
// console.log(klabel);
if (isFirst == 0) {
jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`;
} else {
isFirst = 0;
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:R3');
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 = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
//this is where order in which the excel file generates
ws.addRow().values = [
TAPi18n.__('number'),
TAPi18n.__('title'),
TAPi18n.__('description'),
TAPi18n.__('parent-card'),
TAPi18n.__('owner'),
TAPi18n.__('createdAt'),
TAPi18n.__('last-modified-at'),
TAPi18n.__('card-received'),
TAPi18n.__('card-start'),
TAPi18n.__('card-due'),
TAPi18n.__('card-end'),
TAPi18n.__('list'),
TAPi18n.__('swimlane'),
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');
allBorder('Q5');
allBorder('R5');
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');
cellCenter('Q5');
cellCenter('R5');
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 += ' ';
}
//get parent name
if (jcard.parentId) {
const parentCard = result.cards.find(
(card) => card._id === jcard.parentId,
);
jcard.parentCardTitle = parentCard ? parentCard.title : '';
}
//add card detail
const t = Number(i) + 1;
ws.addRow().values = [
t.toString(),
jcard.title,
jcard.description,
jcard.parentCardTitle,
jmeml[jcard.userId],
addTZhours(jcard.createdAt),
addTZhours(jcard.dateLastActivity),
addTZhours(jcard.receivedAt),
addTZhours(jcard.startAt),
addTZhours(jcard.dueAt),
addTZhours(jcard.endAt),
jlist[jcard.listId],
jswimlane[jcard.swimlaneId],
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}`);
allBorder(`Q${y}`);
allBorder(`R${y}`);
cellCenter(`A${y}`);
ws.getCell(`B${y}`).alignment = {
wrapText: true,
};
ws.getCell(`C${y}`).alignment = {
wrapText: true,
};
ws.getCell(`M${y}`).alignment = {
wrapText: true,
};
ws.getCell(`N${y}`).alignment = {
wrapText: true,
};
ws.getCell(`O${y}`).alignment = {
wrapText: true,
};
}
workbook.xlsx.write(res).then(function () {});
}
canExport(user) {
const board = Boards.findOne(this._boardId);
return board && board.isVisibleBy(user);
}
}
});

70
models/exportPDF.js Normal file
View file

@ -0,0 +1,70 @@
import { runOnServer } from './runOnServer';
runOnServer(function() {
// the ExporterCardPDF class is only available on server and in order to import
// it here we use runOnServer to have it inside a function instead of an
// if (Meteor.isServer) block
import { ExporterCardPDF } from './server/ExporterCardPDF';
// 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
*/
Picker.route('/api/boards/:boardId/lists/:listId/cards/:cardId/exportPDF', function (params, req, res) {
const boardId = params.boardId;
const paramListId = req.params.listId;
const paramCardId = req.params.cardId;
let user = null;
let impersonateDone = false;
let adminId = null;
const loginToken = params.query.authToken;
if (loginToken) {
const hashToken = Accounts._hashLoginToken(loginToken);
user = Meteor.users.findOne({
'services.resume.loginTokens.hashedToken': hashToken,
});
adminId = user._id.toString();
impersonateDone = ImpersonatedUsers.findOne({
adminId: adminId,
});
} else if (!Meteor.settings.public.sandstorm) {
Authentication.checkUserId(req.userId);
user = Users.findOne({
_id: req.userId,
isAdmin: true,
});
}
const exporterCardPDF = new ExporterCardPDF(boardId);
if (exporterCardPDF.canExport(user) || impersonateDone) {
if (impersonateDone) {
ImpersonatedUsers.insert({
adminId: adminId,
boardId: boardId,
reason: 'exportCardPDF',
});
}
exporterCardPDF.build(res);
} else {
res.end(TAPi18n.__('user-can-not-export-card-to-pdf'));
}
});
});

View file

@ -531,8 +531,8 @@ if (Meteor.isServer) {
*/
JsonRoutes.add('POST', '/api/boards/:boardId/lists', function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const board = Boards.findOne(paramBoardId);
const id = Lists.insert({
title: req.body.title,
@ -569,8 +569,8 @@ if (Meteor.isServer) {
res,
) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const paramListId = req.params.listId;
Lists.remove({ _id: paramListId, boardId: paramBoardId });
JsonRoutes.sendResult(res, {

View file

@ -36,11 +36,19 @@ Org.attachSchema(
optional: true,
max: 255,
},
orgIsActive: {
/**
* status of the organization
*/
type: Boolean,
optional: true,
},
createdAt: {
/**
* creation date of the organization
*/
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
@ -68,6 +76,44 @@ Org.attachSchema(
);
if (Meteor.isServer) {
Org.allow({
insert(userId, doc) {
const user = Users.findOne({
_id: userId,
});
if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
return true;
if (!user) {
return false;
}
return doc._id === userId;
},
update(userId, doc) {
const user = Users.findOne({
_id: userId,
});
if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
return true;
if (!user) {
return false;
}
return doc._id === userId;
},
remove(userId, doc) {
const user = Users.findOne({
_id: userId,
});
if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
return true;
if (!user) {
return false;
}
return doc._id === userId;
},
fetch: [],
});
Meteor.methods({
setCreateOrg(
orgDisplayName,
@ -81,7 +127,7 @@ if (Meteor.isServer) {
check(orgDesc, String);
check(orgShortName, String);
check(orgWebsite, String);
check(orgIsActive, String);
check(orgIsActive, Boolean);
const nOrgNames = Org.find({ orgShortName }).count();
if (nOrgNames > 0) {
@ -100,17 +146,17 @@ if (Meteor.isServer) {
setOrgDisplayName(org, orgDisplayName) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(org, String);
check(org, Object);
check(orgDisplayName, String);
Org.update(org, {
$set: { orgDisplayName: orgDisplayName },
$set: { orgDisplayName: orgDisplayNameorgShortName },
});
}
},
setOrgDesc(org, orgDesc) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(org, String);
check(org, Object);
check(orgDesc, String);
Org.update(org, {
$set: { orgDesc: orgDesc },
@ -120,7 +166,7 @@ if (Meteor.isServer) {
setOrgShortName(org, orgShortName) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(org, String);
check(org, Object);
check(orgShortName, String);
Org.update(org, {
$set: { orgShortName: orgShortName },
@ -130,20 +176,48 @@ if (Meteor.isServer) {
setOrgIsActive(org, orgIsActive) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(org, String);
check(orgIsActive, String);
check(org, Object);
check(orgIsActive, Boolean);
Org.update(org, {
$set: { orgIsActive: orgIsActive },
});
}
},
setOrgAllFields(
org,
orgDisplayName,
orgDesc,
orgShortName,
orgWebsite,
orgIsActive,
) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(org, Object);
check(orgDisplayName, String);
check(orgDesc, String);
check(orgShortName, String);
check(orgWebsite, String);
check(orgIsActive, Boolean);
Org.update(org, {
$set: {
orgDisplayName: orgDisplayName,
orgDesc: orgDesc,
orgShortName: orgShortName,
orgWebsite: orgWebsite,
orgIsActive: orgIsActive,
},
});
}
},
});
}
if (Meteor.isServer) {
// Index for Organization name.
Meteor.startup(() => {
Org._collection._ensureIndex({ name: -1 });
// Org._collection._ensureIndex({ name: -1 });
Org._collection._ensureIndex({ orgDisplayName: -1 });
});
}

8
models/runOnServer.js Normal file
View file

@ -0,0 +1,8 @@
/**
* Executes a function only if we are on the server. Use in combination
* with package-sepcific loader functions to create a "nested" import that
* prevents leakage of server-dependencies to the client.
* @param fct {function} the function to be executed on the server
* @return {*} a return value from the function, if there is any
*/
export const runOnServer = fct => Meteor.isServer && fct();

View file

@ -0,0 +1,629 @@
// exporter maybe is broken since Gridfs introduced, add fs and path
import { createWorkbook } from './createWorkbook';
class ExporterCardPDF {
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 workbook = createWorkbook();
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: 40,
},
{
key: 'c',
width: 60,
},
{
key: 'd',
width: 40,
},
{
key: 'e',
width: 20,
},
{
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,
style: {
font: {
name: TAPi18n.__('excel-font'),
size: '10',
},
numFmt: 'yyyy/mm/dd hh:mm:ss',
},
},
{
key: 'l',
width: 20,
},
{
key: 'm',
width: 20,
},
{
key: 'n',
width: 20,
},
{
key: 'o',
width: 20,
},
{
key: 'p',
width: 20,
},
{
key: 'q',
width: 20,
},
{
key: 'r',
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 swimlanes info
const jswimlane = {};
for (const kswimlane in result.swimlanes) {
jswimlane[result.swimlanes[kswimlane]._id] =
result.swimlanes[kswimlane].title;
}
//get kanban label info
const jlabel = {};
var isFirst = 1;
for (const klabel in result.labels) {
// console.log(klabel);
if (isFirst == 0) {
jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`;
} else {
isFirst = 0;
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:R3');
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 = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
//this is where order in which the excel file generates
ws.addRow().values = [
TAPi18n.__('number'),
TAPi18n.__('title'),
TAPi18n.__('description'),
TAPi18n.__('parent-card'),
TAPi18n.__('owner'),
TAPi18n.__('createdAt'),
TAPi18n.__('last-modified-at'),
TAPi18n.__('card-received'),
TAPi18n.__('card-start'),
TAPi18n.__('card-due'),
TAPi18n.__('card-end'),
TAPi18n.__('list'),
TAPi18n.__('swimlane'),
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');
allBorder('Q5');
allBorder('R5');
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');
cellCenter('Q5');
cellCenter('R5');
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 += ' ';
}
//get parent name
if (jcard.parentId) {
const parentCard = result.cards.find(
(card) => card._id === jcard.parentId,
);
jcard.parentCardTitle = parentCard ? parentCard.title : '';
}
//add card detail
const t = Number(i) + 1;
ws.addRow().values = [
t.toString(),
jcard.title,
jcard.description,
jcard.parentCardTitle,
jmeml[jcard.userId],
addTZhours(jcard.createdAt),
addTZhours(jcard.dateLastActivity),
addTZhours(jcard.receivedAt),
addTZhours(jcard.startAt),
addTZhours(jcard.dueAt),
addTZhours(jcard.endAt),
jlist[jcard.listId],
jswimlane[jcard.swimlaneId],
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}`);
allBorder(`Q${y}`);
allBorder(`R${y}`);
cellCenter(`A${y}`);
ws.getCell(`B${y}`).alignment = {
wrapText: true,
};
ws.getCell(`C${y}`).alignment = {
wrapText: true,
};
ws.getCell(`M${y}`).alignment = {
wrapText: true,
};
ws.getCell(`N${y}`).alignment = {
wrapText: true,
};
ws.getCell(`O${y}`).alignment = {
wrapText: true,
};
}
workbook.xlsx.write(res).then(function () {});
*/
var doc = new PDFDocument({size: 'A4', margin: 50});
doc.fontSize(12);
doc.text('Some test text', 10, 30, {align: 'center', width: 200});
this.response.writeHead(200, {
'Content-type': 'application/pdf',
'Content-Disposition': "attachment; filename=test.pdf"
});
this.response.end( doc.outputSync() );
}
canExport(user) {
const board = Boards.findOne(this._boardId);
return board && board.isVisibleBy(user);
}
}
export { ExporterCardPDF };

View file

@ -0,0 +1,617 @@
import { createWorkbook } from './createWorkbook';
// exporter maybe is broken since Gridfs introduced, add fs and path
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 workbook = createWorkbook();
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: 40,
},
{
key: 'c',
width: 60,
},
{
key: 'd',
width: 40,
},
{
key: 'e',
width: 20,
},
{
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,
style: {
font: {
name: TAPi18n.__('excel-font'),
size: '10',
},
numFmt: 'yyyy/mm/dd hh:mm:ss',
},
},
{
key: 'l',
width: 20,
},
{
key: 'm',
width: 20,
},
{
key: 'n',
width: 20,
},
{
key: 'o',
width: 20,
},
{
key: 'p',
width: 20,
},
{
key: 'q',
width: 20,
},
{
key: 'r',
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 swimlanes info
const jswimlane = {};
for (const kswimlane in result.swimlanes) {
jswimlane[result.swimlanes[kswimlane]._id] =
result.swimlanes[kswimlane].title;
}
//get kanban label info
const jlabel = {};
var isFirst = 1;
for (const klabel in result.labels) {
// console.log(klabel);
if (isFirst == 0) {
jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`;
} else {
isFirst = 0;
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:R3');
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 = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
//this is where order in which the excel file generates
ws.addRow().values = [
TAPi18n.__('number'),
TAPi18n.__('title'),
TAPi18n.__('description'),
TAPi18n.__('parent-card'),
TAPi18n.__('owner'),
TAPi18n.__('createdAt'),
TAPi18n.__('last-modified-at'),
TAPi18n.__('card-received'),
TAPi18n.__('card-start'),
TAPi18n.__('card-due'),
TAPi18n.__('card-end'),
TAPi18n.__('list'),
TAPi18n.__('swimlane'),
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');
allBorder('Q5');
allBorder('R5');
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');
cellCenter('Q5');
cellCenter('R5');
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 += ' ';
}
//get parent name
if (jcard.parentId) {
const parentCard = result.cards.find(
(card) => card._id === jcard.parentId,
);
jcard.parentCardTitle = parentCard ? parentCard.title : '';
}
//add card detail
const t = Number(i) + 1;
ws.addRow().values = [
t.toString(),
jcard.title,
jcard.description,
jcard.parentCardTitle,
jmeml[jcard.userId],
addTZhours(jcard.createdAt),
addTZhours(jcard.dateLastActivity),
addTZhours(jcard.receivedAt),
addTZhours(jcard.startAt),
addTZhours(jcard.dueAt),
addTZhours(jcard.endAt),
jlist[jcard.listId],
jswimlane[jcard.swimlaneId],
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}`);
allBorder(`Q${y}`);
allBorder(`R${y}`);
cellCenter(`A${y}`);
ws.getCell(`B${y}`).alignment = {
wrapText: true,
};
ws.getCell(`C${y}`).alignment = {
wrapText: true,
};
ws.getCell(`M${y}`).alignment = {
wrapText: true,
};
ws.getCell(`N${y}`).alignment = {
wrapText: true,
};
ws.getCell(`O${y}`).alignment = {
wrapText: true,
};
}
workbook.xlsx.write(res).then(function () {});
}
canExport(user) {
const board = Boards.findOne(this._boardId);
return board && board.isVisibleBy(user);
}
}
export { ExporterExcel };

View file

@ -0,0 +1,5 @@
import Excel from 'exceljs';
export const createWorkbook = function() {
return new Excel.Workbook();
};

View file

@ -46,6 +46,10 @@ Settings.attachSchema(
type: String,
optional: false,
},
spinnerName: {
type: String,
optional: true,
},
hideLogo: {
type: Boolean,
optional: true,

View file

@ -454,8 +454,8 @@ if (Meteor.isServer) {
*/
JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const board = Boards.findOne(paramBoardId);
const id = Swimlanes.insert({
title: req.body.title,

View file

@ -36,6 +36,13 @@ Team.attachSchema(
optional: true,
max: 255,
},
teamIsActive: {
/**
* status of the team
*/
type: Boolean,
optional: true,
},
createdAt: {
/**
* creation date of the team
@ -68,6 +75,43 @@ Team.attachSchema(
);
if (Meteor.isServer) {
Team.allow({
insert(userId, doc) {
const user = Users.findOne({
_id: userId,
});
if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
return true;
if (!user) {
return false;
}
return doc._id === userId;
},
update(userId, doc) {
const user = Users.findOne({
_id: userId,
});
if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
return true;
if (!user) {
return false;
}
return doc._id === userId;
},
remove(userId, doc) {
const user = Users.findOne({
_id: userId,
});
if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
return true;
if (!user) {
return false;
}
return doc._id === userId;
},
fetch: [],
});
Meteor.methods({
setCreateTeam(
teamDisplayName,
@ -81,7 +125,7 @@ if (Meteor.isServer) {
check(teamDesc, String);
check(teamShortName, String);
check(teamWebsite, String);
check(teamIsActive, String);
check(teamIsActive, Boolean);
const nTeamNames = Team.find({ teamShortName }).count();
if (nTeamNames > 0) {
@ -100,7 +144,7 @@ if (Meteor.isServer) {
setTeamDisplayName(team, teamDisplayName) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(team, String);
check(team, Object);
check(teamDisplayName, String);
Team.update(team, {
$set: { teamDisplayName: teamDisplayName },
@ -110,7 +154,7 @@ if (Meteor.isServer) {
setTeamDesc(team, teamDesc) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(team, String);
check(team, Object);
check(teamDesc, String);
Team.update(team, {
$set: { teamDesc: teamDesc },
@ -120,7 +164,7 @@ if (Meteor.isServer) {
setTeamShortName(team, teamShortName) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(team, String);
check(team, Object);
check(teamShortName, String);
Team.update(team, {
$set: { teamShortName: teamShortName },
@ -130,20 +174,47 @@ if (Meteor.isServer) {
setTeamIsActive(team, teamIsActive) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(team, String);
check(teamIsActive, String);
check(team, Object);
check(teamIsActive, Boolean);
Team.update(team, {
$set: { teamIsActive: teamIsActive },
});
}
},
setTeamAllFields(
team,
teamDisplayName,
teamDesc,
teamShortName,
teamWebsite,
teamIsActive,
) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(team, Object);
check(teamDisplayName, String);
check(teamDesc, String);
check(teamShortName, String);
check(teamWebsite, String);
check(teamIsActive, Boolean);
Team.update(team, {
$set: {
teamDisplayName: teamDisplayName,
teamDesc: teamDesc,
teamShortName: teamShortName,
teamWebsite: teamWebsite,
teamIsActive: teamIsActive,
},
});
}
},
});
}
if (Meteor.isServer) {
// Index for Team name.
Meteor.startup(() => {
Team._collection._ensureIndex({ name: -1 });
Team._collection._ensureIndex({ teamDisplayName: -1 });
});
}

View file

@ -92,7 +92,7 @@ export class TrelloCreator {
check(
trelloBoard,
Match.ObjectIncluding({
closed: Boolean,
// closed: Boolean, // issue #3840, should import closed Trello boards
name: String,
prefs: Match.ObjectIncluding({
// XXX refine control by validating 'background' against a list of

View file

@ -38,6 +38,44 @@ Users.attachSchema(
}
},
},
orgs: {
/**
* the list of organizations that a user belongs to
*/
type: [Object],
optional: true,
},
'orgs.$.orgId':{
/**
* The uniq ID of the organization
*/
type: String,
},
'orgs.$.orgDisplayName':{
/**
* The display name of the organization
*/
type: String,
},
teams: {
/**
* the list of teams that a user belongs to
*/
type: [Object],
optional: true,
},
'teams.$.teamId':{
/**
* The uniq ID of the team
*/
type: String,
},
'teams.$.teamDisplayName':{
/**
* The display name of the team
*/
type: String,
},
emails: {
/**
* the list of emails attached to a user
@ -126,7 +164,7 @@ Users.attachSchema(
},
'profile.showDesktopDragHandles': {
/**
* does the user want to hide system messages?
* does the user want to show desktop drag handles?
*/
type: Boolean,
optional: true,
@ -138,6 +176,13 @@ Users.attachSchema(
type: Boolean,
optional: true,
},
'profile.cardMaximized': {
/**
* has user clicked maximize card?
*/
type: Boolean,
optional: true,
},
'profile.hiddenSystemMessages': {
/**
* does the user want to hide system messages?
@ -329,13 +374,7 @@ Users.attachSchema(
},
'sessionData.totalHits': {
/**
* Total hits from last search
*/
type: Number,
optional: true,
},
'sessionData.lastHit': {
/**
* Total hits from last searchquery['members.userId'] = Meteor.userId();
* last hit that was returned
*/
type: Number,
@ -403,6 +442,8 @@ Users.safeFields = {
'profile.fullname': 1,
'profile.avatarUrl': 1,
'profile.initials': 1,
orgs: 1,
teams: 1,
};
if (Meteor.isClient) {
@ -464,7 +505,30 @@ Users.helpers({
}
return '';
},
orgsUserBelongs() {
if (this.orgs) {
return this.orgs.map(function(org){return org.orgDisplayName}).join(',');
}
return '';
},
orgIdsUserBelongs() {
if (this.orgs) {
return this.orgs.map(function(org){return org.orgId}).join(',');
}
return '';
},
teamsUserBelongs() {
if (this.teams) {
return this.teams.map(function(team){ return team.teamDisplayName}).join(',');
}
return '';
},
teamIdsUserBelongs() {
if (this.teams) {
return this.teams.map(function(team){ return team.teamId}).join(',');
}
return '';
},
boards() {
return Boards.find(
{
@ -586,6 +650,11 @@ Users.helpers({
return profile.hiddenSystemMessages || false;
},
hasCardMaximized() {
const profile = this.profile || {};
return profile.cardMaximized || false;
},
hasHiddenMinicardLabelText() {
const profile = this.profile || {};
return profile.hiddenMinicardLabelText || false;
@ -738,6 +807,14 @@ Users.mutations({
};
},
toggleCardMaximized(value = false) {
return {
$set: {
'profile.cardMaximized': !value,
},
};
},
toggleLabelText(value = false) {
return {
$set: {
@ -832,6 +909,10 @@ Meteor.methods({
const user = Meteor.user();
user.toggleSystem(user.hasHiddenSystemMessages());
},
toggleCardMaximized() {
const user = Meteor.user();
user.toggleCardMaximized(user.hasCardMaximized());
},
toggleMinicardLabelText() {
const user = Meteor.user();
user.toggleLabelText(user.hasHiddenMinicardLabelText());
@ -894,17 +975,20 @@ if (Meteor.isServer) {
isActive,
email,
importUsernames,
userOrgsArray,
userTeamsArray,
) {
check(fullname, String);
check(username, String);
check(initials, String);
check(password, String);
check(isAdmin, String);
check(isActive, String);
check(email, String);
check(importUsernames, Array);
check(userOrgsArray, Array);
check(userTeamsArray, Array);
if (Meteor.user() && Meteor.user().isAdmin) {
check(fullname, String);
check(username, String);
check(initials, String);
check(password, String);
check(isAdmin, String);
check(isActive, String);
check(email, String);
check(importUsernames, Array);
const nUsersWithUsername = Users.find({
username,
}).count();
@ -935,6 +1019,8 @@ if (Meteor.isServer) {
'profile.fullname': fullname,
importUsernames,
'profile.initials': initials,
orgs: userOrgsArray,
teams: userTeamsArray,
},
});
}
@ -942,9 +1028,9 @@ if (Meteor.isServer) {
}
},
setUsername(username, userId) {
check(username, String);
check(userId, String);
if (Meteor.user() && Meteor.user().isAdmin) {
check(username, String);
check(userId, String);
const nUsersWithUsername = Users.find({
username,
}).count();
@ -960,11 +1046,12 @@ if (Meteor.isServer) {
}
},
setEmail(email, userId) {
check(email, String);
check(username, String);
if (Meteor.user() && Meteor.user().isAdmin) {
if (Array.isArray(email)) {
email = email.shift();
}
check(email, String);
const existingUser = Users.findOne(
{
'emails.address': email,
@ -992,31 +1079,31 @@ if (Meteor.isServer) {
}
},
setUsernameAndEmail(username, email, userId) {
check(username, String);
check(email, String);
check(userId, String);
if (Meteor.user() && Meteor.user().isAdmin) {
check(username, String);
if (Array.isArray(email)) {
email = email.shift();
}
check(email, String);
check(userId, String);
Meteor.call('setUsername', username, userId);
Meteor.call('setEmail', email, userId);
}
},
setPassword(newPassword, userId) {
check(userId, String);
check(newPassword, String);
if (Meteor.user() && Meteor.user().isAdmin) {
check(userId, String);
check(newPassword, String);
if (Meteor.user().isAdmin) {
Accounts.setPassword(userId, newPassword);
}
}
},
setEmailVerified(email, verified, userId) {
check(email, String);
check(verified, Boolean);
check(userId, String);
if (Meteor.user() && Meteor.user().isAdmin) {
check(email, String);
check(verified, Boolean);
check(userId, String);
Users.update(userId, {
$set: {
emails: [
@ -1030,9 +1117,9 @@ if (Meteor.isServer) {
}
},
setInitials(initials, userId) {
check(initials, String);
check(userId, String);
if (Meteor.user() && Meteor.user().isAdmin) {
check(initials, String);
check(userId, String);
Users.update(userId, {
$set: {
'profile.initials': initials,
@ -1405,24 +1492,26 @@ if (Meteor.isServer) {
fakeUserId.withValue(doc._id, () => {
/*
// Insert the Welcome Board
Boards.insert({
title: TAPi18n.__('welcome-board'),
permission: 'private',
}, fakeUser, (err, boardId) => {
Swimlanes.insert({
title: TAPi18n.__('welcome-swimlane'),
boardId,
sort: 1,
}, fakeUser);
// Insert the Welcome Board
Boards.insert({
title: TAPi18n.__('welcome-board'),
permission: 'private',
}, fakeUser, (err, boardId) => {
['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => {
Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser);
});
});
*/
Swimlanes.insert({
title: TAPi18n.__('welcome-swimlane'),
boardId,
sort: 1,
}, fakeUser);
['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => {
Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser);
});
});
*/
// Insert Template Container
const Future = require('fibers/future');
const future1 = new Future();
const future2 = new Future();
@ -1507,6 +1596,7 @@ if (Meteor.isServer) {
future1.wait();
future2.wait();
future3.wait();
// End of Insert Template Container
});
});
}
@ -1977,16 +2067,18 @@ if (Meteor.isServer) {
try {
Authentication.checkUserId(req.userId);
const id = req.params.userId;
// Delete is not enabled yet, because it does leave empty user avatars
// Delete user is enabled, but is still has bug of leaving empty user avatars
// to boards: boards members, card members and assignees have
// empty users. See:
// empty users. So it would be better to delete user from all boards before
// deleting user.
// See:
// - wekan/client/components/settings/peopleBody.jade deleteButton
// - wekan/client/components/settings/peopleBody.js deleteButton
// - wekan/client/components/sidebar/sidebar.js Popup.afterConfirm('removeMember'
// that does now remove member from board, card members and assignees correctly,
// but that should be used to remove user from all boards similarly
// - wekan/models/users.js Delete is not enabled
// Meteor.users.remove({ _id: id });
Meteor.users.remove({ _id: id });
JsonRoutes.sendResult(res, {
code: 200,
data: {