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