Просмотр исходного кода

Merge pull request #277 from weseek/imprv/admin-user-group-managemnt

Imprv/admin user group managemnt
Yuki Takei 8 лет назад
Родитель
Сommit
b09a370b29

+ 8 - 0
lib/form/admin/userGroupCreate.js

@@ -0,0 +1,8 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('createGroupForm[userGroupName]', '新規グループ名').trim().required()
+);

+ 1 - 0
lib/form/index.js

@@ -29,5 +29,6 @@ module.exports = {
     userInvite: require('./admin/userInvite'),
     slackIwhSetting: require('./admin/slackIwhSetting'),
     slackSetting: require('./admin/slackSetting'),
+    userGroupCreate: require('./admin/userGroupCreate'),
   },
 };

+ 7 - 5
lib/locales/en-US/translation.json

@@ -75,10 +75,12 @@
 
   "Table of Contents": "Table of Contents",
 
+  "UserGroup management": "UserGroup management",
   "Public": "Public",
   "Anyone with the link": "Anyone with the link",
   "Specified users only": "Specified users only",
   "Just me": "Just me",
+  "Only inside the group": "Only inside the group",
   "Shareable link": "Shareable link",
 
   "Show latest": "Show latest",
@@ -210,8 +212,8 @@
 
               }
   },
-            
-  "admin_top": { 
+
+  "admin_top": {
     "Management Wiki": "Management Wiki",
     "System Information": "System Information",
     "wiki_administrator": "Only Wiki administrator can access this page",
@@ -220,7 +222,7 @@
     "Package name": "Package name",
     "Specified version": "Specified version",
     "Installed version": "Installed version"
-    
+
   },
 
   "app_setting": {
@@ -237,7 +239,7 @@
     "SMTP_but_AWS": "If you do not have SMTP settings but AWS settings,  e-mails will be sent by SES.",
     "neihter_of": "If you do not of neither of these, e-mails will not be sent.",
     "From e-mail address": "From e-mail address",
-    "SMTP settings": "SMTP settings"  , 
+    "SMTP settings": "SMTP settings"  ,
     "Host": "Host",
     "Port": "Port",
     "User": "User",
@@ -256,4 +258,4 @@
   }
 
 }
- 
+

+ 16 - 14
lib/locales/ja/translation.json

@@ -69,10 +69,10 @@
   "Input page name (optional)": "ページ名を入力(空欄OK)",
   "New Page": "新規ページ",
   "Create under": "<code>%s</code>以下に作成",
-  
-  
- 
-  
+
+
+
+
   "Table of Contents": "目次",
   "Management Wiki Home": "Wiki管理トップ",
   "App settings": "アプリ設定",
@@ -81,22 +81,24 @@
   "Customize": "カスタマイズ",
   "Notification settings": "通知設定",
   "User management": "ユーザー管理",
+  "UserGroup management": "グループ管理",
   "Basic settings": "基本設定",
   "Basic authentication": "Basic認証",
   "Password": "パスワード",
   "Guest users access": "ゲストユーザーのアクセス",
   "Register limitation": "登録の制限",
-  "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。", 
+  "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。",
   "Public": "公開",
   "Anyone with the link": "リンクを知っている人のみ",
   "Specified users": "特定ユーザーのみ",
   "Just me": "自分のみ",
+  "Only inside the group": "特定グループのみ",
   "Shareable link": "このページの共有用URL",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
   "Selecting authentication mechanism": "認証機構選択",
 
-  
- 
+
+
   "Show latest": "最新のページを表示",
   "Load latest": "最新版を読み込む",
   "edited this page": "さんがこのページを編集しました。",
@@ -170,7 +172,7 @@
   },
 
 
-  
+
 
   "modal_rename": {
     "label": {
@@ -225,7 +227,7 @@
         "Delete Line": "行削除"
     }
   },
-   
+
   "admin_top": {
     "Management Wiki": "Wiki管理",
     "System Information": "システム情報",
@@ -235,17 +237,17 @@
     "Package name": "パッケージ名",
     "Specified version": "指定バージョン",
     "Installed version": "インストールされているバージョン"
-  
-  
-  
+
+
+
   },
 
