Hi,

PFA initial patch for User management functionality.


-- 
*Harshal Dhumal*
*Software Engineer *



EenterpriseDB <http://www.enterprisedb.com>
diff --git a/requirements_py2.txt b/requirements_py2.txt
index a442e36..f3d8622 100644
--- a/requirements_py2.txt
+++ b/requirements_py2.txt
@@ -44,3 +44,5 @@ unittest2==1.1.0
 Werkzeug==0.9.6
 WTForms==2.0.2
 sqlparse==0.1.19
+flask-marshmallow-0.6.2
+marshmallow-sqlalchemy==0.8.1
diff --git a/requirements_py3.txt b/requirements_py3.txt
index 233b14f..6a6fea4 100644
--- a/requirements_py3.txt
+++ b/requirements_py3.txt
@@ -38,3 +38,5 @@ Werkzeug==0.9.6
 wheel==0.24.0
 WTForms==2.0.2
 sqlparse==0.1.19
+flask-marshmallow-0.6.2
+marshmallow-sqlalchemy==0.8.1
\ No newline at end of file
diff --git a/web/config.py b/web/config.py
index 36f1632..712ee2b 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/__init__.py b/web/pgadmin/__init__.py
index f04897e..6d1a9b2 100644
--- a/web/pgadmin/__init__.py
+++ b/web/pgadmin/__init__.py
@@ -16,7 +16,7 @@ from flask.ext.security import Security, SQLAlchemyUserDatastore
 from flask_security.utils import login_user
 from flask_mail import Mail
 from htmlmin.minify import html_minify
-from pgadmin.model import db, Role, User, Version
+from pgadmin.model import db, Role, User, Version, ma
 from importlib import import_module
 from werkzeug.local import LocalProxy
 from pgadmin.utils import PgAdminModule, driver
@@ -189,6 +189,7 @@ def create_app(app_name=config.APP_NAME):
 
     # Create database connection object and mailer
     db.init_app(app)
+    ma.init_app(app)
     Mail(app)
 
     import pgadmin.utils.paths as paths
diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py
index b8bb256..0aefbf9 100644
--- a/web/pgadmin/model/__init__.py
+++ b/web/pgadmin/model/__init__.py
@@ -20,9 +20,11 @@ things:
 
 from flask.ext.sqlalchemy import SQLAlchemy
 from flask.ext.security import UserMixin, RoleMixin
+from flask_marshmallow import Marshmallow
+from marshmallow import fields
 
 db = SQLAlchemy()
-
+ma = Marshmallow()
 # Define models
 roles_users = db.Table(
                 'roles_users',
@@ -46,6 +48,12 @@ class Role(db.Model, RoleMixin):
     description = db.Column(db.String(256), nullable=False)
 
 
+class RoleSchema(ma.ModelSchema):
+    """Define a role schema for serialization"""
+    class Meta:
+        model = Role
+
+
 class User(db.Model, UserMixin):
     """Define a user object"""
     __tablename__ = 'user'
@@ -58,6 +66,23 @@ class User(db.Model, UserMixin):
                             backref=db.backref('users', lazy='dynamic'))
 
 
+class UserSchema(ma.ModelSchema):
+    """Define a user schema for serialization"""
+    class Meta:
+        model = User
+        exclude = ('password', 'roles')
+
+    # Convert list of user roles to role.
+    # There will be only one role associated with user.
+    role = fields.Function(
+      lambda user: str(user.roles[0].id) if len(user.roles) else None
+    )
+
+    is_admin = fields.Function(
+      lambda user: True if (len(user.roles) and user.roles[0].name == 'Administrators') else False
+    )
+
+
 class Setting(db.Model):
     """Define a setting object"""
     __tablename__ = 'setting'
diff --git a/web/pgadmin/static/css/overrides.css b/web/pgadmin/static/css/overrides.css
index 55d83e0..719a989 100755
--- a/web/pgadmin/static/css/overrides.css
+++ b/web/pgadmin/static/css/overrides.css
@@ -1121,7 +1121,7 @@ button.pg-alertify-button {
   margin: 0 0 10px;
   overflow: auto;
   padding: 5px 10px;
-  word-break: break-all;
+  word-break: keep-all;
   word-wrap: break-word;
 }
 
@@ -1129,6 +1129,11 @@ div.backform_control_notes label.control-label {
   min-width: 0px;
 }
 
+.backform_control_notes span{
+  white-space: pre-wrap;
+  word-break: keep-all !important;
+}
+
 form[name="change_password_form"] .help-block {
     color: #A94442 !important;
 }
