Hi,

PFA updated patch (V6) for user management functionality.

Changes: As per Ashesh's suggestion I have disabled email update of
existing user.


-- 
*Harshal Dhumal*
*Software Engineer*

EnterpriseDB India: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

On Mon, Jun 6, 2016 at 2:16 PM, Dave Page <dp...@pgadmin.org> wrote:

> Hi
>
> On Fri, Jun 3, 2016 at 10:52 PM, Harshal Dhumal
> <harshal.dhu...@enterprisedb.com> wrote:
> > 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.
>
> Done - also restarted my app server, and done a hard refresh of the
> browser...
>
> And I get "(index):310 Uncaught TypeError: Cannot read property
> 'show_users' of undefined" when I try to open the Users menu option.
>

This was an issue. Ideally Users menu shouldn't be visible to non admin
users. I have fixed in this patch.


>
> >> - 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.
>
> Well we either need that, or a message box asking the user if he wants
> to discard his changes and offering OK/Cancel options.
>

I have added confirmation before closing dialog if any unsaved changes are
present.



>
> >> - 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.
>
> Hmm, I'll re-test when I get an updated patch.
>
Ok


>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
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/__init__.py b/web/pgadmin/browser/__init__.py
index 0ef846f..e82b673 100644
--- a/web/pgadmin/browser/__init__.py
+++ b/web/pgadmin/browser/__init__.py
@@ -475,6 +475,7 @@ def index():
     return render_template(
             MODULE_NAME + "/index.html",
             username=current_user.email,
+            is_admin=current_user.has_role("Administrator"),
             _=gettext
             )
 
diff --git a/web/pgadmin/browser/templates/browser/index.html b/web/pgadmin/browser/templates/browser/index.html
index 89fe2a8..a70cb5b 100644
--- a/web/pgadmin/browser/templates/browser/index.html
+++ b/web/pgadmin/browser/templates/browser/index.html
@@ -68,6 +68,10 @@ try {
           <ul class="dropdown-menu navbar-inverse">
             <li><a href="{{ url_for('security.change_password') }}">{{ _('Change Password') }}</a></li>
             <li class="divider"></li>
+            {% if is_admin %}
+            <li><a onclick="pgAdmin.Browser.UserManagement.show_users()">{{ _('Users') }}</a></li>
+            <li class="divider"></li>
+            {% endif %}
             <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..24eead3
--- /dev/null
+++ b/web/pgadmin/tools/user_management/templates/user_management/js/user_management.js
@@ -0,0 +1,651 @@
+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;
+    }
+
+    var BASEURL = '{{ url_for('user_management.index')}}',
+        USERURL = BASEURL + 'user/',
+        ROLEURL = BASEURL + 'role/',
+        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
+      }));
+    },
+        StringDepCell = Backgrid.StringCell.extend({
+      initialize: function() {
+        Backgrid.StringCell.prototype.initialize.apply(this, arguments);
+        Backgrid.Extension.DependentCell.prototype.initialize.apply(this, arguments);
+      },
+      dependentChanged: function () {
+        this.$el.empty();
+
+        var self = this,
+            model = this.model,
+            column = this.column,
+            editable = this.column.get("editable");
+
+        this.render();
+
+        is_editable = _.isFunction(editable) ? !!editable.apply(column, [model]) : !!editable;
+        setTimeout(function() {
+          self.$el.removeClass("editor");
+          if (is_editable){ self.$el.addClass("editable"); }
+          else { self.$el.removeClass("editable"); }
+        }, 10);
+
+        this.delegateEvents();
+        return this;
+      },
+      remove: Backgrid.Extension.DependentCell.prototype.remove
+    });
+
+    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') }}', type: 'text',
+              cell:StringDepCell, cellHeaderClasses:'width_percent_30',
+              deps: ['id'],
+              editable: function(m) {
+                if(m instanceof Backbone.Collection) {
+                  return false;
+                }
+                // Disable email edit for existing user.
+                if (m.isNew()){
+                    return true;
+                }
+                return false;
+              }
+            },{
+              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 (_.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',
+                    attrs:{name:'close'}
+                  }],
+                  // 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,
+                    closable: 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 = this.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) {
+                      // 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;
+                  }
+                });
+              },
+              callback: function(e) {
+                if (e.button.element.name == "close") {
+                  var self = this;
+                  if (!_.all(this.userCollection.pluck('id')) || !_.isEmpty(this.userCollection.invalidUsers)) {
+                    e.cancel = true;
+                    alertify.confirm(
+                      '{{ _('Discard unsaved changes?') }}',
+                      '{{ _('Are you sure you want to close any unsaved changes will be lost.') }}',
+                      function(e) {
+                        self.close();
+                        return true;
+                      },
+                      function(e) {
+                        // Do nothing.
+                        return true;
+                      }
+                    );
+                  }
+                }
+              }
+          };
+       });
+      }
+        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