From da8f7a5bcd5479fb5af206e7cdedd05865461aed Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Fri, 27 Feb 2015 15:57:42 +0100 Subject: [PATCH] [gen] Calendar field: allow to have several events at the same day via the concept of timeslots (ongoing work). --- fields/calendar.py | 388 ++++++++++++++++++++++++++++++--------------- gen/tr/Appy.pot | 8 + gen/tr/ar.po | 8 + gen/tr/de.po | 8 + gen/tr/en.po | 8 + gen/tr/es.po | 8 + gen/tr/fr.po | 8 + gen/tr/it.po | 8 + gen/tr/nl.po | 8 + gen/ui/calendar.js | 42 ++--- 10 files changed, 350 insertions(+), 144 deletions(-) diff --git a/fields/calendar.py b/fields/calendar.py index 55e7b14..4239947 100644 --- a/fields/calendar.py +++ b/fields/calendar.py @@ -8,6 +8,24 @@ from appy.px import Px from DateTime import DateTime from BTrees.IOBTree import IOBTree from persistent.list import PersistentList +from persistent import Persistent + +# ------------------------------------------------------------------------------ +class Timeslot: + '''A timeslot defines a time range within a single day''' + def __init__(self, id, start=None, end=None, name=None, eventTypes=None): + # A short, human-readable string identifier, unique among all timeslots + # for a given Calendar. Id "main" is reserved for the main timeslot that + # represents the whole day. + self.id = id + # The time range can be defined by p_start ~(i_hour, i_minute)~ and + # p_end (idem), or by a simple name, like "AM" or "PM". + self.start = start + self.end = end + self.name = name or id + # The event types (among all event types defined at the Calendar level) + # that can be assigned to this slot. + self.eventTypes = eventTypes # "None" means "all" # ------------------------------------------------------------------------------ class Other: @@ -49,16 +67,33 @@ class Other: info.name = eventNames[eventType] res.append(info) +# ------------------------------------------------------------------------------ +class Event(Persistent): + '''An event as will be stored in the database''' + def __init__(self, eventType, timeslot='main'): + self.eventType = eventType + self.timeslot = timeslot + + def getName(self, allEventNames): + '''Gets the name for this event, that depends on it type and may include + the timeslot if not "main".''' + res = allEventNames[self.eventType] + if self.timeslot != 'main': res += ' ' + self.timeslot + return res + # ------------------------------------------------------------------------------ class Calendar(Field): '''This field allows to produce an agenda (monthly view) and view/edit events on it.''' jsFiles = {'view': ('calendar.js',)} DateTime = DateTime - Other = Other # Access to the Other class via the Calendar class + # Access to Calendar utility classes via the Calendar class + Timeslot = Timeslot + Other = Other + Event = Event IterSub = IterSub - timelineBgColors = {'Fri': '#a6a6a6', 'Sat': '#c0c0c0', 'Sun': '#c0c0c0'} + timelineBgColors = {'Fri': '#dedede', 'Sat': '#c0c0c0', 'Sun': '#c0c0c0'} # For timeline rendering, the row displaying month names pxTimeLineMonths = Px(''' @@ -120,27 +155,114 @@ class Calendar(Field): - - ::tlName - - - - + + ::tlName + + + + ::field.getTimelineCell(events) - - ::tlName - + + ::tlName + + + + + :field.pxTimelineDayNumbers:field.pxTimelineDayLetters :field.pxTimeLineMonths :field.pxTimelineLegend''') + # Popup for adding an event in the month view + pxAddEvent = Px(''' + ''') + + # Popup for removing events in the month view + pxDelEvent = Px(''' + ''') + # Month view for a calendar pxViewMonth = Px('''
- + - -
- :allEventNames[eventType] + src=":url(single and 'delete' or 'deleteMany')" + onclick=":'openEventPopup(%s, %s, %s, %s, %s, null, null)' % \ + (q('del'), q(field.name), q(dayString), q('main'), q(spansDays))"/> + + +
+ :event.getName(allEventNames) + +
+
-
- - - - - - - - -
:_('which_event')
-

