Hi,

PFA attached patch (V5) for user management functionality.

Note: If you've applied any of the previous patch of this functionality
then set ConfigDB value to 10 in version table of and also delete role
'Standard' from role table before applying this patch.


On Fri, Jun 3, 2016 at 4:39 PM, Dave Page <dp...@pgadmin.org> wrote:

>
>
> On Fri, Jun 3, 2016 at 11:37 AM, Ashesh Vashi <
> ashesh.va...@enterprisedb.com> wrote:
>
>> Hi Harshal,
>>
>> Dave asked to put the User Management menu under the 'Change Password'
>> (right top side).
>>
> Done


>
> Correct - that way it won't be displayed in desktop mode. Other comments:
>
> - If I type an email address, then hit tab to select a Role, I immediately
> get an error saying that the role cannot be empty and the control loses
> focus. I should be able to add a user using just the keyboard.
>

Fixed the issue to not to show error immediately. Also role cell does not
lose focus after tab hit, it is select2 cell which does not respond to
focus-in event of tab. This is general issue for all select2 cells.


>
> - In general I'm seeing validation errors before I've had a chance to
> enter values. I should only see them if the appropriate field has lost
> focus.
>

Fixed.


>
> - Role names should be "Administrator" and "User".
>
Fixed (set ConfidDB to 10 before applying this patch if any of previous
patch was applied.)


>
> - The Close button should be disabled if errors are present.
>

I'm not convinced that to deny superuser from closing dialog for his
mistakes (accidental mistakes).

