pgAgent - add modules for jobs, steps and schedules. Fixes #1341

pull/3/head
Ashesh Vashi 2016-09-26 12:04:10 +01:00 committed by Dave Page
parent 7f3ca548cd
commit 237bfd4882
40 changed files with 2797 additions and 0 deletions

View File

@ -0,0 +1,527 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2016, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Implements the pgAgent Jobs Node"""
from functools import wraps
import json
from flask import render_template, make_response, request, jsonify
from flask_babel import gettext as _
from config import PG_DEFAULT_DRIVER
from pgadmin.browser.collection import CollectionNodeModule
from pgadmin.browser.utils import PGChildNodeView
from pgadmin.browser.server_groups import servers
from pgadmin.utils.ajax import make_json_response, internal_server_error, \
make_response as ajax_response, gone, success_return
from pgadmin.utils.driver import get_driver
class JobModule(CollectionNodeModule):
NODE_TYPE = 'pga_job'
COLLECTION_LABEL = _("pgAgent Jobs")
def get_nodes(self, gid, sid):
"""
Generate the collection node
"""
if self.show_node:
yield self.generate_browser_collection_node(sid)
@property
def script_load(self):
"""
Load the module script for server, when any of the server-group node is
initialized.
"""
return servers.ServerModule.NODE_TYPE
def BackendSupported(self, manager, **kwargs):
if hasattr(self, 'show_node'):
if not self.show_node:
return False
conn = manager.connection()
status, res = conn.execute_scalar("""
SELECT
has_table_privilege('pgagent.pga_job', 'INSERT, SELECT, UPDATE') has_priviledge
WHERE EXISTS(
SELECT has_schema_privilege('pgagent', 'USAGE')
WHERE EXISTS(
SELECT cl.oid FROM pg_class cl
LEFT JOIN pg_namespace ns ON ns.oid=relnamespace
WHERE relname='pga_job' AND nspname='pgagent'
)
)
""")
if status and res:
status, res = conn.execute_dict("""
SELECT EXISTS(
SELECT 1 FROM information_schema.columns
WHERE
table_schema='pgagent' AND table_name='pga_jobstep' AND
column_name='jstconnstr'
) has_connstr""")
manager.db_info['pgAgent'] = res['rows'][0]
return True
return False
@property
def csssnippets(self):
"""
Returns a snippet of css to include in the page
"""
snippets = [
render_template(
"browser/css/collection.css",
node_type=self.node_type,
_=_
),
render_template(
"pga_job/css/pga_job.css",
node_type=self.node_type,
_=_
)
]
for submodule in self.submodules:
snippets.extend(submodule.csssnippets)
return snippets
blueprint = JobModule(__name__)
class JobView(PGChildNodeView):
node_type = blueprint.node_type
parent_ids = [
{'type': 'int', 'id': 'gid'},
{'type': 'int', 'id': 'sid'}
]
ids = [
{'type': 'int', 'id': 'jid'}
]
operations = dict({
'obj': [
{'get': 'properties', 'delete': 'delete', 'put': 'update'},
{'get': 'properties', 'post': 'create'}
],
'nodes': [{'get': 'nodes'}, {'get': 'nodes'}],
'sql': [{'get': 'sql'}],
'msql': [{'get': 'msql'}, {'get': 'msql'}],
'run_now': [{'post': 'run_now'}],
'classes': [{}, {'get': 'job_classes'}],
'children': [{'get': 'children'}],
'stats': [{'get': 'statistics'}],
'module.js': [{}, {}, {'get': 'module_js'}]
})
def check_precondition(f):
"""
This function will behave as a decorator which will checks
database connection before running view, it will also attaches
manager,conn & template_path properties to self
"""
@wraps(f)
def wrap(self, *args, **kwargs):
self.manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(kwargs['sid'])
self.conn = self.manager.connection()
# Set the template path for the sql scripts.
self.template_path = 'pga_job/sql/pre3.4'
if not ('pgAgent' in self.manager.db_info):
status, res = self.conn.execute_dict("""
SELECT EXISTS(
SELECT 1 FROM information_schema.columns
WHERE
table_schema='pgagent' AND table_name='pga_jobstep' AND
column_name='jstconnstr'
) has_connstr""")
self.manager.db_info['pgAgent'] = res['rows'][0]
return f(self, *args, **kwargs)
return wrap
@check_precondition
def nodes(self, gid, sid, jid=None):
SQL = render_template(
"/".join([self.template_path, 'nodes.sql']),
jid=jid, conn=self.conn
)
status, rset = self.conn.execute_dict(SQL)
if not status:
return internal_server_error(errormsg=rset)
if jid is not None:
if len(rset['rows']) != 1:
return gone(
errormsg=_(
"Could not find the pgAgent job on the server."
))
return make_json_response(
data=self.blueprint.generate_browser_node(
rset['rows'][0]['jobid'],
sid,
rset['rows'][0]['jobname'],
"icon-pga_job" if rset['rows'][0]['jobenabled'] else
"icon-pga_job-disabled"
),
status=200
)
res = []
for row in rset['rows']:
res.append(
self.blueprint.generate_browser_node(
row['jobid'],
sid,
row['jobname'],
"icon-pga_job" if row['jobenabled'] else
"icon-pga_job-disabled"
)
)
return make_json_response(
data=res,
status=200
)
@check_precondition
def properties(self, gid, sid, jid=None):
SQL = render_template(
"/".join([self.template_path, 'properties.sql']),
jid=jid, conn=self.conn
)
status, rset = self.conn.execute_dict(SQL)
if not status:
return internal_server_error(errormsg=rset)
if jid is not None:
if len(rset['rows']) != 1:
return gone(
errormsg=_(
"Could not find the pgAgent job on the server."
)
)
res = rset['rows'][0]
status, rset = self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'steps.sql']),
jid=jid, conn=self.conn,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
)
)
if not status:
return internal_server_error(errormsg=rset)
res['jsteps'] = rset['rows']
status, rset = self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'schedules.sql']),
jid=jid, conn=self.conn
)
)
if not status:
return internal_server_error(errormsg=rset)
res['jschedules'] = rset['rows']
else:
res = rset['rows']
return ajax_response(
response=res,
status=200
)
def module_js(self):
"""
This property defines (if javascript) exists for this node.
Override this property for your own logic.
"""
return make_response(
render_template(
"pga_job/js/pga_job.js",
_=_
),
200, {'Content-Type': 'application/x-javascript'}
)
@check_precondition
def create(self, gid, sid):
"""Create the pgAgent job."""
required_args = [
u'jobname'
]
data = request.form if request.form else json.loads(
request.data.decode('utf-8')
)
for arg in required_args:
if arg not in data:
return make_json_response(
status=410,
success=0,
errormsg=_(
"Could not find the required parameter (%s)." % arg
)
)
status, res = self.conn.execute_void('BEGIN')
if not status:
return internal_server_error(errormsg=res)
status, res = self.conn.execute_scalar(
render_template(
"/".join([self.template_path, 'create.sql']),
data=data, conn=self.conn, fetch_id=True,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
)
)
if not status:
self.conn.execute_void('END')
return internal_server_error(errormsg=res)
# We need oid of newly created database
status, res = self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'nodes.sql']),
jid=res, conn=self.conn
)
)
self.conn.execute_void('END')
if not status:
return internal_server_error(errormsg=res)
row = res['rows'][0]
return jsonify(
node=self.blueprint.generate_browser_node(
row['jobid'],
sid,
row['jobname'],
icon="icon-pga_job"
)
)
@check_precondition
def update(self, gid, sid, jid):
"""Update the pgAgent Job."""
data = request.form if request.form else json.loads(
request.data.decode('utf-8')
)
status, res = self.conn.execute_void(
render_template(
"/".join([self.template_path, 'update.sql']),
data=data, conn=self.conn, jid=jid,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
)
)
if not status:
return internal_server_error(errormsg=res)
# We need oid of newly created database
status, res = self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'nodes.sql']),
jid=res, conn=self.conn
)
)
if not status:
return internal_server_error(errormsg=res)
row = res['rows'][0]
return jsonify(
node=self.blueprint.generate_browser_node(
jid,
sid,
row['jobname'],
icon="icon-pga_job" if row['jobenabled'] else
"icon-pga_job-disabled"
)
)
@check_precondition
def delete(self, gid, sid, jid):
"""Delete the pgAgent Job."""
status, res = self.conn.execute_void(
render_template(
"/".join([self.template_path, 'delete.sql']),
jid=jid, conn=self.conn
)
)
if not status:
return internal_server_error(errormsg=res)
return make_json_response(success=1)
@check_precondition
def msql(self, gid, sid, jid=None):
"""
This function to return modified SQL.
"""
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(
v.decode('utf-8') if hasattr(v, 'decode') else v
)
except ValueError:
data[k] = v
return make_json_response(
data=render_template(
"/".join([
self.template_path,
'create.sql' if jid is None else 'update.sql'
]),
jid=jid, data=data, conn=self.conn, fetch_id=False,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
),
status=200
)
@check_precondition
def statistics(self, gid, sid, jid):
"""
statistics
Returns the statistics for a particular database if jid is specified,
otherwise it will return statistics for all the databases in that
server.
"""
status, res = self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'stats.sql']),
jid=jid, conn=self.conn
)
)
if not status:
return internal_server_error(errormsg=res)
return make_json_response(
data=res,
status=200
)
@check_precondition
def sql(self, gid, sid, jid):
"""
This function will generate sql for sql panel
"""
SQL = render_template(
"/".join([self.template_path, 'properties.sql']),
jid=jid, conn=self.conn, last_system_oid=0
)
status, res = self.conn.execute_dict(SQL)
if not status:
return internal_server_error(errormsg=res)
row = res['rows'][0]
status, res= self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'steps.sql']),
jid=jid, conn=self.conn,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
)
)
if not status:
return internal_server_error(errormsg=res)
row['jsteps'] = res['rows']
status, res = self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'schedules.sql']),
jid=jid, conn=self.conn
)
)
if not status:
return internal_server_error(errormsg=res)
row['jschedules'] = res['rows']
for schedule in row['jschedules']:
schedule['jscexceptions'] = []
if schedule['jexid']:
idx = 0
for exc in schedule['jexid']:
schedule['jscexceptions'].append({
'jexid': exc,
'jexdate': schedule['jexdate'][idx],
'jextime': schedule['jextime'][idx]
})
idx+=1
del schedule['jexid']
del schedule['jexdate']
del schedule['jextime']
return ajax_response(
response=render_template(
"/".join([self.template_path, 'create.sql']),
jid=jid, data=row, conn=self.conn, fetch_id=False,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
)
)
@check_precondition
def run_now(self, gid, sid, jid):
"""
This function will set the next run to now, to inform the pgAgent to
run the job now.
"""
status, res = self.conn.execute_void(
render_template(
"/".join([self.template_path, 'run_now.sql']),
jid=jid, conn=self.conn
)
)
if not status:
return internal_server_error(errormsg=res)
return success_return(
message=_("Updated the next-runtime to now!")
)
@check_precondition
def job_classes(self, gid, sid):
"""
This function will return the set of job classes.
"""
status, res = self.conn.execute_dict(
render_template("/".join([self.template_path, 'job_classes.sql']))
)
if not status:
return internal_server_error(errormsg=res)
return make_json_response(
data=res['rows'],
status=200
)
JobView.register_node_view(blueprint)

View File

@ -0,0 +1,441 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2016, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Implements pgAgent Job Schedule Node"""
import json
from functools import wraps
from flask import render_template, make_response, request
from flask_babel import gettext
from pgadmin.browser.collection import CollectionNodeModule
from pgadmin.browser.utils import PGChildNodeView
from pgadmin.utils.ajax import make_json_response, gone, \
make_response as ajax_response, internal_server_error
from pgadmin.utils.driver import get_driver
from config import PG_DEFAULT_DRIVER
class JobScheduleModule(CollectionNodeModule):
"""
class JobScheduleModule(CollectionNodeModule)
A module class for JobSchedule node derived from CollectionNodeModule.
Methods:
-------
* get_nodes(gid, sid, jid)
- Method is used to generate the browser collection node.
* node_inode()
- Method is overridden from its base class to make the node as leaf node.
"""
NODE_TYPE = 'pga_schedule'
COLLECTION_LABEL = gettext("Schedules")
def get_nodes(self, gid, sid, jid):
"""
Method is used to generate the browser collection node
Args:
gid: Server Group ID
sid: Server ID
jid: Database Id
"""
yield self.generate_browser_collection_node(jid)
@property
def node_inode(self):
"""
Override this property to make the node a leaf node.
Returns: False as this is the leaf node
"""
return False
@property
def script_load(self):
"""
Load the module script for language, when any of the database nodes are initialized.
Returns: node type of the server module.
"""
return 'pga_job'
blueprint = JobScheduleModule(__name__)
class JobScheduleView(PGChildNodeView):
"""
class JobScheduleView(PGChildNodeView)
A view class for JobSchedule node derived from PGChildNodeView. This class is
responsible for all the stuff related to view like updating language
node, showing properties, showing sql in sql pane.
Methods:
-------
* __init__(**kwargs)
- Method is used to initialize the JobScheduleView and it's base view.
* module_js()
- This property defines (if javascript) exists for this node.
Override this property for your own logic
* check_precondition()
- This function will behave as a decorator which will checks
database connection before running view, it will also attaches
manager,conn & template_path properties to self
* list()
- This function is used to list all the language nodes within that collection.
* nodes()
- This function will used to create all the child node within that collection.
Here it will create all the language node.
* properties(gid, sid, jid, jscid)
- This function will show the properties of the selected language node
* update(gid, sid, jid, jscid)
- This function will update the data for the selected language node
* msql(gid, sid, jid, jscid)
- This function is used to return modified SQL for the selected language node
"""
node_type = blueprint.node_type
parent_ids = [
{'type': 'int', 'id': 'gid'},
{'type': 'int', 'id': 'sid'},
{'type': 'int', 'id': 'jid'}
]
ids = [
{'type': 'int', 'id': 'jscid'}
]
operations = dict({
'obj': [
{'get': 'properties', 'put': 'update'},
{'get': 'list', 'post': 'create'}
],
'nodes': [{'get': 'nodes'}, {'get': 'nodes'}],
'msql': [{'get': 'msql'}, {'get': 'msql'}],
'module.js': [{}, {}, {'get': 'module_js'}]
})
def _init_(self, **kwargs):
"""
Method is used to initialize the JobScheduleView and its base view.
Initialize all the variables create/used dynamically like conn, template_path.
Args:
**kwargs:
"""
self.conn = None
self.template_path = None
self.manager = None
super(JobScheduleView, self).__init__(**kwargs)
def module_js(self):
"""
This property defines whether javascript exists for this node.
"""
return make_response(
render_template(
"pga_schedule/js/pga_schedule.js",
_=gettext
),
200, {'Content-Type': 'application/x-javascript'}
)
def check_precondition(f):
"""
This function will behave as a decorator which will check the
database connection before running the view. It also attaches
manager, conn & template_path properties to self
"""
@wraps(f)
def wrap(*args, **kwargs):
# Here args[0] will hold self & kwargs will hold gid,sid,jid
self = args[0]
self.driver = get_driver(PG_DEFAULT_DRIVER)
self.manager = self.driver.connection_manager(kwargs['sid'])
self.conn = self.manager.connection()
self.template_path = 'pga_schedule/sql/pre3.4'
return f(*args, **kwargs)
return wrap
@check_precondition
def list(self, gid, sid, jid):
"""
This function is used to list all the language nodes within that collection.
Args:
gid: Server Group ID
sid: Server ID
jid: Job ID
"""
sql = render_template(
"/".join([self.template_path, 'properties.sql']),
jid=jid
)
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
return ajax_response(
response=res['rows'],
status=200
)
@check_precondition
def nodes(self, gid, sid, jid, jscid=None):
"""
This function is used to create all the child nodes within the collection.
Here it will create all the language nodes.
Args:
gid: Server Group ID
sid: Server ID
jid: Job ID
"""
res = []
sql = render_template(
"/".join([self.template_path, 'nodes.sql']),
jscid = jscid,
jid = jid
)
status, result = self.conn.execute_2darray(sql)
if not status:
return internal_server_error(errormsg=result)
if jscid is not None:
if len(result['rows']) == 0:
return gone(errormsg="Couldn't find the specified job step.")
row = result['rows'][0]
return make_json_response(
self.blueprint.generate_browser_node(
row['jscid'],
row['jscjobid'],
row['jscname'],
icon="icon-pga_schedule",
enabled=row['jscenabled']
)
)
for row in result['rows']:
res.append(
self.blueprint.generate_browser_node(
row['jscid'],
row['jscjobid'],
row['jscname'],
icon="icon-pga_schedule",
enabled=row['jscenabled']
)
)
return make_json_response(
data=res,
status=200
)
@check_precondition
def properties(self, gid, sid, jid, jscid):
"""
This function will show the properties of the selected language node.
Args:
gid: Server Group ID
sid: Server ID
jid: Job ID
jscid: JobSchedule ID
"""
sql = render_template(
"/".join([self.template_path, 'properties.sql']),
jscid=jscid, jid=jid
)
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
if len(res['rows']) == 0:
return gone(errormsg="Couldn't find the specified job step.")
return ajax_response(
response=res['rows'][0],
status=200
)
@check_precondition
def create(self, gid, sid, jid):
"""
This function will update the data for the selected language node.
Args:
gid: Server Group ID
sid: Server ID
jid: Job ID
"""
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(
v.decode('utf-8') if hasattr(v, 'decode') else v
)
except ValueError:
data[k] = v
sql = render_template(
"/".join([self.template_path, 'create.sql']),
jid=jid,
data=data,
fetch_id=False
)
status, res = self.conn.execute_void('BEGIN')
if not status:
return internal_server_error(errormsg=res)
status, res = self.conn.execute_scalar(sql)
if not status:
if self.conn.connected():
self.conn.execute_void('END')
return internal_server_error(errormsg=res)
self.conn.execute_void('END')
sql = render_template(
"/".join([self.template_path, 'nodes.sql']),
jscid = res,
jid = jid
)
status, res = self.conn.execute_2darray(sql)
if not status:
return internal_server_error(errormsg=res)
row = res['rows'][0]
return make_json_response(
data=self.blueprint.generate_browser_node(
row['jscid'],
row['jscjobid'],
row['jscname'],
icon="icon-pga_schedule"
)
)
@check_precondition
def update(self, gid, sid, jid, jscid):
"""
This function will update the data for the selected language node.
Args:
gid: Server Group ID
sid: Server ID
jid: Job ID
jscid: JobSchedule ID
"""
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(
v.decode('utf-8') if hasattr(v, 'decode') else v
)
except ValueError:
data[k] = v
sql = render_template(
"/".join([self.template_path, 'update.sql']),
jid=jid,
data=data
)
status, res = self.conn.execute_void(sql)
if not status:
return internal_server_error(errormsg=res)
sql = render_template(
"/".join([self.template_path, 'nodes.sql']),
jscid = jscid,
jid = jid
)
status, res = self.conn.execute_2darray(sql)
if not status:
return internal_server_error(errormsg=res)
row = res['rows'][0]
return make_json_response(
self.blueprint.generate_browser_node(
row['jscid'],
row['jscjobid'],
row['jscname'],
icon="icon-pga_schedule"
)
)
@check_precondition
def msql(self, gid, sid, jid, jscid=None):
"""
This function is used to return modified SQL for the selected language node.
Args:
gid: Server Group ID
sid: Server ID
jid: Job ID
jscid: Job Schedule ID (optional)
"""
data = {}
sql = ''
for k, v in request.args.items():
try:
data[k] = json.loads(
v.decode('utf-8') if hasattr(v, 'decode') else v
)
except ValueError:
data[k] = v
if jscid is None:
sql = render_template(
"/".join([self.template_path, 'create.sql']),
jid=jid,
data=data,
fetch_id=False
)
else:
sql = render_template(
"/".join([self.template_path, 'update.sql']),
jid=jid,
jscid=jscid,
data=data
)
return make_json_response(
data=sql,
status=200
)
JobScheduleView.register_node_view(blueprint)

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

