Taichi Masuyama 4 лет назад
Родитель
Сommit
78a0d38ada

+ 25 - 0
packages/app/src/server/models/user-group-relation.js

@@ -277,6 +277,31 @@ class UserGroupRelation {
       .populate('relatedUser');
   }
 
+  static async findUserIdsByGroupId(groupId) {
+    const relations = await this.find({ relatedGroup: groupId }, { _id: 0, relatedUser: 1 }).lean().exec(); // .lean() to get not ObjectId but string
+
+    return relations.map(relation => relation.relatedUser);
+  }
+
+  static async createByGroupIdsAndUserIds(groupIds, userIds) {
+    const insertOperations = [];
+
+    groupIds.forEach((groupId) => {
+      userIds.forEach((userId) => {
+        insertOperations.push({
+          insertOne: {
+            document: {
+              relatedGroup: groupId,
+              relatedUser: userId,
+            },
+          },
+        });
+      });
+    });
+
+    await this.bulkWrite(insertOperations);
+  }
+
 }
 
 module.exports = function(crowi) {

+ 63 - 13
packages/app/src/server/models/user-group.js

@@ -97,16 +97,6 @@ class UserGroup {
     };
   }
 
-  // Check if registerable
-  static isRegisterableName(name) {
-    const query = { name };
-
-    return this.findOne(query)
-      .then((userGroupData) => {
-        return (userGroupData == null);
-      });
-  }
-
   // Delete completely
   static async removeCompletelyById(deleteGroupId, action, transferToUserGroupId, user) {
     const UserGroupRelation = mongoose.model('UserGroupRelation');
@@ -143,9 +133,69 @@ class UserGroup {
     return this.create({ name, description, parent });
   }
 
-  async updateName(name) {
-    this.name = name;
-    await this.save();
+  static async findAllAncestorGroups(parent, ancestors = [parent]) {
+    if (parent == null) {
+      return ancestors;
+    }
+
+    const nextParent = await this.findOne({ _id: parent.parent });
+    if (nextParent == null) {
+      return ancestors;
+    }
+
+    ancestors.push(nextParent);
+
+    return this.findAllAncestorGroups(nextParent, ancestors);
+  }
+
+  // TODO 85062: write test code
+  static async updateGroup(id, name, description, parentId, forceUpdateParents = false) {
+    const userGroup = await this.findById(id);
+    if (userGroup == null) {
+      throw new Error('The group does not exist');
+    }
+
+    // check if the new group name is available
+    const isExist = (await this.countDocuments({ name })) > 0;
+    if (userGroup.name !== name && isExist) {
+      throw new Error('The group name is already taken');
+    }
+
+    userGroup.name = name;
+    userGroup.description = description;
+
+    // return when not update parent
+    if (userGroup.parent === parentId) {
+      return userGroup.save();
+    }
+
+    const parent = await this.findById(parentId);
+
+    // find users for comparison
+    const UserGroupRelation = mongoose.model('UserGroupRelation');
+    const [targetGroupUsers, parentGroupUsers] = await Promise.all(
+      [UserGroupRelation.findUserIdsByGroupId(userGroup._id), UserGroupRelation.findUserIdsByGroupId(parent._id)],
+    );
+
+    const usersBelongsToTargetButNotParent = targetGroupUsers.filter(user => !parentGroupUsers.includes(user));
+    // add the target group's users to all ancestors
+    if (forceUpdateParents) {
+      const ancestorGroups = await this.findAllAncestorGroups(parent);
+      const ancestorGroupIds = ancestorGroups.map(group => group._id);
+
+      await UserGroupRelation.createByGroupIdsAndUserIds(ancestorGroupIds, usersBelongsToTargetButNotParent);
+
+      userGroup.parent = parent._id;
+    }
+    // validate related users
+    else {
+      const isUpdatable = usersBelongsToTargetButNotParent.length === 0;
+      if (!isUpdatable) {
+        throw Error('The parent group does not contain the users in this group.');
+      }
+    }
+
+    return userGroup.save();
   }
 
 }

+ 10 - 14
packages/app/src/server/routes/apiv3/user-group.js

@@ -222,6 +222,9 @@ module.exports = (crowi) => {
 
   validator.update = [
     body('name', 'Group name is required').trim().exists({ checkFalsy: true }),
+    body('description', 'Group description must be a string').optional().isString(),
+    body('parentId', 'parentId must be a string').optional().isString(),
+    body('forceUpdateParents', 'forceUpdateParents must be a boolean').optional().isBoolean(),
   ];
 
   /**
@@ -252,23 +255,16 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: A result of `UserGroup.updateName`
    */
-  router.put('/:id', loginRequiredStrictly, adminRequired, csrf, validator.update, apiV3FormValidator, async(req, res) => {
+  // TODO 85062: enable description & parentId
+  router.put('/:id', /*loginRequiredStrictly, adminRequired, csrf,*/ validator.update, apiV3FormValidator, async(req, res) => {
     const { id } = req.params;
-    const { name } = req.body;
+    const {
+      name, description, parentId, forceUpdateParents = false,
+    } = req.body;
 
+    // TODO 85062: move this process into updateGroup
     try {
-      const userGroup = await UserGroup.findById(id);
-      if (userGroup == null) {
-        throw new Error('The group does not exist');
-      }
-
-      // check if the new group name is available
-      const isRegisterableName = await UserGroup.isRegisterableName(name);
-      if (!isRegisterableName) {
-        throw new Error('The group name is already taken');
-      }
-
-      await userGroup.updateName(name);
+      const userGroup = await UserGroup.updateGroup(id, name, description, parentId, forceUpdateParents);
 
       res.apiv3({ userGroup });
     }