Consider a case when superuser clears email for any old user inadvertently
(obviously this won't reflect on server). At this point there is no proper
way that he can roll back or close the dialog without saving it if we
disable close button. He has to either enter correct email for that user or
refresh the browser.

Another case while adding new user if he plans not to add user then he has
to clear that partially filled user from grid before he can close the
dialog.

Let me know if we still want to implement this.


>
> - If I enter all the details for a new user and then hit Close, the dialog
> is closed and the new user is NOT added. I have to click something else
> first so the row loses focus, and then click close.
>

I was not able to reproduce this issue. I tried with both close buttons
(top-right and bottom-right). Users were created in both the cases by
adding all details and directly closing dialog without clicking anywhere on
the dialog.


>
> - The styling of the Add button and header does not match other grids
> (blue background for the header, title in white on the left, grey "ADD"
> button with no icon. The search box should be pushed right, to the left of
> the button as well I think.
>

Fixed.


>
> - s/Search by email/Filter by email
>
> - s/New Password/New password
>
> - s/Confirm Password/Confirm password
>

Fixed.

>
> - The font on the Close button doesn't match what's on the properties
> dialogues (looks like a global issue)
>
>
Fixed.


>
>
- The minimum size of the dialogue should be set such that the dialogue is
> usable at mimimum size, e.g. all columns shown, with 2 rows.
>
> Fixed.



> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
diff --git a/TODO.txt b/TODO.txt
index 93abba0..1df1305 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -45,3 +45,11 @@ Graphical Explain
 * Explanation on the statistic for the graphical explain plan.
 * Arrow colouring based on the percentage of the cost, and time taken on each
   explain node.
+
+User management
+---------------------------------------
+1. Pagination should be there if there are large number of users.
+2. There should be a way of notifying the user once its password is changed by any administrator.
+3. We can add 'created by' column.
+4. User creation and last modified time.
+5. If current user changes its own name/email, main page should reflect new changes.
\ No newline at end of file
diff --git a/web/config.py b/web/config.py
index ebb85aa..8545add 100644
--- a/web/config.py
+++ b/web/config.py
@@ -150,7 +150,7 @@ MAX_SESSION_IDLE_TIME = 60
 
 # The schema version number for the configuration database
 # DO NOT CHANGE UNLESS YOU ARE A PGADMIN DEVELOPER!!
-SETTINGS_SCHEMA_VERSION = 10
+SETTINGS_SCHEMA_VERSION = 11
 
 # The default path to the SQLite database used to store user accounts and
 # settings. This default places the file in the same directory as this
diff --git a/web/pgadmin/browser/templates/browser/index.html b/web/pgadmin/browser/templates/browser/index.html
index 89fe2a8..ed85c78 100644
--- a/web/pgadmin/browser/templates/browser/index.html
+++ b/web/pgadmin/browser/templates/browser/index.html
@@ -68,6 +68,8 @@ try {
           <ul class="dropdown-menu navbar-inverse">
             <li><a href="{{ url_for('security.change_password') }}">{{ _('Change Password') }}</a></li>
             <li class="divider"></li>
+            <li><a onclick="pgAdmin.Browser.UserManagement.show_users()">{{ _('Users') }}</a></li>
+            <li class="divider"></li>
             <li><a href="{{ url_for('security.logout') }}">{{ _('Logout') }}</a></li>
           </ul>
         </li>
diff --git a/web/pgadmin/static/css/overrides.css b/web/pgadmin/static/css/overrides.css
index 6ebb3d7..3cb7dc8 100755
--- a/web/pgadmin/static/css/overrides.css
+++ b/web/pgadmin/static/css/overrides.css
@@ -1103,7 +1103,7 @@ span.button-label {
 }
 button.pg-alertify-button {
   font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
-  font-size: initial;
+  font-size: 15px;
 }
 .fa.pg-alertify-button:before {
   font: normal normal normal 18px/1 FontAwesome;
@@ -1233,6 +1233,7 @@ form[name="change_password_form"] .help-block {
   }
 }
 
+
 /* Override Backgrid's default z-index */
 .dashboard-tab-container .backgrid-filter .search {
   z-index: 10 !important;
@@ -1264,3 +1265,64 @@ form[name="change_password_form"] .help-block {
   -webkit-appearance: none;
      -moz-appearance: none;
 }
+
+.subnode-footer {
+  text-align: right;
+  border-color: #a9a9a9;
+  border-style: inset inset inset solid;
+  border-width: 2px 1px 0;
+  margin-top: -10px;
+}
+
+.subnode-footer .ajs-button {
+  margin: 2px 2px 0;
+}
+
+.user_management {
+  margin: 0 10px !important;
+  width: calc(100% - 20px);
+  height: 100%;
+  overflow: hidden;
+}
+
+.user_management .search_users form {
+  margin: 0;
+}
+
+.user_management table {
+  display: block;
+  height: 100%;
+  overflow: auto;
+  border: 0 none;
+}
+
+.user_management .backform-tab {
+  height: calc(100% - 75px);
+}
+
+.user_management .search_users {
+  float:right;
+  margin-right: 5px;
+  padding:0 !important;
+}
+
+.user_management .search_users input{
+  height:15px;
+  margin-top: 3px;
+}
+
+.user_management .user_container {
+height: calc(100% - 35px);
+}
+
+.user_management input[placeholder] {
+  font-size: 12px;
+}
+
+.user_management-pg-alertify-button {
+  font-size: 14px !important;
+}
+
+.alertify_tools_dialog_backgrid_properties {
+  top: 43px !important;
+}
\ No newline at end of file
diff --git a/web/pgadmin/tools/user_management/__init__.py b/web/pgadmin/tools/user_management/__init__.py
new file mode 100644
index 0000000..532a610
--- /dev/null
+++ b/web/pgadmin/tools/user_management/__init__.py
@@ -0,0 +1,327 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2016, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""Implements pgAdmin4 User Management Utility"""
+
+import json
+import re
+
+from flask import render_template, request, \
+    url_for, Response, abort
+from flask.ext.babel import gettext as _
+from flask.ext.security import login_required, roles_required, current_user
+
+from pgadmin.utils.ajax import make_response as ajax_response,\
+    make_json_response, bad_request, internal_server_error
+from pgadmin.utils import PgAdminModule
+from pgadmin.model import db, Role, User, UserPreference, Server,\
+    ServerGroup, Process, Setting
+from flask.ext.security.utils import encrypt_password
+
+# set template path for sql scripts
+MODULE_NAME = 'user_management'
+server_info = {}
+
+
+class UserManagementModule(PgAdminModule):
+    """
+    class UserManagementModule(Object):
+
+        It is a utility which inherits PgAdminModule
+        class and define methods to load its own
+        javascript file.
+    """
+
+    LABEL = _('Users')
+
+    def get_own_javascripts(self):
+        """"
+        Returns:
+            list: js files used by this module
+        """
+        return [{
+            'name': 'pgadmin.tools.user_management',
+            'path': url_for('user_management.index') + 'user_management',
+            'when': None
+        }]
+
+    def show_system_objects(self):
+        """
+        return system preference objects
+        """
+        return self.pref_show_system_objects
+
+
+# Create blueprint for BackupModule class
+blueprint = UserManagementModule(
+    MODULE_NAME, __name__, static_url_path=''
+)
+
+
+def validate_user(data):
+    new_data = dict()
+    email_filter = '^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$'
+    if ('newPassword' in data and data['newPassword'] != "" and
+            'confirmPassword' in data and data['confirmPassword'] != ""):
+
+        if data['newPassword'] == data['confirmPassword']:
+            new_data['password'] = encrypt_password(data['newPassword'])
+        else:
+            raise Exception(_("Passwords do not match."))
+
+    if 'email' in data and data['email'] != "":
+        if re.match(email_filter, data['email']):
+            new_data['email'] = data['email']
+        else:
+            raise Exception(_("Invalid Email id."))
+
+    if 'role' in data and data['role'] != "":
+        new_data['roles'] = int(data['role'])
+
+    if 'active' in data and data['active'] != "":
+        new_data['active'] = data['active']
+
+    return new_data
+
+
+@blueprint.route("/")
+@login_required
+def index():
+    return bad_request(errormsg=_("This URL can not be called directly!"))
+
+
+@blueprint.route("/user_management.js")
+@login_required
+def script():
+    """render own javascript"""
+    return Response(
+        response=render_template(
+            "user_management/js/user_management.js", _=_,
+            is_admin=current_user.has_role("Administrator"),
+            user_id=current_user.id
+
+        ),
+        status=200,
+        mimetype="application/javascript"
+    )
+
+
+@blueprint.route('/user/', methods=['GET'], defaults={'uid': None})
+@blueprint.route('/user/<int:uid>', methods=['GET'])
+@roles_required('Administrator')
+def user(uid):
+    """
+
+    Args:
+      uid: User id
+
+    Returns: List of pgAdmin4 users or single user if uid is provided.
+
+    """
+
+    if uid:
+        u = User.query.get(uid)
+
+        res = {'id': u.id,
+               'email': u.email,
+               'active': u.active,
+               'role': u.roles[0].id
+               }
+    else:
+        users = User.query.all()
+
+        users_data = []
+        for u in users:
+            users_data.append({'id': u.id,
+                               'email': u.email,
+                               'active': u.active,
+                               'role': u.roles[0].id
+                               })
+
+        res = users_data
+
+    return ajax_response(
+                response=res,
+                status=200
+    )
+
+
+@blueprint.route('/user/', methods=['POST'])
+@roles_required('Administrator')
+def create():
+    """
+
+    Returns:
+
+    """
+    data = request.form if request.form else json.loads(request.data.decode())
+
+    for f in ('email', 'role', 'active', 'newPassword', 'confirmPassword'):
+        if f in data and data[f] != '':
+            continue
+        else:
+            return bad_request(errormsg=_("Missing field: '{0}'".format(f)))
+
+    try:
+        new_data = validate_user(data)
+
+        if 'roles' in new_data:
+            new_data['roles'] = [Role.query.get(new_data['roles'])]
+
+    except Exception as e:
+        return bad_request(errormsg=_(str(e)))
+
+    try:
+        usr = User(email=new_data['email'],
+                   roles=new_data['roles'],
+                   active=new_data['active'],
+                   password=new_data['password'])
+        db.session.add(usr)
+        db.session.commit()
+        # Add default server group for new user.
+        server_group = ServerGroup(user_id=usr.id, name="Servers")
+        db.session.add(server_group)
+        db.session.commit()
+    except Exception as e:
+        return internal_server_error(errormsg=str(e))
+
+    res = {'id': usr.id,
+           'email': usr.email,
+           'active': usr.active,
+           'role': usr.roles[0].id
+           }
+
+    return ajax_response(
+                response=res,
+                status=200
+    )
+
+
+@blueprint.route('/user/<int:uid>', methods=['DELETE'])
+@roles_required('Administrator')
+def delete(uid):
+    """
+
+    Args:
+      uid:
+
+    Returns:
+
+    """
+    usr = User.query.get(uid)
+
+    if not usr:
+        abort(404)
+
+    try:
+
+        Setting.query.filter_by(user_id=uid).delete()
+
+        UserPreference.query.filter_by(uid=uid).delete()
+
+        Server.query.filter_by(user_id=uid).delete()
+
+        ServerGroup.query.filter_by(user_id=uid).delete()
+
+        Process.query.filter_by(user_id=uid).delete()
+
+        # Finally delete user
+        db.session.delete(usr)
+
+        db.session.commit()
+
+        return make_json_response(
+                        success=1,
+                        info=_("User Deleted."),
+                        data={}
+                        )
+    except Exception as e:
+        return internal_server_error(errormsg=str(e))
+
+
+@blueprint.route('/user/<int:uid>', methods=['PUT'])
+@roles_required('Administrator')
+def update(uid):
+    """
+
+    Args:
+      uid:
+
+    Returns:
+
+    """
+
+    usr = User.query.get(uid)
+
+    if not usr:
+        abort(404)
+
+    data = request.form if request.form else json.loads(request.data.decode())
+
+    try:
+        new_data = validate_user(data)
+
+        if 'roles' in new_data:
+            new_data['roles'] = [Role.query.get(new_data['roles'])]
+
+    except Exception as e:
+        return bad_request(errormsg=_(str(e)))
+
+    try:
+        for k, v in new_data.items():
+            setattr(usr, k, v)
+
+        db.session.commit()
+
+        res = {'id': usr.id,
+               'email': usr.email,
+               'active': usr.active,
+               'role': usr.roles[0].id
+               }
+
+        return ajax_response(
+                    response=res,
+                    status=200
+        )
+
+    except Exception as e:
+        return internal_server_error(errormsg=str(e))
+
+
+@blueprint.route('/role/', methods=['GET'], defaults={'rid': None})
+@blueprint.route('/role/<int:rid>', methods=['GET'])
+@roles_required('Administrator')
+def role(rid):
+    """
+
+    Args:
+      rid: Role id
+
+    Returns: List of pgAdmin4 users roles or single role if rid is provided.
+
+    """
+
+    if rid:
+        r = Role.query.get(rid)
+
+        res = {'id': r.id, 'name': r.name}
+    else:
+        roles = Role.query.all()
+
+        roles_data = []
+        for r in roles:
+            roles_data.append({'id': r.id,
+                               'name': r.name})
+
+        res = roles_data
+
+    return ajax_response(
+                response=res,
+                status=200
+    )
diff --git a/web/pgadmin/tools/user_management/templates/user_management/js/user_management.js b/web/pgadmin/tools/user_management/templates/user_management/js/user_management.js
new file mode 100644
index 0000000..b52a29f
--- /dev/null
+++ b/web/pgadmin/tools/user_management/templates/user_management/js/user_management.js
@@ -0,0 +1,606 @@
+define([
+      'jquery', 'underscore', 'underscore.string', 'alertify',
+      'pgadmin.browser', 'backbone', 'backgrid', 'backform', 'pgadmin.browser.node',
+      'backgrid.select.all', 'backgrid.filter'
+      ],
+
+  // This defines Backup dialog
+  function($, _, S, alertify, pgBrowser, Backbone, Backgrid, Backform, pgNode) {
+
+    // if module is already initialized, refer to that.
+    if (pgBrowser.UserManagement) {
+      return pgBrowser.UserManagement;
+    }
+
+    /**
+      Create new Filter which will filter the
+      rendered grid for Select Type Tabular Data
+      @param {Backbone.PageableCollection} coll
+    */
+    var userFilter = function(collection) {
+      return (new Backgrid.Extension.ClientSideFilter({
+        collection: collection,
+        placeholder: _('Filter by email'),
+
+        // The model fields to search for matches
+        fields: ['email'],
+
+        // How long to wait after typing has stopped before searching can start
+        wait: 150
+      }));
+    }
+
+    var BASEURL = '{{ url_for('user_management.index')}}',
+        USERURL = BASEURL + 'user/',
+        ROLEURL = BASEURL + 'role/';
+
+    pgBrowser.UserManagement  = {
+      init: function() {
+        if (this.initialized)
+          return;
+
+        this.initialized = true;
+
+        return this;
+      }
+      {% if is_admin %},
+
+      // Callback to draw User Management Dialog.
+      show_users: function(action, item, params) {
+        var Roles = [];
+
+        var UserModel = pgAdmin.Browser.Node.Model.extend({
+            idAttribute: 'id',
+            urlRoot: USERURL,
+            defaults: {
+              id: undefined,
+              email: undefined,
+              active: true,
+              role: undefined,
+              newPassword: undefined,
+              confirmPassword: undefined
+            },
+            schema: [
+            {
+              id: 'email', label: '{{ _('Email') }}', editable: true,
+              type: 'text', cell:'string', cellHeaderClasses:'width_percent_30'
+            },{
+              id: 'role', label: '{{ _('Role') }}',
+              type: 'text', control: "Select2", cellHeaderClasses:'width_percent_20',
+              cell: 'select2', select2: {allowClear: false, openOnEnter: false},
+              options: function (controlOrCell) {
+                var options = [];
+
+                if( controlOrCell instanceof Backform.Control){
+                  // This is be backform select2 control
+                  _.each(Roles, function(role) {
+                    options.push({
+                      label: role.name,
+                      value: role.id.toString()}
+                    );
+                  });
+                } else {
+                  // This must be backgrid select2 cell
+                  _.each(Roles, function(role) {
+                    options.push([role.name, role.id.toString()]);
+                  });
+                }
+
+                return options;
+              },
+              editable: function(m) {
+                if(m instanceof Backbone.Collection) {
+                  return true;
+                }
+                if (m.get("id") == {{user_id}}){
+                    return false;
+                } else {
+                    return true;
+                }
+              }
+            },{
+              id: 'active', label: '{{ _('Active') }}',
+              type: 'switch', cell: 'switch', cellHeaderClasses:'width_percent_10',
+              options: { 'onText': 'Yes', 'offText': 'No'},
+              editable: function(m) {
+                if(m instanceof Backbone.Collection) {
+                  return true;
+                }
+                if (m.get("id") == {{user_id}}){
+                    return false;
+                } else {
+                    return true;
+                }
+              }
+            },{
+              id: 'newPassword', label: '{{ _('New password') }}',
+              type: 'password', disabled: false, control: 'input',
+              cell: 'password', cellHeaderClasses:'width_percent_20'
+            },{
+              id: 'confirmPassword', label: '{{ _('Confirm password') }}',
+              type: 'password', disabled: false, control: 'input',
+              cell: 'password', cellHeaderClasses:'width_percent_20'
+            }],
+            validate: function() {
+              var err = {},
+                  errmsg = null,
+                  changedAttrs = this.changed || {},
+                  email_filter = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
+
+              if (('email' in changedAttrs || !this.isNew()) && (_.isUndefined(this.get('email')) ||
+                    _.isNull(this.get('email')) ||
+                    String(this.get('email')).replace(/^\s+|\s+$/g, '') == '')) {
+                errmsg =  '{{ _('Email id cannot be empty.')}}';
+                this.errorModel.set('email', errmsg);
+                return errmsg;
+              } else if (!!this.get('email') && !email_filter.test(this.get('email'))) {
+
+                errmsg =  S("{{ _("Invalid Email id: %%s")}}").sprintf(
+                            this.get('email')
+                          ).value();
+                this.errorModel.set('email', errmsg);
+                return errmsg;
+              } else if (!!this.get('email') && this.collection.where({"email":this.get('email')}).length > 1) {
+
+                errmsg =  S("{{ _("This email id %%s already exist.")}}").sprintf(
+                            this.get('email')
+                          ).value();
+
+                this.errorModel.set('email', errmsg);
+                return errmsg;
+              } else {
+                this.errorModel.unset('email');
+              }
+
+              if ('role' in changedAttrs && (_.isUndefined(this.get('role')) ||
+                    _.isNull(this.get('role')) ||
+                    String(this.get('role')).replace(/^\s+|\s+$/g, '') == '')) {
+
+                errmsg =  S("{{ _("Role cannot be empty for user %%s")}}").sprintf(
+                            (this.get('email') || '')
+                          ).value();
+
+                this.errorModel.set('role', errmsg);
+                return errmsg;
+              } else {
+                this.errorModel.unset('role');
+              }
+
+              if(this.isNew()){
+                // Password is compulsory for new user.
+                if ('newPassword' in changedAttrs && (_.isUndefined(this.get('newPassword')) ||
+                      _.isNull(this.get('newPassword')) ||
+                      this.get('newPassword') == '')) {
+
+                  errmsg =  S("{{ _("Password cannot be empty for user %%s")}}").sprintf(
+                            (this.get('email') || '')
+                          ).value();
+
+                  this.errorModel.set('newPassword', errmsg);
+                  return errmsg;
+                } else if ('newPassword' in changedAttrs && !_.isUndefined(this.get('newPassword')) &&
+                  !_.isNull(this.get('newPassword')) &&
+                  this.get('newPassword').length < 6) {
+
+                  errmsg =  S("{{ _("Password must be at least 6 characters for user %%s")}}").sprintf(
+                            (this.get('email') || '')
+                          ).value();
+
+                  this.errorModel.set('newPassword', errmsg);
+                  return errmsg;
+                } else {
+                  this.errorModel.unset('newPassword');
+                }
+
+                if ('confirmPassword' in changedAttrs && (_.isUndefined(this.get('confirmPassword')) ||
+                      _.isNull(this.get('confirmPassword')) ||
+                      this.get('confirmPassword') == '')) {
+
+                  errmsg =  S("{{ _("Confirm Password cannot be empty for user %%s")}}").sprintf(
+                            (this.get('email') || '')
+                          ).value();
+
+                  this.errorModel.set('confirmPassword', errmsg);
+                  return errmsg;
+                } else {
+                  this.errorModel.unset('confirmPassword');
+                }
+
+                if(!!this.get('newPassword') && !!this.get('confirmPassword') &&
+                    this.get('newPassword') != this.get('confirmPassword')) {
+
+                  errmsg =  S("{{ _("Passwords do not match for user %%s")}}").sprintf(
+                            (this.get('email') || '')
+                          ).value();
+
+                  this.errorModel.set('confirmPassword', errmsg);
+                  return errmsg;
+                } else {
+                  this.errorModel.unset('confirmPassword');
+                }
+
+              } else {
+                if ((_.isUndefined(this.get('newPassword')) || _.isNull(this.get('newPassword')) ||
+                      this.get('newPassword') == '') &&
+                      ((_.isUndefined(this.get('confirmPassword')) || _.isNull(this.get('confirmPassword')) ||
+                      this.get('confirmPassword') == ''))) {
+
+                   this.errorModel.unset('newPassword');
+                   if(this.get('newPassword') == ''){
+                    this.set({'newPassword': undefined})
+                   }
+
+                   this.errorModel.unset('confirmPassword');
+                   if(this.get('confirmPassword') == ''){
+                    this.set({'confirmPassword': undefined})
+                   }
+                } else if (!_.isUndefined(this.get('newPassword')) &&
+                    !_.isNull(this.get('newPassword')) &&
+                    !this.get('newPassword') == '' &&
+                    this.get('newPassword').length < 6) {
+
+                  errmsg =  S("{{ _("Password must be at least 6 characters for user %%s")}}").sprintf(
+                            (this.get('email') || '')
+                          ).value();
+
+                  this.errorModel.set('newPassword', errmsg);
+                  return errmsg;
+                } else if ('confirmPassword' in changedAttrs && (_.isUndefined(this.get('confirmPassword')) ||
+                      _.isNull(this.get('confirmPassword')) ||
+                      this.get('confirmPassword') == '')) {
+
+                  errmsg =  S("{{ _("Confirm Password cannot be empty for user %%s")}}").sprintf(
+                            (this.get('email') || '')
+                          ).value();
+
+                  this.errorModel.set('confirmPassword', errmsg);
+                  return errmsg;
+                } else if (!!this.get('newPassword') && !!this.get('confirmPassword') &&
+                          this.get('newPassword') != this.get('confirmPassword')) {
+
+                  errmsg =  S("{{ _("Passwords do not match for user %%s")}}").sprintf(
+                            (this.get('email') || '')
+                          ).value();
+
+                  this.errorModel.set('confirmPassword', errmsg);
+                  return errmsg;
+                } else {
+                  this.errorModel.unset('newPassword');
+                  this.errorModel.unset('confirmPassword');
+                }
+              }
+              return null;
+            }
+          }),
+          gridSchema = Backform.generateGridColumnsFromModel(
+              null, UserModel, 'edit'),
+          deleteUserCell = Backgrid.Extension.DeleteCell.extend({
+            deleteRow: function(e) {
+              self = this;
+              e.preventDefault();
+
+              if (self.model.get("id") == {{user_id}}) {
+                alertify.alert(
+                  '{{_('Cannot Delete User.') }}',
+                  '{{_('Cannot delete currently logged in user.') }}',
+                  function(){
+                    return true;
+                  }
+                );
+                return true;
+              }
+
+              // We will check if row is deletable or not
+              var canDeleteRow = (!_.isUndefined(this.column.get('canDeleteRow')) &&
+                                  _.isFunction(this.column.get('canDeleteRow')) ) ?
+                                   Backgrid.callByNeed(this.column.get('canDeleteRow'),
+                                    this.column, this.model) : true;
+              if (canDeleteRow) {
+                if(self.model.isNew()){
+                  self.model.destroy();
+                } else {
+                  alertify.confirm(
+                    'Delete User?',
+                    'Are you sure you wish to delete this User?',
+                    function(evt) {
+                      self.model.destroy({
+                        wait: true,
+                        success: function(res) {
+                          alertify.success('{{_('User deleted.') }}');
+                        },
+                        error: function(m, jqxhr) {
+                          alertify.error('{{_('Error during deleting user.') }}');
+                        }
+                      });
+                    },
+                    function(evt) {
+                      return true;
+                    }
+                  );
+                }
+              } else {
+                alertify.alert("This user cannot be deleted.",
+                  function(){
+                    return true;
+                  }
+                );
+              }
+            }
+          });
+
+          gridSchema.columns.unshift({
+            name: "pg-backform-delete", label: "",
+            cell: deleteUserCell,
+            editable: false, cell_priority: -1,
+            canDeleteRow: true
+          });
+
+        // Users Management dialog code here
+        if(!alertify.UserManagement) {
+          alertify.dialog('UserManagement' ,function factory() {
+            return {
+               main: function(title) {
+                this.set('title', title);
+               },
+               setup:function() {
+                return {
+                  buttons: [{
+                    text: '{{ _('Close') }}', key: 27, className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button'
+                  }],
+                  // Set options for dialog
+                  options: {
+                    title: '{{ _('User Management') }}',
+                    //disable both padding and overflow control.
+                    padding : !1,
+                    overflow: !1,
+                    model: 0,
+                    resizable: true,
+                    maximizable: true,
+                    pinnable: false,
+                    closableByDimmer: false
+                  }
+                };
+              },
+              hooks: {
+                // Triggered when the dialog is closed
+                onclose: function() {
+                  if (this.view) {
+                    // clear our backform model/view
+                    this.view.remove({data: true, internal: true, silent: true});
+                    this.$content.remove();
+                  }
+                }
+              },
+              prepare: function() {
+                var self = this,
+                  footerTpl = _.template([
+                    '<div class="pg-prop-footer">',
+                      '<div class="pg-prop-status-bar" style="visibility:hidden">',
+                      '</div>',
+                    '</div>'].join("\n")),
+                  $footer = $(footerTpl()),
+                  $statusBar = $footer.find('.pg-prop-status-bar'),
+                  UserRow = Backgrid.Row.extend({
+                    userInvalidColor: "lightYellow",
+
+                    userValidColor: "#fff",
+
+                    initialize: function(){
+                      Backgrid.Row.prototype.initialize.apply(this, arguments);
+                      this.listenTo(this.model, 'pgadmin:user:invalid', this.userInvalid);
+                      this.listenTo(this.model, 'pgadmin:user:valid', this.userValid);
+                    },
+                    userInvalid: function() {
+                      $(this.el).removeClass("new");
+                      this.el.style.backgroundColor = this.userInvalidColor;
+                    },
+                    userValid: function() {
+                      this.el.style.backgroundColor = this.userValidColor;
+                    }
+                  }),
+                  UserCollection = Backbone.Collection.extend({
+                    model: UserModel,
+                    url: USERURL,
+                    initialize: function() {
+                      Backbone.Collection.prototype.initialize.apply(this, arguments);
+                      var self = this;
+                      self.changedUser = null;
+                      self.invalidUsers = {};
+
+                      self.on('add', self.onModelAdd);
+                      self.on('remove', self.onModelRemove);
+                      self.on('pgadmin-session:model:invalid', function(msg, m, c) {
+                        self.invalidUsers[m.cid] = msg;
+                        m.trigger('pgadmin:user:invalid', m);
+                        $statusBar.html(msg).css("visibility", "visible");
+                      });
+                      self.on('pgadmin-session:model:valid', function(m, c) {
+                        delete self.invalidUsers[m.cid];
+                        m.trigger('pgadmin:user:valid', m);
+                        this.updateErrorMsg();
+                        this.saveUser(m);
+                      });
+                    },
+                    onModelAdd: function(m) {
+                      // Start tracking changes.
+                      m.startNewSession();
+                    },
+                    onModelRemove: function(m) {
+                      delete this.invalidUsers[m.cid];
+                      this.updateErrorMsg();
+                    },
+                    updateErrorMsg: function() {
+                      var self = this,
+                        msg = null;
+
+                      for (var key in self.invalidUsers) {
+                        msg = self.invalidUsers [key];
+                        if (msg) {
+                          break;
+                        }
+                      }
+
+                      if(msg){
+                        $statusBar.html(msg).css("visibility", "visible");
+                      } else {
+                        $statusBar.empty().css("visibility", "hidden");
+                      }
+                    },
+                    saveUser: function(m) {
+                      d = m.toJSON(true);
+                      if(m.isNew() && (!m.get('email') || !m.get('role') ||
+                          !m.get('newPassword') || !m.get('confirmPassword') ||
+                          m.get('newPassword') != m.get('confirmPassword'))
+                      ) {
+                      // New user model is valid but partially filled so return without saving.
+                        return false;
+                      } else if (!m.isNew() && m.get('newPassword') != m.get('confirmPassword')) {
+                      // For old user password change is in progress and user model is valid but admin has not added
+                      // both the passwords so return without saving.
+                        return false;
+                      }
+
+                      if (m.sessChanged() && d && !_.isEmpty(d)) {
+                        m.stopSession();
+                        m.save({}, {
+                          attrs: d,
+                          wait: true,
+                          success: function(res) {
+                            // User created/updated on server now start new session for this user.
+                            m.set({'newPassword':undefined,
+                                   'confirmPassword':undefined});
+
+                            m.startNewSession();
+                            alertify.success(S("{{_("User '%%s' saved.")|safe }}").sprintf(
+                              m.get('email')
+                            ).value());
+                          },
+                          error: function(res, jqxhr) {
+                            m.startNewSession();
+                            alertify.error(
+                              S("{{_("Error during saving user: '%%s'")|safe }}").sprintf(
+                                jqxhr.responseJSON.errormsg
+                              ).value()
+                            );
+                          }
+                        });
+                      }
+                    }
+                  }),
+                  userCollection = new UserCollection(),
+                  header = [
+                    '<div class="subnode-header">',
+                    '  <button class="btn-sm btn-default add" title="<%-add_title%>" <%=canAdd ? "" : "disabled=\'disabled\'"%> ><%=add_label ? add_label : "" %></button>',
+                    '  <div class="control-label search_users"></div>',
+                    '</div>',].join("\n"),
+                  headerTpl = _.template(header),
+                  data = {
+                    canAdd: true,
+                    add_title: '{{ _("Add new user")}}',
+                    add_label:'{{ _('ADD')}}'
+                  },
+                  $gridBody = $("<div></div>", {
+                    class: "user_container"
+                  });
+
+                $.ajax({
+                  url: ROLEURL,
+                  method: 'GET',
+                  async: false,
+                  success: function(res) {
+                    Roles = res
+                  },
+                  error: function(e) {
+                    setTimeout(function() {
+                      alertify.alert(
+                        '{{ _('Cannot load pgadmin4 user roles.') }}'
+                      );
+                    },100);
+                  }
+                });
+
+                var view = this.view = new Backgrid.Grid({
+                  row: UserRow,
+                  columns: gridSchema.columns,
+                  collection: userCollection,
+                  className: "backgrid table-bordered"
+                });
+
+                $gridBody.append(view.render().$el[0]);
+
+                this.$content = $("<div class='user_management object subnode'></div>").append(
+                    headerTpl(data)).append($gridBody
+                    ).append($footer);
+
+                $(this.elements.body.childNodes[0]).addClass(
+                  'alertify_tools_dialog_backgrid_properties');
+
+                this.elements.content.appendChild(this.$content[0]);
+
+                // Render Search Filter
+                $('.search_users').append(
+                  userFilter(userCollection).render().el);
+
+                userCollection.fetch();
+
+                this.$content.find('button.add').first().click(function(e) {
+                  e.preventDefault();
+                  var canAddRow = true;
+
+                  if (canAddRow) {
+                      // Close any existing expanded row before adding new one.
+                      _.each(view.body.rows, function(row){
+                        var editCell = row.$el.find(".subnode-edit-in-process").parent();
+                        // Only close row if it's open.
+                        if (editCell.length > 0){
+                          var event = new Event('click');
+                          editCell[0].dispatchEvent(event);
+                        }
+                      });
+
+                      // There should be only one empty row.
+
+                      var isEmpty = false,
+                        unsavedModel = null;
+
+                      userCollection.each(function(model) {
+                        if(!isEmpty) {
+                          isEmpty = model.isNew();
+                          unsavedModel = model;
+                        }
+                      });
+                      if(isEmpty) {
+                        var idx = userCollection.indexOf(unsavedModel),
+                          row = view.body.rows[idx].$el;
+
+                        row.addClass("new");
+                        $(row).pgMakeVisible('backform-tab');
+                        return false;
+                      }
+
+                      $(view.body.$el.find($("tr.new"))).removeClass("new")
+                      var m = new (UserModel) (null, {
+                        handler: userCollection,
+                        top: userCollection,
+                        collection: userCollection
+                      });
+                      userCollection.add(m);
+
+                      var idx = userCollection.indexOf(m),
+                          newRow = view.body.rows[idx].$el;
+
+                      newRow.addClass("new");
+                      $(newRow).pgMakeVisible('backform-tab');
+                      return false;
+                  }
+                });
+              }
+          };
+       });
+      }
+        alertify.UserManagement(true).resizeTo('680px','240px');
+     }{% endif %}
+
+    };
+    return pgBrowser.UserManagement;
+  });
diff --git a/web/setup.py b/web/setup.py
index 185d02a..4ea28e1 100644
--- a/web/setup.py
+++ b/web/setup.py
@@ -69,12 +69,17 @@ account:\n""")
 
         db.create_all()
         user_datastore.create_role(
-                name='Administrators',
-                description='pgAdmin Administrators Role'
+                name='Administrator',
+                description='pgAdmin Administrator Role'
                 )
+        user_datastore.create_role(
+                name='User',
+                description='pgAdmin User Role'
+                )
+
         user_datastore.create_user(email=email, password=password)
         db.session.flush()
-        user_datastore.add_role_to_user(email, 'Administrators')
+        user_datastore.add_role_to_user(email, 'Administrator')
 
         # Get the user's ID and create the default server group
         user = User.query.filter_by(email=email).first()
@@ -249,6 +254,19 @@ CREATE TABLE process(
     FOREIGN KEY(user_id) REFERENCES user (id)
     )""")
 
+        if int(version.value) < 11:
+            db.engine.execute("""
+UPDATE role
+    SET name = 'Administrator',
+    description = 'pgAdmin Administrator Role'
+    WHERE name = 'Administrators'
+    """)
+
+            db.engine.execute("""
+INSERT INTO role ( name, description )
+            VALUES ('User', 'pgAdmin User Role')
+    """)
+
     # Finally, update the schema version
     version.value = config.SETTINGS_SCHEMA_VERSION
     db.session.merge(version)
-- 
Sent via pgadmin-hackers mailing list (pgadmin-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgadmin-hackers

Reply via email to