- -
- :_('event_span') - -
- - -
-
- - - ''') + + + :field.pxAddEvent:field.pxDelEvent''') pxView = pxCell = Px('''
1: + events.data.sort(key=lambda e: timeslots.index(e.timeslot)) + events._p_changed = 1 # Span the event on the successive days if required - if handleEventSpan and eventSpan: + if handleEventSpan and eventSpan and (timeslot != 'main'): nbOfDays = min(int(eventSpan), self.maxEventLength) for i in range(nbOfDays): date = date + 1 - self.createEvent(obj, date, handleEventSpan=False) + self.createEvent(obj, date, timeslot, handleEventSpan=False) def mayDelete(self, obj, events): '''May the user delete p_events?''' @@ -819,25 +942,38 @@ class Calendar(Field): if callable(self.delete): return self.delete(obj, events[0].eventType) return True - def deleteEvent(self, obj, date, handleEventSpan=True): - '''Deletes an event. It actually deletes all events at p_date. - If p_handleEventSpan is True, we will use rq["deleteNext"] to - delete successive events, too.''' - obj = obj.o # Ensure p_obj is not a wrapper. + def deleteEvent(self, obj, date, timeslot, handleEventSpan=True): + '''Deletes an event. If t_timeslot is "main", it deletes all events at + p_date, be there a single event on the main timeslot or several + events on other timeslots. Else, it only deletes the event at + p_timeslot. If p_handleEventSpan is True, we will use + rq["deleteNext"] to delete successive events, too.''' + obj = obj.o # Ensure p_obj is not a wrapper if not self.getEventsAt(obj, date): return daysDict = getattr(obj, self.name)[date.year()][date.month()] - # Remember events, in case we must delete similar ones for next days. events = self.getEventsAt(obj, date) - del daysDict[date.day()] - rq = obj.REQUEST - if handleEventSpan and rq.has_key('deleteNext') and \ - (rq['deleteNext'] == 'True'): - while True: - date = date + 1 - if self.hasEventsAt(obj, date, events): - self.deleteEvent(obj, date, handleEventSpan=False) - else: + if timeslot == 'main': + # Delete all events; delete them also in the following days when + # relevant. + del daysDict[date.day()] + rq = obj.REQUEST + if handleEventSpan and rq.has_key('deleteNext') and \ + (rq['deleteNext'] == 'True'): + while True: + date = date + 1 + if self.hasEventsAt(obj, date, events): + self.deleteEvent(obj, date, timeslot, + handleEventSpan=False) + else: + break + else: + # Delete the event at p_timeslot + i = len(events) - 1 + while i >= 0: + if events[i].timeslot == timeslot: + del events[i] break + i -= 1 def process(self, obj): '''Processes an action coming from the calendar widget, ie, the creation @@ -846,11 +982,13 @@ class Calendar(Field): action = rq['actionType'] # Security check obj.mayEdit(self.writePermission, raiseError=True) - # Get the date for this action + # Get the date and timeslot for this action + date = DateTime(rq['day']) + timeslot = rq.get('timeslot', 'main') if action == 'createEvent': - return self.createEvent(obj, DateTime(rq['day'])) + return self.createEvent(obj, date, timeslot) elif action == 'deleteEvent': - return self.deleteEvent(obj, DateTime(rq['day'])) + return self.deleteEvent(obj, date, timeslot) def getColumnStyle(self, obj, date, render, today): '''What style(s) must apply to the table column representing p_date diff --git a/gen/tr/Appy.pot b/gen/tr/Appy.pot index 8fdba99..abf4704 100644 --- a/gen/tr/Appy.pot +++ b/gen/tr/Appy.pot @@ -715,6 +715,14 @@ msgstr "" msgid "del_next_events" msgstr "" +#. Default: "Timeslot" +msgid "timeslot" +msgstr "" + +#. Default: "All day" +msgid "timeslot_main" +msgstr "" + #. Default: "Inserted by ${userName}" msgid "history_insert" msgstr "" diff --git a/gen/tr/ar.po b/gen/tr/ar.po index 188e71b..0db6f44 100644 --- a/gen/tr/ar.po +++ b/gen/tr/ar.po @@ -715,6 +715,14 @@ msgstr "" msgid "del_next_events" msgstr "" +#. Default: "Timeslot" +msgid "timeslot" +msgstr "" + +#. Default: "All day" +msgid "timeslot_main" +msgstr "" + #. Default: "Inserted by ${userName}" msgid "history_insert" msgstr "" diff --git a/gen/tr/de.po b/gen/tr/de.po index b1b9cd6..21f8ea1 100644 --- a/gen/tr/de.po +++ b/gen/tr/de.po @@ -715,6 +715,14 @@ msgstr "" msgid "del_next_events" msgstr "" +#. Default: "Timeslot" +msgid "timeslot" +msgstr "" + +#. Default: "All day" +msgid "timeslot_main" +msgstr "" + #. Default: "Inserted by ${userName}" msgid "history_insert" msgstr "" diff --git a/gen/tr/en.po b/gen/tr/en.po index 572b94a..be7c2a0 100644 --- a/gen/tr/en.po +++ b/gen/tr/en.po @@ -716,6 +716,14 @@ msgstr "Extend the event on the following number of days (leave blank to create msgid "del_next_events" msgstr "Also delete successive events of the same type." +#. Default: "Timeslot" +msgid "timeslot" +msgstr "Timeslot" + +#. Default: "All day" +msgid "timeslot_main" +msgstr "All day" + #. Default: "Inserted by ${userName}" msgid "history_insert" msgstr "Inserted by ${userName}" diff --git a/gen/tr/es.po b/gen/tr/es.po index de529a9..8f1ccba 100644 --- a/gen/tr/es.po +++ b/gen/tr/es.po @@ -715,6 +715,14 @@ msgstr "" msgid "del_next_events" msgstr "" +#. Default: "Timeslot" +msgid "timeslot" +msgstr "" + +#. Default: "All day" +msgid "timeslot_main" +msgstr "" + #. Default: "Inserted by ${userName}" msgid "history_insert" msgstr "" diff --git a/gen/tr/fr.po b/gen/tr/fr.po index a1dfdeb..eadd2bd 100644 --- a/gen/tr/fr.po +++ b/gen/tr/fr.po @@ -716,6 +716,14 @@ msgstr "Étendre l'événement sur le nombre de jours suivants (laissez vide pou msgid "del_next_events" msgstr "Supprimer aussi les événements successifs de même type" +#. Default: "Timeslot" +msgid "timeslot" +msgstr "Plage horaire" + +#. Default: "All day" +msgid "timeslot_main" +msgstr "Toute la journée" + #. 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 3066452..5e526c0 100644 --- a/gen/tr/it.po +++ b/gen/tr/it.po @@ -715,6 +715,14 @@ msgstr "" msgid "del_next_events" msgstr "" +#. Default: "Timeslot" +msgid "timeslot" +msgstr "" + +#. Default: "All day" +msgid "timeslot_main" +msgstr "" + #. Default: "Inserted by ${userName}" msgid "history_insert" msgstr "" diff --git a/gen/tr/nl.po b/gen/tr/nl.po index fb0bae4..12d5dca 100644 --- a/gen/tr/nl.po +++ b/gen/tr/nl.po @@ -715,6 +715,14 @@ msgstr "Het event uitbreiden naar de volgende dagen (leeg laten om een event aan msgid "del_next_events" msgstr "Verwijder ook alle opeenvolgende events van hetzelfde type" +#. Default: "Timeslot" +msgid "timeslot" +msgstr "" + +#. Default: "All day" +msgid "timeslot_main" +msgstr "" + #. Default: "Inserted by ${userName}" msgid "history_insert" msgstr "Ingevuld door ${userName}" diff --git a/gen/ui/calendar.js b/gen/ui/calendar.js index 20e9d30..df0e739 100644 --- a/gen/ui/calendar.js +++ b/gen/ui/calendar.js @@ -14,26 +14,28 @@ function askCalendar(hookId, objectUrl, render, fieldName, month) { askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxView', params); } -function openEventPopup(action, fieldName, day, spansDays, +function openEventPopup(action, fieldName, day, timeslot, spansDays, applicableEventTypes, message) { /* 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 - (from p_spansDays) if the event spans more days, in order to propose a - checkbox allowing to delete events for those successive days. When action - is "new", a possibly restricted list of applicable event types for this - day is given in p_applicableEventTypes; p_message contains an optional - message explaining why not applicable types are not applicable. */ + calendar event at some p_day. When action is "del", we need to know the + p_timeslot where the event is assigned and if the event spans more days + (from p_spansDays), in order to propose a checkbox allowing to delete + events for those successive days. When action is "new", a possibly + restricted list of applicable event types for this day is given in + p_applicableEventTypes; p_message contains an optional message explaining + why not applicable types are not applicable. */ var prefix = fieldName + '_' + action + 'Event'; var f = document.getElementById(prefix + 'Form'); f.day.value = day; if (action == 'del') { - // Show or hide the checkbox for deleting the event for successive days. + f.timeslot.value = timeslot; + // Show or hide the checkbox for deleting the event for successive days var elem = document.getElementById(prefix + 'DelNextEvent'); var cb = elem.getElementsByTagName('input'); cb[0].checked = false; cb[1].value = 'False'; - if (spansDays == 'True') { elem.style.display = 'block' } - else { elem.style.display = 'none' } + if (spansDays == 'True') elem.style.display = 'block'; + else elem.style.display = 'none'; } else if (action == 'new') { // First: reinitialise input fields @@ -42,8 +44,8 @@ function openEventPopup(action, fieldName, day, spansDays, for (var i=0; i < allOptions.length; i++) { allOptions[i].selected = false; } - f.eventSpan.style.background = ''; - // Among all event types, show applicable ones and hide the others. + if (f.eventSpan) f.eventSpan.style.background = ''; + // Among all event types, show applicable ones and hide the others var applicable = applicableEventTypes.split(','); var applicableDict = {}; for (var i=0; i < applicable.length; i++) { @@ -76,13 +78,15 @@ function triggerCalendarEvent(action, hookId, fieldName, objectUrl, f.eventType.style.background = wrongTextInput; return; } - // Check that eventSpan is empty or contains a valid number - var spanNumber = f.eventSpan.value.replace(' ', ''); - if (spanNumber) { - spanNumber = parseInt(spanNumber); - if (isNaN(spanNumber) || (spanNumber > maxEventLength)) { - f.eventSpan.style.background = wrongTextInput; - return; + if (f.eventSpan) { + // Check that eventSpan is empty or contains a valid number + var spanNumber = f.eventSpan.value.replace(' ', ''); + if (spanNumber) { + spanNumber = parseInt(spanNumber); + if (isNaN(spanNumber) || (spanNumber > maxEventLength)) { + f.eventSpan.style.background = wrongTextInput; + return; + } } } }