From 5ea822f33e14f7fc9160077f67d6d3df40dbed00 Mon Sep 17 00:00:00 2001 From: Dave Page Date: Mon, 7 Mar 2016 11:48:24 +0000 Subject: [PATCH] Preferences dialogue. Patch by Ashesh and Khushboo Vashi. --- web/config.py | 2 +- web/pgadmin/__init__.py | 2 +- web/pgadmin/browser/__init__.py | 134 ++++- web/pgadmin/browser/collection.py | 51 ++ web/pgadmin/browser/server_groups/__init__.py | 27 +- .../browser/server_groups/servers/__init__.py | 23 +- .../servers/databases/__init__.py | 5 +- .../server_groups/servers/roles/__init__.py | 11 +- .../servers/tablespaces/__init__.py | 6 - web/pgadmin/browser/utils.py | 27 +- .../settings_model.py => model/__init__.py} | 39 ++ web/pgadmin/preferences/__init__.py | 137 +++++ .../preferences/static/css/preferences.css | 40 ++ .../templates/preferences/index.html | 8 + .../templates/preferences/preferences.js | 374 +++++++++++++ web/pgadmin/settings/__init__.py | 9 +- web/pgadmin/static/js/backform.pgadmin.js | 5 +- web/pgadmin/utils/__init__.py | 21 +- web/pgadmin/utils/driver/psycopg2/__init__.py | 4 +- web/pgadmin/utils/preferences.py | 528 ++++++++++++++++++ web/setup.py | 60 +- 21 files changed, 1439 insertions(+), 74 deletions(-) rename web/pgadmin/{settings/settings_model.py => model/__init__.py} (74%) create mode 100644 web/pgadmin/preferences/__init__.py create mode 100644 web/pgadmin/preferences/static/css/preferences.css create mode 100644 web/pgadmin/preferences/templates/preferences/index.html create mode 100644 web/pgadmin/preferences/templates/preferences/preferences.js create mode 100644 web/pgadmin/utils/preferences.py diff --git a/web/config.py b/web/config.py index 85388ad25..5ee3410d1 100644 --- a/web/config.py +++ b/web/config.py @@ -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 diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index e271300af..0c6f7eda4 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.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 diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index 38693d440..aa6c526f1 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -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-'. + + Also, create a preference 'show_node_' 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.' 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 diff --git a/web/pgadmin/browser/collection.py b/web/pgadmin/browser/collection.py index c7d3fa76b..3cbc977d8 100644 --- a/web/pgadmin/browser/collection.py +++ b/web/pgadmin/browser/collection.py @@ -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') + ) diff --git a/web/pgadmin/browser/server_groups/__init__.py b/web/pgadmin/browser/server_groups/__init__.py index 08c0e6788..dc9deb207 100644 --- a/web/pgadmin/browser/server_groups/__init__.py +++ b/web/pgadmin/browser/server_groups/__init__.py @@ -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, diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index 4ca738758..3652f751b 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -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) diff --git a/web/pgadmin/browser/server_groups/servers/databases/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/__init__.py index 320152e6d..82dfc9860 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/__init__.py @@ -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 ) diff --git a/web/pgadmin/browser/server_groups/servers/roles/__init__.py b/web/pgadmin/browser/server_groups/servers/roles/__init__.py index 798a6d373..23c5d8a68 100644 --- a/web/pgadmin/browser/server_groups/servers/roles/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/roles/__init__.py @@ -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']): diff --git a/web/pgadmin/browser/server_groups/servers/tablespaces/__init__.py b/web/pgadmin/browser/server_groups/servers/tablespaces/__init__.py index a1a380ae9..555f15761 100644 --- a/web/pgadmin/browser/server_groups/servers/tablespaces/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/tablespaces/__init__.py @@ -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 diff --git a/web/pgadmin/browser/utils.py b/web/pgadmin/browser/utils.py index db442243e..d7fd89523 100644 --- a/web/pgadmin/browser/utils.py +++ b/web/pgadmin/browser/utils.py @@ -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 diff --git a/web/pgadmin/settings/settings_model.py b/web/pgadmin/model/__init__.py similarity index 74% rename from web/pgadmin/settings/settings_model.py rename to web/pgadmin/model/__init__.py index 4a13f9516..253bd67d9 100644 --- a/web/pgadmin/settings/settings_model.py +++ b/web/pgadmin/model/__init__.py @@ -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) diff --git a/web/pgadmin/preferences/__init__.py b/web/pgadmin/preferences/__init__.py new file mode 100644 index 000000000..05a35e1fb --- /dev/null +++ b/web/pgadmin/preferences/__init__.py @@ -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/", 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() diff --git a/web/pgadmin/preferences/static/css/preferences.css b/web/pgadmin/preferences/static/css/preferences.css new file mode 100644 index 000000000..7946f9049 --- /dev/null +++ b/web/pgadmin/preferences/static/css/preferences.css @@ -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; + } +} diff --git a/web/pgadmin/preferences/templates/preferences/index.html b/web/pgadmin/preferences/templates/preferences/index.html new file mode 100644 index 000000000..a62785835 --- /dev/null +++ b/web/pgadmin/preferences/templates/preferences/index.html @@ -0,0 +1,8 @@ +
+
+ ACI TREE +
+
+ Right hand side content +
+
\ No newline at end of file diff --git a/web/pgadmin/preferences/templates/preferences/preferences.js b/web/pgadmin/preferences/templates/preferences/preferences.js new file mode 100644 index 000000000..a8c7c4f04 --- /dev/null +++ b/web/pgadmin/preferences/templates/preferences/preferences.js @@ -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 = $("
"); + + + /* + * 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( + "
" + ).append( + "
" + + " {{ _('Category is not selected.')|safe }}" + + "
" + ); + + // 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; + }); diff --git a/web/pgadmin/settings/__init__.py b/web/pgadmin/settings/__init__.py index bf4c32c36..a245affc2 100644 --- a/web/pgadmin/settings/__init__.py +++ b/web/pgadmin/settings/__init__.py @@ -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: diff --git a/web/pgadmin/static/js/backform.pgadmin.js b/web/pgadmin/static/js/backform.pgadmin.js index 6faa8aae2..6e7d06203 100644 --- a/web/pgadmin/static/js/backform.pgadmin.js +++ b/web/pgadmin/static/js/backform.pgadmin.js @@ -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([ '
>', + ' <% if (legend != false) { %>', ' data-target="#<%=cId%>"><%=collapse ? "" : "" %><%=label%>', - ' ', + ' <% } %>', '
' ].join("\n")), 'content': _.template( diff --git a/web/pgadmin/utils/__init__.py b/web/pgadmin/utils/__init__.py index cfe872947..6d1eaa74e 100644 --- a/web/pgadmin/utils/__init__.py +++ b/web/pgadmin/utils/__init__.py @@ -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) diff --git a/web/pgadmin/utils/driver/psycopg2/__init__.py b/web/pgadmin/utils/driver/psycopg2/__init__.py index f3887aa72..382d66c7f 100644 --- a/web/pgadmin/utils/driver/psycopg2/__init__.py +++ b/web/pgadmin/utils/driver/psycopg2/__init__.py @@ -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) diff --git a/web/pgadmin/utils/preferences.py b/web/pgadmin/utils/preferences.py new file mode 100644 index 000000000..7c2f03261 --- /dev/null +++ b/web/pgadmin/utils/preferences.py @@ -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 diff --git a/web/setup.py b/web/setup.py index ae47ac34d..5b407c1e0 100644 --- a/web/setup.py +++ b/web/setup.py @@ -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)