mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Introduced a class - NodeView to achieve REST API required by the
PostgreSQL node(s).
Browser Tree Node (PostgreSQL object) requires more than just CRUD.
i.e.
- CRUD (Create, Read, Update & Delete)
- Reversed Engineered SQL for the object
- Modified Query in edit mode
i.e. ALTER TABLE ...
- Statistics
- List of dependents
- List of dependencies
- Children node list
This class can be inherited to achieve the different routes for each of
the object types/collections.
OPERATION | URL | Method
---------------+------------------------+--------
List | /obj/[Parent URL]/ | GET
Properties | /obj/[Parent URL]/id | GET
Create | /obj/[Parent URL]/ | POST
Delete | /obj/[Parent URL]/id | DELETE
Update | /obj/[Parent URL]/id | PUT
SQL (Reversed | /sql/[Parent URL]/id | GET
Engineering) |
SQL (Modified | /sql/[Parent URL]/id | POST
Properties) |
Statistics | /stats/[Parent URL]/id | GET
Dependencies | /deps/[Parent URL]/id | GET
Dependents | /deps/[Parent URL]/id | POST
Children Nodes | /nodes/[Parent URL]/id | GET
NOTE:
Parent URL can be seen as the path to identify the particular node.
i.e.
In order to identify the TABLE object, we requires information
about the server -> database -> schema objects.
Hence, the Parent URL for the TABLE object will be something like
this as below:
<int:sid>/<str:database>/<str:schema>
Inherited a new classes ServerGroupView and ServerView, which are
inherited from the NodeView for the implementation of above operations.
This commit is contained in:
@@ -50,8 +50,8 @@ if not os.path.isfile(config.SQLITE_PATH):
|
||||
# Create the app!
|
||||
app = create_app()
|
||||
|
||||
#if config.DEBUG:
|
||||
# app.debug = True
|
||||
if config.DEBUG:
|
||||
app.debug = True
|
||||
|
||||
# Start the web server. The port number should have already been set by the
|
||||
# runtime if we're running in desktop mode, otherwise we'll just use the
|
||||
|
||||
@@ -21,25 +21,23 @@ MODULE_NAME = 'browser'
|
||||
class BrowserModule(PgAdminModule):
|
||||
|
||||
|
||||
|
||||
def get_own_stylesheets(self):
|
||||
stylesheets = []
|
||||
# Add browser stylesheets
|
||||
for (endpoint, filename) in [
|
||||
('static', 'css/codemirror/codemirror.css'),
|
||||
('static', 'css/wcDocker/theme.css'),
|
||||
('static', 'css/jQuery-contextMenu/jquery.contextMenu.css'),
|
||||
('browser.static', 'css/browser.css'),
|
||||
('browser.static', 'css/aciTree/css/aciTree.css')
|
||||
('static', 'css/wcDocker/wcDockerSkeleton.css' if \
|
||||
current_app.debug else \
|
||||
'css/wcDocker/wcDockerSkeleton.min.css'),
|
||||
('static', 'css/wcDocker/theme.css'),
|
||||
('browser.static', 'css/aciTree/css/aciTree.css'),
|
||||
]:
|
||||
stylesheets.append(url_for(endpoint, filename=filename))
|
||||
stylesheets.append(url_for('browser.browser_css'))
|
||||
if current_app.debug:
|
||||
stylesheets.append(url_for('static', filename='css/wcDocker/wcDockerSkeleton.css'))
|
||||
else:
|
||||
stylesheets.append(url_for('static', filename='css/wcDocker/wcDockerSkeleton.min.css'))
|
||||
return stylesheets
|
||||
|
||||
|
||||
def get_own_javascripts(self):
|
||||
scripts = []
|
||||
for (endpoint, filename) in [
|
||||
@@ -74,7 +72,7 @@ class BrowserPluginModule(PgAdminModule):
|
||||
|
||||
def __init__(self, import_name, **kwargs):
|
||||
kwargs.setdefault("url_prefix", self.node_path)
|
||||
kwargs.setdefault("static_url_path", '')
|
||||
kwargs.setdefault("static_url_path", 'static')
|
||||
super(BrowserPluginModule, self).__init__("NODE-%s" % self.node_type,
|
||||
import_name,
|
||||
**kwargs)
|
||||
@@ -95,8 +93,8 @@ class BrowserPluginModule(PgAdminModule):
|
||||
Returns a snippet of css to include in the page
|
||||
"""
|
||||
# TODO: move those methods to BrowserModule subclass ?
|
||||
return render_template("browser/css/node.css",
|
||||
node_type=self.node_type)
|
||||
return [render_template("browser/css/node.css",
|
||||
node_type=self.node_type)]
|
||||
|
||||
@abstractmethod
|
||||
def get_nodes(self):
|
||||
@@ -137,11 +135,12 @@ def browser_js():
|
||||
snippets = []
|
||||
for submodule in current_blueprint.submodules:
|
||||
snippets.extend(submodule.jssnippets)
|
||||
return make_response(render_template(
|
||||
'browser/js/browser.js',
|
||||
layout=layout,
|
||||
jssnippets=snippets),
|
||||
200, {'Content-Type': 'application/x-javascript'})
|
||||
return make_response(
|
||||
render_template(
|
||||
'browser/js/browser.js',
|
||||
layout=layout,
|
||||
jssnippets=snippets),
|
||||
200, {'Content-Type': 'application/x-javascript'})
|
||||
|
||||
@blueprint.route("/browser.css")
|
||||
@login_required
|
||||
@@ -150,9 +149,9 @@ def browser_css():
|
||||
snippets = []
|
||||
for submodule in current_blueprint.submodules:
|
||||
snippets.extend(submodule.csssnippets)
|
||||
return make_response(render_template('browser/css/browser.css',
|
||||
snippets=snippets),
|
||||
200, {'Content-Type': 'text/css'})
|
||||
return make_response(
|
||||
render_template('browser/css/browser.css', snippets=snippets),
|
||||
200, {'Content-Type': 'text/css'})
|
||||
|
||||
|
||||
@blueprint.route("/nodes/")
|
||||
|
||||
@@ -18,10 +18,10 @@ from pgadmin.utils.ajax import make_json_response
|
||||
from pgadmin.browser import BrowserPluginModule
|
||||
from pgadmin.utils.menu import MenuItem
|
||||
from pgadmin.settings.settings_model import db, ServerGroup
|
||||
from pgadmin.browser.utils import generate_browser_node
|
||||
import config
|
||||
|
||||
|
||||
|
||||
class ServerGroupModule(BrowserPluginModule):
|
||||
|
||||
NODE_TYPE = "server-group"
|
||||
@@ -65,18 +65,20 @@ class ServerGroupModule(BrowserPluginModule):
|
||||
# TODO: Move this JSON generation to a Server method
|
||||
# this code is duplicated somewhere else
|
||||
for group in groups:
|
||||
yield {
|
||||
"id": "%s/%d" % (self.node_type, group.id),
|
||||
"label": group.name,
|
||||
"icon": "icon-%s" % self.node_type,
|
||||
"inode": True,
|
||||
"_type": self.node_type
|
||||
}
|
||||
yield generate_browser_node(
|
||||
"%d" % (group.id),
|
||||
group.name,
|
||||
"icon-%s" % self.node_type,
|
||||
True,
|
||||
self.node_type)
|
||||
|
||||
@property
|
||||
def node_type(self):
|
||||
return self.NODE_TYPE
|
||||
|
||||
@property
|
||||
def node_path(self):
|
||||
return '/browser/' + self.node_type
|
||||
|
||||
|
||||
class ServerGroupMenuItem(MenuItem):
|
||||
@@ -99,109 +101,142 @@ class ServerGroupPluginModule(BrowserPluginModule):
|
||||
pass
|
||||
|
||||
|
||||
# Initialise the module
|
||||
@property
|
||||
def node_path(self):
|
||||
return '/browser/' + self.node_type
|
||||
|
||||
|
||||
blueprint = ServerGroupModule( __name__, static_url_path='')
|
||||
|
||||
@blueprint.route("/<server_group>")
|
||||
@login_required
|
||||
def get_nodes(server_group):
|
||||
"""Build a list of treeview nodes from the child nodes."""
|
||||
nodes = []
|
||||
for module in current_blueprint.submodules:
|
||||
nodes.extend(module.get_nodes(server_group=server_group))
|
||||
return make_json_response(data=nodes)
|
||||
# Initialise the module
|
||||
from pgadmin.browser.utils import NodeView
|
||||
|
||||
|
||||
@blueprint.route('/add/', methods=['POST'])
|
||||
@login_required
|
||||
def add():
|
||||
"""Add a server group node to the settings database"""
|
||||
success = 1
|
||||
errormsg = ''
|
||||
data = { }
|
||||
class ServerGroupView(NodeView):
|
||||
|
||||
if request.form['name'] != '':
|
||||
servergroup = ServerGroup(user_id=current_user.id, name=request.form['name'])
|
||||
node_type = ServerGroupModule.NODE_TYPE
|
||||
parent_ids = []
|
||||
ids = [{'type':'int', 'id':'gid'}]
|
||||
|
||||
try:
|
||||
db.session.add(servergroup)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
success = 0
|
||||
errormsg = e.message
|
||||
|
||||
else:
|
||||
success = 0
|
||||
errormsg = gettext('No server group name was specified')
|
||||
def list(self):
|
||||
res = []
|
||||
for g in blueprint.get_nodes():
|
||||
res.append(g)
|
||||
return make_json_response(result=res)
|
||||
|
||||
if success == 1:
|
||||
data['id'] = servergroup.id
|
||||
data['name'] = servergroup.name
|
||||
|
||||
return make_json_response(success=success,
|
||||
errormsg=errormsg,
|
||||
info=traceback.format_exc(),
|
||||
result=request.form,
|
||||
data=data)
|
||||
def delete(self, gid):
|
||||
"""Delete a server group node in the settings database"""
|
||||
|
||||
@blueprint.route('/delete/', methods=['POST'])
|
||||
@login_required
|
||||
def delete():
|
||||
"""Delete a server group node in the settings database"""
|
||||
success = 1
|
||||
errormsg = ''
|
||||
|
||||
if request.form['id'] != '':
|
||||
# There can be only one record at most
|
||||
servergroup = ServerGroup.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first()
|
||||
servergroup = ServerGroup.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
id=gid)
|
||||
|
||||
if servergroup is None:
|
||||
success = 0
|
||||
errormsg = gettext('The specified server group could not be found.')
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=gettext('The specified server group could not be found.'))
|
||||
else:
|
||||
try:
|
||||
db.session.delete(servergroup)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
success = 0
|
||||
errormsg = e.message
|
||||
return make_json_response(success=0, errormsg=e.message)
|
||||
|
||||
else:
|
||||
success = 0
|
||||
errormsg = gettext('No server group was specified.')
|
||||
return make_json_response(result=request.form)
|
||||
|
||||
return make_json_response(success=success,
|
||||
errormsg=errormsg,
|
||||
info=traceback.format_exc(),
|
||||
result=request.form)
|
||||
|
||||
@blueprint.route('/rename/', methods=['POST'])
|
||||
@login_required
|
||||
def rename():
|
||||
"""Rename a server group node in the settings database"""
|
||||
success = 1
|
||||
errormsg = ''
|
||||
def update(self, gid):
|
||||
"""Update the server-group properties"""
|
||||
|
||||
if request.form['id'] != '':
|
||||
# There can be only one record at most
|
||||
servergroup = ServerGroup.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first()
|
||||
servergroup = ServerGroup.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
id=gid).first()
|
||||
|
||||
if servergroup is None:
|
||||
success = 0
|
||||
errormsg = gettext('The specified server group could not be found.')
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=gettext('The specified server group could not be found.'))
|
||||
else:
|
||||
try:
|
||||
servergroup.name = request.form['name']
|
||||
if 'name' in request.form:
|
||||
servergroup.name = request.form['name']
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
success = 0
|
||||
errormsg = e.message
|
||||
return make_json_response(success=0, errormsg=e.message)
|
||||
|
||||
else:
|
||||
success = 0
|
||||
errormsg = gettext('No server group was specified.')
|
||||
return make_json_response(result=request.form)
|
||||
|
||||
return make_json_response(success=success,
|
||||
errormsg=errormsg,
|
||||
info=traceback.format_exc(),
|
||||
result=request.form)
|
||||
|
||||
def properties(self, gid):
|
||||
"""Update the server-group properties"""
|
||||
|
||||
# There can be only one record at most
|
||||
sg = ServerGroup.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
id=gid).first()
|
||||
data = {}
|
||||
|
||||
if sg is None:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=gettext('The specified server group could not be found.'))
|
||||
else:
|
||||
return make_json_response(data={'id': sg.id, 'name': sg.name})
|
||||
|
||||
|
||||
def create(self):
|
||||
data = []
|
||||
if request.form['name'] != '':
|
||||
servergroup = ServerGroup(
|
||||
user_id=current_user.id,
|
||||
name=request.form['name'])
|
||||
try:
|
||||
db.session.add(servergroup)
|
||||
db.session.commit()
|
||||
|
||||
data['id'] = servergroup.id
|
||||
data['name'] = servergroup.name
|
||||
except Exception as e:
|
||||
return make_json_response(success=0, errormsg=e.message)
|
||||
|
||||
else:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=gettext('No server group name was specified'))
|
||||
|
||||
return make_json_response(data=data)
|
||||
|
||||
|
||||
def nodes(self, gid):
|
||||
"""Build a list of treeview nodes from the child nodes."""
|
||||
nodes = []
|
||||
for module in blueprint.submodules:
|
||||
nodes.extend(module.get_nodes(server_group=gid))
|
||||
return make_json_response(data=nodes)
|
||||
|
||||
|
||||
def sql(self, gid):
|
||||
return make_json_response(data='')
|
||||
|
||||
|
||||
def modified_sql(self, gid):
|
||||
return make_json_response(data='')
|
||||
|
||||
|
||||
def statistics(self, gid):
|
||||
return make_json_response(data='')
|
||||
|
||||
|
||||
def dependencies(self, gid):
|
||||
return make_json_response(data='')
|
||||
|
||||
|
||||
def dependents(self, gid):
|
||||
return make_json_response(data='')
|
||||
|
||||
|
||||
ServerGroupView.register_node_view(blueprint)
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
from flask import render_template, request
|
||||
from pgadmin.browser.server_groups import ServerGroupPluginModule
|
||||
from flask.ext.security import login_required, current_user
|
||||
from pgadmin.settings.settings_model import db, Server
|
||||
from pgadmin.settings.settings_model import db, Server, ServerGroup
|
||||
from pgadmin.utils.menu import MenuItem
|
||||
from pgadmin.utils.ajax import make_json_response
|
||||
from pgadmin.browser.utils import generate_browser_node, NodeView
|
||||
import traceback
|
||||
from flask.ext.babel import gettext
|
||||
|
||||
@@ -30,13 +31,13 @@ class ServerModule(ServerGroupPluginModule):
|
||||
|
||||
# TODO: Move this JSON generation to a Server method
|
||||
for server in servers:
|
||||
yield {
|
||||
"id": "%s/%d" % (NODE_TYPE, server.id),
|
||||
"label": server.name,
|
||||
"icon": "icon-%s" % NODE_TYPE,
|
||||
"inode": True,
|
||||
"_type": NODE_TYPE
|
||||
}
|
||||
yield generate_browser_node(
|
||||
"%d" % server.id,
|
||||
server.name,
|
||||
"icon-%s" % self.NODE_TYPE,
|
||||
True,
|
||||
self.NODE_TYPE
|
||||
)
|
||||
|
||||
def get_own_menuitems(self):
|
||||
return {
|
||||
@@ -49,13 +50,13 @@ class ServerModule(ServerGroupPluginModule):
|
||||
name="create_server",
|
||||
label=gettext('Server...'),
|
||||
priority=50,
|
||||
function='create_server')
|
||||
function='create_server(item)')
|
||||
],
|
||||
'context_items': [
|
||||
ServerMenuItem(name='delete_server',
|
||||
label=gettext('Delete server'),
|
||||
priority=50,
|
||||
onclick='drop_server'),
|
||||
onclick='drop_server(item)'),
|
||||
ServerMenuItem(name='rename_server',
|
||||
label=gettext('Rename server...'),
|
||||
priority=60,
|
||||
@@ -75,98 +76,224 @@ class ServerMenuItem(MenuItem):
|
||||
kwargs.setdefault("type", ServerModule.NODE_TYPE)
|
||||
super(ServerMenuItem, self).__init__(**kwargs)
|
||||
|
||||
|
||||
blueprint = ServerModule(__name__)
|
||||
|
||||
@blueprint.route('/add/', methods=['POST'])
|
||||
@login_required
|
||||
def add():
|
||||
"""Add a server node to the settings database"""
|
||||
success = 1
|
||||
errormsg = ''
|
||||
data = {}
|
||||
|
||||
success = False
|
||||
errormsg = ''
|
||||
if request.form['name'] != '':
|
||||
server = Server(user_id=current_user.id, name=request.form['name'])
|
||||
try:
|
||||
db.session.add(server)
|
||||
db.session.commit()
|
||||
success = True
|
||||
except Exception as e:
|
||||
errormsg = e.message
|
||||
else:
|
||||
errormsg = gettext('No server name was specified')
|
||||
class ServerNode(NodeView):
|
||||
|
||||
if success:
|
||||
data['id'] = server.id
|
||||
data['name'] = server.name
|
||||
node_type = ServerModule.NODE_TYPE
|
||||
parent_ids = [{'type':'int', 'id':'gid'}]
|
||||
ids = [{'type':'int', 'id':'sid'}]
|
||||
|
||||
return make_json_response(success=success,
|
||||
errormsg=errormsg,
|
||||
info=traceback.format_exc(),
|
||||
result=request.form,
|
||||
data=data)
|
||||
|
||||
@blueprint.route('/delete/', methods=['POST'])
|
||||
@login_required
|
||||
def delete():
|
||||
"""Delete a server node in the settings database"""
|
||||
success = 1
|
||||
errormsg = ''
|
||||
def list(self, gid):
|
||||
res = []
|
||||
"""Return a JSON document listing the server groups for the user"""
|
||||
servers = Server.query.filter_by(user_id=current_user.id,
|
||||
servergroup_id=gid)
|
||||
|
||||
if request.form['id'] != '':
|
||||
# There can be only one record at most
|
||||
servergroup = Server.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first()
|
||||
for server in servers:
|
||||
res.append(
|
||||
generate_browser_node(
|
||||
"%d/%d" % (gid, server.id),
|
||||
server.name,
|
||||
"icon-%s" % NODE_TYPE,
|
||||
True,
|
||||
NODE_TYPE
|
||||
)
|
||||
)
|
||||
return make_json_response(result=res)
|
||||
|
||||
|
||||
def delete(self, gid, sid):
|
||||
"""Delete a server node in the settings database"""
|
||||
server = Server.query.filter_by(user_id=current_user.id, id=sid)
|
||||
|
||||
# TODO:: A server, which is connected, can not be deleted
|
||||
if server is None:
|
||||
success = 0
|
||||
errormsg = gettext('The specified server could not be found.')
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=gettext(
|
||||
'The specified server could not be found.\n'
|
||||
'Does the user have permission to access the '
|
||||
'server?'
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
db.session.delete(server)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
success = 0
|
||||
errormsg = e.message
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=e.message)
|
||||
|
||||
else:
|
||||
success = 0
|
||||
errormsg = gettext('No server was specified.')
|
||||
return make_json_response(success=success,
|
||||
errormsg=errormsg,
|
||||
info=traceback.format_exc())
|
||||
|
||||
return make_json_response(success=success,
|
||||
errormsg=errormsg,
|
||||
info=traceback.format_exc(),
|
||||
result=request.form)
|
||||
|
||||
@blueprint.route('/rename/', methods=['POST'])
|
||||
@login_required
|
||||
def rename():
|
||||
"""Rename a server node in the settings database"""
|
||||
success = 1
|
||||
errormsg = ''
|
||||
|
||||
if request.form['id'] != '':
|
||||
# There can be only one record at most
|
||||
servergroup = Server.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first()
|
||||
def update(self, gid, sid):
|
||||
"""Update the server settings"""
|
||||
server = Server.query.filter_by(user_id=current_user.id, id=sid).first()
|
||||
|
||||
if server is None:
|
||||
success = 0
|
||||
errormsg = gettext('The specified server could not be found.')
|
||||
else:
|
||||
try:
|
||||
server.name = request.form['name']
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
success = 0
|
||||
errormsg = e.message
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=gettext("Couldn't find the given server.")
|
||||
)
|
||||
|
||||
else:
|
||||
success = 0
|
||||
errormsg = gettext('No server was specified.')
|
||||
# TODO::
|
||||
# Not all parameters can be modified, while the server is connected
|
||||
possible_args = {
|
||||
'name': 'name',
|
||||
'host': 'host',
|
||||
'port': 'port',
|
||||
'db': 'maintenance_db',
|
||||
'username': 'username',
|
||||
'sslmode': 'sslmode',
|
||||
'gid': 'servergroup_id'
|
||||
}
|
||||
|
||||
return make_json_response(success=success,
|
||||
errormsg=errormsg,
|
||||
info=traceback.format_exc(),
|
||||
result=request.form)
|
||||
idx = 0
|
||||
for arg in possible_args:
|
||||
if arg in request.form:
|
||||
server[possible_args[arg]] = request.form[arg]
|
||||
idx += 1
|
||||
|
||||
if idx == 0:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=gettext('No parameters were chagned!')
|
||||
)
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=e.message
|
||||
)
|
||||
|
||||
return make_json_response(
|
||||
success=1,
|
||||
data={
|
||||
'id': server.id,
|
||||
'gid': server.servergroup_id
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def properties(self, gid, sid):
|
||||
"""Return list of attributes of a server"""
|
||||
server = Server.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
id=sid).first()
|
||||
|
||||
if server is None:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=gettext("Couldn't find the given server")
|
||||
)
|
||||
|
||||
sg = ServerGroup.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
id=server.servergroup_id
|
||||
).first()
|
||||
|
||||
return make_json_response(
|
||||
success=1,
|
||||
data={
|
||||
'id':server.id,
|
||||
'name':server.name,
|
||||
'host':server.host,
|
||||
'port':server.port,
|
||||
'db':server.maintenance_db,
|
||||
'username':server.username,
|
||||
'gid':server.servergroup_id,
|
||||
'group-name':sg.name
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def create(self, gid):
|
||||
"""Add a server node to the settings database"""
|
||||
required_args = [
|
||||
'name',
|
||||
'host',
|
||||
'port',
|
||||
'db',
|
||||
'username',
|
||||
'sslmode'
|
||||
]
|
||||
|
||||
for arg in required_args:
|
||||
if arg not in request.form:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=gettext(
|
||||
"Couldn't find the required parameter (%s)." % arg
|
||||
)
|
||||
)
|
||||
|
||||
server = Server(
|
||||
user_id=current_user.id,
|
||||
servergroup_id=gid,
|
||||
name=request.form['name'],
|
||||
host=request.form['host'],
|
||||
port=request.form['port'],
|
||||
maintenance_db=request.form['db'],
|
||||
username=request.form['username'],
|
||||
sslmode=request.form['username']
|
||||
)
|
||||
|
||||
try:
|
||||
db.session.add(server)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=e.message
|
||||
)
|
||||
|
||||
return make_json_response(success=1,
|
||||
data={
|
||||
'id': server.id,
|
||||
'name': server.name,
|
||||
'gid': gid
|
||||
})
|
||||
|
||||
|
||||
def nodes(self, gid, sid):
|
||||
"""Build a list of treeview nodes from the child nodes."""
|
||||
nodes = []
|
||||
# TODO::
|
||||
# We can have nodes for the server object, only when
|
||||
# the server is connected at the moment.
|
||||
for module in blueprint.submodules:
|
||||
nodes.extend(module.get_nodes(server=sid))
|
||||
return make_json_response(data=nodes)
|
||||
|
||||
|
||||
def sql(self, gid, sid):
|
||||
return make_json_response(data='')
|
||||
|
||||
|
||||
def modified_sql(self, gid, sid):
|
||||
return make_json_response(data='')
|
||||
|
||||
|
||||
def statistics(self, gid, sid):
|
||||
return make_json_response(data='')
|
||||
|
||||
|
||||
def dependencies(self, gid, sid):
|
||||
return make_json_response(data='')
|
||||
|
||||
|
||||
def dependents(self, gid, sid):
|
||||
return make_json_response(data='')
|
||||
|
||||
|
||||
ServerNode.register_node_view(blueprint)
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
// Add a server
|
||||
function create_server() {
|
||||
function create_server(item) {
|
||||
var alert = alertify.prompt(
|
||||
'{{ _('Create a server') }}',
|
||||
'{{ _('Enter a name for the new server') }}',
|
||||
'',
|
||||
function(evt, value) {
|
||||
$.post("{{ url_for('NODE-server.add') }}", { name: value })
|
||||
var d = tree.itemData(item);
|
||||
if (d._type != 'server-group') {
|
||||
d = tree.itemData(tree.parent(item));
|
||||
}
|
||||
$.post(
|
||||
"{{ url_for('browser.index') }}server/obj/" + d.refid + '/',
|
||||
{ name: value }
|
||||
)
|
||||
.done(function(data) {
|
||||
if (data.success == 0) {
|
||||
report_error(data.errormsg, data.info);
|
||||
@@ -38,8 +45,10 @@ function drop_server(item) {
|
||||
'{{ _('Are you sure you wish to drop the server "{0}"?') }}'.replace('{0}', tree.getLabel(item)),
|
||||
function() {
|
||||
var id = tree.getId(item).split('/').pop()
|
||||
$.post("{{ url_for('NODE-server.delete') }}", { id: id })
|
||||
.done(function(data) {
|
||||
$.ajax({
|
||||
url:"{{ url_for('browser.index') }}" + d._type + "/obj/" + d.refid,
|
||||
type:'DELETE',
|
||||
success: function(data) {
|
||||
if (data.success == 0) {
|
||||
report_error(data.errormsg, data.info);
|
||||
} else {
|
||||
@@ -53,7 +62,7 @@ function drop_server(item) {
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
null
|
||||
)
|
||||
@@ -66,17 +75,20 @@ function rename_server(item) {
|
||||
'{{ _('Enter a new name for the server') }}',
|
||||
tree.getLabel(item),
|
||||
function(evt, value) {
|
||||
var id = tree.getId(item).split('/').pop()
|
||||
$.post("{{ url_for('NODE-server.rename') }}", { id: id, name: value })
|
||||
.done(function(data) {
|
||||
var d = tree.itemData(item);
|
||||
$.ajax({
|
||||
url:"{{ url_for('browser.index') }}" + d._type + "/obj/" + d.refid,
|
||||
type:'PUT',
|
||||
params: {name: value},
|
||||
success: function(data) {
|
||||
if (data.success == 0) {
|
||||
report_error(data.errormsg, data.info);
|
||||
} else {
|
||||
tree.setLabel(item, { label: value });
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ function create_server_group() {
|
||||
'{{ _('Enter a name for the new server group') }}',
|
||||
'',
|
||||
function(evt, value) {
|
||||
$.post("{{ url_for('NODE-server-group.add') }}", { name: value })
|
||||
$.post("{{ url_for('browser.index') }}server-group/obj/", { name: value })
|
||||
.done(function(data) {
|
||||
if (data.success == 0) {
|
||||
report_error(data.errormsg, data.info);
|
||||
@@ -37,9 +37,11 @@ function drop_server_group(item) {
|
||||
'{{ _('Delete server group?') }}',
|
||||
'{{ _('Are you sure you wish to delete the server group "{0}"?') }}'.replace('{0}', tree.getLabel(item)),
|
||||
function() {
|
||||
var id = tree.getId(item).split('/').pop()
|
||||
$.post("{{ url_for('NODE-server-group.delete') }}", { id: id })
|
||||
.done(function(data) {
|
||||
var d = tree.itemData(item);
|
||||
$.ajax({
|
||||
url:"{{ url_for('browser.index') }}" + d._type + "/obj/" + d.refid,
|
||||
type:'DELETE',
|
||||
success: function(data) {
|
||||
if (data.success == 0) {
|
||||
report_error(data.errormsg, data.info);
|
||||
} else {
|
||||
@@ -53,7 +55,7 @@ function drop_server_group(item) {
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
null
|
||||
)
|
||||
@@ -66,16 +68,19 @@ function rename_server_group(item) {
|
||||
'{{ _('Enter a new name for the server group') }}',
|
||||
tree.getLabel(item),
|
||||
function(evt, value) {
|
||||
var id = tree.getId(item).split('/').pop()
|
||||
$.post("{{ url_for('NODE-server-group.rename') }}", { id: id, name: value })
|
||||
.done(function(data) {
|
||||
var d = tree.itemData(item);
|
||||
$.ajax({
|
||||
url:"{{ url_for('browser.index') }}" + d._type + "/obj/" + d.refid,
|
||||
type:'PUT',
|
||||
params: { name: value },
|
||||
success: function(data) {
|
||||
if (data.success == 0) {
|
||||
report_error(data.errormsg, data.info);
|
||||
} else {
|
||||
tree.setLabel(item, { label: value });
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
null
|
||||
)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.icon-{{node_type}} {
|
||||
background: url('{{ url_for('NODE-%s.static' % node_type, filename='img/%s.png' % node_type )}}') 0 0 no-repeat;
|
||||
background-image: url('{{ url_for('NODE-%s.static' % node_type, filename='img/%s.png' % node_type )}}') !important;
|
||||
}
|
||||
|
||||
@@ -298,6 +298,12 @@ ALTER TABLE tickets_detail \n\
|
||||
return $.parseJSON(payload).data;
|
||||
}
|
||||
}
|
||||
},
|
||||
ajaxHook: function(item, settings) {
|
||||
if (item != null) {
|
||||
var d = this.itemData(item);
|
||||
settings.url = '{{ url_for('browser.index') }}' + d._type + '/nodes/' + d.refid
|
||||
}
|
||||
}
|
||||
});
|
||||
tree = $('#tree').aciTree('api');
|
||||
|
||||
@@ -9,45 +9,173 @@
|
||||
|
||||
"""Browser helper utilities"""
|
||||
|
||||
import os, sys
|
||||
import config
|
||||
from flask import request
|
||||
from flask.views import View, MethodViewType, with_metaclass
|
||||
|
||||
def register_modules(app, file, all_nodes, sub_nodes, prefix):
|
||||
"""Register any child node blueprints for the specified file"""
|
||||
path = os.path.dirname(os.path.realpath(file))
|
||||
files = os.listdir(path)
|
||||
|
||||
for f in files:
|
||||
d = os.path.join(path, f)
|
||||
if os.path.isdir(d) and os.path.isfile(os.path.join(d, '__init__.py')):
|
||||
def generate_browser_node(node_id, label, icon, inode, node_type):
|
||||
return {
|
||||
"id": "%s/%s" % (node_type, node_id),
|
||||
"label": label,
|
||||
"icon": icon,
|
||||
"inode": inode,
|
||||
"_type": node_type,
|
||||
"refid": node_id
|
||||
}
|
||||
|
||||
if f in config.NODE_BLACKLIST:
|
||||
app.logger.info('Skipping blacklisted node: %s' % f)
|
||||
continue
|
||||
|
||||
# Construct the 'real' module name
|
||||
if prefix != '':
|
||||
f = prefix + '.' + f
|
||||
|
||||
# Looks like a node, so import it, and register the blueprint if present
|
||||
# We rely on the ordering of syspath to ensure we actually get the right
|
||||
# module here.
|
||||
app.logger.info('Examining potential node: %s' % d)
|
||||
node = __import__(f, globals(), locals(), ['hooks', 'views'], -1)
|
||||
class NodeView(with_metaclass(MethodViewType, View)):
|
||||
"""
|
||||
A PostgreSQL Object has so many operaions/functions apart from CRUD
|
||||
(Create, Read, Update, Delete):
|
||||
i.e.
|
||||
- Reversed Engineered SQL
|
||||
- Modified Query for parameter while editing object attributes
|
||||
i.e. ALTER TABLE ...
|
||||
- Statistics of the objects
|
||||
- List of dependents
|
||||
- List of dependencies
|
||||
- Listing of the children object types for the certain node
|
||||
It will used by the browser tree to get the children nodes
|
||||
|
||||
# Add the node to the node lists
|
||||
all_nodes.append(node)
|
||||
sub_nodes.append(node)
|
||||
|
||||
# Register the blueprint if present
|
||||
if 'views' in dir(node) and 'blueprint' in dir(node.views):
|
||||
app.logger.info('Registering blueprint node: %s' % f)
|
||||
app.register_blueprint(node.views.blueprint)
|
||||
app.logger.debug(' - root_path: %s' % node.views.blueprint.root_path)
|
||||
app.logger.debug(' - static_folder: %s' % node.views.blueprint.static_folder)
|
||||
app.logger.debug(' - template_folder: %s' % node.views.blueprint.template_folder)
|
||||
|
||||
# Register any sub-modules
|
||||
if 'hooks' in dir(node) and 'register_submodules' in dir(node.hooks):
|
||||
app.logger.info('Registering sub-modules in %s' % f)
|
||||
node.hooks.register_submodules(app)
|
||||
This class can be inherited to achieve the diffrent routes for each of the
|
||||
object types/collections.
|
||||
|
||||
OPERATION | URL | Method
|
||||
---------------+------------------------+--------
|
||||
List | /obj/[Parent URL]/ | GET
|
||||
Properties | /obj/[Parent URL]/id | GET
|
||||
Create | /obj/[Parent URL]/ | POST
|
||||
Delete | /obj/[Parent URL]/id | DELETE
|
||||
Update | /obj/[Parent URL]/id | PUT
|
||||
|
||||
SQL (Reversed | /sql/[Parent URL]/id | GET
|
||||
Engineering) |
|
||||
SQL (Modified | /sql/[Parent URL]/id | POST
|
||||
Properties) |
|
||||
|
||||
Statistics | /stats/[Parent URL]/id | GET
|
||||
Dependencies | /deps/[Parent URL]/id | GET
|
||||
Dependents | /deps/[Parent URL]/id | POST
|
||||
|
||||
Children Nodes | /nodes/[Parent URL]/id | GET
|
||||
|
||||
NOTE:
|
||||
Parent URL can be seen as the path to identify the particular node.
|
||||
|
||||
i.e.
|
||||
In order to identify the TABLE object, we need server -> database -> schema
|
||||
information.
|
||||
"""
|
||||
operations = {
|
||||
'obj': [
|
||||
{'get': 'properties', 'delete': 'delete', 'put': 'update'},
|
||||
{'get': 'list', 'post': 'create'}
|
||||
],
|
||||
'nodes': [{'get': 'nodes'}, {}],
|
||||
'sql': [{'get': 'sql', 'post': 'modified_sql'}, {}],
|
||||
'stats': [{'get': 'statistics'}, {}],
|
||||
'deps': [{'get': 'dependencies', 'post': 'dependents'}, {}]
|
||||
}
|
||||
|
||||
|
||||
@classmethod
|
||||
def generate_ops(cls):
|
||||
cmds = []
|
||||
for op in cls.operations:
|
||||
idx=0
|
||||
for ops in cls.operations[op]:
|
||||
meths = []
|
||||
for meth in ops:
|
||||
meths.append(meth.upper())
|
||||
if len(meths) > 0:
|
||||
cmds.append({'cmd': op, 'req':idx==0, 'methods': meths})
|
||||
idx+=1
|
||||
|
||||
return cmds
|
||||
|
||||
|
||||
# Inherited class needs to modify these parameters
|
||||
node_type = None
|
||||
# This must be an array object with attributes (type and id)
|
||||
parent_ids = []
|
||||
# This must be an array object with attributes (type and id)
|
||||
ids = []
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_node_urls(cls):
|
||||
assert cls.node_type is not None, "Please set the node_type for this class (%r)" % cls
|
||||
common_url = '/'
|
||||
for p in cls.parent_ids:
|
||||
common_url += '<' + p['type'] + ":" + p['id'] + '>/'
|
||||
|
||||
id_url = common_url
|
||||
idx = 0
|
||||
for p in cls.ids:
|
||||
id_url += '/<' if idx == 1 else '<' + p['type'] + ":" + p['id'] + '>'
|
||||
idx += 1
|
||||
|
||||
return id_url, common_url
|
||||
|
||||
|
||||
def __init__(self, cmd):
|
||||
self.cmd = cmd;
|
||||
|
||||
|
||||
# Check the existance of all the required arguments from parent_ids
|
||||
# and return combination of has parent arguments, and has id arguments
|
||||
def check_args(self, *args, **kwargs):
|
||||
has_id = has_args = True
|
||||
for p in self.parent_ids:
|
||||
if p['id'] not in kwargs:
|
||||
has_args = False
|
||||
break
|
||||
|
||||
for p in self.ids:
|
||||
if p['id'] not in kwargs:
|
||||
has_id = False
|
||||
break
|
||||
|
||||
return has_args, has_id and has_args
|
||||
|
||||
|
||||
def dispatch_request(self, *args, **kwargs):
|
||||
meth = request.method.lower()
|
||||
if meth == 'head':
|
||||
meth = 'get'
|
||||
|
||||
assert self.cmd in NodeView.operations, \
|
||||
"Unimplemented Command (%s) for Node View" % self.cmd
|
||||
has_args, has_id = self.check_args(*args, **kwargs)
|
||||
|
||||
assert (has_id and meth in NodeView.operations[self.cmd][0]) \
|
||||
or (not has_id and meth in NodeView.operations[self.cmd][1]), \
|
||||
"Unimplemented method (%s) for command (%s), which %s an id" \
|
||||
% (meth, self.cmd, 'requires' if has_id else 'does not require')
|
||||
|
||||
meth = NodeView.operations[self.cmd][0][meth] if has_id else \
|
||||
NodeView.operations[self.cmd][1][meth]
|
||||
|
||||
method = getattr(self, meth, None)
|
||||
|
||||
assert method is not None, \
|
||||
"Unimplemented method (%s) for this url (%u)" % \
|
||||
(meth, request.path)
|
||||
|
||||
return method(*args, **kwargs)
|
||||
|
||||
|
||||
@classmethod
|
||||
def register_node_view(cls, blueprint):
|
||||
id_url, url = cls.get_node_urls()
|
||||
|
||||
commands = cls.generate_ops()
|
||||
|
||||
for c in commands:
|
||||
blueprint.add_url_rule(
|
||||
'/%s%s' % (c['cmd'], id_url if c['req'] else url),
|
||||
view_func=cls.as_view(
|
||||
'%s%s' % (c['cmd'], '_id' if c['req'] else ''),
|
||||
cmd=c['cmd']),
|
||||
methods=c['methods'])
|
||||
|
||||
@@ -65,6 +65,7 @@ class ServerGroup(db.Model):
|
||||
__tablename__ = 'servergroup'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
|
||||
name = db.Column(db.String(128), nullable=False)
|
||||
__table_args__ = (db.UniqueConstraint('user_id', 'name'),)
|
||||
|
||||
@@ -73,11 +74,28 @@ class Server(db.Model):
|
||||
"""Define a registered Postgres server"""
|
||||
__tablename__ = 'server'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
servergroup_id = db.Column(db.Integer, db.ForeignKey('servergroup.id'), nullable=False)
|
||||
user_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('user.id'),
|
||||
nullable=False
|
||||
)
|
||||
servergroup_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('servergroup.id'),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
name = db.Column(db.String(128), nullable=False)
|
||||
host = db.Column(db.String(128), nullable=False)
|
||||
port = db.Column(db.Integer(), db.CheckConstraint('port >= 1024 AND port <= 65534'), nullable=False)
|
||||
port = db.Column(
|
||||
db.Integer(),
|
||||
db.CheckConstraint('port >= 1024 AND port <= 65534'),
|
||||
nullable=False)
|
||||
maintenance_db = db.Column(db.String(64), nullable=False)
|
||||
username = db.Column(db.String(64), nullable=False)
|
||||
ssl_mode = db.Column(db.String(16), nullable=False)
|
||||
ssl_mode = db.Column(
|
||||
db.String(16),
|
||||
db.CheckConstraint(
|
||||
"ssl_mode IN ('allow', 'prefer', 'require', 'disable', 'verify-ca', 'verify-full')"
|
||||
),
|
||||
nullable=False)
|
||||
|
||||
@@ -15,25 +15,25 @@
|
||||
<meta name="dcterms.dateCopyrighted" content="2014 - 2015">
|
||||
|
||||
<!-- Base template stylesheets -->
|
||||
{% if config.DEBUG %}<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.css') }}" />{% else %}<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}" />{% endif %}
|
||||
{% if config.DEBUG %}<link rel="stylesheet" href="{{ url_for('static', filename='css/alertifyjs/alertify.css') }}" />{% else %}<link rel="stylesheet" href="{{ url_for('static', filename='css/alertifyjs/alertify.min.css') }}" />{% endif %}
|
||||
{% if config.DEBUG %}<link rel="stylesheet" href="{{ url_for('static', filename='css/alertifyjs/themes/bootstrap.css') }}" />{% else %}<link rel="stylesheet" href="{{ url_for('static', filename='css/alertifyjs/themes/bootstrap.min.css') }}" />{% endif %}
|
||||
{% if config.DEBUG %}<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-theme.min.css') }}">{% else %}<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-theme.min.css') }}">{% endif %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/overrides.css') }}">
|
||||
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.css' if config.DEBUG else 'css/bootstrap.min.css')}}"/>
|
||||
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/alertifyjs/alertify.css' if config.DEBUG else 'css/alertifyjs/alertify.min.css') }}" />
|
||||
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/alertifyjs/themes/bootstrap.css' if config.DEBUG else 'css/alertifyjs/themes/bootstrap.min.css') }}" />
|
||||
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-theme.min.css' if config.DEBUG else 'css/bootstrap-theme.css') }}">
|
||||
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/overrides.css') }}">
|
||||
<!-- View specified stylesheets -->
|
||||
{% for stylesheet in current_app.stylesheets %}
|
||||
<link rel="stylesheet" href="{{ stylesheet }}">
|
||||
<link type="text/css" rel="stylesheet" href="{{ stylesheet }}">
|
||||
{% endfor %}
|
||||
<!-- Base template scripts -->
|
||||
<script src="{{ url_for('static', filename='js/modernizr-2.6.2-respond-1.1.0.min.js') }}"></script>
|
||||
{% if config.DEBUG %}<script src="{{ url_for('static', filename='js/jquery-1.11.2.js') }}">{% else %}<script src="{{ url_for('static', filename='js/jquery-1.11.2.min.js') }}">{% endif %}</script>
|
||||
{% if config.DEBUG %}<script src="{{ url_for('static', filename='js/bootstrap.js') }}">{% else %}<script src="{{ url_for('static', filename='js/bootstrap.min.js') }}">{% endif %}</script>
|
||||
{% if config.DEBUG %}<script src="{{ url_for('static', filename='js/alertifyjs/alertify.js') }}">{% else %}<script src="{{ url_for('static', filename='js/alertifyjs/alertify.min.js') }}">{% endif %}</script>
|
||||
<script src="{{ url_for('static', filename='js/alertifyjs/pgadmin.defaults.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/modernizr-2.6.2-respond-1.1.0.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery-1.11.2.js' if config.DEBUG else 'js/jquery-1.11.2.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap.js' if config.DEBUG else 'js/bootstrap.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/alertifyjs/alertify.js' if config.DEBUG else 'js/alertifyjs/alertify.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/alertifyjs/pgadmin.defaults.js') }}"></script>
|
||||
<!-- View specified scripts -->
|
||||
|
||||
{% for script in current_app.javascripts %}
|
||||
<script src="{{ script }}"></script>
|
||||
<script type="text/javascript" src="{{ script }}"></script>
|
||||
{% endfor %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
13
web/setup.py
13
web/setup.py
@@ -66,6 +66,19 @@ def do_setup():
|
||||
server_group = ServerGroup(user_id=user.id, name="Servers")
|
||||
db.session.merge(server_group)
|
||||
|
||||
# TODO:: Remove this server later
|
||||
# It is here to demo the server listing is workig in
|
||||
# browser tree.
|
||||
server_group = ServerGroup.query.filter_by(name='Servers').first()
|
||||
|
||||
server = Server(
|
||||
user_id=user.id, servergroup_id=server_group.id,
|
||||
name='PostgreSQL 9.3', host='localhost', port=3930,
|
||||
maintenance_db='postgres', username='asheshvashi',
|
||||
ssl_mode='prefer'
|
||||
)
|
||||
db.session.merge(server)
|
||||
|
||||
# Set the schema version
|
||||
version = Version(name='ConfigDB', value=config.SETTINGS_SCHEMA_VERSION)
|
||||
db.session.merge(version)
|
||||
|
||||
Reference in New Issue
Block a user