Preferences dialogue. Patch by Ashesh and Khushboo Vashi.

This commit is contained in:
Dave Page 2016-03-07 11:48:24 +00:00
parent 43116750b4
commit 5ea822f33e
21 changed files with 1439 additions and 74 deletions

View File

@ -138,7 +138,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 = 7
SETTINGS_SCHEMA_VERSION = 8
# 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

View File

@ -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.settings.settings_model import db, Role, User, Version
from pgadmin.model import db, Role, User, Version
from importlib import import_module
from werkzeug.local import LocalProxy
from pgadmin.utils import PgAdminModule, driver

View File

@ -29,8 +29,11 @@ except:
MODULE_NAME = 'browser'
class BrowserModule(PgAdminModule):
LABEL = gettext('Browser')
def get_own_stylesheets(self):
stylesheets = []
# Add browser stylesheets
@ -168,20 +171,56 @@ class BrowserModule(PgAdminModule):
scripts.extend(module.get_own_javascripts())
return scripts
def register_preferences(self):
self.show_system_objects = self.preference.register(
'display', 'show_system_objects',
gettext("Show system objects"), 'boolean', False,
category_label=gettext('Display')
)
blueprint = BrowserModule(MODULE_NAME, __name__)
@six.add_metaclass(ABCMeta)
class BrowserPluginModule(PgAdminModule):
"""
Base class for browser submodules.
Abstract base class for browser submodules.
It helps to define the node for each and every node comes under the browser
tree. It makes sure every module comes under browser will have prefix
'/browser', and sets the 'url_prefix', 'static_url_path', etc.
Also, creates some of the preferences to be used by the node.
"""
browser_url_prefix = blueprint.url_prefix + '/'
SHOW_ON_BROWSER = True
def __init__(self, import_name, **kwargs):
"""
Construct a new 'BrowserPluginModule' object.
:param import_name: Name of the module
:param **kwargs: Extra parameters passed to the base class
pgAdminModule.
:return: returns nothing
It sets the url_prefix to based on the 'node_path'. And,
static_url_path to relative path to '/static'.
Every module extended from this will be identified as 'NODE-<type>'.
Also, create a preference 'show_node_<type>' to fetch whether it
can be shown in the browser or not. Also, refer to the browser-preference.
"""
kwargs.setdefault("url_prefix", self.node_path)
kwargs.setdefault("static_url_path", '/static')
self.browser_preference = None
self.pref_show_system_objects = None
self.pref_show_node = None
super(BrowserPluginModule, self).__init__(
"NODE-%s" % self.node_type,
import_name,
@ -196,6 +235,24 @@ class BrowserPluginModule(PgAdminModule):
return []
def get_own_javascripts(self):
"""
Returns the list of javascripts information used by the module.
Each javascripts information must contain name, path of the script.
The name must be unique for each module, hence - in order to refer them
properly, we do use 'pgadmin.node.<type>' as norm.
That can also refer to when to load the script.
i.e.
We may not need to load the javascript of table node, when we're
not yet connected to a server, and no database is loaded. Hence - it
make sense to load them when a database is loaded.
We may also add 'deps', which also refers to the list of javascripts,
it may depends on.
"""
scripts = []
scripts.extend([{
@ -211,6 +268,27 @@ class BrowserPluginModule(PgAdminModule):
def generate_browser_node(
self, node_id, parent_id, label, icon, inode, node_type, **kwargs
):
"""
Helper function to create a browser node for this particular subnode.
:param node_id: Unique Id for each node
:param parent_id: Id of the parent.
:param label: Label for the node
:param icon: Icon for displaying along with this node on browser
tree. Icon refers to a class name, it refers to.
:param inode: True/False.
Used by the browser tree node to check, if the
current node will have children or not.
:param node_type: String to refer to the node type.
:param **kwargs: A node can have extra information other than this
data, which can be passed as key-value pair as
argument here.
i.e. A database, server node can have extra
information like connected, or not.
Returns a dictionary object representing this node object for the
browser tree.
"""
obj = {
"id": "%s/%s" % (node_type, node_id),
"label": label,
@ -269,12 +347,66 @@ class BrowserPluginModule(PgAdminModule):
@property
def node_path(self):
"""
Defines the url path prefix for this submodule.
"""
return self.browser_url_prefix + self.node_type
@property
def javascripts(self):
"""
Override the javascript of PgAdminModule, so that - we don't return
javascripts from the get_own_javascripts itself.
"""
return []
@property
def label(self):
"""
Module label.
"""
return self.LABEL
@property
def show_node(self):
"""
A proper to check to show node for this module on the browser tree or not.
Relies on show_node preference object, otherwise on the SHOW_ON_BROWSER
default value.
"""
if self.pref_show_node:
return self.pref_show_node.get()
else:
return self.SHOW_ON_BROWSER
@property
def show_system_objects(self):
"""
Show/Hide the system objects in the database server.
"""
if self.pref_show_system_objects:
return self.pref_show_system_objects.get()
else:
return False
def register_preferences(self):
"""
Registers the preferences object for this module.
Sets the browser_preference, show_system_objects, show_node preference
objects for this submodule.
"""
# Add the node informaton for browser, not in respective node preferences
self.browser_preference = blueprint.preference
self.pref_show_system_objects = blueprint.preference.preference(
'display', 'show_system_objects'
)
self.pref_show_node = self.browser_preference.preference(
'node', 'show_node_' + self.node_type,
self.label, 'boolean', self.SHOW_ON_BROWSER, category_label=gettext('Nodes')
)
@blueprint.route("/")
@login_required

View File

@ -14,6 +14,8 @@ from flask.ext.babel import gettext
from pgadmin.utils import PgAdminModule
from pgadmin.browser.utils import PGChildModule
from pgadmin.browser import BrowserPluginModule
from pgadmin.utils.preferences import Preferences
@six.add_metaclass(ABCMeta)
class CollectionNodeModule(PgAdminModule, PGChildModule):
@ -21,6 +23,7 @@ class CollectionNodeModule(PgAdminModule, PGChildModule):
Base class for collection node submodules.
"""
browser_url_prefix = BrowserPluginModule.browser_url_prefix
SHOW_ON_BROWSER = True
def __init__(self, import_name, **kwargs):
kwargs.setdefault("url_prefix", self.node_path)
@ -123,6 +126,10 @@ class CollectionNodeModule(PgAdminModule, PGChildModule):
"""
return self.COLLECTION_LABEL
@property
def label(self):
return self.COLLECTION_LABEL
@property
def collection_icon(self):
"""
@ -173,3 +180,47 @@ class CollectionNodeModule(PgAdminModule, PGChildModule):
@property
def javascripts(self):
return []
@property
def show_node(self):
"""
Property to check whether to show the node for this module on the
browser tree or not.
Relies on show_node preference object, otherwise on the SHOW_ON_BROWSER
default value.
"""
if self.pref_show_node:
return self.pref_show_node.get()
else:
return self.SHOW_ON_BROWSER
@property
def show_system_objects(self):
"""
Show/Hide the system objects in the database server.
"""
if self.pref_show_system_objects:
return self.pref_show_system_objects.get()
else:
return False
def register_preferences(self):
"""
register_preferences
Register preferences for this module.
Keep the browser preference object to be used by overriden submodule,
along with that get two browser level preferences show_system_objects,
and show_node will be registered to used by the submodules.
"""
# Add the node informaton for browser, not in respective node preferences
self.browser_preference = Preferences.module('browser')
self.pref_show_system_objects = self.browser_preference.preference(
'show_system_objects'
)
self.pref_show_node = self.browser_preference.register(
'node', 'show_node_' + self.node_type,
self.collection_label, 'node', self.SHOW_ON_BROWSER,
category_label=gettext('Nodes')
)

View File

@ -9,17 +9,15 @@
"""Defines views for management of server groups"""
from abc import ABCMeta, abstractmethod
import traceback
import json
from flask import request, render_template, make_response, jsonify
from flask.ext.babel import gettext
from flask.ext.security import current_user, login_required
from pgadmin import current_blueprint
from flask.ext.security import current_user
from pgadmin.utils.ajax import make_json_response, \
make_response as ajax_response
from pgadmin.browser import BrowserPluginModule
from pgadmin.utils.menu import MenuItem
from pgadmin.settings.settings_model import db, ServerGroup
from pgadmin.model import db, ServerGroup
from pgadmin.browser.utils import NodeView
import six
@ -41,12 +39,30 @@ class ServerGroupModule(BrowserPluginModule):
@property
def node_type(self):
"""
node_type
Node type for Server Group is server-group. It is defined by NODE_TYPE
static attribute of the class.
"""
return self.NODE_TYPE
@property
def script_load(self):
"""
script_load
Load the server-group javascript module on loading of browser module.
"""
return None
def register_preferences(self):
"""
register_preferences
Overrides the register_preferences PgAdminModule, because - we will not
register any preference for server-group (specially the show_node
preference.)
"""
pass
class ServerGroupMenuItem(MenuItem):
@ -153,7 +169,6 @@ class ServerGroupView(NodeView):
sg = ServerGroup.query.filter_by(
user_id=current_user.id,
id=gid).first()
data = {}
if sg is None:
return make_json_response(
@ -181,7 +196,7 @@ class ServerGroupView(NodeView):
data[u'id'] = sg.id
data[u'name'] = sg.name
return jsonify(
node=self.blueprint.generate_browser_node(
"%d" % (sg.id), None,

View File

@ -8,25 +8,22 @@
##########################################################################
import json
from abc import ABCMeta, abstractproperty
from flask import render_template, request, make_response, jsonify, \
current_app, url_for
from flask.ext.security import login_required, current_user
from pgadmin.settings.settings_model import db, Server, ServerGroup, User
from flask.ext.security import current_user
from pgadmin.model import db, Server, ServerGroup, User
from pgadmin.utils.menu import MenuItem
from pgadmin.utils.ajax import make_json_response, \
make_response as ajax_response, internal_server_error, success_return, \
unauthorized, bad_request, precondition_required, forbidden
from pgadmin.utils.ajax import make_json_response, bad_request, forbidden, \
make_response as ajax_response, internal_server_error, unauthorized
from pgadmin.browser.utils import PGChildNodeView
import traceback
from flask.ext.babel import gettext
import pgadmin.browser.server_groups as sg
from pgadmin.utils.crypto import encrypt
from pgadmin.browser import BrowserPluginModule
from config import PG_DEFAULT_DRIVER
import six
from pgadmin.browser.server_groups.servers.types import ServerType
def has_any(data, keys):
"""
Checks any one of the keys present in the data given
@ -46,6 +43,7 @@ def has_any(data, keys):
class ServerModule(sg.ServerGroupPluginModule):
NODE_TYPE = "server"
LABEL = gettext("Servers")
@property
def node_type(self):
@ -146,6 +144,15 @@ class ServerModule(sg.ServerGroupPluginModule):
super(ServerModule, self).register(app, options, first_registration)
# We do not have any preferences for server node.
def register_preferences(self):
"""
register_preferences
Override it so that - it does not register the show_node preference for
server type.
"""
pass
class ServerMenuItem(MenuItem):
def __init__(self, **kwargs):
kwargs.setdefault("type", ServerModule.NODE_TYPE)

View File

@ -41,7 +41,8 @@ class DatabaseModule(CollectionNodeModule):
"""
Generate the collection node
"""
yield self.generate_browser_collection_node(sid)
if self.show_node:
yield self.generate_browser_collection_node(sid)
@property
def script_load(self):
@ -634,7 +635,7 @@ class DatabaseView(PGChildNodeView):
except Exception as e:
current_app.logger.exception(e)
return make_json_response(
data="-- modified SQL",
data=_("-- modified SQL"),
status=200
)

View File

@ -10,8 +10,7 @@ from flask import render_template, request, jsonify, current_app
from flask.ext.babel import gettext as _
from pgadmin.utils.ajax import make_json_response, \
make_response as ajax_response, precondition_required, \
internal_server_error, forbidden, \
not_implemented, success_return, gone
internal_server_error, forbidden, success_return, gone
from pgadmin.browser.utils import PGChildNodeView
from pgadmin.browser.collection import CollectionNodeModule
import pgadmin.browser.server_groups as sg
@ -37,8 +36,9 @@ class RoleModule(CollectionNodeModule):
"""
Generate the collection node
"""
yield self.generate_browser_collection_node(sid)
if self.show_node:
yield self.generate_browser_collection_node(sid)
pass
@property
def node_inode(self):
@ -480,7 +480,8 @@ rolmembership:{
if check_permission:
user = self.manager.user_info
if not user['is_superuser'] and not user['can_create_role']:
if not user['is_superuser'] and \
not user['can_create_role']:
if (action != 'update' or
'rid' in kwargs and kwargs['rid'] != -1 and
user['id'] != kwargs['rid']):

View File

@ -26,12 +26,6 @@ class TablespaceModule(CollectionNodeModule):
NODE_TYPE = 'tablespace'
COLLECTION_LABEL = gettext("Tablespaces")
def __init__(self, *args, **kwargs):
self.min_ver = None
self.max_ver = None
super(TablespaceModule, self).__init__(*args, **kwargs)
def get_nodes(self, gid, sid):
"""
Generate the collection node

View File

@ -9,29 +9,14 @@
"""Browser helper utilities"""
from abc import ABCMeta, abstractmethod
from abc import abstractmethod
import flask
from flask.views import View, MethodViewType, with_metaclass
from flask.ext.babel import gettext
from flask import render_template, current_app
import six
from config import PG_DEFAULT_DRIVER
from pgadmin.utils.ajax import make_json_response, precondition_required
@six.add_metaclass(ABCMeta)
class NodeAttr(object):
"""
"""
@abstractmethod
def validate(self, mode, value):
pass
@abstractmethod
def schema(self):
pass
class PGChildModule(object):
"""
@ -47,10 +32,6 @@ class PGChildModule(object):
- Return True when it supports certain version.
Uses the psycopg2 server connection manager as input for checking the
compatibility of the current module.
* AddAttr(attr)
- This adds the attribute supported for specific version only. It takes
NodeAttr as input, and update max_ver, min_ver variables for this module.
"""
def __init__(self, *args, **kwargs):
@ -58,9 +39,12 @@ class PGChildModule(object):
self.max_ver = 1000000000
self.server_type = None
super(PGChildModule, self).__init__(*args, **kwargs)
super(PGChildModule, self).__init__()
def BackendSupported(self, manager, **kwargs):
if hasattr(self, 'show_node'):
if not self.show_node:
return False
sversion = getattr(manager, 'sversion', None)
if (sversion is None or not isinstance(sversion, int)):
return False
@ -83,6 +67,7 @@ class PGChildModule(object):
def get_nodes(self, sid=None, **kwargs):
pass
class NodeView(with_metaclass(MethodViewType, View)):
"""
A PostgreSQL Object has so many operaions/functions apart from CRUD

View File

@ -109,3 +109,42 @@ class Server(db.Model):
comment = db.Column(
db.String(1024),
nullable=True)
class ModulePreference(db.Model):
"""Define a preferences table for any modules."""
__tablename__ = 'module_preference'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(256), nullable=False)
class PreferenceCategory(db.Model):
"""Define a preferences category for each modules."""
__tablename__ = 'preference_category'
id = db.Column(db.Integer, primary_key=True)
mid = db.Column(
db.Integer,
db.ForeignKey('module_preference.id'),
nullable=False
)
name = db.Column(db.String(256), nullable=False)
class Preferences(db.Model):
"""Define a particular preference."""
__tablename__ = 'preferences'
id = db.Column(db.Integer, primary_key=True)
cid = db.Column(
db.Integer,
db.ForeignKey('preference_category.id'),
nullable=False
)
name = db.Column(db.String(1024), nullable=False)
class UserPreference(db.Model):
"""Define the preference for a particular user."""
__tablename__ = 'user_preferences'
pid = db.Column(
db.Integer, db.ForeignKey('preferences.id'), primary_key=True
)
uid = db.Column(
db.Integer, db.ForeignKey('user.id'), primary_key=True
)
value = db.Column(db.String(1024), nullable=False)

View File

@ -0,0 +1,137 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2016, The pgAdmin Development Team
#
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""
Implements the routes for creating Preferences/Options Dialog on the client
side and for getting/setting preferences.
"""
from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import success_return, \
make_response as ajax_response, internal_server_error
from flask import render_template, url_for, Response, request
from flask.ext.security import login_required
from flask.ext.login import current_user
from flask.ext.babel import gettext
from pgadmin.utils.menu import MenuItem
from pgadmin.utils.preferences import Preferences
import simplejson as json
MODULE_NAME = 'preferences'
class PreferencesModule(PgAdminModule):
"""
PreferenceModule represets the preferences of different modules to the
user in UI.
And, allows the user to modify (not add/remove) as per their requirement.
"""
def get_own_javascripts(self):
return [{
'name': 'pgadmin.preferences',
'path': url_for('preferences.index') + 'preferences',
'when': None
}]
def get_own_stylesheets(self):
return [url_for('preferences.static', filename='css/preferences.css')]
def get_own_menuitems(self):
return {
'file_items': [
MenuItem(name='mnu_preferences',
priority=999,
module="pgAdmin.Preferences",
callback='show',
icon='fa fa-cog',
label=gettext('Preferences'))
]
}
blueprint = PreferencesModule(MODULE_NAME, __name__)
@blueprint.route("/")
@login_required
def index():
"""Render the preferences dialog."""
return render_template(
MODULE_NAME + "/index.html",
username=current_user.email,
_=gettext
)
@blueprint.route("/preferences.js")
@login_required
def script():
"""render the required javascript"""
return Response(response=render_template("preferences/preferences.js", _=gettext),
status=200,
mimetype="application/javascript")
@blueprint.route("/preferences", methods=["GET"])
@login_required
def preferences():
"""Fetch all the preferences of pgAdmin IV."""
# Load Preferences
preferences = Preferences.preferences()
res = []
for m in preferences:
if len(m['categories']):
om = {
"id": m['id'],
"label": m['label'],
"inode": True,
"open": True,
"branch": []
}
for c in m['categories']:
oc = {
"id": c['id'],
"mid": m['id'],
"label": c['label'],
"inode": False,
"open": False,
"preferences": c['preferences']
}
(om['branch']).append(oc)
res.append(om)
return ajax_response(
response=res,
status=200
)
@blueprint.route("/preferences/<int:pid>", methods=["PUT"])
@login_required
def save(pid):
"""
Save a specific preference.
"""
data = request.form if request.form else json.loads(request.data.decode())
res, msg = Preferences.save(data['mid'], data['cid'], data['id'], data['value'])
if not res:
return internal_server_error(errormsg=msg)
return success_return()

View File

@ -0,0 +1,40 @@
.preferences_dialog {
height: 100%;
position: absolute;
top: 5px;
left: 0px;
bottom: 0px;
right: 0px;
padding-bottom: 30px;
}
.preferences_tree{
padding: 0px;
padding-top: 2px;
height: 100%;
overflow: auto;
border-right: 2px solid #999999;
background-image: #FAFAFA;
}
.preferences_content {
padding-top: 10px;
height: 100%;
overflow: auto;
}
.preferences_content .control-label, .preferences_content .pgadmin-controls {
min-width: 100px !important;
}
.pgadmin-preference-body {
min-width: 300px !important;
min-height: 400px !important;
}
@media (min-width: 768px) {
.pgadmin-preference-body {
min-width: 600px !important;
min-height: 480px !important;
}
}

View File

@ -0,0 +1,8 @@
<div id="options_dialog">
<div id="options_tree" class="">
ACI TREE
</div>
<div id='options_content'>
Right hand side content
</div>
</div>

View File

@ -0,0 +1,374 @@
define(
['jquery', 'alertify', 'pgadmin', 'underscore', 'backform', 'pgadmin.backform'],
// This defines the Preference/Options Dialog for pgAdmin IV.
function($, alertify, pgAdmin, _, Backform) {
pgAdmin = pgAdmin || window.pgAdmin || {};
/*
* Hmm... this module is already been initialized, we can refer to the old
* object from here.
*/
if (pgAdmin.Preferences)
return pgAdmin.Preferences;
pgAdmin.Preferences = {
init: function() {
if (this.initialized)
return;
this.initialized = true;
// Declare the Preferences dialog
alertify.dialog('preferencesDlg', function() {
var jTree, // Variable to create the aci-tree
controls = [], // Keep tracking of all the backform controls
// created by the dialog.
// Dialog containter
$container = $("<div class='preferences_dialog'></div>");
/*
* Preference Model
*
* This model will be used to keep tracking of the changes done for
* an individual option.
*/
var PreferenceModel = Backbone.Model.extend({
idAttribute: 'id',
defaults: {
id: undefined,
value: undefined
}
});
/*
* Preferences Collection object.
*
* We will use only one collection object to keep track of all the
* preferences.
*/
var preferences = this.preferences = new (Backbone.Collection.extend({
model: PreferenceModel,
url: "{{ url_for('preferences.preferences') }}",
updateAll: function() {
// We will send only the modified data to the server.
this.each(function(m) {
if (m.hasChanged()) {
m.save({
fail: function() {
}
});
}
});
return true;
}
}))(null);
/*
* Function: renderPreferencePanel
*
* Renders the preference panel in the content div based on the given
* preferences.
*/
var renderPreferencePanel = function(prefs) {
/*
* Clear the existing html in the preferences content
*/
var content = $container.find('.preferences_content');
content.empty();
/*
* We should clean up the existing controls.
*/
if (controls) {
_.each(controls, function(c) {
c.remove();
});
}
controls = [];
/*
* We will create new set of controls and render it based on the
* list of preferences using the Backform Field, Control.
*/
_.each(prefs, function(p) {
var m = preferences.get(p.id),
f = new Backform.Field(_.extend({}, p, {id: 'value', name: 'value'})),
cntr = new (f.get("control")) ({
field: f,
model: m
});
content.append(cntr.render().$el);
// We will keep track of all the controls rendered at the
// moment.
controls.push(cntr);
});
};
/*
* Function: dialogContentCleanup
*
* Do the dialog container cleanup on openning.
*/
var dialogContentCleanup = function() {
// Remove the existing preferences
if (!jTree)
return;
/*
* Remove the aci-tree (mainly to remove the jquery object of
* aciTree from the system for this container).
*/
try {
jTreeApi = jTree.aciTree('destroy');
} catch(ex) {
// Sometimes - it fails to destroy the tree properly and throws
// exception.
}
jTree.off('acitree', treeEventHandler);
// We need to reset the data from the preferences too
preferences.reset();
/*
* Clean up the existing controls.
*/
if (controls) {
_.each(controls, function(c) {
c.remove();
});
}
controls = [];
// Remove all the objects now.
$container.empty();
},
/*
* Function: selectFirstCategory
*
* Whenever a user select a module instead of a category, we should
* select the first categroy of it.
*/
selectFirstCategory = function(api, item) {
var data = item ? api.itemData(item) : null;
if (data && data.preferences) {
api.select(item);
return;
}
item = api.first(item);
selectFirstCategory(api, item);
},
/*
* A map on how to create controls for each datatype in preferences
* dialog.
*/
getControlMappedForType = function(p) {
switch(p.type) {
case 'boolean':
p.options = {
onText: '{{ _('True') }}',
offText: '{{ _('False') }}',
onColor: 'success',
offColor: 'default',
size: 'mini'
};
return 'switch';
case 'node':
p.options = {
onText: '{{ _('Show') }}',
offText: '{{ _('Hide') }}',
onColor: 'success',
offColor: 'default',
size: 'mini'
};
return 'switch';
case 'integer':
return 'integer';
case 'numeric':
return 'numeric';
case 'date':
// TODO::
// Datetime picker Control is missing at the moment, replace
// once it has been implemented.
return 'datepicker';
case 'datetime':
return 'datepicker';
case 'options':
var opts = [];
// Convert the array to SelectControl understandable options.
_.each(p.options, function(o) {
opts.push({'label': o, 'value': o});
});
p.options = opts;
return 'select2';
case 'multiline':
return 'textarea';
case 'switch':
return 'switch';
default:
if (console && console.log) {
// Warning for developer only.
console.log(
"Hmm.. We don't know how to render this type - ''" + type + "' of control."
);
}
return 'input';
}
},
/*
* function: treeEventHandler
*
* It is basically a callback, which listens to aci-tree events,
* and act accordingly.
*
* + Selection of the node will existance of the preferences for
* the selected tree-node, if not pass on to select the first
* category under a module, else pass on to the render function.
*
* + When a new node is added in the tree, it will add the relavent
* preferences in the preferences model collection, which will be
* called during initialization itself.
*
*
*/
treeEventHandler = function(event, api, item, eventName) {
// Look for selected item (if none supplied)!
item = item || api.selected();
// Event tree item has itemData
var d = item ? api.itemData(item) : null;
/*
* boolean (switch/checkbox), string, enum (combobox - enumvals),
* integer (min-max), font, color
*/
switch (eventName) {
case "selected":
if (!d)
return true;
if (d.preferences) {
/*
* Clear the existing html in the preferences content
*/
renderPreferencePanel(d.preferences);
return true;
} else{
selectFirstCategory(api, item);
}
break;
case 'added':
if (!d)
return true;
// We will add the preferences in to the preferences data
// collection.
if (d.preferences && _.isArray(d.preferences)) {
_.each(d.preferences, function(p) {
preferences.add({
'id': p.id, 'value': p.value, 'cid': d.id, 'mid': d.mid
});
/*
* We don't know until now, how to render the control for
* this preference.
*/
if (!p.control) {
p.control = getControlMappedForType(p);
}
});
}
d.sortable = false;
break;
case 'loaded':
// Let's select the first category from the prefrences.
// We need to wait for sometime before all item gets loaded
// properly.
setTimeout(
function() {
selectFirstCategory(api, null);
}, 300);
break;
}
return true;
};
// Dialog property
return {
main: function() {
// Remove the existing content first.
dialogContentCleanup();
$container.append(
"<div class='col-xs-3 preferences_tree aciTree'></div>"
).append(
"<div class='col-xs-9 preferences_content'>" +
" {{ _('Category is not selected.')|safe }}" +
"</div>"
);
// Create the aci-tree for listing the modules and categories of
// it.
jTree = $container.find('.preferences_tree');
jTree.on('acitree', treeEventHandler);
jTree.aciTree({
selectable: true,
expand: true,
ajax: {
url: "{{ url_for('preferences.preferences') }}"
}
});
this.show();
},
setup:function(){
return {
buttons:[
{
text: "{{ _('OK') }}", key: 13, className: "btn btn-primary"
},
{
text: "{{ _('Cancel') }}", className: "btn btn-danger"
}
],
focus: { element: 0 },
options: {
padding: !1,
overflow: !1,
title: '{{ _('Preferences')|safe }}'
}
};
},
callback: function(closeEvent){
if (closeEvent.button.text == "{{ _('OK') }}"){
preferences.updateAll();
}
},
build: function() {
this.elements.content.appendChild($container.get(0));
},
hooks: {
onshow: function() {
$(this.elements.body).addClass('pgadmin-preference-body');
}
}
};
});
},
show: function() {
alertify.preferencesDlg(true).resizeTo('60%', '60%');
}
};
return pgAdmin.Preferences;
});

View File

@ -9,16 +9,13 @@
"""Utility functions for storing and retrieving user configuration settings."""
from flask import current_app
from flask.ext.login import current_user
from flask.ext.sqlalchemy import SQLAlchemy
from .settings_model import db, Setting
from pgadmin.model import db, Setting
import traceback
from flask import Blueprint, Response, abort, request, render_template
from flask import Response, request, render_template
from flask.ext.security import login_required
import config
from pgadmin.utils.ajax import make_json_response
from pgadmin.utils import PgAdminModule
@ -59,7 +56,6 @@ def store(setting=None, value=None):
"""Store a configuration setting, or if this is a POST request and a
count value is present, store multiple settings at once."""
success = 1
errorcode = 0
errormsg = ''
try:
@ -97,7 +93,6 @@ def get(setting=None, default=None):
default = request.form['default']
success = 1
errorcode = 0
errormsg = ''
try:

View File

@ -425,6 +425,7 @@
/* Array of objects having attributes [label, fields] */
schema: undefined,
tagName: "form",
legend: true,
className: function() {
return 'col-sm-12 col-md-12 col-lg-12 col-xs-12';
},
@ -441,6 +442,7 @@
o.cId = o.cId || _.uniqueId('pgC_');
o.hId = o.hId || _.uniqueId('pgH_');
o.disabled = o.disabled || false;
o.legend = opts.legend;
});
if (opts.tabPanelClassName && _.isFunction(opts.tabPanelClassName)) {
this.tabPanelClassName = opts.tabPanelClassName;
@ -547,8 +549,9 @@
template: {
'header': _.template([
'<fieldset class="<%=fieldsetClass%>" <%=disabled ? "disabled" : ""%>>',
' <% if (legend != false) { %>',
' <legend class="<%=legendClass%>" <%=collapse ? "data-toggle=\'collapse\'" : ""%> data-target="#<%=cId%>"><%=collapse ? "<span class=\'caret\'></span>" : "" %><%=label%></legend>',
' ',
' <% } %>',
'</fieldset>'
].join("\n")),
'content': _.template(

View File

@ -10,7 +10,7 @@
from flask import Blueprint
from collections import defaultdict
from operator import attrgetter
import sys
from .preferences import Preferences
class PgAdminModule(Blueprint):
@ -26,8 +26,25 @@ class PgAdminModule(Blueprint):
kwargs.setdefault('template_folder', 'templates')
kwargs.setdefault('static_folder', 'static')
self.submodules = []
super(PgAdminModule, self).__init__(name, import_name, **kwargs)
def create_module_preference():
# Create preference for each module by default
if hasattr(self, 'LABEL'):
self.preference = Preferences(self.name, self.LABEL)
else:
self.preference = Preferences(self.name, None)
self.register_preferences()
# Create and register the module preference object and preferences for
# it just before the first request
self.before_app_first_request(create_module_preference)
def register_preferences(self):
pass
def register(self, app, options, first_registration=False):
"""
Override the default register function to automagically register
@ -35,7 +52,9 @@ class PgAdminModule(Blueprint):
"""
if first_registration:
self.submodules = list(app.find_submodules(self.import_name))
super(PgAdminModule, self).register(app, options, first_registration)
for module in self.submodules:
app.register_blueprint(module)

View File

@ -24,7 +24,7 @@ from flask.ext.babel import gettext
from flask.ext.security import current_user
from ..abstract import BaseDriver, BaseConnection
from pgadmin.settings.settings_model import Server, User
from pgadmin.model import Server, User
from pgadmin.utils.crypto import decrypt
import random
import select
@ -1003,7 +1003,7 @@ class Driver(BaseDriver):
managers['pinged'] = datetime.datetime.now()
if str(sid) not in managers:
from pgadmin.settings.settings_model import Server
from pgadmin.model import Server
s = Server.query.filter_by(id=sid).first()
managers[str(sid)] = ServerManager(s)

View File

@ -0,0 +1,528 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2016, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""
Utility classes to register, getter, setter functions for the preferences of a
module within the system.
"""
from flask import current_app
from flask.ext.security import current_user
from pgadmin.model import db, Preferences as PrefTable, \
ModulePreference as ModulePrefTable, UserPreference as UserPrefTable, \
PreferenceCategory as PrefCategoryTbl
from flask.ext.babel import gettext
import dateutil.parser as dateutil_parser
import decimal
class _Preference(object):
"""
Internal class representing module, and categoy bound preference.
"""
def __init__(
self, cid, name, label, _type, default, help_str=None, min_val=None,
max_val=None, options=None
):
"""
__init__
Constructor/Initializer for the internal _Preference object.
It creates a new entry for this preference in configuration table based
on the name (if not exists), and keep the id of it for on demand value
fetching from the configuration table in later stage. Also, keeps track
of type of the preference/option, and other supporting parameters like
min, max, options, etc.
:param cid: configuration id
:param name: Name of the preference (must be unique for each
configuration)
:param label: Display name of the options/preference
:param _type: Type for proper validation on value
:param default: Default value
:param help_str: Help string to be shown in preferences dialog.
:param min_val: minimum value
:param max_val: maximum value
:param options: options (Array of list objects)
:returns: nothing
"""
self.cid = cid
self.name = name
self.default = default
self.label = label
self._type = _type
self.help_str = help_str
self.min_val = min_val
self.max_val = max_val
self.options = options
# Look into the configuration table to find out the id of the specific
# preference.
res = PrefTable.query.filter_by(
name=name
).first()
if res is None:
# Couldn't find in the configuration table, we will create new
# entry for it.
res = PrefTable(name=self.name, cid=cid)
db.session.add(res)
db.session.commit()
res = PrefTable.query.filter_by(
name=name
).first()
# Save this id for letter use.
self.pid = res.id
def get(self):
"""
get
Fetch the value from the server for the current user from the
configuration table (if available), otherwise returns the default value
for it.
:returns: value for this preference.
"""
res = UserPrefTable.query.filter_by(
pid=self.pid
).filter_by(uid=current_user.id).first()
# Couldn't find any preference for this user, return default value.
if res is None:
return self.default
# The data stored in the configuration will be in string format, we
# need to convert them in proper format.
if self._type == 'boolean' or self._type == 'switch' or \
self._type == 'node':
return res.value == 'True'
if self._type == 'integer':
try:
return int(res.value)
except Exception as e:
current_app.logger.exeception(e)
return self.default
if self._type == 'numeric':
try:
return decimal.Decimal(res.value)
except Exception as e:
current_app.logger.exeception(e)
return self.default
if self._type == 'date' or self._type == 'datetime':
try:
return dateutil_parser.parse(res.value)
except Exception as e:
current_app.logger.exeception(e)
return self.default
if self._type == 'options':
if res.value in self.options:
return res.value
return self.default
return res.value
def set(self, value):
"""
set
Set the value into the configuration table for this current user.
:param value: Value to be set
:returns: nothing.
"""
# We can't store the values in the given format, we need to convert
# them in string first. We also need to validate the value type.
if self._type == 'boolean' or self._type == 'switch' or \
self._type == 'node':
if type(value) != bool:
return False, gettext("Invalid value for boolean type!")
elif self._type == 'integer':
if type(value) != int:
return False, gettext("Invalid value for integer type!")
elif self._type == 'numeric':
t = type(value)
if t != float and t != int and t != decimal.Decimal:
return False, gettext("Invalid value for numeric type!")
elif self._type == 'date':
try:
value = dateutil_parser.parse(value).date()
except Exception as e:
current_app.logger.exeception(e)
return False, gettext("Invalid value for date type!")
elif self._type == 'datetime':
try:
value = dateutil_parser.parse(value)
except Exception as e:
current_app.logger.exeception(e)
return False, gettext("Invalid value for datetime type!")
elif self._type == 'options':
if value not in self.options:
return False, gettext("Invalid value for options type!")
pref = UserPrefTable.query.filter_by(
pid=self.pid
).filter_by(uid=current_user.id).first()
if pref is None:
pref = UserPrefTable(
uid=current_user.id, pid=self.pid, value=str(value)
)
db.session.add(pref)
else:
pref.value = str(value)
db.session.commit()
return True, None
def to_json(self):
"""
to_json
Returns the JSON object representing this preferences.
:returns: the JSON representation for this preferences
"""
res = {
'id': self.pid,
'cid': self.cid,
'name': self.name,
'label': self.label or self.name,
'type': self._type,
'help_str': self.help_str,
'min_val': self.min_val,
'max_val': self.max_val,
'options': self.options,
'value': self.get()
}
return res
class Preferences(object):
"""
class Preferences
It helps to manage all the preferences/options related to a specific
module.
It keeps track of all the preferences registered with it using this class
in the group of categories.
Also, create the required entries for each module, and categories in the
preferences tables (if required). If it is already present, it will refer
to the existing data from those tables.
class variables:
---------------
modules:
Dictionary of all the modules, can be refered by its name.
Keeps track of all the modules in it, so that - only one object per module
gets created. If the same module refered by different object, the
categories dictionary within it will be shared between them to keep the
consistent data among all the object.
Instance Definitions:
-------- -----------
"""
modules = dict()
def __init__(self, name, label=None):
"""
__init__
Constructor/Initializer for the Preferences class.
:param name: Name of the module
:param label: Display name of the module, it will be displayed in the
preferences dialog.
:returns nothing
"""
self.name = name
self.label = label
self.categories = dict()
# Find the entry for this module in the configuration database.
module = ModulePrefTable.query.filter_by(name=name).first()
# Can't find the reference for it in the configuration database,
# create on for it.
if module is None:
module = ModulePrefTable(name=name)
db.session.add(module)
db.session.commit()
module = ModulePrefTable.query.filter_by(name=name).first()
self.mid = module.id
if name in Preferences.modules:
m = Preferences.modules
self.categories = m.categories
else:
Preferences.modules[name] = self
def to_json(self):
"""
to_json
Converts the preference object to the JSON Format.
:returns: a JSON object contains information.
"""
res = {
'id': self.mid,
'label': self.label or self.name,
'categories': []
}
for c in self.categories:
cat = self.categories[c]
interm = {
'id': cat['id'],
'label': cat['label'] or cat['name'],
'preferences': []
}
res['categories'].append(interm)
for p in cat['preferences']:
pref = (cat['preferences'][p]).to_json().copy()
pref.update({'mid': self.mid, 'cid': cat['id']})
interm['preferences'].append(pref)
return res
def __category(self, name, label):
"""
__category
A private method to create/refer category for/of this module.
:param name: Name of the category
:param label: Display name of the category, it will be send to
client/front end to list down in the preferences/options
dialog.
:returns: A dictionary object reprenting this category.
"""
if name in self.categories:
res = self.categories[name]
# Update the category label (if not yet defined)
res['label'] = res['label'] or label
return res
cat = PrefCategoryTbl.query.filter_by(
mid=self.mid
).filter_by(name=name).first()
if cat is None:
cat = PrefCategoryTbl(name=name, mid=self.mid)
db.session.add(cat)
db.session.commit()
cat = PrefCategoryTbl.query.filter_by(
mid=self.mid
).filter_by(name=name).first()
self.categories[name] = res = {
'id': cat.id,
'name': name,
'label': label,
'preferences': dict()
}
return res
def register(
self, category, name, label, _type, default, min_val=None,
max_val=None, options=None, help_str=None, category_label=None
):
"""
register
Register/Refer the particular preference in this module.
:param category: name of the category, in which this preference/option
will be displayed.
:param name: name of the preference/option
:param label: Display name of the preference
:param _type: [optional] Type of the options.
It is an optional argument, only if this
option/preference is registered earlier.
:param default: [optional] Default value of the options
It is an optional argument, only if this
option/preference is registered earlier.
:param min_val:
:param max_val:
:param options:
:param help_str:
:param category_label:
"""
cat = self.__category(category, category_label)
if name in cat['preferences']:
return (cat['preferences'])[name]
assert label is not None, "Label for a preference can not be none!"
assert _type is not None, "Type for a preference can not be none!"
assert _type in (
'boolean', 'integer', 'numeric', 'date', 'datetime',
'options', 'multiline', 'switch', 'node'
), "Type can not be found in the defined list!"
(cat['preferences'])[name] = res = _Preference(
cat['id'], name, label, _type, default, help_str, min_val,
max_val, options
)
return res
def preference(self, name):
"""
preference
Refer the particular preference in this module.
:param name: name of the preference/option
"""
for key in self.categories:
cat = self.categories[key]
if name in cat['preferences']:
return (cat['preferences'])[name]
assert False, """Couldn't find the preference in this preference!
Did you forget to register it?"""
@classmethod
def preferences(cls):
"""
preferences
Convert all the module preferences in the JSON format.
:returns: a list of the preferences for each of the modules.
"""
res = []
for m in Preferences.modules:
res.append(Preferences.modules[m].to_json())
return res
@classmethod
def register_preference(
cls, module, category, name, label, _type, default, min_val=None,
max_val=None, options=None, help_str=None, module_label=None,
category_label=None
):
"""
register
Register/Refer a preference in the system for any module.
:param module: Name of the module
:param category: Name of category
:param name: Name of the option
:param label: Label of the option, shown in the preferences dialog.
:param _type: Type of the option.
Allowed type of options are as below:
boolean, integer, numeric, date, datetime,
options, multiline, switch, node
:param default: Default value for the preference/option
:param min_val: Minimum value for integer, and numeric type
:param max_val: Maximum value for integer, and numeric type
:param options: Allowed list of options for 'option' type
:param help_str: Help string show for that preference/option.
:param module_label: Label for the module
:param category_label: Label for the category
"""
m = None
if module in Preferences.modules:
m = Preferences.modules[module]
# Update the label (if not defined yet)
m.label = m.label or module_label
else:
m = Preferences(module, module_label)
return m.register(
category, name, label, _type, default, min_val, max_val,
options, help_str, category_label
)
@classmethod
def module(cls, name):
"""
module (classmethod)
Get the module preferences object
:param name: Name of the module
:returns: a Preferences object representing for the module.
"""
if name in Preferences.modules:
m = Preferences.modules[name]
# Update the label (if not defined yet)
if m.label is None:
m.label = name
return m
else:
m = Preferences(name, None)
return m
@classmethod
def save(cls, mid, cid, pid, value):
"""
save
Update the value for the preference in the configuration database.
:param mid: Module ID
:param cid: Category ID
:param pid: Preference ID
:param value: Value for the options
"""
# Find the entry for this module in the configuration database.
module = ModulePrefTable.query.filter_by(id=mid).first()
# Can't find the reference for it in the configuration database,
# create on for it.
if module is None:
return False, gettext("Couldn't find the specified module.")
m = cls.modules[module.name]
if m is None:
return False, gettext(
"Module '{0}' is no longer in use!"
).format(module.name)
category = None
for c in m.categories:
cat = m.categories[c]
if cid == cat['id']:
category = cat
break
if category is None:
return False, gettext(
"Module '{0}' does not have category with id '{1}'"
).format(module.name, cid)
preference = None
for p in category['preferences']:
pref = (category['preferences'])[p]
if pref.pid == pid:
preference = pref
break
if preference is None:
return False, gettext(
"Couldn't find the given preference!"
)
try:
pref.set(value)
except Exception as e:
return False, str(e)
return True, None

View File

@ -17,10 +17,9 @@ import random
import string
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.security import Security, SQLAlchemyUserDatastore
from flask.ext.security.utils import encrypt_password
from pgadmin.settings.settings_model import db, Role, User, Server, \
from pgadmin.model import db, Role, User, Server, \
ServerGroup, Version
# Configuration settings
@ -146,7 +145,7 @@ Exiting...""".format(version.value))
)
if int(version.value) < 5:
db.engine.execute('ALTER TABLE server ADD COLUMN role text(64)')
if int(version.value) == 6:
if int(version.value) < 6:
db.engine.execute("ALTER TABLE server RENAME TO server_old")
db.engine.execute("""
CREATE TABLE server (
@ -177,9 +176,46 @@ INSERT INTO server (
FROM server_old""")
db.engine.execute("DROP TABLE server_old")
# Finally, update the schema version
version.value = config.SETTINGS_SCHEMA_VERSION
db.session.merge(version)
if int(version.value) < 8:
app.logger.info(
"Creating the preferences tables..."
)
db.engine.execute("""
CREATE TABLE module_preference(
id INTEGER PRIMARY KEY,
name VARCHAR(256) NOT NULL
)""")
db.engine.execute("""
CREATE TABLE preference_category(
id INTEGER PRIMARY KEY,
mid INTEGER,
name VARCHAR(256) NOT NULL,
FOREIGN KEY(mid) REFERENCES module_preference(id)
)""")
db.engine.execute("""
CREATE TABLE preferences (
id INTEGER PRIMARY KEY,
cid INTEGER NOT NULL,
name VARCHAR(256) NOT NULL,
FOREIGN KEY(cid) REFERENCES preference_category (id)
)""")
db.engine.execute("""
CREATE TABLE user_preferences (
pid INTEGER,
uid INTEGER,
value VARCHAR(1024) NOT NULL,
PRIMARY KEY (pid, uid),
FOREIGN KEY(pid) REFERENCES preferences (pid),
FOREIGN KEY(uid) REFERENCES user (id)
)""")
# Finally, update the schema version
version.value = config.SETTINGS_SCHEMA_VERSION
@ -190,7 +226,7 @@ FROM server_old""")
# Done!
app.logger.info(
"The configuration database %s has been upgraded to version %d" %
(config.SQLITE_PATH, config.SETTINGS_SCHEMA_VERSION)
(config.SQLITE_PATH, config.SETTINGS_SCHEMA_VERSION)
)
###############################################################################
@ -222,8 +258,8 @@ if __name__ == '__main__':
# Check if the database exists. If it does, tell the user and exit.
if os.path.isfile(config.SQLITE_PATH):
print("""
The configuration database %s already exists.
Entering upgrade mode...""".format(config.SQLITE_PATH))
The configuration database '%s' already exists.
Entering upgrade mode...""" % config.SQLITE_PATH)
# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
@ -238,12 +274,12 @@ Entering upgrade mode...""".format(config.SQLITE_PATH))
print("""
The database schema version is %d, whilst the version required by the \
software is %d.
Exiting...""".format(version.value, config.SETTINGS_SCHEMA_VERSION))
Exiting...""" % (version.value, config.SETTINGS_SCHEMA_VERSION))
sys.exit(1)
elif int(version.value) == int(config.SETTINGS_SCHEMA_VERSION):
print("""
The database schema version is %d as required.
Exiting...""".format(version.value))
Exiting...""" % (version.value))
sys.exit(1)
print("NOTE: Upgrading database schema from version %d to %d." % (
@ -252,6 +288,6 @@ Exiting...""".format(version.value))
do_upgrade(app, user_datastore, security, version)
else:
print("""
The configuration database - {0} does not exist.
The configuration database - '{0}' does not exist.
Entering initial setup mode...""".format(config.SQLITE_PATH))
do_setup(app)