-  "app_setting": { 
+  "app_setting": {
     "Wiki name": "Wikiの名前",
     "wiki_change": "ヘッダーやHTMLタイトルに使用されるWikiの名前を変更できます。",
     "header_content": "ここに入力した内容は、ヘッダー等に表示されます。",
     "Confidential name": "コンフィデンシャル表示",
-    "コンフィデンシャル表示": "Confidential name",  
+    "コンフィデンシャル表示": "Confidential name",
     "enable_files_except_image": "画像以外のファイルアップロードを許可",
     "attach_enable": "許可をしている場合、画像以外のファイルをページに添付可能になります。",
     "Reload": "更新",

+ 3 - 2
lib/models/index.js

@@ -2,10 +2,11 @@
 
 module.exports = {
   Page: require('./page'),
+  PageGroupRelation: require('./page-group-relation'),
   User: require('./user'),
   ExternalAccount: require('./external-account'),
-  UserGroup: require('./userGroup'),
-  UserGroupRelation: require('./userGroupRelation'),
+  UserGroup: require('./user-group'),
+  UserGroupRelation: require('./user-group-relation'),
   Revision: require('./revision'),
   Bookmark: require('./bookmark'),
   Comment: require('./comment'),

+ 280 - 0
lib/models/page-group-relation.js

@@ -0,0 +1,280 @@
+const debug = require('debug')('crowi:models:pageGroupRelation');
+const mongoose = require('mongoose');
+const mongoosePaginate = require('mongoose-paginate');
+const ObjectId = mongoose.Schema.Types.ObjectId;
+
+
+/*
+ * define schema
+ */
+const schema = new mongoose.Schema({
+  relatedGroup: { type: ObjectId, ref: 'UserGroup', required: true },
+  targetPage: { type: ObjectId, ref: 'Page', required: true },
+  createdAt: { type: Date, default: Date.now },
+}, {
+  toJSON: { getters: true },
+  toObject: { getters: true }
+});
+// apply plugins
+schema.plugin(mongoosePaginate);
+
+
+/**
+ * PageGroupRelation Class
+ *
+ * @class PageGroupRelation
+ */
+class PageGroupRelation {
+
+    /**
+   * limit items num for pagination
+   *
+   * @readonly
+   * @static
+   * @memberof PageGroupRelation
+   */
+   static get PAGE_ITEMS() {
+     return 50;
+    }
+
+  static set crowi(crowi) {
+    this._crowi = crowi;
+  }
+
+  static get crowi() {
+    return this._crowi;
+  }
+
+  /**
+   * find all page and group relation
+   *
+   * @static
+   * @returns {Promise<PageGroupRelation[]>}
+   * @memberof PageGroupRelation
+   */
+  static findAllRelation() {
+
+    return this
+      .find()
+      .populate('targetPage')
+      .exec();
+  }
+
+  /**
+   * find all page and group relation for UserGroup
+   *
+   * @static
+   * @param {UserGroup} userGroup
+   * @returns {Promise<PageGroupRelation[]>}
+   * @memberof PageGroupRelation
+   */
+  static findAllRelationForUserGroup(userGroup) {
+    debug('findAllRelationForUserGroup is called', userGroup);
+
+    return this
+      .find({ relatedGroup: userGroup.id })
+      .populate('targetPage')
+      .exec();
+  }
+
+  /**
+   * find all entities with pagination
+   *
+   * @see https://github.com/edwardhotchkiss/mongoose-paginate
+   *
+   * @static
+   * @param {UserGroup} userGroup
+   * @param {any} opts mongoose-paginate options object
+   * @returns {Promise<any>} mongoose-paginate result object
+   * @memberof UserGroupRelation
+   */
+  static findPageGroupRelationsWithPagination(userGroup, opts) {
+    const query = { relatedGroup: userGroup };
+    const options = Object.assign({}, opts);
+    if (options.page == null) {
+      options.page = 1;
+    }
+    if (options.limit == null) {
+      options.limit = UserGroupRelation.PAGE_ITEMS;
+    }
+
+    return this.paginate(query, options)
+      .catch((err) => {
+        debug('Error on pagination:', err);
+      });
+  }
+
+  /**
+   * find the relation or create(if not exists) for page and group
+   *
+   * @static
+   * @param {Page} page
+   * @param {UserGroup} userGroup
+   * @returns {Promise<PageGroupRelation>}
+   * @memberof PageGroupRelation
+   */
+  static findOrCreateRelationForPageAndGroup(page, userGroup) {
+    const query = { targetPage: page.id, relatedGroup: userGroup.id };
+
+    return this
+      .count(query)
+      .then((count) => {
+        // return (0 < count);
+        if (0 < count) {
+          return this.find(query).exec();
+        }
+        else {
+          return this.createRelation(userGroup, page);
+        }
+      })
+      .catch((err) => {
+        debug('An Error occured.', err);
+        return reject(err);
+      });
+  }
+
+  /**
+   * find page and group relation for Page
+   *
+   * @static
+   * @param {Page} page
+   * @returns {Promise<PageGroupRelation[]>}
+   * @memberof PageGroupRelation
+   */
+  static findByPage(page) {
+
+    return this
+      .find({ targetPage: page.id })
+      .populate('relatedGroup')
+      .exec();
+  }
+
+  /**
+   * get is exists granted group for relatedPage and relatedUser
+   *
+   * @static
+   * @param {any} pageData relatedPage
+   * @param {any} userData relatedUser
+   * @returns is exists granted group(or not)
+   * @memberof PageGroupRelation
+   */
+  static isExistsGrantedGroupForPageAndUser(pageData, userData) {
+    var UserGroupRelation = crowi.model('UserGroupRelation');
+
+    return this.findByPage(pageData)
+      .then((pageRelations) => {
+        return pageRelations.map((pageRelation) => {
+          return UserGroupRelation.isRelatedUserForGroup(userData, pageRelation.relatedGroup)
+        });
+      })
+      .then((checkPromises) => {
+        return Promise.all(checkPromises)
+      })
+      .then((checkResults) => {
+        var checkResult = false;
+        checkResults.map((result) => {
+          if (result) {
+            checkResult = true;
+          }
+        });
+        return resolve(checkResult);
+      })
+      .catch((err) => {
+        return reject(err);
+      });
+  }
+
+  /**
+   * create page and group relation
+   *
+   * @static
+   * @param {any} userGroup
+   * @param {any} page
+   * @returns
+   * @memberof PageGroupRelation
+   */
+  static createRelation(userGroup, page) {
+    return this.create({
+      relatedGroup: userGroup.id,
+      targetPage: page.id,
+    });
+  };
+
+  /**
+   * remove all relation for UserGroup
+   *
+   * @static
+   * @param {UserGroup} userGroup related group for remove
+   * @returns {Promise<any>}
+   * @memberof PageGroupRelation
+   */
+  static removeAllByUserGroup(userGroup) {
+
+    return this.findAllRelationForUserGroup(userGroup)
+      .then((relations) => {
+        if (relations == null) {
+          return;
+        }
+        else {
+          relations.map((relation) => {
+            relation.remove();
+          });
+        }
+      });
+  }
+
+  /**
+   * remove all relation for Page
+   *
+   * @static
+   * @param {Page} page related page for remove
+   * @returns {Promise<any>}
+   * @memberof PageGroupRelation
+   */
+  static removeAllByPage(page) {
+
+    return this.findByPage(page)
+      .then((relations) => {
+        debug('remove relations are ', relations);
+        if (relations == null) {
+          return;
+        }
+        else {
+          relations.map((relation) => {
+            relation.remove();
+          });
+        }
+      });
+  }
+
+  /**
+   * remove relation by id
+   *
+   * @static
+   * @param {ObjectId} id for remove
+   * @returns {Promise<any>}
+   * @memberof PageGroupRelation
+   */
+  static removeById(id) {
+
+    return this.findById(id)
+      .then((relationData) => {
+        if (relationData == null) {
+          throw new Exception('PageGroupRelation data is not exists. id:', id);
+        }
+        else {
+          relationData.remove();
+        }
+      })
+      .catch((err) => {
+        debug('Error on find a removing page-group-relation', err);
+        return reject(err);
+      });
+  }
+}
+
+module.exports = function (crowi) {
+  PageGroupRelation.crowi = crowi;
+  schema.loadClass(PageGroupRelation);
+  return mongoose.model('PageGroupRelation', schema);
+}

+ 77 - 11
lib/models/page.js

@@ -7,6 +7,7 @@ module.exports = function(crowi) {
     , GRANT_RESTRICTED = 2
     , GRANT_SPECIFIED = 3
     , GRANT_OWNER = 4
+    , GRANT_USER_GROUP = 5
     , PAGE_GRANT_ERROR = 1
 
     , STATUS_WIP        = 'wip'
@@ -338,6 +339,7 @@ module.exports = function(crowi) {
     grantLabels[GRANT_PUBLIC]     = 'Public'; // 公開
     grantLabels[GRANT_RESTRICTED] = 'Anyone with the link'; // リンクを知っている人のみ
     //grantLabels[GRANT_SPECIFIED]  = 'Specified users only'; // 特定ユーザーのみ
+    grantLabels[GRANT_USER_GROUP] = 'Only inside the group'; // 特定グループのみ
     grantLabels[GRANT_OWNER]      = 'Just me'; // 自分のみ
 
     return grantLabels;
@@ -452,15 +454,26 @@ module.exports = function(crowi) {
 
   pageSchema.statics.findPageByIdAndGrantedUser = function(id, userData) {
     var Page = this;
+    var PageGroupRelation = crowi.model('PageGroupRelation');
+    var pageData = null;
 
     return new Promise(function(resolve, reject) {
       Page.findPageById(id)
-      .then(function(pageData) {
+      .then(function(result) {
+        pageData = result;
         if (userData && !pageData.isGrantedFor(userData)) {
+          return PageGroupRelation.isExistsGrantedGroupForPageAndUser(pageData, userData);
+        }
+        else {
+          return resolve(true);
+        }
+      }).then((checkResult) => {
+        if (checkResult) {
+          return resolve(pageData);
+        }
+        else  {
           return reject(new Error('Page is not granted for the user')); //PAGE_GRANT_ERROR, null);
         }
-
-        return resolve(pageData);
       }).catch(function(err) {
         return reject(err);
       });
@@ -470,6 +483,7 @@ module.exports = function(crowi) {
   // find page and check if granted user
   pageSchema.statics.findPage = function(path, userData, revisionId, ignoreNotFound) {
     var self = this;
+    var PageGroupRelation = crowi.model('PageGroupRelation');
 
     return new Promise(function(resolve, reject) {
       self.findOne({path: path}, function(err, pageData) {
@@ -488,10 +502,22 @@ module.exports = function(crowi) {
         }
 
         if (!pageData.isGrantedFor(userData)) {
-          return reject(new Error('Page is not granted for the user')); //PAGE_GRANT_ERROR, null);
+          PageGroupRelation.isExistsGrantedGroupForPageAndUser(pageData, userData)
+            .then(function (checkResult) {
+              if (!checkResult) {
+                return reject(new Error('Page is not granted for the user')); //PAGE_GRANT_ERROR, null);
+              } else {
+                // return resolve(pageData);
+                self.populatePageData(pageData, revisionId || null).then(resolve).catch(reject);
+              }
+            })
+            .catch(function (err) {
+              return reject(err);
+            });
+        }
+        else {
+          self.populatePageData(pageData, revisionId || null).then(resolve).catch(reject);
         }
-
-        self.populatePageData(pageData, revisionId || null).then(resolve).catch(reject);
       });
     });
   };
@@ -756,12 +782,18 @@ module.exports = function(crowi) {
     });
   };
 
-  pageSchema.statics.updateGrant = function(page, grant, userData) {
+  pageSchema.statics.updateGrant = function (page, grant, userData, grantUserGroupId) {
     var Page = this;
+    var PageGroupRelation = crowi.model('PageGroupRelation');
+    var UserGroupRelation = crowi.model('UserGroupRelation');
+    var provGrant = page.grant;
 
+    if (grant == GRANT_USER_GROUP && grantUserGroupId == null) {
+      throw new Error('grant userGroupId is not specified');
+    }
     return new Promise(function(resolve, reject) {
       page.grant = grant;
-      if (grant == GRANT_PUBLIC) {
+      if (grant == GRANT_PUBLIC || grant == GRANT_USER_GROUP) {
         page.grantedUsers = [];
       } else {
         page.grantedUsers = [];
@@ -774,11 +806,37 @@ module.exports = function(crowi) {
           return reject(err);
         }
 
-        return resolve(data);
+        Page.updateGrantUserGroup(page, grant, grantUserGroupId, userData)
+        .then(() => {
+          return resolve(data);
+        });
       });
     });
   };
 
+  pageSchema.statics.updateGrantUserGroup = function (page, grant, grantUserGroupId, userData) {
+
+    // グループの場合
+    if (grant == GRANT_USER_GROUP) {
+      debug('grant is usergroup', grantUserGroupId);
+      UserGroupRelation.findByGroupIdAndUser(grantUserGroupId, userData)
+      .then((relation) => {
+        if (relation == null) {
+          reject(new Error('no relations were exist for group and user.'));
+        }
+        return PageGroupRelation.findOrCreateRelationForPageAndGroup(page, relation.relatedGroup);
+
+      })
+      .catch((err) => {
+        return reject(new Error('No UserGroup is exists. userGroupId : ', grantUserGroupId));
+      });
+    }
+    else {
+      PageGroupRelation.removeAllByPage(page);
+    }
+
+  };
+
   // Instance method でいいのでは
   pageSchema.statics.pushToGrantedUsers = function(page, userData) {
 
@@ -835,7 +893,8 @@ module.exports = function(crowi) {
       , Revision = crowi.model('Revision')
       , format = options.format || 'markdown'
       , grant = options.grant || GRANT_PUBLIC
-      , redirectTo = options.redirectTo || null;
+      , redirectTo = options.redirectTo || null
+      , grantUserGroupId = options.grantUserGroupId || null;
 
     // force public
       if (isPortalPath(path)) {
@@ -865,6 +924,12 @@ module.exports = function(crowi) {
               return reject(err);
             }
 
+            if (newPage.grant == Page.GRANT_USER_GROUP && grantUserGroupId != null) {
+              Page.updateGrantUserGroup(newPage, grant, grantUserGroupId, user)
+              .catch((err) => {
+                return reject(err);
+              });
+            }
             var newRevision = Revision.prepareRevision(newPage, body, user, {format: format});
             Page.pushRevision(newPage, newRevision, user).then(function(data) {
               resolve(data);
@@ -882,6 +947,7 @@ module.exports = function(crowi) {
     var Page = this
       , Revision = crowi.model('Revision')
       , grant = options.grant || null
+      , grantUserGroupId = options.grantUserGroupId || null
       ;
     // update existing page
     var newRevision = Revision.prepareRevision(pageData, body, user);
@@ -890,7 +956,7 @@ module.exports = function(crowi) {
       Page.pushRevision(pageData, newRevision, user)
       .then(function(revision) {
         if (grant != pageData.grant) {
-          return Page.updateGrant(pageData, grant, user).then(function(data) {
+          return Page.updateGrant(pageData, grant, user, grantUserGroupId).then(function(data) {
             debug('Page grant update:', data);
             resolve(data);
             pageEvent.emit('update', data, user);

+ 279 - 0
lib/models/user-group-relation.js

@@ -0,0 +1,279 @@
+const debug = require('debug')('crowi:models:userGroupRelation');
+const mongoose = require('mongoose');
+const mongoosePaginate = require('mongoose-paginate');
+const ObjectId = mongoose.Schema.Types.ObjectId;
+
+
+/*
+ * define schema
+ */
+const schema = new mongoose.Schema({
+  relatedGroup: { type: ObjectId, ref: 'UserGroup', required: true },
+  relatedUser: { type: ObjectId, ref: 'User', required: true },
+  createdAt: { type: Date, default: Date.now, required: true },
+});
+schema.plugin(mongoosePaginate);
+
+/**
+ * UserGroupRelation Class
+ *
+ * @class UserGroupRelation
+ */
+class UserGroupRelation {
+
+  /**
+   * limit items num for pagination
+   *
+   * @readonly
+   * @static
+   * @memberof UserGroupRelation
+   */
+  static get PAGE_ITEMS() {
+    return 50;
+  }
+
+  static set crowi(crowi) {
+    this._crowi = crowi;
+  }
+
+  static get crowi() {
+    return this._crowi;
+  }
+
+  /**
+   * find all user and group relation
+   *
+   * @static
+   * @returns {Promise<UserGroupRelation[]>}
+   * @memberof UserGroupRelation
+   */
+  static findAllRelation() {
+
+    return this
+      .find()
+      .populate('relatedUser')
+      .populate('relatedGroup')
+      .exec();
+  };
+
+  /**
+   * find all user and group relation of UserGroup
+   *
+   * @static
+   * @param {UserGroup} userGroup
+   * @returns {Promise<UserGroupRelation[]>}
+   * @memberof UserGroupRelation
+   */
+  static findAllRelationForUserGroup(userGroup) {
+    debug('findAllRelationForUserGroup is called', userGroup);
+    var UserGroupRelation = this;
+
+    return this
+      .find({ relatedGroup: userGroup })
+      .populate('relatedUser')
+      .exec();
+  }
+
+  /**
+   * find all user and group relation of UserGroups
+   *
+   * @static
+   * @param {UserGroup[]} userGroups
+   * @returns {Promise<UserGroupRelation[]>}
+   * @memberof UserGroupRelation
+   */
+  static findAllRelationForUserGroups(userGroups) {
+
+    return this
+      .find({ relatedGroup: { $in: userGroups } })
+      .populate('relatedUser')
+      .exec();
+  }
+
+  /**
+   * find all user and group relation of User
+   *
+   * @static
+   * @param {User} user
+   * @returns {Promise<UserGroupRelation[]>}
+   * @memberof UserGroupRelation
+   */
+  static findAllRelationForUser(user) {
+
+    return this
+      .find({ relatedUser: user.id })
+      .populate('relatedGroup')
+      .exec();
+  }
+
+  /**
+   * find all entities with pagination
+   *
+   * @see https://github.com/edwardhotchkiss/mongoose-paginate
+   *
+   * @static
+   * @param {UserGroup} userGroup
+   * @param {any} opts mongoose-paginate options object
+   * @returns {Promise<any>} mongoose-paginate result object
+   * @memberof UserGroupRelation
+   */
+  static findUserGroupRelationsWithPagination(userGroup, opts) {
+    const query = { relatedGroup: userGroup };
+    const options = Object.assign({}, opts);
+    if (options.page == null) {
+      options.page = 1;
+    }
+    if (options.limit == null) {
+      options.limit = UserGroupRelation.PAGE_ITEMS;
+    }
+
+    return this.paginate(query, options)
+      .catch((err) => {
+        debug('Error on pagination:', err);
+      });
+  }
+
+  /**
+   * find one result by related group id and related user
+   *
+   * @static
+   * @param {string} userGroupId find query param for relatedGroup
+   * @param {User} userData find query param for relatedUser
+   * @returns {Promise<UserGroupRelation>}
+   * @memberof UserGroupRelation
+   */
+  static findByGroupIdAndUser(userGroupId, userData) {
+    const query = {
+      relatedGroup: userGroupId,
+      relatedUser: userData.id
+    }
+
+    return this
+      .findOne(query)
+      .populate('relatedUser')
+      .populate('relatedGroup')
+      .exec();
+  }
+
+  /**
+   * find all "not" related user for UserGroup
+   *
+   * @static
+   * @param {UserGroup} userGroup for find users not related
+   * @returns {Promise<User>}
+   * @memberof UserGroupRelation
+   */
+  static findUserByNotRelatedGroup(userGroup) {
+    const User = UserGroupRelation.crowi.model('User');
+
+    return this.findAllRelationForUserGroup(userGroup)
+      .then((relations) => {
+        const relatedUserIds = relations.map((relation) => {
+          return relation.relatedUser.id;
+        });
+        const query = { _id: { $nin: relatedUserIds }, status: User.STATUS_ACTIVE };
+
+        debug("findUserByNotRelatedGroup ", query);
+        return User.find(query).exec();
+      });
+  }
+
+  /**
+   * get if the user has relation for group
+   *
+   * @static
+   * @param {User} userData
+   * @param {UserGroup} userGroup
+   * @returns {Promise<boolean>} is user related for group(or not)
+   * @memberof UserGroupRelation
+   */
+  static isRelatedUserForGroup(userData, userGroup) {
+    const query = {
+      relatedGroup: userGroup.id,
+      relatedUser: userData.id
+    }
+
+    return this
+      .count(query)
+      .exec()
+      .then((count) => {
+        // return true or false of the relation is exists(not count)
+        return (0 < count);
+      })
+      .catch((err) => {
+        debug('An Error occured.', err);
+        reject(err);
+      });
+  }
+
+  /**
+   * create user and group relation
+   *
+   * @static
+   * @param {UserGroup} userGroup
+   * @param {User} user
+   * @returns {Promise<UserGroupRelation>} created relation
+   * @memberof UserGroupRelation
+   */
+  static createRelation(userGroup, user) {
+    return this.create({
+      relatedGroup: userGroup.id,
+      relatedUser: user.id
+    });
+  }
+
+  /**
+   * remove all relation for UserGroup
+   *
+   * @static
+   * @param {UserGroup} userGroup related group for remove
+   * @returns {Promise<any>}
+   * @memberof UserGroupRelation
+   */
+  static removeAllByUserGroup(userGroup) {
+
+    return this.findAllRelationForUserGroup(userGroup)
+      .then((relations) => {
+        if (relations == null) {
+          return;
+        }
+        else {
+          relations.map((relation) => {
+            relation.remove();
+          });
+        }
+      });
+  }
+
+  /**
+   * remove relation by id
+   *
+   * @static
+   * @param {ObjectId} id
+   * @returns {Promise<any>}
+   * @memberof UserGroupRelation
+   */
+  static removeById(id) {
+
+    return this.findById(id)
+      .then((relationData) => {
+        if (relationData == null) {
+          throw new Exception('UserGroupRelation data is not exists. id:', id);
+        }
+        else {
+          relationData.remove();
+        }
+      })
+      .catch((err) => {
+        debug('Error on find a removing user-group-relation', err);
+        reject(err);
+      });
+  }
+
+}
+
+module.exports = function (crowi) {
+  UserGroupRelation.crowi = crowi;
+  schema.loadClass(UserGroupRelation);
+  return mongoose.model('UserGroupRelation', schema);
+}

+ 151 - 0
lib/models/user-group.js

@@ -0,0 +1,151 @@
+const debug = require('debug')('crowi:models:userGroup');
+const mongoose = require('mongoose');
+const mongoosePaginate = require('mongoose-paginate');
+const ObjectId = mongoose.Schema.Types.ObjectId;
+
+
+/*
+ * define schema
+ */
+const schema = new mongoose.Schema({
+  userGroupId: String,
+  image: String,
+  name: { type: String, required: true, unique: true },
+  createdAt: { type: Date, default: Date.now },
+});
+schema.plugin(mongoosePaginate);
+
+class UserGroup {
+
+  /**
+   * public fields for UserGroup model
+   *
+   * @readonly
+   * @static
+   * @memberof UserGroup
+   */
+  static get USER_GROUP_PUBLIC_FIELDS() {
+    return '_id image name createdAt';
+  }
+
+  /**
+   * limit items num for pagination
+   *
+   * @readonly
+   * @static
+   * @memberof UserGroup
+   */
+  static get PAGE_ITEMS() {
+    return 10;
+  }
+
+  /*
+   * model static methods
+   */
+
+   // グループ画像パスの生成
+  static createUserGroupPictureFilePath(userGroup, name) {
+    var ext = '.' + name.match(/(.*)(?:\.([^.]+$))/)[2];
+
+    return 'userGroup/' + userGroup._id + ext;
+  };
+
+  // すべてのグループを取得(オプション指定可)
+  static findAllGroups(option) {
+
+    return this.find().exec();
+  };
+
+  /**
+   * find all entities with pagination
+   *
+   * @see https://github.com/edwardhotchkiss/mongoose-paginate
+   *
+   * @static
+   * @param {any} opts mongoose-paginate options object
+   * @returns {Promise<any>} mongoose-paginate result object
+   * @memberof UserGroup
+   */
+  static findUserGroupsWithPagination(opts) {
+    const query = {};
+    const options = Object.assign({}, opts);
+    if (options.page == null) {
+      options.page = 1;
+    }
+    if (options.limit == null) {
+      options.limit = UserGroup.PAGE_ITEMS;
+    }
+
+    return this.paginate(query, options)
+      .catch((err) => {
+        debug('Error on pagination:', err);
+      });
+  };
+
+  // TBD: グループ名によるグループ検索
+  static findUserGroupByName(name) {
+    const query = { name: name };
+    return this.findOne(query);
+  };
+
+  // 登録可能グループ名確認
+  static isRegisterableName(name) {
+    const query = { name: name };
+
+    return this.findOne(query)
+      .then((userGroupData) => {
+        return (userGroupData == null);
+      });
+  };
+
+  // グループの完全削除
+  static removeCompletelyById(id) {
+
+    return this.findById(id)
+      .then((userGroupData) => {
+        if (userGroupData == null) {
+          throw new Exception('UserGroup data is not exists. id:', id);
+        }
+        else {
+          userGroupData.remove();
+        }
+      });
+  }
+
+  // グループ生成(名前が要る)
+  static createGroupByName(name) {
+
+    return this.create(name);
+  }
+
+  /*
+   * instance methods
+   */
+
+  // グループ画像の更新
+  updateImage(image) {
+    this.image = image;
+    return this.save();
+  }
+
+  // グループ画像の削除
+  deleteImage() {
+    return this.updateImage(null);
+  }
+
+  // グループ名の更新
+  updateName(name) {
+    // 名前を設定して更新
+    this.name = name;
+    return this.save();
+  }
+
+}
+
+
+module.exports = function (crowi) {
+  UserGroup.crowi = crowi;
+  schema.loadClass(UserGroup);
+  return mongoose.model('UserGroup', schema);
+}
+

+ 0 - 205
lib/models/userGroup.js

@@ -1,205 +0,0 @@
-module.exports = function(crowi) {
-  var debug = require('debug')('crowi:models:userGroup')
-    , mongoose = require('mongoose')
-    , mongoosePaginate = require('mongoose-paginate')
-    , uniqueValidator = require('mongoose-unique-validator')
-    , ObjectId = mongoose.Schema.Types.ObjectId
-
-    , USER_GROUP_PUBLIC_FIELDS = '_id image name createdAt'
-
-    , PAGE_ITEMS = 50
-
-    , userGroupSchema;
-
-  userGroupSchema = new mongoose.Schema({
-    userGroupId: String,
-    image: String,
-    name: { type: String, required: true, unique: true },
-    createdAt: { type: Date, default: Date.now },
-  });
-  userGroupSchema.plugin(mongoosePaginate);
-  userGroupSchema.plugin(uniqueValidator);
-
-
-  // TBD: グループ画像の更新
-  // userGroupSchema.methods.updateImage = function(image, callback) {
-  //   this.image = image;
-  //   this.save(function(err, userGroupData) {
-  //     return callback(err, userGroupData);
-  //   });
-  // };
-
-  // TBD: グループ画像の削除
-  // userGroupSchema.methods.deleteImage = function(callback) {
-  //   return this.updateImage(null, callback);
-  // };
-
-  // グループ公開情報のフィルター
-  userGroupSchema.statics.filterToPublicFields = function(userGroup) {
-    debug('UserGroup is', typeof userGroup, userGroup);
-    if (typeof userGroup !== 'object' || !userGroup._id) {
-      return userGroup;
-    }
-
-    var filteredGroup = {};
-    var fields = USER_GROUP_PUBLIC_FIELDS.split(' ');
-    for (var i = 0; i < fields.length; i++) {
-      var key = fields[i];
-      if (userGroup[key]) {
-        filteredGroup[key] = userGroup[key];
-      }
-    }
-
-    return filteredGroup;
-  };
-
-  // TBD: グループ検索
-  // userGroupSchema.statics.findGroups = function(options, callback) {
-  //   var sort = options.sort || {createdAt: 1};
-
-  //   this.find()
-  //     .sort(sort)
-  //     .skip(options.skip || 0)
-  //     .limit(options.limit || 21)
-  //     .exec(function (err, userGroupData) {
-  //       callback(err, userGroupData);
-  //     });
-
-  // };
-
-  // すべてのグループを取得(オプション指定可)
-  userGroupSchema.statics.findAllGroups = function(option) {
-    debug('NoErrorOccured');
-
-    var UserGroup = this;
-    var option = option || {}
-      , sort = option.sort || {createdAt: -1}
-      , fields = option.fields || USER_GROUP_PUBLIC_FIELDS
-      ;
-
-    return new Promise(function(resolve, reject) {
-      UserGroup
-        .find()
-        .select(fields)
-        .sort(sort)
-        .exec(function (err, userGroupData) {
-          if (err) {
-            return reject(err);
-          }
-
-          return resolve(userGroupData);
-        });
-    });
-  };
-
-  // TBD: IDによるグループ検索
-  // userGroupSchema.statics.findGroupsByIds = function(ids, option) {
-  //   var UserGroup = this;
-  //   var option = option || {}
-  //     , sort = option.sort || {createdAt: -1}
-  //     , fields = option.fields || USER_GROUP_PUBLIC_FIELDS
-  //     ;
-
-  //   return new Promise(function(resolve, reject) {
-  //     UserGroup
-  //       .find({ _id: { $in: ids }})
-  //       .select(fields)
-  //       .sort(sort)
-  //       .exec(function (err, userGroupData) {
-  //         if (err) {
-  //           return reject(err);
-  //         }
-
-  //         return resolve(userGroupData);
-  //       });
-  //   });
-  // };
-
-  // ページネーション利用のグループ検索
-  userGroupSchema.statics.findUserGroupsWithPagination = function(options, callback) {
-    var sort = options.sort || {name: 1, createdAt: 1};
-
-    // return callback(err, null);
-    this.paginate({ page: options.page || 1, limit: options.limit || PAGE_ITEMS }, function(err, result) {
-      if (err) {
-        debug('Error on pagination:', err);
-        return callback(err, null);
-      }
-
-      return callback(err, result);
-    }, { sortBy : sort });
-  };
-
-  // TBD: グループ名によるグループ検索
-  // userGroupSchema.statics.findUserGroupByName = function(name) {
-  //   var UserGroup = this;
-  //   return new Promise(function(resolve, reject) {
-  //     UserGroup.findOne({name: name}, function (err, userGroupData) {
-  //       if (err) {
-  //         return reject(err);
-  //       }
-
-  //       return resolve(userGroupData);
-  //     });
-  //   });
-  // };
-
-  // TBD: 登録可能グループ名確認
-  // userGroupSchema.statics.isRegisterableName = function(name, callback) {
-  //   var UserGroup = this;
-  //   var userGroupnameUsable = true;
-
-  //   this.findOne({name: name}, function (err, userGroupData) {
-  //     if (userGroupData) {
-  //       userGroupnameUsable = false;
-  //     }
-  //     return callback(userGroupnameUsable);
-  //   });
-  // };
-
-  // TBD: グループの完全削除
-  // userGroupSchema.statics.removeCompletelyById = function(id, callback) {
-  //   var UserGroup = this;
-  //   UserGroup.findById(id, function (err, userGroupData) {
-  //     if (!userGroupData) {
-  //       return callback(err, null);
-  //     }
-
-  //     debug('Removing userGroup:', userGroupData);
-
-  //     userGroupData.remove(function(err) {
-  //       if (err) {
-  //         return callback(err, null);
-  //       }
-
-  //       return callback(null, 1);
-  //     });
-  //   });
-  // };
-
-  // TBD: グループ生成(名前が要る)
-  userGroupSchema.statics.createGroupByName = function(name, callback) {
-    var UserGroup = this
-      , newUserGroup = new UserGroup();
-
-    newUserGroup.name = name;
-    newUserGroup.createdAt = Date.now();
-
-    newUserGroup.save(function(err, userGroupData) {
-      return callback(err, userGroupData);
-    });
-  };
-
-  // TBD: グループ画像パスの生成
-  // userGroupSchema.statics.createGroupPictureFilePath = function(userGroup, name) {
-  //   var ext = '.' + name.match(/(.*)(?:\.([^.]+$))/)[2];
-
-  //   return 'userGroup/' + userGroup._id + ext;
-  // };
-
-
-  userGroupSchema.statics.USER_GROUP_PUBLIC_FIELDS = USER_GROUP_PUBLIC_FIELDS;
-  userGroupSchema.statics.PAGE_ITEMS         = PAGE_ITEMS;
-
-  return mongoose.model('UserGroup', userGroupSchema);
-};

+ 0 - 84
lib/models/userGroupRelation.js

@@ -1,84 +0,0 @@
-module.exports = function(crowi) {
-  var debug = require('debug')('crowi:models:userGroupRelation')
-    , mongoose = require('mongoose')
-    , mongoosePaginate = require('mongoose-paginate')
-    , ObjectId = mongoose.Schema.Types.ObjectId
-
-    , PAGE_ITEMS = 50
-
-    , userGroupRelationSchema;
-
-  userGroupRelationSchema = new mongoose.Schema({
-    relatedGroup: { type: ObjectId, ref: 'UserGroup' },
-    relatedUser: { type: ObjectId, ref: 'User' },
-    createdAt: { type: Date, default: Date.now },
-  });
-  userGroupRelationSchema.plugin(mongoosePaginate);
-
-  // すべてのグループ所属関係を取得
-  userGroupRelationSchema.statics.findAllRelation = function() {
-    debug('findAllGroups is called');
-    var UserGroupRelation = this;
-
-    return new Promise(function(resolve, reject) {
-      UserGroupRelation
-        .find({ relatedGroup: group} )
-        .exec(function (err, userGroupRelationData) {
-          if (err) {
-            return reject(err);
-          }
-
-          return resolve(userGroupRelationData);
-        });
-    });
-  };
-
-  // すべてのグループ所属関係を取得
-  userGroupRelationSchema.statics.findAllRelation = function (group) {
-    debug('findAllGroups is called');
-    var UserGroupRelation = this;
-
-    return new Promise(function (resolve, reject) {
-      UserGroupRelation
-        .find({ relatedGroup: group })
-        .exec(function (err, userGroupRelationData) {
-          if (err) {
-            return reject(err);
-          }
-
-          return resolve(userGroupRelationData);
-        });
-    });
-  };
-
-  // ページネーション利用の検索
-  userGroupRelationSchema.statics.findUserGroupRelationsWithPagination = function(options, callback) {
-
-    this.paginate({ page: options.page || 1, limit: options.limit || PAGE_ITEMS }, function(err, result) {
-      if (err) {
-        debug('Error on pagination:', err);
-        return callback(err, null);
-      }
-
-      return callback(err, result);
-    });
-  };
-
-  // 関係性の生成
-  userGroupRelationSchema.statics.createRelation = function(userGroup, user, callback) {
-    var UserGroupRelation = this
-      , newUserGroupRelation = new UserGroupRelation();
-
-    newUserGroupRelation.relatedGroup = group;
-    newUserGroupRelation.relatedUser = user;
-    newUserGroupRelation.createdAt = Date.now();
-
-    newUserGroupRelation.save(function(err, userGroupRelationData) {
-      return callback(err, userGroupRelationData);
-    });
-  };
-
-  userGroupRelationSchema.statics.PAGE_ITEMS         = PAGE_ITEMS;
-
-  return mongoose.model('UserGroupRelation', userGroupRelationSchema);
-};

+ 300 - 23
lib/routes/admin.js

@@ -2,17 +2,20 @@ module.exports = function(crowi, app) {
   'use strict';
 
   var debug = require('debug')('crowi:routes:admin')
+    , fs = require('fs')
     , models = crowi.models
     , Page = models.Page
+    , PageGroupRelation = models.PageGroupRelation
     , User = models.User
     , ExternalAccount = models.ExternalAccount
     , UserGroup = models.UserGroup
+    , UserGroupRelation = models.UserGroupRelation
     , Config = models.Config
     , PluginUtils = require('../plugins/plugin-utils')
     , pluginUtils = new PluginUtils()
     , ApiResponse = require('../util/apiResponse')
 
-    , MAX_PAGE_LIST = 5
+    , MAX_PAGE_LIST = 50
     , actions = {};
 
   function createPager(total, limit, page, pagesCount, maxPageList) {
@@ -517,34 +520,308 @@ module.exports = function(crowi, app) {
   actions.userGroup = {};
   actions.userGroup.index = function (req, res) {
     var page = parseInt(req.query.page) || 1;
+    var renderVar = {
+      userGroups : [],
+      userGroupRelations : new Map(),
+      pager : null,
+    }
 
-    UserGroup.findUserGroupsWithPagination({ page: page }, function (err, result) {
-      const pager = createPager(result.total, result.limit, result.page, result.pages, MAX_PAGE_LIST);
+    UserGroup.findUserGroupsWithPagination({ page: page })
+      .then((result) => {
+        const pager = createPager(result.total, result.limit, result.page, result.pages, MAX_PAGE_LIST);
+        var userGroups = result.docs;
+        renderVar.userGroups = userGroups;
+        renderVar.pager = pager;
+        return userGroups.map((userGroup) => {
+          return new Promise((resolve, reject) => {
+            UserGroupRelation.findAllRelationForUserGroup(userGroup)
+            .then((relations) => {
+              return resolve([userGroup, relations]);
+            });
+          });
+        });
+      })
+      .then((allRelationsPromise) => {
+        return Promise.all(allRelationsPromise)
+      })
+      .then((relations) => {
+        renderVar.userGroupRelations = new Map(relations);
+        debug("in findUserGroupsWithPagination findAllRelationForUserGroupResult", renderVar.userGroupRelations);
+        return res.render('admin/user-groups', renderVar);
+      })
+      .catch( function(err) {
+          debug('Error on find all relations', err);
+          return res.json(ApiResponse.error('Error'));
+      });
+  };
 
-      return res.render('admin/user_groups', {
-        userGroups: result.docs,
-        pager: pager
+  // グループ詳細
+  actions.userGroup.detail = function (req, res) {
+    var name = req.params.name;
+    var renderVar = {
+      userGroup: null,
+      userGroupRelations: [],
+      pageGroupRelations: [],
+      notRelatedusers: []
+    }
+    var targetUserGroup = null;
+    UserGroup.findUserGroupByName(name)
+      .then(function (userGroup) {
+        targetUserGroup = userGroup;
+        if (targetUserGroup == null) {
+          req.flash('errorMessage', 'グループがありません');
+          throw new Error('no userGroup is exists. ', name);
+        }
+        else {
+          renderVar.userGroup = targetUserGroup;
+
+          return Promise.all([
+            // get all user and group relations
+            UserGroupRelation.findAllRelationForUserGroup(targetUserGroup),
+            // get all page and group relations
+            PageGroupRelation.findAllRelationForUserGroup(targetUserGroup),
+            // get all not related users for group
+            UserGroupRelation.findUserByNotRelatedGroup(targetUserGroup),
+          ]);
+        }
+      })
+      .then((resolves) => {
+        renderVar.userGroupRelations = resolves[0];
+        renderVar.pageGroupRelations = resolves[1];
+        renderVar.notRelatedusers = resolves[2];
+        debug('notRelatedusers', renderVar.notRelatedusers);
+
+        return res.render('admin/user-group-detail', renderVar);
+      })
+      .catch((err) => {
+        req.flash('errorMessage', 'ユーザグループの検索に失敗しました');
+        debug('Error on get userGroupDetail', err);
+        return res.redirect('/admin/user-groups');
+      });
+  }
+
+  //グループの生成
+  actions.userGroup.create = function (req, res) {
+    var form = req.form.createGroupForm;
+    if (req.form.isValid) {
+      UserGroup.createGroupByName(form.userGroupName)
+      .then((newUserGroup) => {
+        req.flash('successMessage', newUserGroup.name)
+        req.flash('createdUserGroup', newUserGroup);
+        return res.redirect('/admin/user-groups');
+      })
+      .catch((err) => {
+        debug('create userGroup error:', err);
+        req.flash('errorMessage', '同じグループ名が既に存在します。');
       });
+    } else {
+      req.flash('errorMessage', req.form.errors.join('\n'));
+      return res.redirect('/admin/user-groups');
+    }
+  };
+
+  //
+  actions.userGroup.update = function (req, res) {
+
+    var userGroupId = req.params.userGroupId;
+    var name = req.body.name;
+
+    UserGroup.findById(userGroupId)
+    .then((userGroupData) => {
+      if (userGroupData == null) {
+        req.flash('errorMessage', 'グループの検索に失敗しました。');
+        return new Promise();
+      }
+      else {
+        // 名前存在チェック
+        return UserGroup.isRegisterableName(name)
+        .then((isRegisterableName) => {
+          // 既に存在するグループ名に更新しようとした場合はエラー
+          if (!isRegisterableName) {
+            req.flash('errorMessage', 'グループ名が既に存在します。');
+          }
+          else {
+            return userGroupData.updateName(name)
+            .then(() => {
+              req.flash('successMessage', 'グループ名を更新しました。');
+            })
+            .catch((err) => {
+              req.flash('errorMessage', 'グループ名の更新に失敗しました。');
+            });
+          }
+        });
+      }
+    })
+    .then(() => {
+      return res.redirect('/admin/user-group-detail/' + userGroupData.name);
+    });
+  };
+
+  actions.userGroup.uploadGroupPicture = function (req, res) {
+    var fileUploader = require('../util/fileUploader')(crowi, app);
+    //var storagePlugin = new pluginService('storage');
+    //var storage = require('../service/storage').StorageService(config);
+
+    var userGroupId = req.params.userGroupId;
+
+    var tmpFile = req.file || null;
+    if (!tmpFile) {
+      return res.json({
+        'status': false,
+        'message': 'File type error.'
+      });
+    }
+
+    UserGroup.findById(userGroupId, function (err, userGroupData) {
+      if (!userGroupData) {
+        return res.json({
+          'status': false,
+          'message': 'UserGroup error.'
+        });
+      }
+
+      var tmpPath = tmpFile.path;
+      var filePath = UserGroup.createUserGroupPictureFilePath(userGroupData, tmpFile.filename + tmpFile.originalname);
+      var acceptableFileType = /image\/.+/;
+
+      if (!tmpFile.mimetype.match(acceptableFileType)) {
+        return res.json({
+          'status': false,
+          'message': 'File type error. Only image files is allowed to set as user picture.',
+        });
+      }
+
+      var tmpFileStream = fs.createReadStream(tmpPath, { flags: 'r', encoding: null, fd: null, mode: '0666', autoClose: true });
+
+      fileUploader.uploadFile(filePath, tmpFile.mimetype, tmpFileStream, {})
+        .then(function (data) {
+          var imageUrl = fileUploader.generateUrl(filePath);
+          userGroupData.updateImage(imageUrl)
+          .then(() => {
+            fs.unlink(tmpPath, function (err) {
+              if (err) {
+                debug('Error while deleting tmp file.', err);
+              }
+
+              return res.json({
+                'status': true,
+                'url': imageUrl,
+                'message': '',
+              });
+            });
+          });
+        }).catch(function (err) {
+          debug('Uploading error', err);
+
+          return res.json({
+            'status': false,
+            'message': 'Error while uploading to ',
+          });
+        });
     });
+
   };
 
-  // actions.userGroup.create = function (req, res) {
-  //   var form = req.form.createGroupForm;
-  //   // var toSendEmail = form.sendEmail || false;
-  //   // if (req.form.isValid) {
-  //     User.createUsersByInvitation(form.userGroupName, function (err, newUserGroup) {
-  //       if (err) {
-  //         req.flash('errorMessage', req.form.errors.join('\n'));
-  //       } else {
-  //         req.flash('createdUserGroup', newUserGroup);
-  //       }
-  //       return res.redirect('/admin/user_groups');
-  //     });
-  //   // } else {
-  //   //   req.flash('errorMessage', req.form.errors.join('\n'));
-  //   //   return res.redirect('/admin/user_groups');
-  //   // }
-  // };
+  actions.userGroup.deletePicture = function (req, res) {
+
+    var userGroupId = req.params.userGroupId;
+    let userGroupName = null;
+
+    UserGroup.findById(userGroupId)
+    .then((userGroupData) => {
+      if (userGroupData == null) {
+        return Promise.reject();
+      }
+      else {
+        userGroupName = userGroupData.name;
+        return userGroupData.deleteImage();
+      }
+    })
+    .then((updated) => {
+      req.flash('successMessage', 'Deleted group picture');
+
+      return res.redirect('/admin/user-group-detail/' + userGroupName);
+    })
+    .catch((err) => {
+      debug('An error occured.', err);
+
+      req.flash('errorMessage', 'Error while deleting group picture');
+      if (userGroupName == null) {
+        return res.redirect('/admin/user-groups/');
+      }
+      else {
+        return res.redirect('/admin/user-group-detail/' + userGroupName);
+      }
+    });
+  };
+
+  // app.post('/_api/admin/user-group/delete' , admin.userGroup.removeCompletely);
+  actions.userGroup.removeCompletely = function (req, res) {
+    const id = req.body.user_group_id;
+
+    UserGroup.removeCompletelyById(id, function (err, removed) {
+      if (err) {
+        debug('Error while removing userGroup.', err, id);
+        req.flash('errorMessage', '完全な削除に失敗しました。');
+      } else {
+        req.flash('successMessage', '削除しました');
+      }
+      return res.redirect('/admin/user-groups');
+    });
+  }
+
+  actions.userGroupRelation = {};
+  actions.userGroupRelation.index = function(req, res) {
+
+  }
+
+  actions.userGroupRelation.create = function(req, res) {
+    const User = crowi.model('User');
+    const UserGroup = crowi.model('UserGroup');
+    const UserGroupRelation = crowi.model('UserGroupRelation');
+
+    // req params
+    const userName = req.body.user_name;
+    const userGroupId = req.body.user_group_id;
+
+    let user = null;
+    let userGroup = null;
+
+    Promise.all([
+      // ユーザグループをIDで検索
+      UserGroup.findById(userGroupId),
+      // ユーザを名前で検索
+      User.findUserByUsername(userName),
+    ])
+    .then((resolves) => {
+      // Relation を作成
+      UserGroupRelation.createRelation(resolves[0], resolves[1])
+    })
+    .then((result) => {
+      return res.redirect('/admin/user-group-detail/' + userGroup.name);
+    }).catch((err) => {
+      debug('Error on create user-group relation', err);
+      req.flash('errorMessage', 'Error on create user-group relation');
+          return res.redirect('/admin/user-group-detail/' + userGroup.name);
+    });
+  }
+
+  actions.userGroupRelation.remove = function (req, res) {
+    const UserGroupRelation = crowi.model('UserGroupRelation');
+    var name = req.params.name;
+    var relationId = req.params.relationId;
+
+    debug(name, relationId);
+    UserGroupRelation.removeById(relationId)
+    .then(() =>{
+      return res.redirect('/admin/user-group-detail/' + name);
+    })
+    .catch((err) => {
+      debug('Error on remove user-group-relation', err);
+      req.flash('errorMessage', 'グループのユーザ削除に失敗しました。');
+    });
+
+  }
 
   actions.api = {};
   actions.api.appSetting = function(req, res) {

+ 13 - 3
lib/routes/index.js

@@ -105,9 +105,19 @@ module.exports = function(crowi, app) {
 
   app.get('/admin/users/external-accounts'               , loginRequired(crowi, app) , middleware.adminRequired() , admin.externalAccount.index);
   app.post('/admin/users/external-accounts/:id/remove'   , loginRequired(crowi, app) , middleware.adminRequired() , admin.externalAccount.remove);
-  // groups admin
-  app.get('/admin/user-groups'          , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.index);
-  // app.post('/admin/user-groups/create'  , form.admin.userGroupCreate, loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.create);
+
+  // user-groups admin
+  app.get('/admin/user-groups'             , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.index);
+  app.get('/admin/user-group-detail/:name'          , loginRequired(crowi, app), middleware.adminRequired(), admin.userGroup.detail);
+  app.post('/admin/user-group/create'      , form.admin.userGroupCreate, loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.create);
+  app.post('/admin/user-group/:userGroupId/update', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.update);
+  app.post('/admin/user-group/:userGroupId/picture/delete', loginRequired(crowi, app), admin.userGroup.deletePicture);
+  app.post('/_api/admin/user-group.remove' , loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.removeCompletely);
+  app.post('/_api/admin/user-group/:userGroupId/picture/upload', loginRequired(crowi, app), uploads.single('userGroupPicture'), admin.userGroup.uploadGroupPicture);
+
+  // user-group-relations admin
+  app.post('/admin/user-group-relation/create', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.create)
+  app.post('/admin/user-group-relation/:name/remove-relation/:relationId', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.remove)
 
   app.get('/me'                       , loginRequired(crowi, app) , me.index);
   app.get('/me/password'              , loginRequired(crowi, app) , me.password);

+ 20 - 3
lib/routes/page.js

@@ -8,6 +8,7 @@ module.exports = function(crowi, app) {
     , config   = crowi.getConfig()
     , Revision = crowi.model('Revision')
     , Bookmark = crowi.model('Bookmark')
+    , UserGroupRelation = crowi.model('UserGroupRelation')
     , ApiResponse = require('../util/apiResponse')
     , interceptorManager = crowi.getInterceptorManager()
 
@@ -231,6 +232,7 @@ module.exports = function(crowi, app) {
       author: false,
       pages: [],
       tree: [],
+      userRelatedGroups: [],
     };
 
     var pageTeamplate = 'customlayout-selector/page';
@@ -331,6 +333,15 @@ module.exports = function(crowi, app) {
           debug('Error on rendering pageListShowForCrowiPlus', err);
         });
       }
+    })
+    .then(function() {
+      return UserGroupRelation.findAllRelationForUser(req.user);
+    }).then(function (groupRelations) {
+      debug('findPage : relatedGroups ', groupRelations);
+      renderVars.userRelatedGroups = groupRelations.map(relation => relation.relatedGroup);
+      debug('findPage : groups ', renderVars.userRelatedGroups);
+
+      return Promise.resolve();
     });
   }
 
@@ -548,6 +559,7 @@ module.exports = function(crowi, app) {
     var currentRevision = pageForm.currentRevision;
     var grant = pageForm.grant;
     var path = pageForm.path;
+    var grantUserGroupId = pageForm.grantUserGroupId
 
     // TODO: make it pluggable
     var notify = pageForm.notify || {};
@@ -586,11 +598,11 @@ module.exports = function(crowi, app) {
 
       if (data) {
         previousRevision = data.revision;
-        return Page.updatePage(data, body, req.user, {grant: grant});
+        return Page.updatePage(data, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
       } else {
         // new page
         updateOrCreate = 'create';
-        return Page.create(path, body, req.user, {grant: grant});
+        return Page.create(path, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
       }
     }).then(function(data) {
       // data is a saved page data.
@@ -788,6 +800,7 @@ module.exports = function(crowi, app) {
     var body = req.body.body || null;
     var pagePath = req.body.path || null;
     var grant = req.body.grant || null;
+    var grantUserGroupId = req.body.grantUserGroupId || null;
 
     if (body === null || pagePath === null) {
       return res.json(ApiResponse.error('Parameters body and path are required.'));
@@ -800,7 +813,7 @@ module.exports = function(crowi, app) {
         throw new Error('Page exists');
       }
 
-      return Page.create(pagePath, body, req.user, {grant: grant});
+      return Page.create(pagePath, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
     }).then(function(data) {
       if (!data) {
         throw new Error('Failed to create page.');
@@ -835,6 +848,7 @@ module.exports = function(crowi, app) {
     var pageId = req.body.page_id || null;
     var revisionId = req.body.revision_id || null;
     var grant = req.body.grant || null;
+    var grantUserGroupId = req.body.grantUserGroupId || null;
 
     if (pageId === null || pageBody === null) {
       return res.json(ApiResponse.error('page_id and body are required.'));
@@ -850,6 +864,9 @@ module.exports = function(crowi, app) {
       if (grant !== null) {
         grantOption.grant = grant;
       }
+      if (grantUserGroupId != null) {
+        grantOption.grantUserGroupId = grantUserGroupId;
+      }
       return Page.updatePage(pageData, pageBody, req.user, grantOption);
     }).then(function(pageData) {
       var result = {

+ 8 - 1
lib/views/_form.html

@@ -56,7 +56,14 @@
       {% else %}
       <select name="pageForm[grant]" class="form-control">
         {% for grantId, grantLabel in consts.pageGrants %}
-        <option value="{{ grantId }}" {% if pageForm.grant|default(page.grant) == grantId %}selected{% endif %}>{{ t(grantLabel) }}</option>
+        <option value="{{ grantId }}" {% if pageForm.grant|default(page.grant) == grantId %}selected{% endif %} {% if grantId == 5 && userRelatedGroups.length == 0 %}disabled{% endif %}>{{ t(grantLabel) }}</option>
+        {% endfor %}
+      </select>
+      {% endif %}
+      {% if userRelatedGroups.length != 0 %}
+      <select name="pageForm[grantUserGroupId]" class="form-control">
+        {% for userGroup in userRelatedGroups %}
+        <option value="{{ userGroup.id }}">{{ userGroup.name }}</option>
         {% endfor %}
       </select>
       {% endif %}

+ 242 - 0
lib/views/admin/user-group-detail.html

@@ -0,0 +1,242 @@
+{% extends '../layout/admin.html' %}
+
+{% block html_title %}グループ管理 · {% endblock %}
+
+{% block content_head %}
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 class="title" id="">グループ管理(グループ詳細)</h1>
+  </header>
+</div>
+{% endblock %}
+
+{% block content_main %}
+<div class="content-main">
+  {% set smessage = req.flash('successMessage') %}
+  {% if smessage.length %}
+  <div class="alert alert-success">
+    {{ smessage }}
+  </div>
+  {% endif %}
+
+  {% set emessage = req.flash('errorMessage') %}
+  {% if emessage.length %}
+  <div class="alert alert-danger">
+    {{ emessage }}
+  </div>
+  {% endif %}
+
+  <div class="row">
+    <div class="col-md-3">
+      {% include './widget/menu.html' with {current: 'user-group'} %}
+    </div>
+
+    <div class="col-md-9">
+          <a href="/admin/user-groups"><i class="fa fa-arrow-left"></i> グループ一覧に戻る</a>
+
+      <div class="modal fade" id="admin-add-user-group-relation-modal">
+        <div class="modal-dialog">
+          <div class="modal-content">
+            <div class="modal-header">
+              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+              <h4 class="modal-title">
+                グループにユーザを追加します</h4>
+            </div>
+
+            <div class="modal-body">
+              <p>
+                ユーザ名を入力してください。
+              </p>
+              <form class="form-inline" role="form" action="/admin/user-group-relation/create" method="post">
+                <div class="form-group">
+                  <label for="inputRelatedUserName">Add related user:</label>
+                  <input type="text" name="user_name" class="form-control input-sm" id="inputRelatedUserName" placeholder="username">
+                </div>
+                <input type="hidden" name="user_group_id" value="{{userGroup.id}}">
+                <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                <button type="submit" class="btn btn-sm btn-danger">実行</button>
+              </form>
+
+              {% if 0 < notRelatedusers.length %}
+              <hr>
+              <p>
+                ユーザ名を選択してください。
+              </p>
+
+              <ul class="list-inline">
+                {% for sUser in notRelatedusers %}
+                <li>
+                  <form role="form" action="/admin/user-group-relation/create" method="post">
+                    <!-- <input type="hidden" name="user_name" value="{{sUser.username}}"> -->
+                    <input type="hidden" name="user_group_id" value="{{userGroup.id}}">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                    <button type="submit" name="user_name" value="{{sUser.username}}" class="btn btn-xs btn-primary">{{sUser.username}}</button>
+                  </form>
+                </li>
+                {% endfor %}
+              </ul>
+              {% endif %}
+
+            </div>
+
+          </div>
+          <!-- /.modal-content -->
+        </div>
+        <!-- /.modal-dialog -->
+      </div>
+
+      <div class="form-box">
+        <form action="/admin/user-group/{{userGroup.id}}/update" method="post" class="form-horizontal" role="form">
+          <fieldset>
+            <legend>基本情報</legend>
+            <div class="form-group">
+              <label for="name" class="col-sm-2 control-label">{{ t('Name') }}</label>
+              <div class="col-sm-4">
+                <input class="form-control" type="text" name="name" value="{{ userGroup.name }}" required>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="col-sm-2 control-label">{{ t('Created') }}</label>
+              <div class="col-sm-4">
+                <input class="form-control" type="text" disabled value="{{userGroup.createdAt|date('Y-m-d', sRelation.relatedUser.createdAt.getTimezoneOffset()) }}">
+              </div>
+            </div>
+            <div class="form-group">
+              <div class="col-sm-offset-2 col-sm-10">
+                <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
+              </div>
+            </div>
+          </fieldset>
+        </form>
+      </div>
+
+      <div class="form-box">
+        <fieldset>
+          <legend>グループ画像の設定</legend>
+          <div class="form-group col-sm-8">
+            <h4>
+              {{ t('Upload Image') }}
+            </h4>
+            <div class="form-group">
+              <div id="pictureUploadFormMessage"></div>
+              <label for="" class="col-sm-4 control-label">
+                {{ t('Current Image') }}
+              </label>
+              <div class="col-sm-8">
+                <p>
+                  <img src="{{ userGroup|uploadedpicture }}" width="64" id="settingUserPicture">
+                  <br>
+                </p>
+                <p>
+                  {% if userGroup.image %}
+                  <form action="/admin/user-group/{{userGroup.id}}/picture/delete" method="post" class="form-horizontal" role="form" onsubmit="return window.confirm('{{ t('Delete this image?') }}');">
+                    <button type="submit" class="btn btn-danger">{{ t('Delete Image') }}</button>
+                  </form>
+                  {% endif %}
+                </p>
+              </div>
+            </div><!-- /.form-group -->
+
+            <div class="form-group">
+              <label for="" class="col-sm-4 control-label">
+                {{ t('Upload new image') }}
+              </label>
+              <div class="col-sm-8">
+                {% if isUploadable() %}
+                <form action="/_api/admin/user-group/{{userGroup.id}}/picture/upload" id="pictureUploadForm" method="post" class="form-horizontal" role="form" enctype="multipart/form-data">
+                  <input name="userGroupPicture" type="file" accept="image/*">
+                  <div id="pictureUploadFormProgress">
+                  </div>
+                </form>
+                {% else %} * {{ t('page_me.form_help.profile_image1') }}
+                <br> * {{ t('page_me.form_help.profile_image2') }}
+                <br> {% endif %}
+              </div>
+            </div><!-- /.form-group -->
+
+          </div><!-- /.col-sm- -->
+
+        </fieldset>
+      </div><!-- /.form-box -->
+
+      <legend>ユーザー一覧</legend>
+
+      <table class="table table-hover table-striped table-bordered table-user-list">
+        <thead>
+          <tr>
+            <th width="100px">#</th>
+            <th>
+              <code>username</code>
+            </th>
+            <th>名前</th>
+            <th width="100px">作成日</th>
+            <th width="150px">最終ログイン</th>
+            <th width="90px">操作</th>
+          </tr>
+        </thead>
+        <tbody>
+          {% for sRelation in userGroupRelations %}
+          {% set sUser = sRelation.relatedUser%}
+          <tr>
+            <td>
+              <img src="{{ sRelation.relatedUser|picture }}" class="picture picture-rounded" />
+            </td>
+            <td>
+              <strong>{{ sRelation.relatedUser.username }}</strong>
+            </td>
+            <td>{{ sRelation.relatedUser.name }}</td>
+            <td>{{ sRelation.relatedUser.createdAt|date('Y-m-d', sRelation.relatedUser.createdAt.getTimezoneOffset()) }}</td>
+            <td>
+              {% if sRelation.relatedUser.lastLoginAt %} {{ sRelation.relatedUser.lastLoginAt|date('Y-m-d H:i', sRelation.relatedUser.createdAt.getTimezoneOffset()) }} {% endif %}
+            </td>
+            <td>
+              <div class="btn-group admin-user-menu">
+                <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
+                  編集
+                  <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu" role="menu">
+                  <li class="dropdown-header">編集メニュー</li>
+                  <li class="divider"></li>
+                  <li class="dropdown-button">
+                    <form action="/admin/user-group-relation/{{userGroup.name}}/remove-relation/{{ sRelation._id.toString() }}" method="post">
+                      <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                      <button type="submit" class="btn btn-block btn-danger">グループから外す</button>
+                    </form>
+                  </li>
+                </ul>
+              </div>
+            </td>
+          </tr>
+          {% endfor %}
+
+          {% if 0 < notRelatedusers.length %}
+          <tr>
+            <td></td>
+            <td><button type="button" class="btn btn-primary" data-target="#admin-add-user-group-relation-modal" data-toggle="modal"><i class="fa fa-plus"></i></button></td>
+            <td></td>
+            <td></td>
+            <td></td>
+            <td></td>
+          </tr>
+          {% endif %}
+        </tbody>
+      </table>
+
+      <!-- {% include '../widget/pager.html' with {path: "/admin/user-group-detail", pager: pager} %} -->
+
+      <legend>ページ一覧</legend>
+
+      {% if pageGroupRelations.length == 0 %}<p>グループが閲覧権限を保有するページはありません</p>{% endif %}
+      {% include '../widget/page_list.html' with { pages: pageGroupRelations, pagePropertyName: 'targetPage' } %}
+
+    </div>
+  </div>
+</div>
+{% endblock content_main %}
+
+{% block content_footer %}
+{% endblock content_footer %}
+
+

+ 59 - 11
lib/views/admin/user_groups.html → lib/views/admin/user-groups.html

@@ -28,14 +28,14 @@
 
   <div class="row">
     <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'user_group'} %}
+      {% include './widget/menu.html' with {current: 'user-group'} %}
     </div>
 
     <div class="col-md-9">
       <p>
         <button  data-toggle="collapse" class="btn btn-default" href="#createGroupForm">新規グループの作成</button>
       </p>
-      <form role="form" action="/admin/group/create" method="post">
+      <form role="form" action="/admin/user-group/create" method="post">
         <div id="createGroupForm" class="collapse">
           <div class="form-group">
             <label for="createGroupForm[userGroupName]">グループ名</label>
@@ -46,8 +46,8 @@
         <input type="hidden" name="_csrf" value="{{ csrf() }}">
       </form>
 
-      {% set createdGroup = req.flash('createdGroup') %}
-      {% if createdGroup.length %}
+      {% set createdUserGroup = req.flash('createdUserGroup') %}
+      {% if createdUserGroup.length %}
       <div class="modal fade in" id="createdGroupModal">
         <div class="modal-dialog">
           <div class="modal-content">
@@ -59,10 +59,10 @@
 
             <div class="modal-body">
               <p>
-                作成したにユーザを追加してください
+                作成したグループにユーザを追加してください
               </p>
 
-              <pre>{% for cGroup in createdGroup %}{{ cGroup.name }}<br>{% endfor %}</pre>
+              <pre>{{ createdUserGroup.name }}</pre>
             </div>
 
           </div><!-- /.modal-content -->
@@ -70,6 +70,35 @@
       </div><!-- /.modal -->
       {% endif %}
 
+      <div class="modal fade" id="admin-delete-user-group-modal">
+        <div class="modal-dialog">
+          <div class="modal-content">
+            <div class="modal-header">
+              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+              <h4 class="modal-title"><code id="admin-delete-user-group-name"></code>グループを削除しますか?</h4>
+            </div>
+
+            <div class="modal-body">
+              <p>
+                グループの削除を行うと元に戻すことはできませんのでご注意ください。
+              </p>
+
+              <form method="post" id="admin-user-groups-delete">
+                <input type="hidden" name="user_group_id" value="">
+                <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                <button type="submit" value="" class="btn btn-danger">
+                  実行
+                </button>
+              </form>
+
+            </div>
+
+          </div>
+          <!-- /.modal-content -->
+        </div>
+        <!-- /.modal-dialog -->
+      </div>
+
       <h2>グループ一覧</h2>
 
       <table class="table table-hover table-striped table-bordered table-user-list">
@@ -77,6 +106,7 @@
           <tr>
             <th width="100px">#</th>
             <th>名前</th>
+            <th>ユーザ一覧</th>
             <th width="100px">作成日</th>
             <th width="90px">操作</th>
           </tr>
@@ -88,18 +118,36 @@
               <img src="{{ sGroup|picture }}" class="picture picture-rounded" />
             </td>
             <td>{{ sGroup.name }}</td>
+            <td><ul class="list-inline">
+              {% for relation in userGroupRelations.get(sGroup) %}
+              <li class="list-inline-item badge badge-primary">{{relation.relatedUser.username}}</li>
+              {% endfor %}
+            </ul></td>
             <td>{{ sGroup.createdAt|date('Y-m-d', sGroup.createdAt.getTimezoneOffset()) }}</td>
             <td>
               <div class="btn-group admin-group-menu">
                 <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
-                  編集
+                  操作
                   <span class="caret"></span>
                 </button>
                 <ul class="dropdown-menu" role="menu">
-                  <li class="dropdown-header">編集メニュー</li>
-                  <li>
-                    <a href="">編集</a>
+                  <li class="dropdown-header">メニュー</li>
+                  <li class="divider"></li>
+
+                  <li class="dropdown-button">
+                    <a href="/admin/user-group-detail/{{sGroup.name}}" class="btn btn-block btn-default">詳細</a>
                   </li>
+
+                  <li class="dropdown-button">
+                    <a href="#"
+                      data-user-group-id="{{ sGroup._id.toString() }}"
+                      data-user-group-name="{{ sGroup.name.toString() }}"
+                      data-target="#admin-delete-user-group-modal"
+                      data-toggle="modal" class="btn btn-block btn-danger">
+                      グループの削除
+                    </a>
+                  </li>
+
                 </ul>
               </div>
             </td>
@@ -108,7 +156,7 @@
         </tbody>
       </table>
 
-      {% include '../widget/pager.html' with {path: "/admin/groups", pager: pager} %}
+      {% include '../widget/pager.html' with {path: "/admin/user-groups", pager: pager} %}
 
     </div>
   </div>

+ 1 - 1
lib/views/admin/widget/menu.html

@@ -9,7 +9,7 @@
   <li class="{% if current == 'customize'%}active{% endif %}"><a href="/admin/customize"><i class="fa fa-object-group"></i> {{ t('Customize') }}</a></li>
   <li class="{% if current == 'notification'%}active{% endif %}"><a href="/admin/notification"><i class="fa fa-bell"></i> {{ t('Notification settings') }}</a></li>
   <li class="{% if current == 'user' || current == 'external-account' %}active{% endif %}"><a href="/admin/users"><i class="fa fa-users"></i> {{ t('User management') }}</a></li>
-  <li class="{% if current == 'user_group'%}active{% endif %}"><a href="/admin/user-groups"><i class="fa fa-users"></i> グループ管理</a></li>
+  <li class="{% if current == 'user-group'%}active{% endif %}"><a href="/admin/user-groups"><i class="fa fa-users"></i> {{ t('UserGroup management') }}</a></li>
   {% if searchConfigured() %}
   <li class="{% if current == 'search'%}active{% endif %}"><a href="/admin/search"><i class="fa fa-search"></i> 検索管理</a></li>
   {% endif %}

+ 7 - 0
resource/css/_admin.scss

@@ -10,6 +10,13 @@
     }
   }
 
+  .admin-group-menu {
+    .dropdown-menu {
+      left: auto;
+      right: 0;
+    }
+  }
+
   .admin-customize {
     .ss-container img {
       padding: .5em;

+ 53 - 0
resource/js/legacy/crowi-admin.js

@@ -52,4 +52,57 @@ $(function() {
 
     return false;
   });
+
+  $('#admin-delete-user-group-modal').on('show.bs.modal', function (button) {
+    var data = $(button.relatedTarget);
+    var userGroupId = data.data('user-group-id');
+    var userGroupName = data.data('user-group-name');
+
+    $('#admin-delete-user-group-name').text(userGroupName);
+    $('#admin-user-groups-delete input[name=user_group_id]').val(userGroupId);
+  });
+
+  $('form#admin-user-groups-delete').on('submit', function (e) {
+    $.post('/_api/admin/user-group.remove', $(this).serialize(), function (res) {});
+  });
+
+  $('form#user-group-relation-create').on('submit', function (e) {
+    $.post('/admin/user-group-relation/create', $(this).serialize(), function (res) {
+      $('#admin-add-user-group-relation-modal').modal('hide');
+      return;
+     });
+  });
+
+
+  $("#pictureUploadForm input[name=userGroupPicture]").on('change', function () {
+    var $form = $('#pictureUploadForm');
+    var fd = new FormData($form[0]);
+    if ($(this).val() == '') {
+      return false;
+    }
+
+    $('#pictureUploadFormProgress').html('<img src="/images/loading_s.gif"> アップロード中...');
+    $.ajax($form.attr("action"), {
+      type: 'post',
+      processData: false,
+      contentType: false,
+      data: fd,
+      dataType: 'json',
+      success: function (data) {
+        if (data.status) {
+          $('#settingUserPicture').attr('src', data.url + '?time=' + (new Date()));
+          $('#pictureUploadFormMessage')
+            .addClass('alert alert-success')
+            .html('変更しました');
+        } else {
+          $('#pictureUploadFormMessage')
+            .addClass('alert alert-danger')
+            .html('変更中にエラーが発生しました。');
+        }
+        $('#pictureUploadFormProgress').html('');
+      }
+    });
+    return false;
+  });
+
 });

+ 2 - 0
test/models/page.test.js

@@ -9,6 +9,8 @@ chai.use(sinonChai);
 describe('Page', () => {
   var Page = utils.models.Page,
     User   = utils.models.User,
+    PageGroupRelation = utils.models.PageGroupRelation,
+    UserGroupRelation = utils.models.UserGroupRelation,
     conn   = utils.mongoose.connection,
     createdPages,
     createdUsers;