Hi,

PFA updated patch for user management.

-- 
*Harshal Dhumal*
*Software Engineer *



EenterpriseDB <http://www.enterprisedb.com>

On Mon, May 30, 2016 at 5:51 PM, Surinder Kumar <
surinder.ku...@enterprisedb.com> wrote:

> Hi Harshal,
>
>
> Please find the review comments so far:
>
> *Issues:*
> 1. The UI design doesn't look interactive, especially add new user button.
>
Moved to TODO


> 2. In requirements.txt, while adding new dependent library the name of
> library and version should be separate by '==' instead of '--'
>
Fixed


> 3. Under File menu, "Users" menu item should not be visible for non-admin
> users. For now it is only disabled.
>
Moved to TODO


> 4. Change role of admin user to non-admin user, then navigate to File >
> Users. It doesn't show users in Dialog and Add user doesn't work. Instead
> it is better to reload the page. Please find attached screenshot.
>

Moved to TODO (We can write code to reload the page but we have
"onbeforeunload" listener on window which is preventing page reload unless
user clicks on "Leave page" button).


> 5. The data grid content should fill the unnecessary gap above the footer.
>
Moved to TODO (This is generic issue as I'm using common dialog)

6. The title of delete user confirmation dialog should be "Delete user ?",
> instead of "Delete User".
>
Fixed

7. The Edit and Delete column shouldn't be sortable.
>
Moved to TODO (This issue exist throughout the application)


> 8. Columns 'creation time' & 'last modified' are missing.
>

Moved to TODO. (We are not recording these user properties at this stage.
The purpose of User management functionality is to only update existing
user properties, add user and delete user.)


> 9. Password should have validation of minimum number of characters.
>
Fixed


>
> *Can be added into TODO list:*
> 1. There is no way to search a user by name or email id.
> 2. Pagination should be there if there are large number of users.
> 3. There should be a way of notifying the user once its password is
> changed by any administrator.
> 4. We can add 'created by' column.
>
All of above 4 Moved to TODO.

5. One administrator shouldn't have privilege to modify the details of
> other administrator.
> This is the role of super admin.
>
As per my offline discussion with Ashesh we don't need this.



>
>
> Thanks
> Surinder Kumar
>
> On Fri, May 27, 2016 at 7:02 PM, Harshal Dhumal <
> harshal.dhu...@enterprisedb.com> wrote:
>
>> Hi,
>>
>> PFA initial patch for User management functionality.
>>
>>
>> --
>> *Harshal Dhumal*
>> *Software Engineer *
>>
>>
>>
>> EenterpriseDB <http://www.enterprisedb.com>
>>
>>
>> --
>> Sent via pgadmin-hackers mailing list (pgadmin-hackers@postgresql.org)
>> To make changes to your subscription:
>> http://www.postgresql.org/mailpref/pgadmin-hackers
>>
>>
>
diff --git a/TODO.txt b/TODO.txt
index 93abba0..05f84b3 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -45,3 +45,16 @@ 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. Dialog UI design improvement.
+2. Under File menu, "Users" menu item should not be visible for non-admin users. For now it is only disabled.
+3. Search a user by name or email id.
+4. Pagination should be there if there are large number of users.
+5. There should be a way of notifying the user once its password is changed by any administrator.
+6. We can add 'created by' column.
+7. User creation and last modified time.
+8. If current user changes its own name/email, main page should reflect new changes.
+9. In-place editing in backgrid.
\ No newline at end of file
diff --git a/requirements_py2.txt b/requirements_py2.txt
index a442e36..cff40b5 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..70ab746 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..56233f0
--- /dev/null
+++ b/web/pgadmin/tools/user/__init__.py
@@ -0,0 +1,310 @@
+##########################################################################
+#
+# 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"),
+            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('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..b6c57b5
--- /dev/null
+++ b/web/pgadmin/tools/user/templates/user/js/user.js
@@ -0,0 +1,565 @@
+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="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));
+
+        // 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(res, jqxhr) {
+              alertify.error('{{_('Error during saving user:') }} ' + jqxhr.responseJSON.errormsg);
+            }
+          });
+        }
+      }
+    });
+
+    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 if (this.collection.where({"email":this.get('email')}).length > 1) {
+                  errmsg =  '{{ _('This email id already exist.')}}';
+                  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 if (!_.isUndefined(this.get('newPassword')) && this.get('newPassword').length < 6) {
+                    errmsg =  '{{ _('Password must be at least 6 characters.')}}';
+                    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');
+                     if(this.get('newPassword') == ''){
+                      this.set({'newPassword': undefined})
+                     }
+
+                     if(this.get('confirmPassword') == ''){
+                      this.set({'confirmPassword': undefined})
+                     }
+
+                  } else if (!_.isUndefined(this.get('newPassword')) && this.get('newPassword').length < 6) {
+                    errmsg =  '{{ _('Password must be at least 6 characters.')}}';
+                    this.errorModel.set('newPassword', errmsg);
+                    return errmsg;
+                  } 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.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) {
+                  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..98d72e1 100644
--- a/web/setup.py
+++ b/web/setup.py
@@ -72,6 +72,12 @@ account:\n""")
                 name='Administrators',
                 description='pgAdmin Administrators Role'
                 )
+
+        user_datastore.create_role(
+                name='Standard',
+                description='pgAdmin Standard Role'
+                )
+
         user_datastore.create_user(email=email, password=password)
         db.session.flush()
         user_datastore.add_role_to_user(email, 'Administrators')
@@ -249,6 +255,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