You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@guacamole.apache.org by vn...@apache.org on 2018/05/04 08:26:57 UTC

[03/21] guacamole-client git commit: GUACAMOLE-220: Migrate user management controller to ManagementPermissions.

GUACAMOLE-220: Migrate user management controller to ManagementPermissions.


Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/4f43ddc4
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/4f43ddc4
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/4f43ddc4

Branch: refs/heads/master
Commit: 4f43ddc4203a59c7aea6834cfd8b5961c74ba57e
Parents: 507202d
Author: Michael Jumper <mj...@apache.org>
Authored: Tue May 1 00:03:52 2018 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Tue May 1 00:12:58 2018 -0700

----------------------------------------------------------------------
 .../manage/controllers/manageUserController.js  | 464 ++++++-------------
 .../webapp/app/manage/templates/manageUser.html |  18 +-
 2 files changed, 153 insertions(+), 329 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/4f43ddc4/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
index c385065..388e717 100644
--- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
+++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
@@ -24,14 +24,16 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
         function manageUserController($scope, $injector) {
             
     // Required types
-    var PageDefinition    = $injector.get('PageDefinition');
-    var PermissionFlagSet = $injector.get('PermissionFlagSet');
-    var PermissionSet     = $injector.get('PermissionSet');
-    var User              = $injector.get('User');
+    var ManagementPermissions = $injector.get('ManagementPermissions');
+    var PageDefinition        = $injector.get('PageDefinition');
+    var PermissionFlagSet     = $injector.get('PermissionFlagSet');
+    var PermissionSet         = $injector.get('PermissionSet');
+    var User                  = $injector.get('User');
 
     // Required services
     var $location                = $injector.get('$location');
     var $routeParams             = $injector.get('$routeParams');
+    var $q                       = $injector.get('$q');
     var authenticationService    = $injector.get('authenticationService');
     var dataSourceService        = $injector.get('dataSourceService');
     var guacNotification         = $injector.get('guacNotification');
@@ -128,13 +130,31 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
     $scope.permissionFlags = null;
 
     /**
-     * A map of data source identifiers to the set of all permissions
-     * associated with the current user under that data source, or null if the
-     * user's permissions have not yet been loaded.
+     * The set of permissions that will be added to the user when the user is
+     * saved. Permissions will only be present in this set if they are
+     * manually added, and not later manually removed before saving.
      *
-     * @type Object.<String, PermissionSet>
+     * @type PermissionSet
      */
-    $scope.permissions = null;
+    $scope.permissionsAdded = new PermissionSet();
+
+    /**
+     * The set of permissions that will be removed from the user when the user
+     * is saved. Permissions will only be present in this set if they are
+     * manually removed, and not later manually added before saving.
+     *
+     * @type PermissionSet
+     */
+    $scope.permissionsRemoved = new PermissionSet();
+
+    /**
+     * The managment-related actions that the current user may perform on the
+     * user currently being created/modified, or null if the current user's
+     * permissions have not yet been loaded.
+     *
+     * @type ManagementPermissions
+     */
+    $scope.managementPermissions = null;
 
     /**
      * All available user attributes. This is only the set of attribute
@@ -162,11 +182,10 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
      */
     $scope.isLoaded = function isLoaded() {
 
-        return $scope.users               !== null
-            && $scope.permissionFlags     !== null
-            && $scope.rootGroups          !== null
-            && $scope.permissions         !== null
-            && $scope.attributes          !== null;
+        return $scope.users                 !== null
+            && $scope.permissionFlags       !== null
+            && $scope.managementPermissions !== null
+            && $scope.attributes            !== null;
 
     };
 
@@ -196,97 +215,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
     };
 
     /**
-     * Returns whether the current user can change attributes explicitly
-     * associated with the user being edited within the given data source.
-     *
-     * @param {String} [dataSource]
-     *     The identifier of the data source to check. If omitted, this will
-     *     default to the currently-selected data source.
-     *
-     * @returns {Boolean}
-     *     true if the current user can change attributes associated with the
-     *     user being edited, false otherwise.
-     */
-    $scope.canChangeAttributes = function canChangeAttributes(dataSource) {
-
-        // Do not check if permissions are not yet loaded
-        if (!$scope.permissions)
-            return false;
-
-        // Use currently-selected data source if unspecified
-        dataSource = dataSource || $scope.dataSource;
-
-        // Attributes can always be set if we are creating the user
-        if (!$scope.userExists(dataSource))
-            return true;
-
-        // The administrator can always change attributes
-        if (PermissionSet.hasSystemPermission($scope.permissions[dataSource],
-                PermissionSet.SystemPermissionType.ADMINISTER))
-            return true;
-
-        // Otherwise, can change attributes if we have permission to update this user
-        return PermissionSet.hasUserPermission($scope.permissions[dataSource],
-            PermissionSet.ObjectPermissionType.UPDATE, username);
-
-    };
-
-    /**
-     * Returns whether the current user can change/set all user attributes for
-     * the user being edited, regardless of whether those attributes are
-     * already explicitly associated with that user.
-     *
-     * @returns {Boolean}
-     *     true if the current user can change all attributes for the user
-     *     being edited, regardless of whether those attributes are already
-     *     explicitly associated with that user, false otherwise.
-     */
-    $scope.canChangeAllAttributes = function canChangeAllAttributes() {
-
-        // All attributes can be set if we are creating the user
-        return !$scope.userExists($scope.dataSource);
-
-    };
-
-    /**
-     * Returns whether the current user can change permissions of any kind
-     * which are associated with the user being edited within the given data
-     * source.
-     *
-     * @param {String} [dataSource]
-     *     The identifier of the data source to check. If omitted, this will
-     *     default to the currently-selected data source.
-     *
-     * @returns {Boolean}
-     *     true if the current user can grant or revoke permissions of any kind
-     *     which are associated with the user being edited, false otherwise.
-     */
-    $scope.canChangePermissions = function canChangePermissions(dataSource) {
-
-        // Do not check if permissions are not yet loaded
-        if (!$scope.permissions)
-            return false;
-
-        // Use currently-selected data source if unspecified
-        dataSource = dataSource || $scope.dataSource;
-
-        // Permissions can always be set if we are creating the user
-        if (!$scope.userExists(dataSource))
-            return true;
-
-        // The administrator can always modify permissions
-        if (PermissionSet.hasSystemPermission($scope.permissions[dataSource],
-                PermissionSet.SystemPermissionType.ADMINISTER))
-            return true;
-
-        // Otherwise, can only modify permissions if we have explicit
-        // ADMINISTER permission
-        return PermissionSet.hasUserPermission($scope.permissions[dataSource],
-            PermissionSet.ObjectPermissionType.ADMINISTER, username);
-
-    };
-
-    /**
      * Returns whether the current user can edit the username of the user being
      * edited within the given data source.
      *
@@ -303,151 +231,158 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
     };
 
     /**
-     * Returns whether the current user can save the user being edited within
-     * the given data source. Saving will create or update that user depending
-     * on whether the user already exists.
+     * Loads the data associated with the user having the given username,
+     * preparing the interface for making modifications to that existing user.
      *
-     * @param {String} [dataSource]
-     *     The identifier of the data source to check. If omitted, this will
-     *     default to the currently-selected data source.
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user to
+     *     load.
      *
-     * @returns {Boolean}
-     *     true if the current user can save changes to the user being edited,
-     *     false otherwise.
-     */
-    $scope.canSaveUser = function canSaveUser(dataSource) {
-
-        // Do not check if permissions are not yet loaded
-        if (!$scope.permissions)
-            return false;
-
-        // Use currently-selected data source if unspecified
-        dataSource = dataSource || $scope.dataSource;
+     * @param {String} username
+     *     The username of the user to load.
+     *
+     * @returns {Promise}
+     *     A promise which is resolved when the interface has been prepared for
+     *     editing the given user.
+     */
+    var loadExistingUser = function loadExistingUser(dataSource, username) {
+        return $q.all({
+            users : dataSourceService.apply(userService.getUser, dataSources, username),
+            permissions : permissionService.getPermissions(dataSource, username)
+        })
+        .then(function userDataRetrieved(values) {
 
-        // The administrator can always save users
-        if (PermissionSet.hasSystemPermission($scope.permissions[dataSource],
-                PermissionSet.SystemPermissionType.ADMINISTER))
-            return true;
+            $scope.users = values.users;
+            $scope.user  = values.users[dataSource];
 
-        // If user does not exist, can only save if we have permission to create users
-        if (!$scope.userExists(dataSource))
-           return PermissionSet.hasSystemPermission($scope.permissions[dataSource],
-               PermissionSet.SystemPermissionType.CREATE_USER);
+            // Create skeleton user if user does not exist
+            if (!$scope.user)
+                $scope.user = new User({
+                    'username' : username
+                });
 
-        // Otherwise, can only save if we have permission to update this user
-        return PermissionSet.hasUserPermission($scope.permissions[dataSource],
-            PermissionSet.ObjectPermissionType.UPDATE, username);
+            // The current user will be associated with username of the existing
+            // user in the retrieved permission set
+            $scope.selfUsername = username;
+            $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(values.permissions);
 
+        });
     };
 
     /**
-     * Returns whether the current user can clone the user being edited within
-     * the given data source.
+     * Loads the data associated with the user having the given username,
+     * preparing the interface for cloning that existing user.
      *
-     * @param {String} [dataSource]
-     *     The identifier of the data source to check. If omitted, this will
-     *     default to the currently-selected data source.
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user to
+     *     be cloned.
      *
-     * @returns {Boolean}
-     *     true if the current user can clone the user being edited, false
-     *     otherwise.
-     */
-    $scope.canCloneUser = function canCloneUser(dataSource) {
-
-        // Do not check if permissions are not yet loaded
-        if (!$scope.permissions)
-            return false;
-
-        // Use currently-selected data source if unspecified
-        dataSource = dataSource || $scope.dataSource;
-
-        // If we are not editing an existing user, we cannot clone
-        if (!$scope.userExists($scope.dataSource))
-            return false;
+     * @param {String} username
+     *     The username of the user being cloned.
+     *
+     * @returns {Promise}
+     *     A promise which is resolved when the interface has been prepared for
+     *     cloning the given user.
+     */
+    var loadClonedUser = function loadClonedUser(dataSource, username) {
+        return $q.all({
+            users : dataSourceService.apply(userService.getUser, [dataSource], username),
+            permissions : permissionService.getPermissions(dataSource, username)
+        })
+        .then(function userDataRetrieved(values) {
 
-        // The administrator can always clone users
-        if (PermissionSet.hasSystemPermission($scope.permissions[dataSource],
-                PermissionSet.SystemPermissionType.ADMINISTER))
-            return true;
+            $scope.users = {};
+            $scope.user  = values.users[dataSource];
 
-        // Otherwise we need explicit CREATE_USER permission
-        return PermissionSet.hasSystemPermission($scope.permissions[dataSource],
-            PermissionSet.SystemPermissionType.CREATE_USER);
+            // The current user will be associated with cloneSourceUsername in the
+            // retrieved permission set
+            $scope.selfUsername = username;
+            $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(values.permissions);
+            $scope.permissionsAdded = values.permissions;
 
+        });
     };
 
     /**
-     * Returns whether the current user can delete the user being edited from
-     * the given data source.
-     *
-     * @param {String} [dataSource]
-     *     The identifier of the data source to check. If omitted, this will
-     *     default to the currently-selected data source.
+     * Loads skeleton user data, preparing the interface for creating a new
+     * user.
      *
-     * @returns {Boolean}
-     *     true if the current user can delete the user being edited, false
-     *     otherwise.
+     * @returns {Promise}
+     *     A promise which is resolved when the interface has been prepared for
+     *     creating a new user.
      */
-    $scope.canDeleteUser = function canDeleteUser(dataSource) {
-
-        // Do not check if permissions are not yet loaded
-        if (!$scope.permissions)
-            return false;
+    var loadSkeletonUser = function loadSkeletonUser() {
 
-        // Use currently-selected data source if unspecified
-        dataSource = dataSource || $scope.dataSource;
+        // No users exist regardless of data source if there is no username
+        $scope.users = {};
 
-        // Can't delete what doesn't exist
-        if (!$scope.userExists(dataSource))
-            return false;
+        // Use skeleton user object with no associated permissions
+        $scope.user = new User();
+        $scope.permissionFlags = new PermissionFlagSet();
 
-        // The administrator can always delete users
-        if (PermissionSet.hasSystemPermission($scope.permissions[dataSource],
-                PermissionSet.SystemPermissionType.ADMINISTER))
-            return true;
+        // As no permissions are yet associated with the user, it is safe to
+        // use any non-empty username as a placeholder for self-referential
+        // permissions
+        $scope.selfUsername = 'SELF';
 
-        // Otherwise, require explicit DELETE permission on the user
-        return PermissionSet.hasUserPermission($scope.permissions[dataSource],
-            PermissionSet.ObjectPermissionType.DELETE, username);
+        return $q.resolve();
 
     };
 
     /**
-     * Returns whether the user being edited within the given data source is
-     * read-only, and thus cannot be modified by the current user.
-     *
-     * @param {String} [dataSource]
-     *     The identifier of the data source to check. If omitted, this will
-     *     default to the currently-selected data source.
+     * Loads the data requred for performing the management task requested
+     * through the route parameters given at load time, automatically preparing
+     * the interface for editing an existing user, cloning an existing user, or
+     * creating an entirely new user.
      *
-     * @returns {Boolean}
-     *     true if the user being edited is actually read-only and cannot be
-     *     edited at all, false otherwise.
+     * @returns {Promise}
+     *     A promise which is resolved when the interface has been prepared
+     *     for performing the requested management task.
      */
-    $scope.isReadOnly = function isReadOnly(dataSource) {
+    var loadRequestedUser = function loadRequestedUser() {
 
-        // Use currently-selected data source if unspecified
-        dataSource = dataSource || $scope.dataSource;
+        // Pull user data and permissions if we are editing an existing user
+        if (username)
+            return loadExistingUser($scope.dataSource, username);
+
+        // If we are cloning an existing user, pull his/her data instead
+        if (cloneSourceUsername)
+            return loadClonedUser($scope.dataSource, cloneSourceUsername);
 
-        // User is read-only if they cannot be saved
-        return !$scope.canSaveUser(dataSource);
+        return loadSkeletonUser();
 
     };
 
-    // Update visible account pages whenever available users/permissions changes
-    $scope.$watchGroup(['users', 'permissions'], function updateAccountPages() {
+    // Populate interface with requested data
+    $q.all({
+        userData    : loadRequestedUser(),
+        permissions : dataSourceService.apply(permissionService.getEffectivePermissions, dataSources, currentUsername),
+        attributes  : schemaService.getUserAttributes($scope.dataSource)
+    })
+    .then(function dataReceived(values) {
+
+        var managementPermissions = {};
+
+        $scope.attributes = values.attributes;
 
         // Generate pages for each applicable data source
         $scope.accountPages = [];
         angular.forEach(dataSources, function addAccountPage(dataSource) {
 
             // Determine whether data source contains this user
-            var linked   = $scope.userExists(dataSource);
-            var readOnly = $scope.isReadOnly(dataSource);
+            var exists = (dataSource in $scope.users);
+
+            // Calculate management actions available for this specific account
+            managementPermissions[dataSource] = ManagementPermissions.fromPermissionSet(
+                    values.permissions[dataSource],
+                    PermissionSet.SystemPermissionType.CREATE_USER,
+                    PermissionSet.hasUserPermission,
+                    exists ? username : null);
 
             // Account is not relevant if it does not exist and cannot be
             // created
-            if (!linked && readOnly)
+            var readOnly = !managementPermissions[dataSource].canSaveObject;
+            if (!exists && readOnly)
                 return;
 
             // Only the selected data source is relevant when cloning
@@ -457,7 +392,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
             // Determine class name based on read-only / linked status
             var className;
             if (readOnly)    className = 'read-only';
-            else if (linked) className = 'linked';
+            else if (exists) className = 'linked';
             else             className = 'unlinked';
 
             // Add page entry
@@ -469,121 +404,10 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
 
         });
 
-    });
+        $scope.managementPermissions = managementPermissions[$scope.dataSource];
 
-    // Pull user attribute schema
-    schemaService.getUserAttributes($scope.dataSource).then(function attributesReceived(attributes) {
-        $scope.attributes = attributes;
     }, requestService.WARN);
 
-    // Pull user data and permissions if we are editing an existing user
-    if (username) {
-
-        // Pull user data
-        dataSourceService.apply(userService.getUser, dataSources, username)
-        .then(function usersReceived(users) {
-
-            // Get user for currently-selected data source
-            $scope.users = users;
-            $scope.user  = users[$scope.dataSource];
-
-            // Create skeleton user if user does not exist
-            if (!$scope.user)
-                $scope.user = new User({
-                    'username' : username
-                });
-
-        }, requestService.WARN);
-
-        // The current user will be associated with username of the existing
-        // user in the retrieved permission set
-        $scope.selfUsername = username;
-
-        // Pull user permissions
-        permissionService.getPermissions($scope.dataSource, username).then(function gotPermissions(permissions) {
-            $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions);
-        })
-
-        // If permissions cannot be retrieved, use empty permissions
-        ['catch'](requestService.createErrorCallback(function permissionRetrievalFailed() {
-            $scope.permissionFlags = new PermissionFlagSet();
-        }));
-    }
-
-    // If we are cloning an existing user, pull his/her data instead
-    else if (cloneSourceUsername) {
-
-        dataSourceService.apply(userService.getUser, dataSources, cloneSourceUsername)
-        .then(function usersReceived(users) {
-
-            // Get user for currently-selected data source
-            $scope.users = {};
-            $scope.user  = users[$scope.dataSource];
-
-        }, requestService.WARN);
-
-        // The current user will be associated with cloneSourceUsername in the
-        // retrieved permission set
-        $scope.selfUsername = cloneSourceUsername;
-
-        // Pull user permissions
-        permissionService.getPermissions($scope.dataSource, cloneSourceUsername)
-        .then(function gotPermissions(permissions) {
-            $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions);
-            $scope.permissionsAdded = permissions;
-        })
-
-        // If permissions cannot be retrieved, use empty permissions
-        ['catch'](requestService.createErrorCallback(function permissionRetrievalFailed() {
-            $scope.permissionFlags = new PermissionFlagSet();
-        }));
-    }
-
-    // Use skeleton data if we are creating a new user
-    else {
-
-        // No users exist regardless of data source if there is no username
-        $scope.users = {};
-
-        // Use skeleton user object with no associated permissions
-        $scope.user = new User();
-        $scope.permissionFlags = new PermissionFlagSet();
-
-        // As no permissions are yet associated with the user, it is safe to
-        // use any non-empty username as a placeholder for self-referential
-        // permissions
-        $scope.selfUsername = 'SELF';
-
-    }
-
-    // Query the user's permissions for the current user
-    dataSourceService.apply(
-        permissionService.getEffectivePermissions,
-        dataSources,
-        currentUsername
-    )
-    .then(function permissionsReceived(permissions) {
-        $scope.permissions = permissions;
-    }, requestService.WARN);
-
-    /**
-     * The set of permissions that will be added to the user when the user is
-     * saved. Permissions will only be present in this set if they are
-     * manually added, and not later manually removed before saving.
-     *
-     * @type PermissionSet
-     */
-    $scope.permissionsAdded = new PermissionSet();
-
-    /**
-     * The set of permissions that will be removed from the user when the user 
-     * is saved. Permissions will only be present in this set if they are
-     * manually removed, and not later manually added before saving.
-     *
-     * @type PermissionSet
-     */
-    $scope.permissionsRemoved = new PermissionSet();
-
     /**
      * Cancels all pending edits, returning to the management page.
      */

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/4f43ddc4/guacamole/src/main/webapp/app/manage/templates/manageUser.html
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html
index 5fed148..24db74e 100644
--- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html
+++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html
@@ -11,12 +11,12 @@
     </div>
 
     <!-- Warn if user is read-only -->
-    <div class="section" ng-show="isReadOnly()">
+    <div class="section" ng-hide="managementPermissions.canSaveObject">
         <p class="notice read-only">{{'MANAGE_USER.INFO_READ_ONLY' | translate}}</p>
     </div>
 
     <!-- Sections applicable to non-read-only users -->
-    <div ng-show="!isReadOnly()">
+    <div ng-show="managementPermissions.canSaveObject">
 
         <!-- User password section -->
         <div class="section">
@@ -40,13 +40,13 @@
         </div>
 
         <!-- User attributes section -->
-        <div class="attributes" ng-show="canChangeAttributes()">
+        <div class="attributes" ng-show="managementPermissions.canChangeAttributes">
             <guac-form namespace="'USER_ATTRIBUTES'" content="attributes"
-                       model="user.attributes" model-only="!canChangeAllAttributes()"></guac-form>
+                       model="user.attributes" model-only="!managementPermissions.canChangeAllAttributes"></guac-form>
         </div>
 
         <!-- System permissions section -->
-        <system-permission-editor ng-show="canChangePermissions()"
+        <system-permission-editor ng-show="managementPermissions.canChangePermissions"
               username="selfUsername"
               data-data-source="dataSource"
               permission-flags="permissionFlags"
@@ -55,7 +55,7 @@
         </system-permission-editor>
 
         <!-- Connection permissions section -->
-        <connection-permission-editor ng-show="canChangePermissions()"
+        <connection-permission-editor ng-show="managementPermissions.canChangePermissions"
               data-data-source="dataSource"
               permission-flags="permissionFlags"
               permissions-added="permissionsAdded"
@@ -64,10 +64,10 @@
 
         <!-- Form action buttons -->
         <div class="action-buttons">
-            <button ng-show="canSaveUser()" ng-click="saveUser()">{{'MANAGE_USER.ACTION_SAVE' | translate}}</button>
-            <button ng-show="canCloneUser()" ng-click="cloneUser()">{{'MANAGE_USER.ACTION_CLONE' | translate}}</button>
+            <button ng-show="managementPermissions.canSaveObject" ng-click="saveUser()">{{'MANAGE_USER.ACTION_SAVE' | translate}}</button>
+            <button ng-show="managementPermissions.canCloneObject" ng-click="cloneUser()">{{'MANAGE_USER.ACTION_CLONE' | translate}}</button>
             <button ng-click="cancel()">{{'MANAGE_USER.ACTION_CANCEL' | translate}}</button>
-            <button ng-show="canDeleteUser()" ng-click="deleteUser()" class="danger">{{'MANAGE_USER.ACTION_DELETE' | translate}}</button>
+            <button ng-show="managementPermissions.canDeleteObject" ng-click="deleteUser()" class="danger">{{'MANAGE_USER.ACTION_DELETE' | translate}}</button>
         </div>
 
     </div>