From 09bf03f9bf0e63b581833ddc8d0b88ee631e54ca Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Wed, 4 Mar 2015 14:35:02 +0100 Subject: [PATCH] [gen] Calendar field: added a validation mechanism. --- fields/calendar.py | 156 +++++++++++++++++++++++++++++++++------------ fields/ref.py | 4 +- gen/tr/Appy.pot | 12 ++++ gen/tr/ar.po | 12 ++++ gen/tr/de.po | 12 ++++ gen/tr/en.po | 12 ++++ gen/tr/es.po | 12 ++++ gen/tr/fr.po | 12 ++++ gen/tr/it.po | 12 ++++ gen/tr/nl.po | 12 ++++ gen/ui/appy.css | 1 + gen/ui/appy.js | 5 +- gen/ui/calendar.js | 54 +++++++++------- 13 files changed, 251 insertions(+), 65 deletions(-) diff --git a/fields/calendar.py b/fields/calendar.py index 821617c..86ade80 100644 --- a/fields/calendar.py +++ b/fields/calendar.py @@ -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(''' @@ -202,14 +220,12 @@ class Calendar(Field): # Popup for adding an event in the month view pxAddPopup = Px(''' - + onclick=":'triggerCalendarEvent(%s, %s, %s_maxEventLength)' % \ + (q(ajaxHookId), q('new'), field.name)"/> @@ -247,14 +262,12 @@ class Calendar(Field): # Popup for removing events in the month view pxDelPopup = Px(''' -
@@ -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))"/> + onclick=":'openEventPopup(%s,%s,%s,%s,%s)' % (q(ajaxHookId), \ + q('del'), q(dayString), q('main'), q(spansDays))"/>
+ + ::event.getName(allEventNames) + onclick=":'openEventPopup(%s,%s,%s,%s)' % (q(ajaxHookId), \ + q('del'), q(dayString), q(event.timeslot))"/>
@@ -384,12 +402,15 @@ class Calendar(Field): namesOfDays=field.getNamesOfDays(_); showTimeslots=len(field.timeslots) > 1; slotIds=[slot.id for slot in field.timeslots]; - slotIdsStr=','.join(slotIds)" + slotIdsStr=','.join(slotIds); + mayValidate=field.mayValidate(zobj)" id=":ajaxHookId"> + - +
+ onclick=":'askMonth(%s,%s)' % (q(ajaxHookId), q(previousMonth))"/> + onclick=":'askMonth(%s,%s)' % (q(ajaxHookId), q(nextMonth))"/> :_('month_%s' % monthDayOne.aMonth()) :month.split('/')[0] + +
:getattr(field, 'pxView%s' % render.capitalize())''') @@ -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) # ------------------------------------------------------------------------------ diff --git a/fields/ref.py b/fields/ref.py index b0e8ad7..6d77956 100644 --- a/fields/ref.py +++ b/fields/ref.py @@ -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] diff --git a/gen/tr/Appy.pot b/gen/tr/Appy.pot index 779a10e..ae4c2d4 100644 --- a/gen/tr/Appy.pot +++ b/gen/tr/Appy.pot @@ -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 "" diff --git a/gen/tr/ar.po b/gen/tr/ar.po index 49cd84f..52b19fd 100644 --- a/gen/tr/ar.po +++ b/gen/tr/ar.po @@ -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 "" diff --git a/gen/tr/de.po b/gen/tr/de.po index 2120389..5d1918b 100644 --- a/gen/tr/de.po +++ b/gen/tr/de.po @@ -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 "" diff --git a/gen/tr/en.po b/gen/tr/en.po index 27f84d4..e3e0608 100644 --- a/gen/tr/en.po +++ b/gen/tr/en.po @@ -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}" diff --git a/gen/tr/es.po b/gen/tr/es.po index 918a440..e7c2023 100644 --- a/gen/tr/es.po +++ b/gen/tr/es.po @@ -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 "" diff --git a/gen/tr/fr.po b/gen/tr/fr.po index 62b8444..495d73a 100644 --- a/gen/tr/fr.po +++ b/gen/tr/fr.po @@ -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}" diff --git a/gen/tr/it.po b/gen/tr/it.po index 975e367..478ed73 100644 --- a/gen/tr/it.po +++ b/gen/tr/it.po @@ -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 "" diff --git a/gen/tr/nl.po b/gen/tr/nl.po index 31292c1..033b841 100644 --- a/gen/tr/nl.po +++ b/gen/tr/nl.po @@ -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}" diff --git a/gen/ui/appy.css b/gen/ui/appy.css index 88deb9a..0db0ab9 100644 --- a/gen/ui/appy.css +++ b/gen/ui/appy.css @@ -198,3 +198,4 @@ td.search { padding-top: 8px } .highlight { background-color: yellow } .globalActions { margin-bottom: 4px } .objectActions { margin: 2px 0 } +.smallbox { margin: 0 } diff --git a/gen/ui/appy.js b/gen/ui/appy.js index a678cfc..c895174 100644 --- a/gen/ui/appy.js +++ b/gen/ui/appy.js @@ -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) { diff --git a/gen/ui/calendar.js b/gen/ui/calendar.js index aab9913..334c0f1 100644 --- a/gen/ui/calendar.js +++ b/gen/ui/calendar.js @@ -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