@@ -1232,3 +1237,15 @@ form[name="change_password_form"] .help-block {
     visibility: hidden;
   }
 }
+
+.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;
+}
diff --git a/web/pgadmin/tools/user/__init__.py b/web/pgadmin/tools/user/__init__.py
new file mode 100644
index 0000000..e7cac5a
--- /dev/null
+++ b/web/pgadmin/tools/user/__init__.py
@@ -0,0 +1,307 @@
+##########################################################################
+#
+# 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, current_app, \
+    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, html
+from pgadmin.model import db, Role, User, ServerGroup, UserPreference, Server,\
+    ServerGroup, Process,Setting, UserSchema, RoleSchema
+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/js/user.js", _=_, is_admin=current_user.has_role("Administrators")
+        ),
+        status=200,
+        mimetype="application/javascript"
+    )
+
+
+@blueprint.route('/user/', methods=['GET'], defaults={'uid': None})
+@blueprint.route('/user/<int:uid>', methods=['GET'])
+@roles_required('Administrators')
+def user(uid):
+    """
+
+    Args:
+      uid: User id
+
+    Returns: List of pgAdmin4 users or single user if uid is provided.
+
+    """
+
+    user_schema = UserSchema()
+
+    if uid:
+        u = User.query.get(uid)
+
+        res = user_schema.dump(u)
+    else:
+        users = User.query.all()
+
+        res = user_schema.dump(users, many=True)
+
+    return ajax_response(
+                response=res.data,
+                status=200
+    )
+
+
+@blueprint.route('/user/', methods=['POST'])
+@roles_required('Administrators')
+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))
+
+    user_schema = UserSchema()
+
+    res = user_schema.dump(usr)
+
+    return ajax_response(
+                response=res.data,
+                status=200
+    )
+
+
+@blueprint.route('/user/<int:uid>', methods=['DELETE'])
+@roles_required('Administrators')
+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('Administrators')
+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()
+
+        user_schema = UserSchema()
+
+        res = user_schema.dump(usr)
+
+        return ajax_response(
+                    response=res.data,
+                    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('Administrators')
+def role(rid):
+    """
+
+    Args:
+      rid: Role id
+
+    Returns: List of pgAdmin4 users roles or single role if rid is provided.
+
+    """
+
+    role_schema = RoleSchema()
+
+    if rid:
+        r = Role.query.get(rid)
+
+        res = role_schema.dump(r)
+    else:
+        roles = Role.query.all()
+
+        res = role_schema.dump(roles, many=True)
+
+    return ajax_response(
+                response=res.data,
+                status=200
+    )
diff --git a/web/pgadmin/tools/user/templates/user/js/user.js b/web/pgadmin/tools/user/templates/user/js/user.js
new file mode 100644
index 0000000..960877f
--- /dev/null
+++ b/web/pgadmin/tools/user/templates/user/js/user.js
@@ -0,0 +1,588 @@
+define([
+      'jquery', 'underscore', 'underscore.string', 'alertify',
+      'pgadmin.browser', 'backbone', 'backgrid', 'backform', 'pgadmin.browser.node'
+      ],
+
+  // 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 UserCellEditor  = Backgrid.Extension.ObjectCellEditor.extend({
+      postRender: function(model, column) {
+        Backgrid.Extension.ObjectCellEditor.prototype.postRender.apply(this, arguments);
+
+        var editor = this,
+            el = this.el,
+            btnTemplate = _.template(['<div class="subnode-footer">',
+                                '<div class="ajs-primary ajs-buttons">',
+                                  '<button name="delete" class="ajs-button btn btn-danger fa fa-lg fa-trash pg-alertify-button">Delete</button>',
+                                  '<button name="save" <%=disabled ? "disabled" : ""%> class="ajs-button btn btn-primary fa fa-lg fa-floppy-o pg-alertify-button">Save</button>',
+                                '</div>',
+                                '</div>'].join("\n"));
+
+        var $btn = $(btnTemplate({disabled: true}));
+
+        editor.$saveBtn = $btn.find('button[name="save"]');
+        editor.$saveBtn.click(this.userSave.bind(editor));
+
+        editor.$deleteBtn = $btn.find('button[name="delete"]');
+        editor.$deleteBtn.click(this.userDelete.bind(editor));
+
+        // Wait till dialog is rendered.
+        setTimeout(function() {
+          editor.$dialog.append($btn);
+        }, 10);
+
+        return this;
+      },
+      userSave: function() {
+        var self = this,
+            m = self.model,
+            d = m.toJSON(true);
+        if (d && !_.isEmpty(d)) {
+          m.save({}, {
+            attrs: d,
+            success: function(res) {
+              // User created/updated on server now start new session for this user.
+              m.stopSession();
+              m.startNewSession();
+              m.trigger('pgadmin-user:valid', m);
+
+              alertify.success('{{_('User saved.') }}');
+            },
+            error: function(m, jqxhr) {
+              alertify.error('{{_('Error during saving user.') }}');
+            }
+          });
+        }
+      },
+      userDelete: function() {
+        var self = this;
+
+        if (!self.model.isNew() &&
+            self.model.get("is_admin") &&
+            self.model.get("active") &&
+            self.model.collection.where({is_admin:true, active: true}).length < 2) {
+          alertify.alert(
+            '{{_('Cannot Delete User') }}',
+            '{{_('This user cannot be deleted as this is the only active Administrator.') }}',
+            function(){
+              return true;
+            }
+          );
+          return true;
+        }
+
+        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;
+          }
+        );
+      }
+    });
+
+    var BASEURL = '{{ url_for('user_management.index')}}',
+        USERURL = BASEURL + 'user/',
+        ROLEURL = BASEURL + 'role/';
+
+    pgBrowser.UserManagement  = {
+      init: function() {
+        if (this.initialized)
+          return;
+
+        this.initialized = true;
+
+        // Define the nodes on which the menus to be appear
+        var menus = [{
+          name: 'users', module: this,
+          applies: ['file'], callback: 'show_users',
+          priority: 2, label: '{{_("Users...") }}',
+          icon: 'fa fa-users', enable: function (itemData, item, data) {
+            return {% if is_admin %} true {% else %} false {% endif %};
+          }
+        }];
+
+        pgAdmin.Browser.add_menus(menus);
+
+        return this;
+      },
+
+      // Callback to draw User Management Dialog.
+      show_users: function(action, item, params) {
+        var Roles = [];
+
+        var gridCols = ['email', 'role', 'active'],
+            UserModel = pgAdmin.Browser.Node.Model.extend({
+              idAttribute: 'id',
+              urlRoot: USERURL,
+              defaults: {
+                id: undefined,
+                email: undefined,
+                active: true,
+                role: undefined,
+                newPassword: undefined,
+                confirmPassword: undefined,
+                is_admin: undefined
+              },
+              schema: [{
+                id: 'email', label: '{{ _('Email') }}',
+                type: 'text', cell:'string', cellHeaderClasses:'width_percent_50'
+              },{
+                id: 'role', label: '{{ _('Role') }}',
+                type: 'text', control: "Select2",
+                cell: "Select2", select2: {allowClear: false},
+                editable: false,
+                cellHeaderClasses:'width_percent_30',
+                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;
+                },
+                disabled: function(m) {
+                  if(m instanceof Backbone.Collection) {
+                    return false;
+                  }
+                  if (!m.isNew() &&
+                    m.get("is_admin") &&
+                    m.get("active") &&
+                    m.collection.where({is_admin:true, active: true}).length < 2){
+                      return true;
+                  } else {
+                      return false;
+                  }
+                }
+              },{
+                id: 'active', label: '{{ _('Active') }}',
+                type: 'switch', cell: 'switch', cellHeaderClasses:'width_percent_20',
+                options: { 'onText': 'Yes', 'offText': 'No'},
+                editable: false,
+                disabled: function(m) {
+                  if(m instanceof Backbone.Collection) {
+                    return false;
+                  }
+                  if (!m.isNew() &&
+                    m.get("is_admin") &&
+                    m.get("active") &&
+                    m.collection.where({is_admin:true, active: true}).length < 2){
+                      return true;
+                  } else {
+                      return false;
+                  }
+                }
+              },{
+                id: 'role_note', label: '{{ _('Note') }}',
+                text: "{{ _("You cannot change role of this user from 'Administrators' to another and active status to deactivated as this is the only active Administrator.") }}",
+                type: 'note', visible: function(m){
+                  if (!m.isNew() &&
+                    m.get("is_admin") &&
+                    m.get("active") &&
+                    m.collection.where({is_admin:true, active: true}).length < 2){
+                      return true;
+                  } else {
+                      return false;
+                  }
+                }
+              },{
+                id: 'newPassword', label: '{{ _('New Password') }}',
+                type: 'password', disabled: false, control: 'input',
+                required: true
+              },{
+                id: 'confirmPassword', label: '{{ _('Confirm Password') }}',
+                type: 'password', disabled: false, control: 'input',
+                required: true
+              },{
+                id: 'password_note', label: '{{ _('Note') }}',
+                text: "{{ _("Leave password fields blank if you don't want to change it.") }}",
+                type: 'note', visible: function(m){
+                  return !m.isNew();
+                }
+              }],
+              validate: function() {
+                var err = {},
+                    errmsg = null,
+                    email_filter = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
+
+                if (_.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 (!email_filter.test(this.get('email'))) {
+                  errmsg =  '{{ _('Invalid Email id.')}}';
+                  this.errorModel.set('email', errmsg);
+                  return errmsg;
+                } else {
+                  this.errorModel.unset('email');
+                }
+
+                if (_.isUndefined(this.get('role')) || _.isNull(this.get('role')) ||
+                      String(this.get('role')).replace(/^\s+|\s+$/g, '') == '') {
+                  errmsg =  '{{ _('Role cannot be empty.')}}';
+                  this.errorModel.set('role', errmsg);
+                  return errmsg;
+                } else {
+                  this.errorModel.unset('role');
+                }
+
+                if(this.isNew()){
+                  // Password is compulsory for new user.
+                  if ((_.isUndefined(this.get('newPassword')) || _.isNull(this.get('newPassword')) ||
+                        String(this.get('newPassword')).replace(/^\s+|\s+$/g, '') == '')) {
+                    errmsg =  '{{ _('Password cannot be empty.')}}';
+                    this.errorModel.set('newPassword', errmsg);
+                    return errmsg;
+                  } else {
+                    this.errorModel.unset('newPassword');
+                  }
+
+                  if ((_.isUndefined(this.get('confirmPassword')) || _.isNull(this.get('confirmPassword')) ||
+                        String(this.get('confirmPassword')).replace(/^\s+|\s+$/g, '') == '')) {
+                    errmsg =  '{{ _('Confirm Password cannot be empty.')}}';
+                    this.errorModel.set('confirmPassword', errmsg);
+                    return errmsg;
+                  } else {
+                    this.errorModel.unset('confirmPassword');
+                  }
+
+                  if(this.get('newPassword') != this.get('confirmPassword')) {
+                    errmsg =  '{{ _('Passwords do not match.')}}';
+                    this.errorModel.set('confirmPassword', errmsg);
+                    return errmsg;
+                  } else {
+                    this.errorModel.unset('confirmPassword');
+                  }
+
+                } else {
+                  if ((_.isUndefined(this.get('newPassword')) || _.isNull(this.get('newPassword')) ||
+                        String(this.get('newPassword')).replace(/^\s+|\s+$/g, '') == '') &&
+                        ((_.isUndefined(this.get('confirmPassword')) || _.isNull(this.get('confirmPassword')) ||
+                        String(this.get('confirmPassword')).replace(/^\s+|\s+$/g, '') == ''))) {
+                     this.errorModel.unset('confirmPassword');
+                  } else if (this.get('newPassword') != this.get('confirmPassword')) {
+                    errmsg =  '{{ _('Passwords do not match.')}}';
+                    this.errorModel.set('confirmPassword', errmsg);
+                    return errmsg;
+                  } else {
+                    this.errorModel.unset('confirmPassword');
+                  }
+                }
+
+                return null;
+              }
+            }),
+            gridSchema = Backform.generateGridColumnsFromModel(
+                null, UserModel, 'edit', gridCols),
+            editUserCell = Backgrid.Extension.ObjectCell.extend({
+              editor: UserCellEditor,
+              schema: gridSchema.schema,
+              enterEditMode: function() {
+                var self = this;
+                Backgrid.Extension.ObjectCell.prototype.enterEditMode.apply(this, arguments);
+
+                setTimeout(function() {
+                  self.model.on('pgadmin-user:invalid', self.userInvalid.bind(self));
+                  self.model.on('pgadmin-user:valid', self.userValid.bind(self));
+                }, 50);
+              },
+              exitEditMode: function() {
+                var self = this;
+
+                self.model.off('pgadmin-user:invalid', self.userInvalid);
+                self.model.off('pgadmin-user:valid', self.userValid);
+
+                Backgrid.Extension.ObjectCell.prototype.exitEditMode.apply(this, arguments);
+              },
+              userValid: function() {
+                if(this.model.sessChanged()) {
+                  this.currentEditor.$saveBtn.prop('disabled', false);
+                  this.currentEditor.$saveBtn.removeAttr('disabled', 'disabled');
+                } else {
+                  this.userInvalid.bind(this)();
+                }
+              },
+              userInvalid: function() {
+                this.currentEditor.$saveBtn.prop('disabled', true);
+                this.currentEditor.$saveBtn.attr('disabled', 'disabled');
+              }
+            });
+            deleteUserCell = Backgrid.Extension.DeleteCell.extend({
+              deleteRow: function(e) {
+                self = this;
+                e.preventDefault();
+
+                if (!self.model.isNew() &&
+                    self.model.get("is_admin") &&
+                    self.model.get("active") &&
+                    self.model.collection.where({is_admin:true, active: true}).length < 2) {
+                  alertify.alert(
+                    '{{_('Cannot Delete User') }}',
+                    '{{_('This user cannot be deleted as this is the only active Administrator.') }}',
+                    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) {
+                  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: {% if is_admin %} true {% else %} false {% endif %}
+            });
+
+            gridSchema.columns.unshift({
+              name: "pg-backform-edit", label: "", cell : editUserCell,
+              cell_priority: -2, editable: true,
+              canEditRow: true
+            });
+
+        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,
+                    UserCollection = new (pgAdmin.Browser.Node.Collection.extend(
+                      {
+                        model: UserModel,
+                        url: USERURL
+                      }
+                    ))(),
+                    header = [
+                      '<div class="subnode-header-form">',
+                      ' <div class="container-fluid">',
+                      '  <div class="row">',
+                      '   <div class="col-md-4"></div>',
+                      '   <div class="col-md-4" ></div>',
+                      '   <div class="col-md-4">',
+                      '     <button class="btn-sm btn-default add" <%=canAdd ? "" : "disabled=\'disabled\'"%> ><%-add_label%></buttton>',
+                      '   </div>',
+                      '  </div>',
+                      ' </div>',
+                      '</div>',].join("\n"),
+                    headerTmpl = _.template(header),
+                    data = {canAdd: {% if is_admin %} true {% else %} false {% endif %},
+                            add_label:'{{ _('Add New User')}}'},
+                    $gridBody = $("<div class='pgadmin-control-group backgrid form-group col-xs-12 object subnode backform-tab'></div>").append(headerTmpl(data));
+
+                // Start tracking changes.
+                UserCollection.on('add', function(m) {
+                  m.startNewSession();
+                });
+
+                UserCollection.on('pgadmin-session:model:invalid', function (msg, m, col) {
+                  m.trigger('pgadmin-user:invalid', m);
+                });
+
+                UserCollection.on('pgadmin-session:model:valid', function (m, col) {
+                  m.trigger('pgadmin-user:valid', m);
+                });
+
+                // Listen for any row which is about to enter in edit mode.
+                UserCollection.on( "enteringEditMode", function(args){
+                    var self = this,
+                      cell = args[0];
+                    // Search for any other rows which are open.
+                    this.each(function(m){
+                      // Check if row which we are about to close is not current row.
+                      if (cell.model != m) {
+                        var idx = self.indexOf(m);
+                        if (idx > -1) {
+                          var row = view.body.rows[idx],
+                              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);
+                          }
+                        }
+                      }
+                    });
+                  },
+                  UserCollection);
+
+                var view = this.view = new Backgrid.Grid({
+                  columns: gridSchema.columns,
+                  collection: UserCollection,
+                  className: "backgrid table-bordered"
+                });
+
+                var $gridContent = $("<div class='tab-content'></div>").append(view.render().$el[0]);
+                $gridBody.append($gridContent);
+                this.$content = $("<div class='obj_properties'></div>").append($gridBody[0]);
+                this.elements.content.appendChild(this.$content[0]);
+
+                $gridBody.find('button.add').first().click(function(e) {
+                  e.preventDefault();
+                  var canAddRow = {% if is_admin %} true {% else %} false {% endif %}
+
+                  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;
+                      UserCollection.each(function(model) {
+                        var modelValues = [];
+                        _.each(model.attributes, function(val, key) {
+                          modelValues.push(val);
+                        })
+                        if(!_.some(modelValues, _.identity)) {
+                          isEmpty = true;
+                        }
+                      });
+                      if(isEmpty) {
+                        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;
+                  }
+                });
+
+                $.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);
+                  }
+                });
+
+                UserCollection.fetch();
+
+              }
+          };
+       });
+      }
+        alertify.UserManagement(true).resizeTo('60%','60%');
+     },
+
+    };
+    return pgBrowser.UserManagement;
+  });
diff --git a/web/setup.py b/web/setup.py
index 185d02a..a0e8dbe 100644
--- a/web/setup.py
+++ b/web/setup.py
@@ -249,6 +249,12 @@ CREATE TABLE process(
     FOREIGN KEY(user_id) REFERENCES user (id)
     )""")
 
+        if int(version.value) < 11:
+            db.engine.execute("""
+INSERT INTO role ( name, description )
+            VALUES ('Standard', 'pgAdmin Standard 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