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

This commit is contained in:
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,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'];
});