View File

@ -0,0 +1,7 @@
.icon-pga_schedule {
background-image: url('{{ url_for('NODE-pga_schedule.static', filename='img/pga_schedule.png') }}') !important;
background-repeat: no-repeat;
align-content: center;
vertical-align: middle;
height: 1.3em;
}

View File

@ -0,0 +1,475 @@
define([
'jquery', 'underscore', 'underscore.string', 'pgadmin', 'moment',
'pgadmin.browser', 'alertify', 'backform', 'pgadmin.backform'
],
function($, _, S, pgAdmin, moment, pgBrowser, Alertify, Backform) {
if (!pgBrowser.Nodes['coll-pga_schedule']) {
pgBrowser.Nodes['coll-pga_schedule'] =
pgBrowser.Collection.extend({
node: 'pga_schedule',
label: '{{ _('Schedules') }}',
type: 'coll-pga_schedule',
columns: ['jscid', 'jscname', 'jscenabled'],
hasStatistics: false
});
}
if (!pgBrowser.Nodes['pga_schedule']) {
var weekdays = [
'{{ _("Sunday") }}', '{{ _("Monday") }}', '{{ _("Tuesday") }}',
'{{ _("Wednesday") }}', '{{ _("Thursday") }}', '{{ _("Friday") }}',
'{{ _("Saturday") }}'
],
monthdays = [
'{{ _("1st") }}', '{{ _("2nd") }}', '{{ _("3rd") }}',
'{{ _("4th") }}', '{{ _("5th") }}', '{{ _("6th") }}',
'{{ _("7th") }}', '{{ _("8th") }}', '{{ _("9th") }}',
'{{ _("10th") }}', '{{ _("11th") }}', '{{ _("12th") }}',
'{{ _("13th") }}', '{{ _("14th") }}', '{{ _("15th") }}',
'{{ _("16th") }}', '{{ _("17th") }}', '{{ _("18th") }}',
'{{ _("19th") }}', '{{ _("20th") }}', '{{ _("21st") }}',
'{{ _("22nd") }}', '{{ _("23rd") }}', '{{ _("24th") }}',
'{{ _("25th") }}', '{{ _("26th") }}', '{{ _("27th") }}',
'{{ _("28th") }}', '{{ _("29th") }}', '{{ _("30th") }}',
'{{ _("31st") }}', '{{ _("Last day") }}'
],
months = [
'{{ _("January") }}', '{{ _("February") }}', '{{ _("March") }}',
'{{ _("April") }}', '{{ _("May") }}', '{{ _("June") }}',
'{{ _("July") }}', '{{ _("August") }}', '{{ _("September") }}',
'{{ _("October") }}', '{{ _("November") }}', '{{ _("December") }}'
],
hours = [
'{{ _("00") }}', '{{ _("01") }}', '{{ _("02") }}', '{{ _("03") }}',
'{{ _("04") }}', '{{ _("05") }}', '{{ _("06") }}', '{{ _("07") }}',
'{{ _("08") }}', '{{ _("09") }}', '{{ _("10") }}', '{{ _("11") }}',
'{{ _("12") }}', '{{ _("13") }}', '{{ _("14") }}', '{{ _("15") }}',
'{{ _("16") }}', '{{ _("17") }}', '{{ _("18") }}', '{{ _("19") }}',
'{{ _("20") }}', '{{ _("21") }}', '{{ _("22") }}', '{{ _("23") }}'
],
minutes = [
'{{ _("00") }}', '{{ _("01") }}', '{{ _("02") }}', '{{ _("03") }}',
'{{ _("04") }}', '{{ _("05") }}', '{{ _("06") }}', '{{ _("07") }}',
'{{ _("08") }}', '{{ _("09") }}', '{{ _("10") }}', '{{ _("11") }}',
'{{ _("12") }}', '{{ _("13") }}', '{{ _("14") }}', '{{ _("15") }}',
'{{ _("16") }}', '{{ _("17") }}', '{{ _("18") }}', '{{ _("19") }}',
'{{ _("20") }}', '{{ _("21") }}', '{{ _("22") }}', '{{ _("23") }}',
'{{ _("24") }}', '{{ _("25") }}', '{{ _("26") }}', '{{ _("27") }}',
'{{ _("28") }}', '{{ _("29") }}', '{{ _("30") }}', '{{ _("31") }}',
'{{ _("32") }}', '{{ _("33") }}', '{{ _("34") }}', '{{ _("35") }}',
'{{ _("36") }}', '{{ _("37") }}', '{{ _("38") }}', '{{ _("39") }}',
'{{ _("40") }}', '{{ _("41") }}', '{{ _("42") }}', '{{ _("43") }}',
'{{ _("44") }}', '{{ _("45") }}', '{{ _("46") }}', '{{ _("47") }}',
'{{ _("48") }}', '{{ _("49") }}', '{{ _("50") }}', '{{ _("51") }}',
'{{ _("52") }}', '{{ _("53") }}', '{{ _("54") }}', '{{ _("55") }}',
'{{ _("56") }}', '{{ _("57") }}', '{{ _("58") }}', '{{ _("59") }}'
],
AnyDatetimeCell = Backgrid.Extension.MomentCell.extend({
editor: Backgrid.Extension.DatetimePickerEditor,
render: function() {
this.$el.empty();
var model = this.model;
this.$el.text(this.formatter.fromRaw(model.get(this.column.get("name")), model) || '{{ _('<Any>') }}');
this.delegateEvents();
return this;
}
}),
BooleanArrayFormatter = function(selector, indexes) {
var self = this;
self.selector = selector;
self.indexes = indexes;
this.fromRaw = function(rawData) {
if (!_.isArray(rawData)) {
return rawData;
}
var res = [], idx = 0, resIdx = [];
for (; idx < rawData.length; idx++) {
if (!rawData[idx])
continue;
res.push(self.selector[idx]);
resIdx.push(idx + 1);
}
return self.indexes ? resIdx : res.join(', ');
}
this.toRaw = function(d) {
if (!self.indexes)
return d;
var res = [], i = 0, l = self.selector.length;
for (; i < l; i++) {
res.push(_.indexOf(d, String(i + 1)) != -1);
}
console.log(res);
return res;
}
return self;
},
BooleanArrayOptions = function(ctrl) {
var selector = ctrl.field.get('selector'),
val = ctrl.model.get(ctrl.field.get('name')),
res = [];
if (selector) {
res = _.map(
selector, function(v, i) {
return {label: v, value: i + 1, selected: val[i]};
}
);
}
return res;
},
ExceptionModel = pgBrowser.Node.Model.extend({
defaults: {
jexid: undefined,
jexdate: null,
jextime: null
},
idAttribute: 'jexid',
schema: [{
id: 'jexdate', type: 'text', label: '{{ _('Date') }}',
editable: true, placeholder: '{{ _('<any>') }}',
cell: AnyDatetimeCell, options: {format: 'YYYY-MM-DD'},
displayFormat: 'YYYY-MM-DD', modelFormat: 'YYYY-MM-DD',
cellHeaderClasses:'width_percent_50', allowEmpty: true
},{
id: 'jextime', type: 'text', placeholder: '{{ _('<any>') }}',
label: '{{ _('Time') }}', editable: true, cell: AnyDatetimeCell,
options: {format: 'HH:mm'}, displayFormat: 'HH:mm',
modelFormat: 'HH:mm:ss', displayInUTC: false, allowEmpty: true,
cellHeaderClasses:'width_percent_50', modalInUTC: false
}],
validate: function() {
var self = this, exceptions = this.collection,
dates = {}, errMsg, hasExceptionErr = false,
d = (this.get('jexdate') || '<any>'),
t = this.get('jextime') || '<any>',
id = this.get('jexid') || this.cid;
self.errorModel.unset('jscdate');
if (d == t && d == '<any>') {
errMsg = '{{ _('Please specify date/time.') }}';
self.errorModel.set('jscdate', errMsg);
return errMsg ;
}
exceptions.each(function(ex) {
if (hasExceptionErr || id == (ex.get('jexid') || ex.cid))
return;
if (
d == (ex.get('jexdate') || '<any>') &&
t == (ex.get('jextime') || '<any>')
) {
errMsg = '{{ _('Please specify unique set of exceptions.') }}';
if (ex.errorModel.get('jscdate') != errMsg)
self.errorModel.set('jscdate', errMsg);
hasExceptionErr = true;
}
});
return errMsg;
}
});
pgBrowser.Nodes['pga_schedule'] = pgBrowser.Node.extend({
parent_type: 'pga_job',
type: 'pga_schedule',
hasSQL: false,
hasDepends: false,
hasStatistics: false,
canDrop: function(node) {
return true;
},
label: '{{ _('Schedule') }}',
node_image: 'icon-pga_schedule',
Init: function() {
/* Avoid mulitple registration of menus */
if (this.initialized)
return;
this.initialized = true;
pgBrowser.add_menus([{
name: 'create_pga_schedule_on_job', node: 'pga_job', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
category: 'create', priority: 4, label: '{{ _('Schedule...') }}',
icon: 'wcTabIcon icon-pga_schedule', data: {action: 'create'}
},{
name: 'create_pga_schedule_on_coll', node: 'coll-pga_schedule', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
category: 'create', priority: 4, label: '{{ _('Schedule...') }}',
icon: 'wcTabIcon icon-pga_schedule', data: {action: 'create'}
},{
name: 'create_pga_schedule', node: 'pga_schedule', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
category: 'create', priority: 4, label: '{{ _('Schedule...') }}',
icon: 'wcTabIcon icon-pga_schedule', data: {action: 'create'}
}]);
},
model: pgBrowser.Node.Model.extend({
defaults: {
jscid: null,
jscjobid: null,
jscname: '',
jscdesc: '',
jscenabled: true,
jscstart: null,
jscend: null,
jscweekdays: _.map(weekdays, function() { return false; }),
jscmonthdays: _.map(monthdays, function() { return false; }),
jscmonths: _.map(months, function() { return false; }),
jschours: _.map(hours, function() { return false; }),
jscminutes: _.map(minutes, function() { return false; }),
jscexceptions: []
},
idAttribute: 'jscid',
parse: function(d) {
d.jscexceptions = [];
if (d.jexid && d.jexid.length) {
var idx = 0;
for (; idx < d.jexid.length; idx++) {
d.jscexceptions.push({
'jexid': d.jexid[idx],
'jexdate': d.jexdate[idx],
'jextime': d.jextime[idx]
});
}
}
delete d.jexid;
delete d.jexdate;
delete d.jextime;
return pgBrowser.Node.Model.prototype.parse.apply(this, arguments);
},
schema: [{
id: 'jscid', label: '{{ _('ID') }}', type: 'integer',
cellHeaderClasses: 'width_percent_5', mode: ['properties']
},{
id: 'jscname', label: '{{ _('Name') }}', type: 'text',
cellHeaderClasses: 'width_percent_45',
disabled: function() { return false; }
},{
id: 'jscenabled', label: '{{ _('Enabled') }}', type: 'switch',
disabled: function() { return false; },
cellHeaderClasses: 'width_percent_5'
},{
id: 'jscstart', label: '{{ _('Start') }}', type: 'text',
control: 'datetimepicker', cell: 'moment',
disabled: function() { return false; },
displayFormat: 'YYYY-MM-DD HH:mm:SS Z',
modelFormat: 'YYYY-MM-DD HH:mm:SS Z', options: {
format: 'YYYY-MM-DD HH:mm:SS Z',
}, cellHeaderClasses: 'width_percent_25'
},{
id: 'jscend', label: '{{ _('End') }}', type: 'text',
control: 'datetimepicker', cell: 'moment',
disabled: function() { return false; }, displayInUTC: false,
displayFormat: 'YYYY-MM-DD HH:mm:SS Z', options: {
format: 'YYYY-MM-DD HH:mm:SS Z', useCurrent: false
}, cellHeaderClasses: 'width_percent_25',
modelFormat: 'YYYY-MM-DD HH:mm:SS Z'
},{
id: 'jscweekdays', label:'{{ _('Week Days') }}', type: 'text',
control: Backform.Control.extend({
formatter: new BooleanArrayFormatter(weekdays, false)
}), mode: ['properties']
},{
id: 'jscmonthdays', label:'{{ _('Month Days') }}', type: 'text',
control: Backform.Control.extend({
formatter: new BooleanArrayFormatter(monthdays, false)
}), mode: ['properties']
},{
id: 'jscmonths', label:'{{ _('Months') }}', type: 'text',
control: Backform.Control.extend({
formatter: new BooleanArrayFormatter(months, false)
}), mode: ['properties']
},{
id: 'jschours', label:'{{ _('Hours') }}', type: 'text',
control: Backform.Control.extend({
formatter: new BooleanArrayFormatter(hours, false)
}), mode: ['properties']
},{
id: 'jscminutes', label:'{{ _('Minutes') }}', type: 'text',
control: Backform.Control.extend({
formatter: new BooleanArrayFormatter(minutes, false)
}), mode: ['properties']
},{
id: 'jscexceptions', label:'{{ _('Exceptions') }}', type: 'text',
control: Backform.Control.extend({
formatter: new function() {
this.fromRaw = function(rawData) {
var res = '', idx = 0, d;
if (!rawData) {
return res;
}
for (; idx < rawData.length; idx++) {
d = rawData[idx];
if (idx)
res += ', ';
res += '[' + String((d.jexdate || '') + ' ' + (d.jextime || '')).replace(/^\s+|\s+$/g, '') + ']';
}
return res;
}
this.toRaw = function(data) { return data; }
return this;
}
}), mode: ['properties']
},{
type: 'nested', label: '{{ _('Days') }}', group: '{{ _('Repeat') }}',
mode: ['create', 'edit'],
control: Backform.FieldsetControl.extend({
render: function() {
var res = Backform.FieldsetControl.prototype.render.apply(
this, arguments
);
this.$el.prepend(
'<div class="' + Backform.helpMessageClassName + ' set-group pg-el-xs-12">{{ _("Schedules are specified using a <b>cron-style</b> format.<br/><ul><li>For each selected time or date element, the schedule will execute.<br/>e.g. To execute at 5 minutes past every hour, simply select 05 in the Minutes list box.<br/></li><li>Values from more than one field may be specified in order to further control the schedule.<br/>e.g. To execute at 12:05 and 14:05 every Monday and Thursday, you would click minute 05, hours 12 and 14, and weekdays Monday and Thursday.</li><li>For additional flexibility, the Month Days check list includes an extra Last Day option. This matches the last day of the month, whether it happens to be the 28th, 29th, 30th or 31st.</li></ul>") }}</div>'
);
return res;
}
}),
schema:[{
id: 'jscweekdays', label:'{{ _('Week Days') }}', cell: 'select2',
group: '{{ _('Days') }}', control: 'select2', type: 'array',
select2: {
first_empty: false,
multiple: true,
allowClear: true,
placeholder: '{{ _("Select the weekdays...") }}',
width: 'style',
dropdownAdapter: $.fn.select2.amd.require(
'select2/selectAllAdapter'
)
},
selector: weekdays,
formatter: new BooleanArrayFormatter(weekdays, true),
options: BooleanArrayOptions
},{
id: 'jscmonthdays', label:'{{ _('Month Days') }}', cell: 'select2',
group: '{{ _('Days') }}', control: 'select2', type: 'array',
select2: {
first_empty: false,
multiple: true,
allowClear: true,
placeholder: '{{ _("Select the month days...") }}',
width: 'style',
dropdownAdapter: $.fn.select2.amd.require(
'select2/selectAllAdapter'
)
},
formatter: new BooleanArrayFormatter(monthdays, true),
selector: monthdays, options: BooleanArrayOptions
},{
id: 'jscmonths', label:'{{ _('Months') }}', cell: 'select2',
group: '{{ _('Days') }}', control: 'select2', type: 'array',
select2: {
first_empty: false,
multiple: true,
allowClear: true,
placeholder: '{{ _("Select the months...") }}',
width: 'style',
dropdownAdapter: $.fn.select2.amd.require(
'select2/selectAllAdapter'
)
},
formatter: new BooleanArrayFormatter(months, true),
selector: months, options: BooleanArrayOptions
}]
},{
type: 'nested', control: 'fieldset', label: '{{ _('Times') }}',
group: '{{ _('Repeat') }}', mode: ['create', 'edit'],
schema:[{
id: 'jschours', label:'{{ _('Hours') }}', cell: 'select2',
group: '{{ _('Times') }}', control: 'select2', type: 'array',
select2: {
first_empty: false,
multiple: true,
allowClear: true,
placeholder: '{{ _("Select the hours...") }}',
width: 'style',
dropdownAdapter: $.fn.select2.amd.require(
'select2/selectAllAdapter'
)
},
formatter: new BooleanArrayFormatter(hours, true),
selector: hours, options: BooleanArrayOptions
},{
id: 'jscminutes', label:'{{ _('Minutes') }}', cell: 'select2',
group: '{{ _('Times') }}', control: 'select2', type: 'array',
select2: {
first_empty: false,
multiple: true,
allowClear: true,
placeholder: '{{ _("Select the minutes...") }}',
width: 'style',
dropdownAdapter: $.fn.select2.amd.require(
'select2/selectAllAdapter'
)
},
formatter: new BooleanArrayFormatter(minutes, true),
selector: minutes, options: BooleanArrayOptions
}]
},{
id: 'jscexceptions', type: 'collection', mode: ['edit', 'create'],
label: "", canEdit: false, model: ExceptionModel, canAdd: true,
group: '{{ _('Exceptions') }}', canDelete: true,
cols: ['jexdate', 'jextime'], control: 'sub-node-collection'
},{
id: 'jscdesc', label: '{{ _('Comment') }}', type: 'multiline'
}],
validate: function(keys) {
var val = this.get('jscname'),
errMsg = null;
if (_.isUndefined(val) || _.isNull(val) ||
String(val).replace(/^\s+|\s+$/g, '') == '') {
var msg = '{{ _('Name cannot be empty.') }}';
this.errorModel.set('jscname', msg);
errMsg = msg;
} else {
this.errorModel.unset('jscname');
}
val = this.get('jscstart');
if (_.isUndefined(val) || _.isNull(val) ||
String(val).replace(/^\s+|\s+$/g, '') == '') {
var msg = '{{ _('Please enter the start time.') }}';
this.errorModel.set('jscstart', msg);
errMsg = errMsg || msg;
} else {
this.errorModel.unset('jscstart');
}
val = this.get('jscend');
if (_.isUndefined(val) || _.isNull(val) ||
String(val).replace(/^\s+|\s+$/g, '') == '') {
var msg = '{{ _('Please enter the end time.') }}';
this.errorModel.set('jscend', msg);
errMsg = errMsg || msg;
} else {
this.errorModel.unset('jscend');
}
return errMsg;
}
})
});
}
return pgBrowser.Nodes['pga_schedule'];
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

View File

@ -0,0 +1,536 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2016, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Implements pgAgent Job Step Node"""
import json
from functools import wraps
from flask import render_template, make_response, request
from flask_babel import gettext
from pgadmin.browser.collection import CollectionNodeModule
from pgadmin.browser.utils import PGChildNodeView
from pgadmin.utils.ajax import make_json_response, gone, \
make_response as ajax_response, internal_server_error
from pgadmin.utils.driver import get_driver
from config import PG_DEFAULT_DRIVER
class JobStepModule(CollectionNodeModule):
"""
class JobStepModule(CollectionNodeModule)
A module class for JobStep node derived from CollectionNodeModule.
Methods:
-------
* get_nodes(gid, sid, jid)
- Method is used to generate the browser collection node.
* node_inode()
- Method is overridden from its base class to make the node as leaf node.
"""
NODE_TYPE = 'pga_jobstep'
COLLECTION_LABEL = gettext("Steps")
def get_nodes(self, gid, sid, jid):
"""
Method is used to generate the browser collection node
Args:
gid: Server Group ID
sid: Server ID
jid: Database Id
"""
yield self.generate_browser_collection_node(jid)
@property
def node_inode(self):
"""
Override this property to make the node a leaf node.
Returns: False as this is the leaf node
"""
return False
@property
def script_load(self):
"""
Load the module script for language, when any of the database nodes are initialized.
Returns: node type of the server module.
"""
return 'pga_job'
blueprint = JobStepModule(__name__)
class JobStepView(PGChildNodeView):
"""
class JobStepView(PGChildNodeView)
A view class for JobStep node derived from PGChildNodeView. This class is
responsible for all the stuff related to view like updating language
node, showing properties, showing sql in sql pane.
Methods:
-------
* __init__(**kwargs)
- Method is used to initialize the JobStepView and it's base view.
* module_js()
- This property defines (if javascript) exists for this node.
Override this property for your own logic
* check_precondition()
- This function will behave as a decorator which will checks
database connection before running view, it will also attaches
manager,conn & template_path properties to self
* list()
- This function is used to list all the language nodes within that collection.
* nodes()
- This function will used to create all the child node within that collection.
Here it will create all the language node.
* properties(gid, sid, jid, jstid)
- This function will show the properties of the selected language node
* update(gid, sid, jid, jstid)
- This function will update the data for the selected language node
* msql(gid, sid, jid, jstid)
- This function is used to return modified SQL for the selected language node
"""
node_type = blueprint.node_type
parent_ids = [
{'type': 'int', 'id': 'gid'},
{'type': 'int', 'id': 'sid'},
{'type': 'int', 'id': 'jid'}
]
ids = [
{'type': 'int', 'id': 'jstid'}
]
operations = dict({
'obj': [
{'get': 'properties', 'put': 'update'},
{'get': 'list', 'post': 'create'}
],
'nodes': [{'get': 'nodes'}, {'get': 'nodes'}],
'msql': [{'get': 'msql'}, {'get': 'msql'}],
'stats': [{'get': 'statistics'}],
'module.js': [{}, {}, {'get': 'module_js'}]
})
def _init_(self, **kwargs):
"""
Method is used to initialize the JobStepView and its base view.
Initialize all the variables create/used dynamically like conn, template_path.
Args:
**kwargs:
"""
self.conn = None
self.template_path = None
self.manager = None
super(JobStepView, self).__init__(**kwargs)
def module_js(self):
"""
This property defines whether javascript exists for this node.
"""
return make_response(
render_template(
"pga_jobstep/js/pga_jobstep.js",
_=gettext
),
200, {'Content-Type': 'application/x-javascript'}
)
def check_precondition(f):
"""
This function will behave as a decorator which will check the
database connection before running the view. It also attaches
manager, conn & template_path properties to self
"""
@wraps(f)
def wrap(*args, **kwargs):
# Here args[0] will hold self & kwargs will hold gid,sid,jid
self = args[0]
self.driver = get_driver(PG_DEFAULT_DRIVER)
self.manager = self.driver.connection_manager(kwargs['sid'])
self.conn = self.manager.connection()
self.template_path = 'pga_jobstep/sql/pre3.4'
if not ('pgAgent' in self.manager.db_info):
status, res = self.conn.execute_dict("""
SELECT EXISTS(
SELECT 1 FROM information_schema.columns
WHERE
table_schema='pgagent' AND table_name='pga_jobstep' AND
column_name='jstconnstr'
) has_connstr""")
self.manager.db_info['pgAgent'] = res['rows'][0]
return f(*args, **kwargs)
return wrap
@check_precondition
def list(self, gid, sid, jid):
"""
This function is used to list all the language nodes within that collection.
Args:
gid: Server Group ID
sid: Server ID
jid: Job ID
"""
sql = render_template(
"/".join([self.template_path, 'properties.sql']),
jid=jid,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
)
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
return ajax_response(
response=res['rows'],
status=200
)
@check_precondition
def nodes(self, gid, sid, jid, jstid=None):
"""
This function is used to create all the child nodes within the collection.
Here it will create all the language nodes.
Args:
gid: Server Group ID
sid: Server ID
jid: Job ID
"""
res = []
sql = render_template(
"/".join([self.template_path, 'nodes.sql']),
jstid = jstid,
jid = jid
)
status, result = self.conn.execute_2darray(sql)
if not status:
return internal_server_error(errormsg=result)
if jstid is not None:
if len(result['rows']) == 0:
return gone(errormsg="Couldn't find the specified job step.")
row = result['rows'][0]
return make_json_response(
self.blueprint.generate_browser_node(
row['jstid'],
row['jstjobid'],
row['jstname'],
icon="icon-pga_jobstep",
enabled=row['jstenabled'],
kind=row['jstkind']
)
)
for row in result['rows']:
res.append(
self.blueprint.generate_browser_node(
row['jstid'],
row['jstjobid'],
row['jstname'],
icon="icon-pga_jobstep",
enabled=row['jstenabled'],
kind=row['jstkind']
)
)
return make_json_response(
data=res,
status=200
)
@check_precondition
def properties(self, gid, sid, jid, jstid):
"""
This function will show the properties of the selected language node.
Args:
gid: Server Group ID
sid: Server ID
jid: Job ID
jstid: JobStep ID
"""
sql = render_template(
"/".join([self.template_path, 'properties.sql']),
jstid=jstid,
jid=jid,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
)
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
if len(res['rows']) == 0:
return gone(errormsg="Couldn't find the specified job step.")
return ajax_response(
response=res['rows'][0],
status=200
)
@check_precondition
def create(self, gid, sid, jid):
"""
This function will update the data for the selected language node.
Args:
gid: Server Group ID
sid: Server ID
jid: Job ID
"""
data = {}
for k, v in request.args.items():
try:
data[k] = json.loads(
v.decode('utf-8') if hasattr(v, 'decode') else v
)
except ValueError:
data[k] = v
sql = render_template(
"/".join([self.template_path, 'create.sql']),
jid=jid,
data=data,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
)
status, res = self.conn.execute_scalar(sql)
if not status:
return internal_server_error(errormsg=res)
sql = render_template(
"/".join([self.template_path, 'nodes.sql']),
jstid = res,
jid = jid
)
status, res = self.conn.execute_2darray(sql)
if not status:
return internal_server_error(errormsg=res)
row = res['rows'][0]
return make_json_response(
data=self.blueprint.generate_browser_node(
row['jstid'],
row['jstjobid'],
row['jstname'],
icon="icon-pga_jobstep"
)
)
@check_precondition
def update(self, gid, sid, jid, jstid):
"""
This function will update the data for the selected language node.
Args:
gid: Server Group ID
sid: Server ID
jid: Job ID
jstid: JobStep ID
"""
data = request.form if request.form else json.loads(
request.data.decode('utf-8')
)
if (
self.manager.db_info['pgAgent']['has_connstr'] and
'jstconntype' not in data and
('jstdbname' in data or 'jstconnstr' in data)
):
sql = render_template(
"/".join([self.template_path, 'properties.sql']),
jstid=jstid,
jid=jid,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
)
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
if len(res['rows']) == 0:
return gone(
errormsg=gettext(
"Couldn't find the specified job step."
)
)
row = res['rows'][0]
data['jstconntype'] = row['jstconntype']
if row['jstconntype']:
if not ('jstdbname' in data):
data['jstdbname'] = row['jstdbname']
else:
if not ('jstconnstr' in data):
data['jstconnstr'] = row['jstconnstr']
sql = render_template(
"/".join([self.template_path, 'update.sql']),
jid=jid,
jstid=jstid,
data=data,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
)
status, res = self.conn.execute_void(sql)
if not status:
return internal_server_error(errormsg=res)
sql = render_template(
"/".join([self.template_path, 'nodes.sql']),
jstid = jstid,
jid = jid
)
status, res = self.conn.execute_2darray(sql)
if not status:
return internal_server_error(errormsg=res)
row = res['rows'][0]
return make_json_response(
self.blueprint.generate_browser_node(
row['jstid'],
row['jstjobid'],
row['jstname'],
icon="icon-pga_jobstep"
)
)
@check_precondition
def msql(self, gid, sid, jid, jstid=None):
"""
This function is used to return modified SQL for the selected language node.
Args:
gid: Server Group ID
sid: Server ID
jid: Job ID
jstid: Job Step ID
"""
data = {}
sql = ''
for k, v in request.args.items():
try:
data[k] = json.loads(
v.decode('utf-8') if hasattr(v, 'decode') else v
)
except ValueError:
data[k] = v
if jstid is None:
sql = render_template(
"/".join([self.template_path, 'create.sql']),
jid=jid,
data=data,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
)
else:
if (
self.manager.db_info['pgAgent']['has_connstr'] and
'jstconntype' not in data and
('jstdbname' in data or 'jstconnstr' in data)
):
sql = render_template(
"/".join([self.template_path, 'properties.sql']),
jstid=jstid,
jid=jid,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
)
status, res = self.conn.execute_dict(sql)
if not status:
return internal_server_error(errormsg=res)
if len(res['rows']) == 0:
return gone(
errormsg=gettext(
"Couldn't find the specified job step."
)
)
row = res['rows'][0]
data['jstconntype'] = row['jstconntype']
if row['jstconntype']:
if not ('jstdbname' in data):
data['jstdbname'] = row['jstdbname']
else:
if not ('jstconnstr' in data):
data['jstconnstr'] = row['jstconnstr']
sql = render_template(
"/".join([self.template_path, 'update.sql']),
jid=jid,
jstid=jstid,
data=data,
has_connstr=self.manager.db_info['pgAgent']['has_connstr']
)
return make_json_response(
data=sql,
status=200
)
@check_precondition
def statistics(self, gid, sid, jid, jstid):
"""
statistics
Returns the statistics for a particular database if jid is specified,
otherwise it will return statistics for all the databases in that
server.
"""
status, res = self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'stats.sql']),
jid=jid, jstid=jstid, conn=self.conn
)
)
if not status:
return internal_server_error(errormsg=res)
return make_json_response(
data=res,
status=200
)
JobStepView.register_node_view(blueprint)

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

View File

@ -0,0 +1,7 @@
.icon-pga_jobstep {
background-image: url('{{ url_for('NODE-pga_jobstep.static', filename='img/pga_jobstep.png') }}') !important;
background-repeat: no-repeat;
align-content: center;
vertical-align: middle;
height: 1.3em;
}

View File

@ -0,0 +1,275 @@
define([
'jquery', 'underscore', 'underscore.string', 'pgadmin',
'pgadmin.browser', 'alertify', 'backform', 'pgadmin.backform'
],
function($, _, S, pgAdmin, pgBrowser, Alertify, Backform) {
if (!pgBrowser.Nodes['coll-pga_jobstep']) {
pgBrowser.Nodes['coll-pga_jobstep'] =
pgBrowser.Collection.extend({
node: 'pga_jobstep',
label: '{{ _('Steps') }}',
type: 'coll-pga_jobstep',
columns: [
'jstid', 'jstname', 'jstenabled', 'jstkind', 'jstconntype',
'jstonerror'
],
hasStatistics: false
});
}
if (!pgBrowser.Nodes['pga_jobstep']) {
pgBrowser.Nodes['pga_jobstep'] = pgBrowser.Node.extend({
parent_type: 'pga_job',
type: 'pga_jobstep',
hasSQL: true,
hasDepends: false,
hasStatistics: true,
hasCollectiveStatistics: true,
width: '70%',
height: '80%',
canDrop: function(node) {
return true;
},
label: '{{ _('Steps') }}',
node_image: function() {
console.log(arguments);
return 'icon-pga_jobstep';
},
Init: function() {
/* Avoid mulitple registration of menus */
if (this.initialized)
return;
this.initialized = true;
pgBrowser.add_menus([{
name: 'create_pga_jobstep_on_job', node: 'pga_job', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
category: 'create', priority: 4, label: '{{ _('Job Step...') }}',
data: {'action': 'create'}, icon: 'wcTabIcon icon-pga_jobstep'
},{
name: 'create_pga_jobstep_on_coll', node: 'coll-pga_jobstep', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
category: 'create', priority: 4, label: '{{ _('Job Step...') }}',
data: {'action': 'create'}, icon: 'wcTabIcon icon-pga_jobstep'
},{
name: 'create_pga_jobstep', node: 'pga_jobstep', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
category: 'create', priority: 4, label: '{{ _('Job Step...') }}',
data: {'action': 'create'}, icon: 'wcTabIcon icon-pga_jobstep'
}]);
},
model: pgBrowser.Node.Model.extend({
defaults: {
jstid: null,
jstjobid: null,
jstname: '',
jstdesc: '',
jstenabled: true,
jstkind: true,
jstconntype: true,
jstcode: '',
jstconnstr: null,
jstdbname: null,
jstonerror: 'f',
jstnextrun: ''
},
initialize: function() {
pgBrowser.Node.Model.prototype.initialize.apply(this, arguments);
if (this.isNew() && this.get('jstconntype')) {
var args = arguments && arguments.length > 1 && arguments[1];
if (args) {
this.set(
'jstdbname',
(args['node_info'] || args.collection.top['node_info'])['server']['db']
);
}
}
},
idAttribute: 'jstid',
schema: [{
id: 'jstid', label: '{{ _('ID') }}', type: 'integer',
cellHeaderClasses: 'width_percent_5', mode: ['properties']
},{
id: 'jstname', label: '{{ _('Name') }}', type: 'text',
disabled: function(m) { return false; },
cellHeaderClasses: 'width_percent_60'
},{
id: 'jstenabled', label: '{{ _('Enabled') }}', type: 'switch',
disabled: function(m) { return false; }
},{
id: 'jstkind', label: '{{ _('Kind') }}', type: 'switch',
options: {
'onText': '{{ _('SQL') }}', 'offText': '{{ _('Batch') }}',
'onColor': 'primary', 'offColor': 'primary'
}, control: Backform.SwitchControl,
disabled: function(m) { return false; }
},{
id: 'jstconntype', label: '{{ _('Connection type') }}',
type: 'switch', deps: ['jstkind'], mode: ['properties'],
disabled: function(m) { return !m.get('jstkind'); },
options: {
'onText': '{{ _('Local') }}', 'offText': '{{ _('Remote') }}',
'onColor': 'primary', 'offColor': 'primary'
}
},{
id: 'jstconntype', label: '{{ _('Connection type') }}',
type: 'switch', deps: ['jstkind'], mode: ['create', 'edit'],
disabled: function(m) { return !m.get('jstkind'); },
options: {
'onText': '{{ _('Local') }}', 'offText': '{{ _('Remote') }}',
'onColor': 'primary', 'offColor': 'primary'
}, helpMessage: '{{ _('Select <b>Local</b> if the job step will execute on the local database server, or <b>Remote</b> to specify a remote database server.') }}'
},{
id: 'jstdbname', label: '{{ _('Database') }}', type: 'text',
mode: ['properties'], disabled: function(m) { return false; }
},{
id: 'jstconnstr', type: 'text', mode: ['properties'],
label: '{{ _('Connection string') }}'
},{
id: 'jstdbname', label: '{{ _('Database') }}', type: 'text',
control: 'node-list-by-name', node: 'database',
cache_node: 'database', select2: {allowClear: true, placeholder: ''},
disabled: function(m) {
return !m.get('jstkind') || !m.get('jstconntype');
}, deps: ['jstkind', 'jstconntype'], mode: ['create', 'edit'],
helpMessage: '{{ _('Please select the database on which the job step will run.') }}'
},{
id: 'jstconnstr', label: '{{ _('Connection string') }}', type: 'text',
deps: ['jstkind', 'jstconntype'], disabled: function(m) {
return !m.get('jstkind') || m.get('jstconntype');
}, helpMessage: S(
'{{ _("Please specify the connection string for the remote database server. Each parameter setting is in the form keyword = value. Spaces around the equal sign are optional. To write an empty value, or a value containing spaces, surround it with single quotes, e.g., keyword = \\'a value\\'. Single quotes and backslashes within the value must be escaped with a backslash, i.e., \\\' and \\\\.<br>For more information, please see the documentation on %s") }}'
).sprintf(
'<a href="https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING" target="_blank">libpq connection strings</a>'
).value(), mode: ['create', 'edit']
},{
id: 'jstonerror', label: '{{ _('On error') }}', cell: 'select2',
control: 'select2', options: [
{'label': '{{ _("Fail") }}', 'value': "f"},
{'label': '{{ _("Success") }}', 'value': "s"},
{'label': '{{ _("Ignore") }}', 'value': "i"}
], select2: {allowClear: false}, disabled: function(m) {
return false;
}
},{
id: 'jstdesc', label: '{{ _('Comment') }}', type: 'multiline'
},{
id: 'jstcode', label: '', cell: 'string', deps: ['jstkind'],
type: 'text', control: 'sql-field', group: '{{ _('Code') }}',
control: Backform.SqlFieldControl.extend({
render: function() {
if (this.model.get('jstkind')) {
this.field.set('label', '{{ _('SQL query') }}');
} else {
this.field.set('label', '{{ _('Script') }}');
}
return Backform.SqlFieldControl.prototype.render.apply(
this, arguments
);
}
})
}],
validate: function(keys) {
var val = this.get('jstname'),
errMsg = null;
if (
_.isUndefined(val) || _.isNull(val) ||
String(val).replace(/^\s+|\s+$/g, '') == ''
) {
errMsg = '{{ _('Name cannot be empty.') }}';
this.errorModel.set('jstname', errMsg);
} else {
this.errorModel.unset('jstname');
}
if (this.get('jstkind')) {
if (this.get('jstconntype')) {
this.errorModel.unset('jstconnstr');
val = this.get('jstdbname');
if (
_.isUndefined(val) || _.isNull(val) ||
String(val).replace(/^\s+|\s+$/g, '') == ''
) {
var msg = '{{ _('Please select a database.') }}';
errMsg = errMsg || msg;
this.errorModel.set('jstdbname', msg);
} else {
this.errorModel.unset('jstdbname');
}
} else {
this.errorModel.unset('jstdbname');
var msg,
r = /\s*\b(\w+)\s*=\s*('([^'\\]*(?:\\.[^'\\]*)*)'|\w*)/g;
val = this.get('jstconnstr');
if (
_.isUndefined(val) || _.isNull(val) ||
String(val).replace(/^\s+|\s+$/g, '') == ''
) {
msg = '{{ _("Please enter a connection string.") }}';
} else if (String(val).replace(r, '') != '') {
msg = '{{ _("Please enter a valid connection string.") }}';
} else {
var m,
params = {
'host': true, 'hostaddr': true, 'port': true,
'dbname': true, 'user': true, 'password': true,
'connect_timeout': true, 'client_encoding': true,
'application_name': true, 'options': true,
'fallback_application_name': true, 'sslmode': true,
'sslcert': true, 'sslkey': true, 'sslrootcert': true,
'sslcrl': true, 'keepalives': true, 'service': true,
'keepalives_idle': true, 'keepalives_interval': true,
'keepalives_count': true, 'sslcompression': true,
'requirepeer': true, 'krbsrvname': true, 'gsslib': true,
};
while((m = r.exec(val))) {
if (params[m[1]]) {
if (m[2])
continue;
msg = '{{ _("Please enter a valid connection string.") }}';
break;
}
msg = S(
'{{ _("Invalid parameter in the connection string - %s.") }}'
).sprintf(m[1]).value();
break;
}
}
if (msg) {
errMsg = errMsg || msg;
this.errorModel.set('jstconnstr', msg);
} else {
this.errorModel.unset('jstconnstr');
}
}
} else {
this.errorModel.unset('jstconnstr');
this.errorModel.unset('jstdbname');
}
val = this.get('jstcode');
if (
_.isUndefined(val) || _.isNull(val) ||
String(val).replace(/^\s+|\s+$/g, '') == ''
) {
var msg = '{{ _('Please specify code to execute.') }}';
errMsg = errMsg || msg;
this.errorModel.set('jstcode', msg);
} else {
this.errorModel.unset('jstcode');
}
return errMsg;
}
})
});
}
return pgBrowser.Nodes['pga_job'];
});

View File

@ -0,0 +1,24 @@
{##################################################}
{# This will be specific macro for pga_exception. #}
{##################################################}
{% macro INSERT(jscid, data) -%}
-- Inserting a schedule exception {% if jscid %}(schedule: {{ jscid|qtLiteral}}){% endif %}
INSERT INTO pgagent.pga_exception (
jexscid, jexdate, jextime
) VALUES (
{% if jscid %}{{ jscid|qtLiteral }}{% else %}scid{% endif %}, {% if data.jexdate %}to_date({{ data.jexdate|qtLiteral }}, 'MM/DD/YYYY')::date{% else %}NULL::date{% endif %}, {% if data.jextime %}{{ data.jextime|qtLiteral }}::time without time zone{% else %}NULL::time without time zone{% endif %}
);
{%- endmacro %}
{% macro UPDATE(jscid, data) -%}
-- Updating an existing schedule exception (id: {{ data.jexid|qtLiteral }}, schedule: {{ jscid|qtLiteral }})
UPDATE pgagent.pga_exception SET
{% if 'jexdate' in data %}jexdate={% if data.jexdate %}to_date({{ data.jexdate|qtLiteral }}, 'MM/DD/YYYY')::date{% else %}NULL::date{% endif %}{% endif %}{% if 'jextime' in data%}{% if 'jexdate' in data %}, {% endif %}jextime={% if data.jextime %}{{ data.jextime|qtLiteral }}::time without time zone{% else %}NULL::time without time zone{% endif %}{% endif %}
WHERE jexid={{ data.jexid|qtLiteral }}::integer AND jscid={{ jscid|qtLiteral }}::integer;
{%- endmacro %}
{% macro DELETE(jscid, data) -%}
-- Deleting a schedule exception (id: {{ data.jexid|qtLiteral }}, schedule: {{ jscid|qtLiteral }})
DELETE FROM pgagent.pga_exception WHERE jexid={{ data.jexid|qtLiteral }}::integer AND jscid={{ jscid|qtLiteral }}::integer;
{%- endmacro %}

View File

@ -0,0 +1,51 @@
{################################################}
{# This will be specific macro for pga_jobstep. #}
{################################################}
{% macro INSERT(has_connstr, jid, data) -%}
-- Inserting a step (jobid: {{ jid|qtLiteral }})
INSERT INTO pgagent.pga_jobstep (
jstjobid, jstname, jstenabled, jstkind,
{% if has_connstr %}jstconnstr, {% endif %}jstdbname, jstonerror,
jstcode, jstdesc
) VALUES (
{% if jid %}{{ jid|qtLiteral }}{% else %}jid{% endif %}, {{ data.jstname|qtLiteral }}::text, {% if data.jstenabled %}true{% else %}false {% endif %}, {% if data.jstkind %}'s'{% else %}'b'{% endif %}::character(1),
{% if has_connstr %}{% if data.jstconntype %}''{% else %}{{ data.jstconnstr|qtLiteral }}{% endif %}::text, {% if not data.jstconntype %}''::name{% else %}{{ data.jstdbname|qtLiteral }}{% endif %}::name{% else %}{{ data.jstdbname|qtLiteral }}::name{% endif %}, {{ data.jstonerror|qtLiteral }}::character(1),
{{ data.jstcode|qtLiteral }}::text, {{ data.jstdesc|qtLiteral }}::text
) {% if jid %}RETURNING jstid{% endif %};
{%- endmacro %}
{% macro UPDATE(has_connstr, jid, jstid, data) -%}
-- Updating the existing step (id: {{ jstid|qtLiteral }} jobid: {{ jid|qtLiteral }})
UPDATE pgagent.pga_jobstep
SET
{% if has_connstr %}{% if 'jstconntype' in data %}{% if data.jstconntype %}jstdbname={{ data.jstdbname|qtLiteral }}, jstconnstr=''{% else %}jstdbname='', jstconnstr={{ data.jstconnstr|qtLiteral }}{% endif %}{% if 'jstname' in data or 'jstenabled' in data or 'jstdesc' in data or 'jstonerror' in data or 'jstcode' in data %},{% endif %}{% endif %}{% else %}{% if 'jstdbname' in data %}jstdbname={{ data.jstdbname|qtLiteral }}::name{% endif %}{% if 'jstname' in data or 'jstenabled' in data or 'jstdesc' in data or 'jstonerror' in data or 'jstcode' in data %},{% endif %}{% endif %}{% if 'jstname' in data %}
jstname={{ data.jstname|qtLiteral }}::text{% if 'jstenabled' in data or 'jstdesc' in data or 'jstonerror' in data or 'jstcode' in data %},{% endif %}{% endif %}{% if 'jstenabled' in data %}
jstenabled={% if data.jstenabled %}true{% else %}false{% endif %}{% if 'jstdesc' in data or 'jstonerror' in data or 'jstcode' in data %},{% endif %}{% endif %}{% if 'jstdesc' in data %}
jstdesc={{ data.jstdesc|qtLiteral }}{% if 'jstonerror' in data or 'jstcode' in data %},{% endif %}{% endif %}{% if 'jstonerror' in data %}
jstonerror={{ data.jstonerror|qtLiteral }}{% if 'jstcode' in data %},{% endif %}{% endif %}{% if 'jstcode' in data %}
jstcode={{ data.jstcode|qtLiteral }}{% endif %}
WHERE jstid={{ jstid|qtLiteral }}::integer AND jstjobid={{ jid|qtLiteral }}::integer;
{%- endmacro %}
{% macro DELETE(jid, jstid) -%}
-- Deleting a step (id: {{ jstid|qtLiteral }}, jobid: {{ jid|qtLiteral }})
DELETE FROM pgagent.pga_jobstep WHERE jstid={{ jstid|qtLiteral }}::integer AND jstjobid={{ jid|qtLiteral }}::integer;
{%- endmacro %}
{% macro PROPERTIES(has_connstr, jid, jstid) -%}
SELECT
jstid, jstjobid, jstname, jstdesc, jstenabled, jstkind = 's'::bpchar as jstkind,
jstcode, CASE WHEN jstdbname != '' THEN true ELSE false END AS jstconntype,
{% if has_connstr %}jstconnstr, {% endif %} jstdbname, jstonerror, jscnextrun
FROM
pgagent.pga_jobstep
WHERE
{% if jstid %}
jstid = {{ jstid|qtLiteral }}::integer AND
{% endif %}
jstjobid = {{ jid|qtLiteral }}::integer
ORDER BY jstname;
{%- endmacro %}

View File

@ -0,0 +1,107 @@
{#################################################}
{# This will be specific macro for pga_schedule. #}
{#################################################}
{% import 'macros/pga_exception.macros' as EXCEPTIONS %}
{% macro INSERT(jid, data) -%}
-- Inserting a schedule{% if jid %} (jobid: {{ jid|qtLiteral }}){% endif %}
INSERT INTO pgagent.pga_schedule(
jscjobid, jscname, jscdesc, jscenabled,
jscstart, {% if data.jscend %}jscend,{% endif %}
jscminutes, jschours, jscweekdays, jscmonthdays, jscmonths
) VALUES (
{% if jid %}{{ jid|qtLiteral }}{% else %}jid{% endif %}, {{ data.jscname|qtLiteral }}::text, {{ data.jscdesc|qtLiteral }}::text, {% if data.jscenabled %}true{% else %}false{% endif %},
{{ data.jscstart|qtLiteral }}::timestamp with time zone, {% if data.jscend %}{{ data.jscend|qtLiteral }}::timestamp with time zone,{% endif %}
-- Minutes
{{ data.jscminutes|qtLiteral }}::boolean[],
-- Hours
{{ data.jschours|qtLiteral }}::boolean[],
-- Week days
{{ data.jscweekdays|qtLiteral }}::boolean[],
-- Month days
{{ data.jscmonthdays|qtLiteral }}::boolean[],
-- Months
{{ data.jscmonths|qtLiteral }}::boolean[]
) RETURNING jscid INTO scid;{% if 'jscexceptions' in data %}
{% for exc in data.jscexceptions %}
{{ EXCEPTIONS.INSERT(None, exc) }}{% endfor %}{% endif %}
{%- endmacro %}
{% macro UPDATE(jid, jscid, data) -%}
{% if 'jscname' in data or 'jscenabled' in data or 'jscdesc' in data or 'jscstart' in data or 'jscend' in data or 'jscend' in data or 'jscmonths' in data or 'jscminutes' in data or 'jscmonthdays' in data or 'jschours' in data or 'jscweekdays' in data %}
-- Updating the schedule (id: {{ jscid|qtLiteral }}, jobid: {{ jid|qtLiteral }})
UPDATE pgagent.pga_schedule
SET{% if 'jscname' in data %}
jscname={{ data.jscname|qtLiteral }}::text{% if 'jscdesc' in data or 'jscstart' in data or 'jscend' in data or 'jscend' in data or 'jscmonths' in data or 'jscminutes' in data or 'jscmonthdays' in data or 'jschours' in data or 'jscweekdays' in data or 'jscenabled' in data %},{% endif %}{% endif %}{% if 'jscenabled' in data %}
jscenabled={% if data.jscenabled %}true{% else %}false{% endif %}{% if 'jscdesc' in data or 'jscstart' in data or 'jscend' in data or 'jscend' in data or 'jscmonths' in data or 'jscminutes' in data or 'jscmonthdays' in data or 'jschours' in data or 'jscweekdays' in data %},{% endif %}{% endif %}{% if 'jscdesc' in data %}
jscdesc={% if data.jscdesc %}{{ data.jscdesc|qtLiteral }}::text{% else %}''::text{% endif %}{% if 'jscstart' in data or 'jscend' in data or 'jscend' in data or 'jscmonths' in data or 'jscminutes' in data or 'jscmonthdays' in data or 'jschours' in data or 'jscweekdays' in data %},{% endif %}{% endif %}{% if 'jscstart' in data %}
jscstart={{ data.jscstart|qtLiteral }}::text{% if 'jscend' in data or 'jscend' in data or 'jscmonths' in data or 'jscminutes' in data or 'jscmonthdays' in data or 'jschours' in data or 'jscweekdays' in data %},{% endif %}{% endif %}{% if 'jscend' in data %}
jscend={% if data.jscend %}{{ data.jscend|qtLiteral }}::timestamptz{% else %}NULL::timestamptz{% endif %}{% if 'jscend' in data or 'jscmonths' in data or 'jscminutes' in data or 'jscmonthdays' in data or 'jschours' in data or 'jscweekdays' in data %},{% endif %}{% endif %}{% if 'jscmonths' in data %}
jscmonths={{ data.jscmonths|qtLiteral }}::boolean[]{% if 'jscminutes' in data or 'jscmonthdays' in data or 'jschours' in data or 'jscweekdays' in data %},{% endif %}{% endif %}{% if 'jscminutes' in data %}
jscminutes={{ data.jscminutes|qtLiteral }}::boolean[]{% if 'jscmonthdays' in data or 'jschours' in data or 'jscweekdays' in data %},{% endif %}{% endif %}{% if 'jscmonthdays' in data %}
jscmonthdays={{ data.jscmonthdays|qtLiteral }}::boolean[]{% if 'jschours' in data or 'jscweekdays' in data %},{% endif %}{% endif %}{% if 'jschours' in data %}
jschours={{ data.jschours|qtLiteral }}::boolean[]{% if 'jscweekdays' in data %},{% endif %}{% endif %}{% if 'jscweekdays' in data %}
jscweekdays={{ data.jscweekdays|qtLiteral }}::boolean[]{% endif %}
WHERE jscid={{ jscid|qtLiteral }}::integer AND jscjobid={{ jid|qtLiteral }}::integer;{% endif %}{% if 'jscexceptions' in data %}
{% if 'added' in data.jscexceptions and data.jscexceptions.added|length > 0 %}
{% for exc in data.jscexceptions.added %}
{{ EXCEPTIONS.INSERT(jscid, exc) }}
{% endfor %}
{% endif %}
{% if 'deleted' in data.jscexceptions and data.jscexceptions.deleted|length > 0 %}
{% for exc in data.jscexceptions.deleted %}
{{ EXCEPTIONS.DELETE(jscid, exc) }}
{% endfor %}
{% endif %}
{% if 'changed' in data.jscexceptions and data.jscexceptions.changed|length > 0 %}
{% for exc in data.jscexceptions.changed %}
{{ EXCEPTIONS.UPDATE(jscid, exc) }}
{% endfor %}
{% endif %}
{% endif %}
{%- endmacro %}
{% macro DELETE(jid, jscid) -%}
-- Removing the existing schedule (id: {{ jscid|qtLiteral }}, jobid: {{ jid|qtLiteral }})
DELETE FROM pgagent.pga_schedule WHERE jscid={{ jscid|qtLiteral }}::integer AND jscjobid={{ jid|qtLiteral }}::integer;
{%- endmacro %}
{% macro FETCH_CURRENT() -%}
SELECT jscid FROM pgagent.pga_schedule WHERE xmin::text = (txid_current() % (2^32)::bigint)::text;
{%- endmacro %}
{% macro PROPERTIES(jid, jscid) -%}
SELECT
jscid, jscjobid, jscname, jscdesc, jscenabled, jscstart, jscend,
jscminutes, jschours, jscweekdays, jscmonthdays, jscmonths,
jexid, jexdate, jextime
FROM
pgagent.pga_schedule s
LEFT JOIN (
SELECT
jexscid, array_agg(jexid) AS jexid, array_agg(to_char(jexdate, 'YYYY-MM-DD')) AS jexdate,
array_agg(jextime) AS jextime
FROM
pgagent.pga_exception ex
GROUP BY
jexscid
) e ON s.jscid = e.jexscid
WHERE
{% if jscid %}
s.jscid = {{ jscid|qtLiteral }}::integer AND
{% endif %}
s.jscjobid = {{ jid|qtLiteral }}::integer
ORDER BY jscname;
{%- endmacro %}

View File

@ -0,0 +1,28 @@
.icon-pga_job {
background-image: url('{{ url_for('NODE-pga_job.static', filename='img/pga_job.png') }}') !important;
background-repeat: no-repeat;
align-content: center;
vertical-align: middle;
height: 1.3em;
}
.icon-pga_job-disabled {
background-image: url('{{ url_for('NODE-pga_job.static', filename='img/pga_job-disabled.png') }}') !important;
background-repeat: no-repeat;
align-content: center;
vertical-align: middle;
height: 1.3em;
}
.pg-el-container[el=sm] .pga-round-border {
border: 1px solid darkgray;
border-radius: 12px;
}
.pg-el-container[el=sm] .pga-round-border {
margin: 5px;
}
div[role=tabpanel] > .pgadmin-control-group.form-group.pg-el-xs-12.jscexceptions {
min-height: 400px;
}

View File

@ -0,0 +1,161 @@
define(
[
'jquery', 'underscore', 'underscore.string', 'pgadmin',
'pgadmin.browser', 'alertify', 'pgadmin.node.pga_jobstep',
'pgadmin.node.pga_schedule'
],
function($, _, S, pgAdmin, pgBrowser, Alertify) {
if (!pgBrowser.Nodes['coll-pga_job']) {
var pga_jobs = pgBrowser.Nodes['coll-pga_job'] =
pgBrowser.Collection.extend({
node: 'pga_job',
label: '{{ _('pga_jobs') }}',
type: 'coll-pga_job',
columns: ['jobid', 'jobname', 'jobenabled', 'jlgstatus', 'jobnextrun', 'joblastrun', 'jobdesc'],
hasStatistics: false
});
};
if (!pgBrowser.Nodes['pga_job']) {
pgBrowser.Nodes['pga_job'] = pgBrowser.Node.extend({
parent_type: 'server',
type: 'pga_job',
hasSQL: true,
hasDepends: false,
hasStatistics: true,
hasCollectiveStatistics: true,
width: '80%',
height: '80%',
canDrop: function(node) {
return true;
},
label: '{{ _('pgAgent Job') }}',
node_image: function() {
return 'icon-pga_job';
},
Init: function() {
/* Avoid mulitple registration of menus */
if (this.initialized)
return;
this.initialized = true;
pgBrowser.add_menus([{
name: 'create_pga_job_on_coll', node: 'coll-pga_job', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
category: 'create', priority: 4, label: '{{ _('pgAgent Job...') }}',
icon: 'wcTabIcon icon-pga_job', data: {action: 'create'}
},{
name: 'create_pga_job', node: 'pga_job', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
category: 'create', priority: 4, label: '{{ _('pgAgent Job...') }}',
icon: 'wcTabIcon icon-pga_job', data: {action: 'create'}
}]);
},
model: pgBrowser.Node.Model.extend({
defaults: {
jobname: '',
jobid: undefined,
jobenabled: true,
jobhostagent: '',
jobjclid: 1,
jobcreated: undefined,
jobchanged: undefined,
jobnextrun: undefined,
joblastrun: undefined,
jlgstatus: undefined,
jobrunningat: undefined,
jobdesc: '',
jsteps: [],
jschedules: []
},
idAttribute: 'jobid',
parse: function() {
var d = pgBrowser.Node.Model.prototype.parse.apply(this, arguments);
if (d) {
d.jobrunningat = d.jaghostagent || "{{ _('Not running currently.') }}";
d.jlgstatus = d.jlgstatus || "{{ _('Unknown') }}";
}
return d;
},
schema: [{
id: 'jobname', label: '{{ _('Name') }}', type: 'text',
cellHeaderClasses: 'width_percent_30'
},{
id: 'jobid', label:'{{ _('ID') }}', mode: ['properties'],
type: 'int'
},{
id: 'jobenabled', label:'{{ _('Enabled') }}', type: 'switch',
cellHeaderClasses: 'width_percent_5'
},{
id: 'jobclass', label: '{{ _('Job Class') }}', type: 'text',
mode: ['properties']
},{
id: 'jobjclid', label: '{{ _('Job Class') }}', type: 'integer',
control: 'node-ajax-options', url: 'classes', url_with_id: false,
cache_node: 'server', mode: ['create', 'edit'],
select2: {allowClear: false},
helpMessage: '{{ _('Please a class to categorize the job. This option will not affect the way the job runs.') }}'
},{
id: 'jobhostagent', label: '{{ _('Host Agent') }}', type: 'text',
mode: ['edit', 'create'],
helpMessage: '{{ _('Enter the hostname of a machine running pgAgent if you wish to ensure only that machine will run this job. Leave blank if any host may run the job.') }}'
},{
id: 'jobhostagent', label: '{{ _('Host Agent') }}', type: 'text',
mode: ['properties']
},{
id: 'jobcreated', type: 'text', mode: ['properties'],
label: '{{ _('Created') }}'
},{
id: 'jobchanged', type: 'text', mode: ['properties'],
label: '{{ _('Changed') }}'
},{
id: 'jobnextrun', type: 'text', mode: ['properties'],
label: '{{ _('Next run') }}', cellHeaderClasses: 'width_percent_20'
},{
id: 'joblastrun', type: 'text', mode: ['properties'],
label: '{{ _('Last run') }}', cellHeaderClasses: 'width_percent_20'
},{
id: 'jlgstatus', type: 'text', label: '{{ _('Last result') }}',
cellHeaderClasses: 'width_percent_5', mode: ['properties']
},{
id: 'jobrunningat', type: 'text', mode: ['properties'],
label: '{{ _('Running at') }}'
},{
id: 'jobdesc', label:'{{ _('Comment') }}', type: 'multiline',
cellHeaderClasses: 'width_percent_15'
},{
id: 'jsteps', label: '', group: '{{ _("Steps") }}',
type: 'collection', mode: ['edit', 'create'],
model: pgBrowser.Nodes['pga_jobstep'].model, canEdit: true,
control: 'sub-node-collection', canAdd: true, canDelete: true,
columns: [
'jstname', 'jstenabled', 'jstkind', 'jstconntype', 'jstonerror'
]
},{
id: 'jschedules', label: '', group: '{{ _("Schedules") }}',
type: 'collection', mode: ['edit', 'create'],
control: 'sub-node-collection', canAdd: true, canDelete: true,
canEdit: true, model: pgBrowser.Nodes['pga_schedule'].model,
columns: ['jscname', 'jscenabled', 'jscstart', 'jscend']
}],
validate: function(keys) {
var name = this.get('jobname');
if (_.isUndefined(name) || _.isNull(name) ||
String(name).replace(/^\s+|\s+$/g, '') == '') {
var msg = '{{ _('Name cannot be empty.') }}';
this.errorModel.set('jobname', msg);
return msg;
} else {
this.errorModel.unset('jobname');
}
return null;
}
})
});
}
return pgBrowser.Nodes['pga_job'];
});

View File

@ -0,0 +1,31 @@
{% import 'macros/pga_jobstep.macros' as STEP %}
{% import 'macros/pga_schedule.macros' as SCHEDULE %}
DO $$
DECLARE
jid integer;{% if 'jschedules' in data and data.jschedules|length > 0 %}
scid integer;{% endif %}
BEGIN
-- Creating a new job
INSERT INTO pgagent.pga_job(
jobjclid, jobname, jobdesc, jobhostagent, jobenabled
) VALUES (
{{ data.jobjclid|qtLiteral }}::integer, {{ data.jobname|qtLiteral }}::text, {{ data.jobdesc|qtLiteral }}::text, {{ data.jobhostagent|qtLiteral }}::text, {% if data.jobenabled %}true{% else %}false{% endif %}
) RETURNING jobid INTO jid;{% if 'jsteps' in data and data.jsteps|length > 0 %}
-- Steps
{% for step in data.jsteps %}{{ STEP.INSERT(has_connstr, None, step) }}{% endfor %}
{% endif %}{% if 'jschedules' in data and data.jschedules|length > 0 %}
-- Schedules
{% for schedule in data.jschedules %}{{ SCHEDULE.INSERT(None, schedule) }}{% endfor %}
{% endif %}
END
$$;{% if fetch_id %}
SELECT jobid FROM pgagent.pga_job WHERE xmin::text = (txid_current() % (2^32)::bigint)::text;{% endif %}

View File

@ -0,0 +1 @@
DELETE FROM pgagent.pga_job WHERE jobid = {{ jid|qtLiteral }}::integer;

View File

@ -0,0 +1 @@
SELECT jclid AS value, jclname AS label FROM pgagent.pga_jobclass

View File

@ -0,0 +1,8 @@
SELECT
jobid, jobname, jobenabled
FROM
pgagent.pga_job
{% if jid %}
WHERE jobid = {{ jid|qtLiteral }}::integer
{% endif %}
ORDER BY jobname;

View File

@ -0,0 +1,21 @@
SELECT
j.jobid AS jobid, j.jobname as jobname, j.jobenabled as jobenabled,
j.jobdesc AS jobdesc, j.jobhostagent AS jobhostagent,
j.jobcreated AS jobcreated, j.jobchanged AS jobchanged,
ag.jagstation AS jagagent, sub.jlgstatus AS jlgstatus,
j.jobagentid AS jobagentid, j.jobnextrun AS jobnextrun,
j.joblastrun AS joblastrun, j.jobjclid AS jobjclid,
jc.jclname AS jobclass
FROM
pgagent.pga_job j
LEFT OUTER JOIN pgagent.pga_jobagent ag ON ag.jagpid=jobagentid
LEFT OUTER JOIN (
SELECT DISTINCT ON (jlgjobid) jlgstatus, jlgjobid
FROM pgagent.pga_joblog
ORDER BY jlgjobid, jlgid DESC
) sub ON sub.jlgjobid = j.jobid
LEFT JOIN pgagent.pga_jobclass jc ON (j.jobjclid = jc.jclid)
{% if jid %}
WHERE j.jobid = {{ jid|qtLiteral }}::integer
{% endif %}
ORDER BY j.jobname;

View File

@ -0,0 +1,3 @@
UPDATE pgagent.pga_job
SET jobnextrun=now()::timestamptz
WHERE jobid={{ jid|qtLiteral }}::integer

View File

@ -0,0 +1,2 @@
{% import 'macros/pga_schedule.macros' as SCHEDULE %}
{{ SCHEDULE.PROPERTIES(jid, jscid) }}

View File

@ -0,0 +1,11 @@
SELECT
jlgid AS {{ conn|qtIdent(_('Run')) }},
jlgstatus AS {{ conn|qtIdent(_('Status')) }},
jlgstart AS {{ conn|qtIdent(_('Start time')) }},
jlgduration AS {{ conn|qtIdent(_('Duration')) }},
(jlgstart + jlgduration) AS {{ conn|qtIdent(_('End time')) }}
FROM
pgagent.pga_joblog
WHERE
jlgjobid = {{ jid|qtLiteral }}::integer
ORDER BY jlgid DESC;

View File

@ -0,0 +1,2 @@
{% import 'macros/pga_jobstep.macros' as STEP %}
{{ STEP.PROPERTIES(has_connstr, jid, jstid) }}

View File

@ -0,0 +1,20 @@
{% import 'macros/pga_jobstep.macros' as STEP %}
{% import 'macros/pga_schedule.macros' as SCHEDULE %}
{% if 'jobjclid' in data or 'jobname' in data or 'jobdesc' in data or 'jobhostagent' in data or 'jobenabled' in data %}
UPDATE pgagent.pga_job
SET {% if 'jobjclid' in data %}jobjclid={{ data.jobjclid|qtLiteral }}::integer{% if 'jobname' in data or 'jobdesc' in data or 'jobhostagent' in data or 'jobenabled' in data %}, {% endif %}{% endif %}
{% if 'jobname' in data %}jobname={{ data.jobname|qtLiteral }}::text{%if 'jobdesc' in data or 'jobhostagent' in data or 'jobenabled' in data %}, {% endif %}{% endif %}
{% if 'jobdesc' in data %}jobdesc={{ data.jobdesc|qtLiteral }}::text{%if 'jobhostagent' in data or 'jobenabled' in data %}, {% endif %}{% endif %}
{%if 'jobhostagent' in data %}jobhostagent={{ data.jobhostagent|qtLiteral }}::text{% if 'jobenabled' in data %}, {% endif %}{% endif %}
{% if 'jobenabled' in data %}jobenabled={% if data.jobenabled %}true{% else %}false{% endif %}{% endif %}
WHERE jobid = {{ jid }};
{% endif %}{% if 'jsteps' in data %}
{% if 'deleted' in data.jsteps %}{% for step in data.jsteps.deleted %}{{ STEP.DELETE(jid, step.jstid) }}{% endfor %}{% endif %}
{% if 'changed' in data.jsteps %}{% for step in data.jsteps.changed %}{{ STEP.UPDATE(has_connstr, jid, step.jstid, step) }}{% endfor %}{% endif %}
{% if 'added' in data.jsteps %}{% for step in data.jsteps.added %}{{ STEP.INSERT(has_connstr, jid, step) }}{% endfor %}{% endif %}{% endif %}{% if 'jschedules' in data %}
{% if 'deleted' in data.jschedules %}{% for schedule in data.jschedules.deleted %}{{ SCHEDULE.DELETE(jid, schedule.jscid) }}{% endfor %}{% endif %}
{% if 'changed' in data.jschedules %}{% for schedule in data.jschedules.changed %}{{ SCHEDULE.UPDATE(has_connstr, jid, schedule.jscid, schedule) }}{% endfor %}{% endif %}
{% if 'added' in data.jschedules %}{% for schedule in data.jschedules.added %}{{ SCHEDULE.INSERT(has_connstr, jid, schedule) }}{% endfor %}{% endif %}{% endif %}

View File

@ -0,0 +1,2 @@
{% import 'macros/pga_jobstep.macros' as STEP %}
{{ STEP.INSERT(has_connstr, jid, data) }}

View File

@ -0,0 +1,2 @@
{% import 'macros/pga_jobstep.macros' as STEP %}
{{ STEP.DELETE(jid, jstid) }}

View File

@ -0,0 +1,10 @@
SELECT
jstid, jstjobid, jstname, jstenabled, jstkind = 's'::bpchar AS jstkind
FROM
pgagent.pga_jobstep
WHERE
{% if jstid %}
jstid = {{ jstid|qtLiteral }}::integer AND
{% endif %}
jstjobid = {{ jid|qtLiteral }}::integer
ORDER BY jstname;

View File

@ -0,0 +1,2 @@
{% import 'macros/pga_jobstep.macros' as STEP %}
{{ STEP.PROPERTIES(has_connstr, jid, jstid) }}

View File

@ -0,0 +1,13 @@
SELECT
jslid AS {{ conn|qtIdent(_('Run')) }},
jslstatus AS {{ conn|qtIdent(_('Status')) }},
jslresult AS {{ conn|qtIdent(_('Result')) }},
jslstart AS {{ conn|qtIdent(_('Start time')) }},
(jslstart + jslduration) AS {{ conn|qtIdent(_('End time')) }},
jslduration AS {{ conn|qtIdent(_('Duration')) }},
jsloutput AS {{ conn|qtIdent(_('Output')) }}
FROM
pgagent.pga_jobsteplog
WHERE
jsljstid = {{ jstid|qtLiteral }}::integer
ORDER BY jslid DESC;

View File

@ -0,0 +1,2 @@
{% import 'macros/pga_jobstep.macros' as STEP %}
{{ STEP.UPDATE(has_connstr, jid, jstid, data) }}

View File

@ -0,0 +1,10 @@
{% import 'macros/pga_schedule.macros' as SCHEDULE %}
DO $$
DECLARE
jscid integer;
BEGIN
{{ SCHEDULE.INSERT(jid, data) }}
END
$$ LANGUAGE 'plpgsql';{% if fetch_id %}
{{ SCHEDULE.FETCH_CURRENT() }}{% endif %}

View File

@ -0,0 +1,2 @@
{% import 'macros/pga_schedule.macros' as SCHEDULE %}
{{ SCHEDULE.DELETE(jid, jscid) }}

View File

@ -0,0 +1,11 @@
SELECT
jscid, jscjobid, jscname, jscenabled
FROM
pgagent.pga_schedule
WHERE
{% if jscid %}
jscid = {{ jscid|qtLiteral }}::integer
{% else %}
jscjobid = {{ jid|qtLiteral }}::integer
{% endif %}
ORDER BY jscname;

View File

@ -0,0 +1,2 @@
{% import 'macros/pga_schedule.macros' as SCHEDULE %}
{{ SCHEDULE.PROPERTIES(jid, jstid) }}

View File

@ -0,0 +1,2 @@
{% import 'macros/pga_schedule.macros' as SCHEDULE %}
{{ SCHEDULE.UPDATE(jid, jscid, data) }}