[gen] Calendar field: added a validation mechanism.

This commit is contained in:
Gaetan Delannay 2015-03-04 14:35:02 +01:00
parent d1aec8d5e6
commit 09bf03f9bf
13 changed files with 251 additions and 65 deletions

View file

@ -2,7 +2,7 @@
# ------------------------------------------------------------------------------
import types
from appy import Object
from appy.shared.utils import splitList, IterSub
from appy.shared import utils as sutils
from appy.gen import Field
from appy.px import Px
from DateTime import DateTime
@ -33,6 +33,22 @@ class Timeslot:
if not self.eventTypes: return True
return eventType in self.eventTypes
# ------------------------------------------------------------------------------
class Validation:
'''The validation process for a calendar consists in "converting" some event
types being "wishes" to other event types being the corresponding
validated events. This class holds information about this validation
process. For more information, see the Calendar constructor, parameter
"validation".'''
def __init__(self, method, schema):
# p_method holds a method that must return True if the currently logged
# user can validate whish events.
self.method = method
# p_schema must hold a dict whose keys are the event types being wishes
# and whose values are the event types being the corresponding validated
# event types.
self.schema = schema
# ------------------------------------------------------------------------------
class Other:
'''Identifies a Calendar field that must be shown within another Calendar
@ -104,9 +120,10 @@ class Calendar(Field):
DateTime = DateTime
# Access to Calendar utility classes via the Calendar class
Timeslot = Timeslot
Validation = Validation
Other = Other
Event = Event
IterSub = IterSub
IterSub = sutils.IterSub
# Error messages
TIMESLOT_USED = 'An event is already defined at this timeslot.'
DAY_FULL = 'No more place for adding this event.'
@ -155,6 +172,7 @@ class Calendar(Field):
# Timeline view for a calendar
pxViewTimeline = Px('''
<table cellpadding="0" cellspacing="0" class="list timeline"
id=":ajaxHookId + '_cal'"
var="monthsInfos=field.getTimelineMonths(grid, zobj)">
<!-- Column specifiers -->
<colgroup>
@ -202,14 +220,12 @@ class Calendar(Field):
# Popup for adding an event in the month view
pxAddPopup = Px('''
<div var="prefix='%s_newEvent' % field.name;
popupId=prefix + 'Popup'"
<div var="popupId=ajaxHookId + '_new'"
id=":popupId" class="popup" align="center">
<form id=":prefix + 'Form'" method="post">
<form id=":popupId + 'Form'" method="post" action="/process">
<input type="hidden" name="fieldName" value=":field.name"/>
<input type="hidden" name="month" value=":month"/>
<input type="hidden" name="name" value=":field.name"/>
<input type="hidden" name="action" value="process"/>
<input type="hidden" name="actionType" value="createEvent"/>
<input type="hidden" name="day"/>
@ -236,9 +252,8 @@ class Calendar(Field):
</div>
<input type="button"
value=":_('object_save')"
onclick=":'triggerCalendarEvent(%s, %s, %s, %s, \
%s_maxEventLength)' % (q('new'), q(ajaxHookId), \
q(field.name), q(objUrl), field.name)"/>
onclick=":'triggerCalendarEvent(%s, %s, %s_maxEventLength)' % \
(q(ajaxHookId), q('new'), field.name)"/>
<input type="button"
value=":_('object_cancel')"
onclick=":'closePopup(%s)' % q(popupId)"/>
@ -247,14 +262,12 @@ class Calendar(Field):
# Popup for removing events in the month view
pxDelPopup = Px('''
<div var="prefix='%s_delEvent' % field.name;
popupId=prefix + 'Popup'"
<div var="popupId=ajaxHookId + '_del'"
id=":popupId" class="popup" align="center">
<form id=":prefix + 'Form'" method="post">
<form id=":popupId + 'Form'" method="post" action="/process">
<input type="hidden" name="fieldName" value=":field.name"/>
<input type="hidden" name="month" value=":month"/>
<input type="hidden" name="name" value=":field.name"/>
<input type="hidden" name="action" value="process"/>
<input type="hidden" name="actionType" value="deleteEvent"/>
<input type="hidden" name="timeslot" value="main"/>
<input type="hidden" name="day"/>
@ -263,8 +276,8 @@ class Calendar(Field):
<!-- Delete successive events ? -->
<div class="discreet" style="margin-bottom: 10px"
id=":prefix + 'DelNextEvent'"
var="cbId=prefix + '_cb'; hdId=prefix + '_hd'">
id=":ajaxHookId + '_DelNextEvent'"
var="cbId=popupId + '_cb'; hdId=popupId + '_hd'">
<input type="checkbox" name="deleteNext_cb" id=":cbId"
onClick=":'toggleCheckbox(%s, %s)' % (q(cbId), q(hdId))"/>
<input type="hidden" id=":hdId" name="deleteNext"/>
@ -272,8 +285,8 @@ class Calendar(Field):
style="text-transform: none">:_('del_next_events')</label>
</div>
<input type="button" value=":_('yes')"
onClick=":'triggerCalendarEvent(%s, %s, %s, %s)' % \
(q('del'), q(ajaxHookId), q(field.name), q(objUrl))"/>
onClick=":'triggerCalendarEvent(%s, %s)' % \
(q(ajaxHookId), q('del'))"/>
<input type="button" value=":_('no')"
onclick=":'closePopup(%s)' % q(popupId)"/>
</form>
@ -282,7 +295,7 @@ class Calendar(Field):
# Month view for a calendar
pxViewMonth = Px('''
<table cellpadding="0" cellspacing="0" width="100%" class="list"
style="font-size: 95%"
style="font-size: 95%" id=":ajaxHookId + '_cal'"
var="rowHeight=int(field.height/float(len(grid)))">
<!-- 1st row: names of days -->
<tr height="22px">
@ -321,23 +334,28 @@ class Calendar(Field):
var2="freeSlots=field.getFreeSlotsAt(date, events, slotIds,\
slotIdsStr, True)"
onclick=":'openEventPopup(%s,%s,%s,null,null,%s,%s,%s)' % \
(q('new'), q(field.name), q(dayString), q(info.eventTypes), \
(q(ajaxHookId), q('new'), q(dayString), q(info.eventTypes), \
q(info.message), q(freeSlots))"/>
</x>
<!-- Icon for deleting event(s) -->
<img if="mayDelete" class="clickable" style="visibility:hidden"
src=":url(single and 'delete' or 'deleteMany')"
onclick=":'openEventPopup(%s,%s,%s,%s,%s)' % (q('del'), \
q(field.name), q(dayString), q('main'), q(spansDays))"/>
onclick=":'openEventPopup(%s,%s,%s,%s,%s)' % (q(ajaxHookId), \
q('del'), q(dayString), q('main'), q(spansDays))"/>
<!-- Events -->
<x if="events">
<div for="event in events" style="color: grey">
<!-- Checkbox for validating the event -->
<input type="checkbox" checked="checked" class="smallbox"
if="mayValidate and (event.eventType in field.validation.schema)"
id=":'%s_%s_%s' % (date.strftime('%Y%m%d'), event.eventType, \
event.timeslot)"/>
<x>::event.getName(allEventNames)</x>
<!-- Icon for delete this particular event -->
<img if="mayDelete and not single" class="clickable"
src=":url('delete')" style="visibility:hidden"
onclick=":'openEventPopup(%s,%s,%s,%s)' % (q('del'), \
q(field.name), q(dayString), q(event.timeslot))"/>
onclick=":'openEventPopup(%s,%s,%s,%s)' % (q(ajaxHookId), \
q('del'), q(dayString), q(event.timeslot))"/>
</div>
</x>
<!-- Events from other calendars -->
@ -384,12 +402,15 @@ class Calendar(Field):
namesOfDays=field.getNamesOfDays(_);
showTimeslots=len(field.timeslots) &gt; 1;
slotIds=[slot.id for slot in field.timeslots];
slotIdsStr=','.join(slotIds)"
slotIdsStr=','.join(slotIds);
mayValidate=field.mayValidate(zobj)"
id=":ajaxHookId">
<script>:'var %s_maxEventLength = %d;' % \
(field.name, field.maxEventLength)</script>
<script>:field.getAjaxData(ajaxHookId, zobj, render=render, \
month=defaultDateMonth)</script>
<!-- Month chooser -->
<!-- Actions (month chooser, validation) -->
<div style="margin-bottom: 5px"
var="fmt='%Y/%m/%d';
goBack=not startDate or (startDate.strftime(fmt) &lt; \
@ -398,23 +419,26 @@ class Calendar(Field):
grid[-1][-1].strftime(fmt))">
<!-- Go to the previous month -->
<img class="clickable" if="goBack" src=":url('arrowLeft')"
onclick=":'askCalendar(%s,%s,%s,%s,%s)' % (q(ajaxHookId), \
q(objUrl), q(render), q(field.name), q(previousMonth))"/>
onclick=":'askMonth(%s,%s)' % (q(ajaxHookId), q(previousMonth))"/>
<!-- Go back to the default date -->
<input type="button" if="goBack or goForward"
var="fmt='%Y/%m';
label=(defaultDate.strftime(fmt)==today.strftime(fmt)) and \
'today' or 'goto_source'"
value=":_(label)"
onclick=":'askCalendar(%s,%s,%s,%s,%s)' % (q(ajaxHookId), \
q(objUrl), q(render), q(field.name), q(defaultDateMonth))"
onclick=":'askMonth(%s,%s)' % (q(ajaxHookId),q(defaultDateMonth))"
disabled=":defaultDate.strftime(fmt)==monthDayOne.strftime(fmt)"/>
<!-- Go to the next month -->
<img class="clickable" if="goForward" src=":url('arrowRight')"
onclick=":'askCalendar(%s,%s,%s,%s,%s)' % (q(ajaxHookId), \
q(objUrl), q(render), q(field.name), q(nextMonth))"/>
onclick=":'askMonth(%s,%s)' % (q(ajaxHookId), q(nextMonth))"/>
<span>:_('month_%s' % monthDayOne.aMonth())</span>
<span>:month.split('/')[0]</span>
<!-- Validate button -->
<input if="mayValidate" type="button" value=":_('validate_events')"
class="buttonSmall button" style=":url('validate', bg=True)"
var2="js='validateEvents(%s)' % q(ajaxHookId)"
onclick=":'askConfirm(%s,%s,%s)' % (q('script'), q(js, False), \
q(_('validate_events_confirm')))"/>
</div>
<x>:getattr(field, 'pxView%s' % render.capitalize())</x>
</div>''')
@ -430,7 +454,8 @@ class Calendar(Field):
others=None, timelineName=None, additionalInfo=None,
startDate=None, endDate=None, defaultDate=None, timeslots=None,
colors=None, showUncolored=False, preCompute=None,
applicableEvents=None, view=None, xml=None, delete=True):
applicableEvents=None, validation=None, view=None, xml=None,
delete=True):
Field.__init__(self, validator, (0,1), default, show, page, group,
layouts, move, False, True, False, specificReadPermission,
specificWritePermission, width, height, None, colspan,
@ -533,6 +558,12 @@ class Calendar(Field):
# for explaining him why he can, for this day, only create events of a
# sub-set of the possible event types (or even no event at all).
self.applicableEvents = applicableEvents
# A validation process can be associated to a Calendar event. It
# consists in identifying validators and letting them "convert" event
# types being wished to final, validated event types. If you want to
# enable this, define a Validation instance (see the hereabove class)
# in parameter "validation".
self.validation = validation
# May the user delete events in this calendar? If "delete" is a method,
# it must accept an event type as single arg.
self.delete = delete
@ -717,17 +748,21 @@ class Calendar(Field):
return ','.join(res)
def getEventsAt(self, obj, date):
'''Returns the list of events that exist at some p_date (=day).'''
'''Returns the list of events that exist at some p_date (=day). p_date
can be a DateTime instance or a tuple (i_year, i_month, i_day).'''
obj = obj.o # Ensure p_obj is not a wrapper
if not hasattr(obj.aq_base, self.name): return
years = getattr(obj, self.name)
year = date.year()
# Get year, month and name from p_date
if isinstance(date, tuple):
year, month, day = date
else:
year, month, day = date.year(), date.month(), date.day()
# Dig into the oobtree
if year not in years: return
months = years[year]
month = date.month()
if month not in months: return
days = months[month]
day = date.day()
if day not in days: return
return days[day]
@ -865,7 +900,7 @@ class Calendar(Field):
if isinstance(others, Other):
others.getEventsAt(res, self, date, eventNames, isTimeline, colors)
else:
for other in IterSub(others):
for other in sutils.IterSub(others):
other.getEventsAt(res, self, date, eventNames,isTimeline,colors)
return res
@ -888,7 +923,7 @@ class Calendar(Field):
res[0].append(et)
res[1][et] = self.getEventName(obj, et)
if not others: return res
for other in IterSub(others):
for other in sutils.IterSub(others):
eventTypes = other.field.getEventTypes(other.obj)
if eventTypes:
for et in eventTypes:
@ -1126,5 +1161,48 @@ class Calendar(Field):
m.month = text
return res
def splitList(self, l, sub): return splitList(l, sub)
def splitList(self, l, sub): return sutils.splitList(l, sub)
def mayValidate(self, obj):
'''May the currently logged user validate wish events ?'''
if not self.validation: return
return self.validation.method(obj.appy())
def getAjaxData(self, hook, zobj, **params):
'''Initializes an AjaxData object on the DOM node corresponding to
this calendar field.'''
params = sutils.getStringDict(params)
return "new AjaxData('%s', '%s:pxView', %s, null, '%s')" % \
(hook, self.name, params, zobj.absolute_url())
def validateEvents(self, obj):
'''Validate or discard events from the request.'''
rq = obj.REQUEST.form
counts = {'validated': 0, 'discarded': 0}
for action in ('validated', 'discarded'):
if not rq[action]: continue
for info in rq[action].split(','):
sdate, eventType, timeslot = info.split('_')
# Get the events defined at that date
date = int(sdate[:4]), int(sdate[4:6]), int(sdate[6:8])
events = self.getEventsAt(obj, date)
i = len(events) - 1
while i >= 0:
# Get the event at that timeslot
event = events[i]
if event.timeslot == timeslot:
# We have found the event
if event.eventType != eventType:
raise Exception('Wrong event type')
# Validate or discard it
if action == 'validated':
event.eventType = self.validation.schema[eventType]
else:
del events[i]
counts[action] += 1
i -= 1
obj.log('%s:%s: %d event(s) validated and %d discarded.' % \
(obj.id, self.name, counts['validated'], counts['discarded']))
if not counts['validated'] and not counts['discarded']:
return obj.translate('action_null')
return obj.translate('validate_events_done', mapping=counts)
# ------------------------------------------------------------------------------

View file

@ -1254,8 +1254,8 @@ class Ref(Field):
(obj.id, self.name, code % ('objs', 'objs'), poss)
def getAjaxData(self, hook, zobj, **params):
'''Initializes an AjaxData object on the DOM node corresponding to
p_hook = the whole search result.'''
'''Initializes an AjaxData object on the DOM node corresponding to this
Ref field.'''
# Complete params with default parameters
params['ajaxHookId'] = hook;
params['scope'] = hook.rsplit('_', 1)[-1]

View file

@ -727,6 +727,18 @@ msgstr ""
msgid "timeslot_misfit"
msgstr ""
#. Default: "Validate events"
msgid "validate_events"
msgstr ""
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
msgid "validate_events_confirm"
msgstr ""
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
msgid "validate_events_done"
msgstr ""
#. Default: "Inserted by ${userName}"
msgid "history_insert"
msgstr ""

View file

@ -727,6 +727,18 @@ msgstr ""
msgid "timeslot_misfit"
msgstr ""
#. Default: "Validate events"
msgid "validate_events"
msgstr ""
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
msgid "validate_events_confirm"
msgstr ""
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
msgid "validate_events_done"
msgstr ""
#. Default: "Inserted by ${userName}"
msgid "history_insert"
msgstr ""

View file

@ -727,6 +727,18 @@ msgstr ""
msgid "timeslot_misfit"
msgstr ""
#. Default: "Validate events"
msgid "validate_events"
msgstr ""
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
msgid "validate_events_confirm"
msgstr ""
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
msgid "validate_events_done"
msgstr ""
#. Default: "Inserted by ${userName}"
msgid "history_insert"
msgstr ""

View file

@ -728,6 +728,18 @@ msgstr "All day"
msgid "timeslot_misfit"
msgstr "Cannot create such an event in the ${slot} slot."
#. Default: "Validate events"
msgid "validate_events"
msgstr "Validate events"
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
msgid "validate_events_confirm"
msgstr "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
msgid "validate_events_done"
msgstr "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
#. Default: "Inserted by ${userName}"
msgid "history_insert"
msgstr "Inserted by ${userName}"

View file

@ -727,6 +727,18 @@ msgstr ""
msgid "timeslot_misfit"
msgstr ""
#. Default: "Validate events"
msgid "validate_events"
msgstr ""
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
msgid "validate_events_confirm"
msgstr ""
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
msgid "validate_events_done"
msgstr ""
#. Default: "Inserted by ${userName}"
msgid "history_insert"
msgstr ""

View file

@ -728,6 +728,18 @@ msgstr "Toute la journée"
msgid "timeslot_misfit"
msgstr "Impossible de créer ce type d'événement dans la plage horaire ${slot}."
#. Default: "Validate events"
msgid "validate_events"
msgstr "Valider les événements"
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
msgid "validate_events_confirm"
msgstr "Tous les événements sélectionnés seront confirmés, tandis que ceux qui sont désélectionnés seront rejetés. Le ou les utilisateurs concernés seront prévenus. Êtes-vous sûr?"
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
msgid "validate_events_done"
msgstr "${validated} événement(s) a (ont) été validé(s) et ${discarded} a (ont) été rejeté(s)."
#. Default: "Inserted by ${userName}"
msgid "history_insert"
msgstr "Inséré par ${userName}"

View file

@ -727,6 +727,18 @@ msgstr ""
msgid "timeslot_misfit"
msgstr ""
#. Default: "Validate events"
msgid "validate_events"
msgstr ""
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
msgid "validate_events_confirm"
msgstr ""
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
msgid "validate_events_done"
msgstr ""
#. Default: "Inserted by ${userName}"
msgid "history_insert"
msgstr ""

View file

@ -727,6 +727,18 @@ msgstr ""
msgid "timeslot_misfit"
msgstr ""
#. Default: "Validate events"
msgid "validate_events"
msgstr ""
#. Default: "All the checked events will be confirmed, while the unchecked ones will be discarded. The concerned user(s) will be warned. Are you sure?"
msgid "validate_events_confirm"
msgstr ""
#. Default: "${validated} event(s) was (were) validated and ${discarded} was (were) discarded."
msgid "validate_events_done"
msgstr ""
#. Default: "Inserted by ${userName}"
msgid "history_insert"
msgstr "Ingevuld door ${userName}"

View file

@ -198,3 +198,4 @@ td.search { padding-top: 8px }
.highlight { background-color: yellow }
.globalActions { margin-bottom: 4px }
.objectActions { margin: 2px 0 }
.smallbox { margin: 0 }

View file

@ -295,9 +295,10 @@ function askAjax(hook, form, params) {
}
else var mode = d.mode;
// Get p_params if given. Note that they override anything else.
if (params && ('mode' in params)) {
mode = params['mode']; delete params['mode'] }
if (params) { for (var key in params) d.params[key] = params[key]; }
askAjaxChunk(hook, mode, d.url, d.px, d.params, d.beforeSend,
evalInnerScripts);
askAjaxChunk(hook,mode,d.url,d.px,d.params,d.beforeSend,evalInnerScripts);
}
function askBunch(hookId, startNumber) {

View file

@ -8,11 +8,8 @@ function toggleVisibility(node, nodeType){
}
}
function askCalendar(hookId, objectUrl, render, fieldName, month) {
// Sends an Ajax request for getting the calendar, at p_month
var params = {'month': month, 'render': render};
askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxView', params);
}
// Sends an Ajax request for getting the calendar, at p_month
function askMonth(hookId, month) {askAjax(hookId, null, {'month': month})}
function enableOptions(select, enabled, selectFirst, message){
/* This function disables, in p_select, all options that are not in p_enabled.
@ -46,7 +43,7 @@ function enableOptions(select, enabled, selectFirst, message){
}
}
function openEventPopup(action, fieldName, day, timeslot, spansDays,
function openEventPopup(hookId, action, day, timeslot, spansDays,
applicableEventTypes, message, freeSlots) {
/* Opens the popup for creating (or deleting, depending on p_action) a
calendar event at some p_day. When action is "del", we need to know the
@ -57,13 +54,13 @@ function openEventPopup(action, fieldName, day, timeslot, spansDays,
p_applicableEventTypes; p_message contains an optional message explaining
why not applicable types are not applicable. When "new", p_freeSlots may
list the available timeslots at p_day. */
var prefix = fieldName + '_' + action + 'Event';
var f = document.getElementById(prefix + 'Form');
var popupId = hookId + '_' + action;
var f = document.getElementById(popupId + 'Form');
f.day.value = day;
if (action == 'del') {
if (f.timeslot) f.timeslot.value = timeslot;
// Show or hide the checkbox for deleting the event for successive days
var elem = document.getElementById(prefix + 'DelNextEvent');
var elem = document.getElementById(hookId + '_DelNextEvent');
var cb = elem.getElementsByTagName('input');
cb[0].checked = false;
cb[1].value = 'False';
@ -78,15 +75,15 @@ function openEventPopup(action, fieldName, day, timeslot, spansDays,
enableOptions(f.eventType, applicableEventTypes, false, message);
if (f.timeslot) enableOptions(f.timeslot, freeSlots, true, 'Not free');
}
openPopup(prefix + 'Popup');
openPopup(popupId);
}
function triggerCalendarEvent(action, hookId, fieldName, objectUrl,
maxEventLength) {
function triggerCalendarEvent(hookId, action, maxEventLength) {
/* Sends an Ajax request for triggering a calendar event (create or delete an
event) and refreshing the view month. */
var prefix = fieldName + '_' + action + 'Event';
var f = document.getElementById(prefix + 'Form');
var popupId = hookId + '_' + action;
var formId = popupId + 'Form';
var f = document.getElementById(formId);
if (action == 'new') {
// Check that an event span has been specified
if (f.eventType.selectedIndex == 0) {
@ -105,12 +102,25 @@ function triggerCalendarEvent(action, hookId, fieldName, objectUrl,
}
}
}
var elems = f.elements;
var params = {};
// Put form elements into "params"
for (var i=0; i < elems.length; i++) {
params[elems[i].name] = elems[i].value;
}
closePopup(prefix + 'Popup');
askAjaxChunk(hookId, 'POST', objectUrl, fieldName+':pxView', params);
closePopup(popupId);
askAjax(hookId, formId);
}
// Function for validating and discarding calendar events
function validateEvents(hookId) {
// Collect checkboxes from hookId and identify checked and unchecked ones
var validated = [];
var discarded = [];
var node = document.getElementById(hookId + '_cal');
var cbs = node.getElementsByTagName('input');
for (var i=0; i<cbs.length; i++) {
if (cbs[i].type != 'checkbox') continue;
if (cbs[i].checked) validated.push(cbs[i].id);
else discarded.push(cbs[i].id);
}
validated = validated.join()
discarded = discarded.join()
var params = {'action': 'validateEvents', 'validated': validated,
'discarded': discarded, 'mode': 'POST'};
askAjax(hookId, null, params);
}