summaryrefslogtreecommitdiffstats
path: root/models/csvCreator.js
diff options
context:
space:
mode:
Diffstat (limited to 'models/csvCreator.js')
-rw-r--r--models/csvCreator.js313
1 files changed, 313 insertions, 0 deletions
diff --git a/models/csvCreator.js b/models/csvCreator.js
new file mode 100644
index 00000000..025a3179
--- /dev/null
+++ b/models/csvCreator.js
@@ -0,0 +1,313 @@
+import Boards from './boards';
+
+export class CsvCreator {
+ constructor(data) {
+ // date to be used for timestamps during import
+ this._nowDate = new Date();
+ // index to help keep track of what information a column stores
+ // each row represents a card
+ this.fieldIndex = {};
+ this.lists = {};
+ // Map of members using username => wekanid
+ this.members = data.membersMapping ? data.membersMapping : {};
+ this.swimlane = null;
+ }
+
+ /**
+ * If dateString is provided,
+ * return the Date it represents.
+ * If not, will return the date when it was first called.
+ * This is useful for us, as we want all import operations to
+ * have the exact same date for easier later retrieval.
+ *
+ * @param {String} dateString a properly formatted Date
+ */
+ _now(dateString) {
+ if (dateString) {
+ return new Date(dateString);
+ }
+ if (!this._nowDate) {
+ this._nowDate = new Date();
+ }
+ return this._nowDate;
+ }
+
+ _user(wekanUserId) {
+ if (wekanUserId && this.members[wekanUserId]) {
+ return this.members[wekanUserId];
+ }
+ return Meteor.userId();
+ }
+
+ /**
+ * Map the header row titles to an index to help assign proper values to the cards' fields
+ * Valid headers (name of card fields):
+ * title, description, status, owner, member, label, due date, start date, finish date, created at, updated at
+ * Some header aliases can also be accepted.
+ * Headers are NOT case-sensitive.
+ *
+ * @param {Array} headerRow array from row of headers of imported CSV/TSV for cards
+ */
+ mapHeadertoCardFieldIndex(headerRow) {
+ const index = {};
+ for (let i = 0; i < headerRow.length; i++) {
+ switch (headerRow[i].trim().toLowerCase()) {
+ case 'title':
+ index.title = i;
+ break;
+ case 'description':
+ index.description = i;
+ break;
+ case 'stage':
+ case 'status':
+ case 'state':
+ index.stage = i;
+ break;
+ case 'owner':
+ index.owner = i;
+ break;
+ case 'members':
+ case 'member':
+ index.members = i;
+ break;
+ case 'labels':
+ case 'label':
+ index.labels = i;
+ break;
+ case 'due date':
+ case 'deadline':
+ case 'due at':
+ index.dueAt = i;
+ break;
+ case 'start date':
+ case 'start at':
+ index.startAt = i;
+ break;
+ case 'finish date':
+ case 'end at':
+ index.endAt = i;
+ break;
+ case 'creation date':
+ case 'created at':
+ index.createdAt = i;
+ break;
+ case 'update date':
+ case 'updated at':
+ case 'modified at':
+ case 'modified on':
+ index.modifiedAt = i;
+ break;
+ }
+ }
+ this.fieldIndex = index;
+ }
+
+ createBoard(csvData) {
+ const boardToCreate = {
+ archived: false,
+ color: 'belize',
+ createdAt: this._now(),
+ labels: [],
+ members: [
+ {
+ userId: Meteor.userId(),
+ wekanId: Meteor.userId(),
+ isActive: true,
+ isAdmin: true,
+ isNoComments: false,
+ isCommentOnly: false,
+ swimlaneId: false,
+ },
+ ],
+ modifiedAt: this._now(),
+ //default is private, should inform user.
+ permission: 'private',
+ slug: 'board',
+ stars: 0,
+ title: `Imported Board ${this._now()}`,
+ };
+
+ // create labels
+ const labelsToCreate = new Set();
+ for (let i = 1; i < csvData.length; i++) {
+ if (csvData[i][this.fieldIndex.labels]) {
+ for (const importedLabel of csvData[i][this.fieldIndex.labels].split(
+ ' ',
+ )) {
+ if (importedLabel && importedLabel.length > 0) {
+ labelsToCreate.add(importedLabel);
+ }
+ }
+ }
+ }
+ for (const label of labelsToCreate) {
+ let labelName, labelColor;
+ if (label.indexOf('-') > -1) {
+ labelName = label.split('-')[0];
+ labelColor = label.split('-')[1];
+ } else {
+ labelName = label;
+ }
+ const labelToCreate = {
+ _id: Random.id(6),
+ color: labelColor ? labelColor : 'black',
+ name: labelName,
+ };
+ boardToCreate.labels.push(labelToCreate);
+ }
+
+ const boardId = Boards.direct.insert(boardToCreate);
+ Boards.direct.update(boardId, {
+ $set: {
+ modifiedAt: this._now(),
+ },
+ });
+ // log activity
+ Activities.direct.insert({
+ activityType: 'importBoard',
+ boardId,
+ createdAt: this._now(),
+ source: {
+ id: boardId,
+ system: 'CSV/TSV',
+ },
+ // We attribute the import to current user,
+ // not the author from the original object.
+ userId: this._user(),
+ });
+ return boardId;
+ }
+
+ createSwimlanes(boardId) {
+ const swimlaneToCreate = {
+ archived: false,
+ boardId,
+ createdAt: this._now(),
+ title: 'Default',
+ sort: 1,
+ };
+ const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate);
+ Swimlanes.direct.update(swimlaneId, { $set: { updatedAt: this._now() } });
+ this.swimlane = swimlaneId;
+ }
+
+ createLists(csvData, boardId) {
+ let numOfCreatedLists = 0;
+ for (let i = 1; i < csvData.length; i++) {
+ const listToCreate = {
+ archived: false,
+ boardId,
+ createdAt: this._now(),
+ };
+ if (csvData[i][this.fieldIndex.stage]) {
+ const existingList = Lists.find({
+ title: csvData[i][this.fieldIndex.stage],
+ boardId,
+ }).fetch();
+ if (existingList.length > 0) {
+ continue;
+ } else {
+ listToCreate.title = csvData[i][this.fieldIndex.stage];
+ }
+ } else listToCreate.title = `Imported List ${this._now()}`;
+
+ const listId = Lists.direct.insert(listToCreate);
+ this.lists[csvData[i][this.fieldIndex.stage]] = listId;
+ numOfCreatedLists++;
+ Lists.direct.update(listId, {
+ $set: {
+ updatedAt: this._now(),
+ sort: numOfCreatedLists,
+ },
+ });
+ }
+ }
+
+ createCards(csvData, boardId) {
+ for (let i = 1; i < csvData.length; i++) {
+ const cardToCreate = {
+ archived: false,
+ boardId,
+ createdAt: csvData[i][this.fieldIndex.createdAt]
+ ? this._now(new Date(csvData[i][this.fieldIndex.createdAt]))
+ : null,
+ dateLastActivity: this._now(),
+ description: csvData[i][this.fieldIndex.description],
+ listId: this.lists[csvData[i][this.fieldIndex.stage]],
+ swimlaneId: this.swimlane,
+ sort: -1,
+ title: csvData[i][this.fieldIndex.title],
+ userId: this._user(),
+ startAt: csvData[i][this.fieldIndex.startAt]
+ ? this._now(new Date(csvData[i][this.fieldIndex.startAt]))
+ : null,
+ dueAt: csvData[i][this.fieldIndex.dueAt]
+ ? this._now(new Date(csvData[i][this.fieldIndex.dueAt]))
+ : null,
+ endAt: csvData[i][this.fieldIndex.endAt]
+ ? this._now(new Date(csvData[i][this.fieldIndex.endAt]))
+ : null,
+ spentTime: null,
+ labelIds: [],
+ modifiedAt: csvData[i][this.fieldIndex.modifiedAt]
+ ? this._now(new Date(csvData[i][this.fieldIndex.modifiedAt]))
+ : null,
+ };
+ // add the labels
+ if (csvData[i][this.fieldIndex.labels]) {
+ const board = Boards.findOne(boardId);
+ for (const importedLabel of csvData[i][this.fieldIndex.labels].split(
+ ' ',
+ )) {
+ if (importedLabel && importedLabel.length > 0) {
+ let labelToApply;
+ if (importedLabel.indexOf('-') === -1) {
+ labelToApply = board.getLabel(importedLabel, 'black');
+ } else {
+ labelToApply = board.getLabel(
+ importedLabel.split('-')[0],
+ importedLabel.split('-')[1],
+ );
+ }
+ cardToCreate.labelIds.push(labelToApply._id);
+ }
+ }
+ }
+ // add the members
+ if (csvData[i][this.fieldIndex.members]) {
+ const wekanMembers = [];
+ for (const importedMember of csvData[i][this.fieldIndex.members].split(
+ ' ',
+ )) {
+ if (this.members[importedMember]) {
+ const wekanId = this.members[importedMember];
+ if (!wekanMembers.find(wId => wId === wekanId)) {
+ wekanMembers.push(wekanId);
+ }
+ }
+ }
+ if (wekanMembers.length > 0) {
+ cardToCreate.members = wekanMembers;
+ }
+ }
+ Cards.direct.insert(cardToCreate);
+ }
+ }
+
+ create(board, currentBoardId) {
+ const isSandstorm =
+ Meteor.settings &&
+ Meteor.settings.public &&
+ Meteor.settings.public.sandstorm;
+ if (isSandstorm && currentBoardId) {
+ const currentBoard = Boards.findOne(currentBoardId);
+ currentBoard.archive();
+ }
+ this.mapHeadertoCardFieldIndex(board[0]);
+ const boardId = this.createBoard(board);
+ this.createLists(board, boardId);
+ this.createSwimlanes(boardId);
+ this.createCards(board, boardId);
+ return boardId;
+ }
+}