[gen] Calendar field: allow to have several events at the same day via the concept of timeslots (ongoing work).
This commit is contained in:
		
							parent
							
								
									0c706c695e
								
							
						
					
					
						commit
						da8f7a5bcd
					
				
					 10 changed files with 350 additions and 144 deletions
				
			
		|  | @ -8,6 +8,24 @@ from appy.px import Px | ||||||
| from DateTime import DateTime | from DateTime import DateTime | ||||||
| from BTrees.IOBTree import IOBTree | from BTrees.IOBTree import IOBTree | ||||||
| from persistent.list import PersistentList | 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: | class Other: | ||||||
|  | @ -49,16 +67,33 @@ class Other: | ||||||
|             info.name = eventNames[eventType] |             info.name = eventNames[eventType] | ||||||
|         res.append(info) |         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): | class Calendar(Field): | ||||||
|     '''This field allows to produce an agenda (monthly view) and view/edit |     '''This field allows to produce an agenda (monthly view) and view/edit | ||||||
|        events on it.''' |        events on it.''' | ||||||
|     jsFiles = {'view': ('calendar.js',)} |     jsFiles = {'view': ('calendar.js',)} | ||||||
|     DateTime = DateTime |     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 |     IterSub = IterSub | ||||||
| 
 | 
 | ||||||
|     timelineBgColors = {'Fri': '#a6a6a6', 'Sat': '#c0c0c0', 'Sun': '#c0c0c0'} |     timelineBgColors = {'Fri': '#dedede', 'Sat': '#c0c0c0', 'Sun': '#c0c0c0'} | ||||||
| 
 | 
 | ||||||
|     # For timeline rendering, the row displaying month names |     # For timeline rendering, the row displaying month names | ||||||
|     pxTimeLineMonths = Px(''' |     pxTimeLineMonths = Px(''' | ||||||
|  | @ -120,7 +155,8 @@ class Calendar(Field): | ||||||
|        <td></td> |        <td></td> | ||||||
|       </tr> |       </tr> | ||||||
|       <!-- Other calendars --> |       <!-- Other calendars --> | ||||||
|       <tr for="other in field.IterSub(others)" |       <x for="otherGroup in others"> | ||||||
|  |        <tr for="other in otherGroup" | ||||||
|            var2="tlName=field.getTimelineName(other)"> |            var2="tlName=field.getTimelineName(other)"> | ||||||
|         <td class="tlLeft">::tlName</td> |         <td class="tlLeft">::tlName</td> | ||||||
|         <!-- A cell in this other calendar --> |         <!-- A cell in this other calendar --> | ||||||
|  | @ -135,82 +171,21 @@ class Calendar(Field): | ||||||
|         </x> |         </x> | ||||||
|         <td class="tlRight">::tlName</td> |         <td class="tlRight">::tlName</td> | ||||||
|        </tr> |        </tr> | ||||||
|  |        <!-- A separator between groups of other calendars --> | ||||||
|  |        <tr if="not loop.otherGroup.last" height="5px"> | ||||||
|  |         <th colspan=":len(grid)+2"></th></tr> | ||||||
|  |       </x> | ||||||
|       <!-- Footer (repetition of months and days) --> |       <!-- Footer (repetition of months and days) --> | ||||||
|       <x>:field.pxTimelineDayNumbers</x><x>:field.pxTimelineDayLetters</x> |       <x>:field.pxTimelineDayNumbers</x><x>:field.pxTimelineDayLetters</x> | ||||||
|       <x>:field.pxTimeLineMonths</x> |       <x>:field.pxTimeLineMonths</x> | ||||||
|      </table> |      </table> | ||||||
|      <x>:field.pxTimelineLegend</x>''') |      <x>:field.pxTimelineLegend</x>''') | ||||||
| 
 | 
 | ||||||
|     # Month view for a calendar |     # Popup for adding an event in the month view | ||||||
|     pxViewMonth = Px(''' |     pxAddEvent = Px(''' | ||||||
|       <table cellpadding="0" cellspacing="0" width="100%" class="list" |      <div var="prefix='%s_newEvent' % field.name; | ||||||
|              style="font-size: 95%" |                popupId=prefix + 'Popup'; | ||||||
|              var="rowHeight=int(field.height/float(len(grid)))"> |                showTimeslots=len(field.timeslots) > 2" | ||||||
|        <!-- 1st row: names of days --> |  | ||||||
|        <tr height="22px"> |  | ||||||
|         <th for="dayId in field.weekDays" |  | ||||||
|             width="14%">:namesOfDays[dayId].short</th> |  | ||||||
|        </tr> |  | ||||||
|        <!-- The calendar in itself --> |  | ||||||
|        <tr for="row in grid" valign="top" height=":rowHeight"> |  | ||||||
|         <x for="date in row" |  | ||||||
|            var2="inRange=field.dateInRange(date, startDate, endDate); |  | ||||||
|                  cssClasses=field.getCellClass(zobj, date, render, today)"> |  | ||||||
|          <!-- Dump an empty cell if we are out of the supported date range --> |  | ||||||
|          <td if="not inRange" class=":cssClasses"></td> |  | ||||||
|          <!-- Dump a normal cell if we are in range --> |  | ||||||
|          <td if="inRange" |  | ||||||
|              var2="events=field.getEventsAt(zobj, date); |  | ||||||
|                    spansDays=field.hasEventsAt(zobj, date+1, events); |  | ||||||
|                    mayCreate=mayEdit and not events; |  | ||||||
|                    mayDelete=mayEdit and events and field.mayDelete(obj,events); |  | ||||||
|                    day=date.day(); |  | ||||||
|                    dayString=date.strftime('%Y/%m/%d'); |  | ||||||
|                    js=mayEdit and 'toggleVisibility(this, %s)' % q('img') \ |  | ||||||
|                       or ''" |  | ||||||
|              style=":date.isCurrentDay() and 'font-weight:bold' or \ |  | ||||||
|                                              'font-weight:normal'" |  | ||||||
|              class=":cssClasses" onmouseover=":js" onmouseout=":js"> |  | ||||||
|           <span>:day</span> |  | ||||||
|           <span if="day == 1">:_('month_%s_short' % date.aMonth())</span> |  | ||||||
|           <!-- Icon for adding an event --> |  | ||||||
|           <x if="mayCreate"> |  | ||||||
|            <img class="clickable" style="visibility:hidden" |  | ||||||
|                 var="info=field.getApplicableEventsTypesAt(zobj, date, \ |  | ||||||
|                            eventTypes, preComputed, True)" |  | ||||||
|                 if="info and info.eventTypes" src=":url('plus')" |  | ||||||
|                 onclick=":'openEventPopup(%s, %s, %s, null, %s, %s)' % \ |  | ||||||
|                  (q('new'), q(field.name), q(dayString), q(info.eventTypes),\ |  | ||||||
|                   q(info.message))"/> |  | ||||||
|           </x> |  | ||||||
|           <!-- Icon for deleting an event --> |  | ||||||
|           <img if="mayDelete" class="clickable" style="visibility:hidden" |  | ||||||
|                src=":url('delete')" |  | ||||||
|                onclick=":'openEventPopup(%s, %s, %s, %s, null, null)' % \ |  | ||||||
|                  (q('del'), q(field.name), q(dayString), q(spansDays))"/> |  | ||||||
|           <!-- A single event is allowed for the moment --> |  | ||||||
|           <div if="events" var2="eventType=events[0].eventType"> |  | ||||||
|            <span style="color: grey">:allEventNames[eventType]</span> |  | ||||||
|           </div> |  | ||||||
|           <!-- Events from other calendars --> |  | ||||||
|           <x if="others" |  | ||||||
|              var2="otherEvents=field.getOtherEventsAt(zobj, date, \ |  | ||||||
|                                 others, allEventNames, render, colors)"> |  | ||||||
|            <div style=":'color: %s; font-style: italic' % event.color" |  | ||||||
|                 for="event in otherEvents">:event.name</div> |  | ||||||
|           </x> |  | ||||||
|           <!-- Additional info --> |  | ||||||
|           <x var="info=field.getAdditionalInfoAt(zobj, date, preComputed)" |  | ||||||
|              if="info">::info</x> |  | ||||||
|          </td> |  | ||||||
|         </x> |  | ||||||
|        </tr> |  | ||||||
|       </table> |  | ||||||
| 
 |  | ||||||
|       <!-- Popup for creating a calendar event --> |  | ||||||
|       <div if="eventTypes" |  | ||||||
|            var="prefix='%s_newEvent' % field.name; |  | ||||||
|                 popupId=prefix + 'Popup'" |  | ||||||
|           id=":popupId" class="popup" align="center"> |           id=":popupId" class="popup" align="center"> | ||||||
|       <form id=":prefix + 'Form'" method="post"> |       <form id=":prefix + 'Form'" method="post"> | ||||||
|        <input type="hidden" name="fieldName" value=":field.name"/> |        <input type="hidden" name="fieldName" value=":field.name"/> | ||||||
|  | @ -222,16 +197,27 @@ class Calendar(Field): | ||||||
| 
 | 
 | ||||||
|        <!-- Choose an event type --> |        <!-- Choose an event type --> | ||||||
|        <div align="center" style="margin-bottom: 3px">:_('which_event')</div> |        <div align="center" style="margin-bottom: 3px">:_('which_event')</div> | ||||||
|         <select name="eventType"> |        <select name="eventType" style="margin-bottom: 10px"> | ||||||
|         <option value="">:_('choose_a_value')</option> |         <option value="">:_('choose_a_value')</option> | ||||||
|         <option for="eventType in eventTypes" |         <option for="eventType in eventTypes" | ||||||
|                 value=":eventType">:allEventNames[eventType]</option> |                 value=":eventType">:allEventNames[eventType]</option> | ||||||
|         </select><br/><br/> |        </select> | ||||||
|  |        <!-- Choose a timeslot --> | ||||||
|  |        <div if="showTimeslots" style="margin-bottom: 10px"> | ||||||
|  |         <span class="discreet">:_('timeslot')</span> | ||||||
|  |         <select if="showTimeslots" name="timeslot"> | ||||||
|  |          <option value="main">:_('timeslot_main')</option> | ||||||
|  |          <option for="timeslot in field.timeslots" | ||||||
|  |                  if="timeslot.id != 'main'">:timeslot.name</option> | ||||||
|  |         </select> | ||||||
|  |        </div> | ||||||
|        <!-- Span the event on several days --> |        <!-- Span the event on several days --> | ||||||
|  |        <x if="not showTimeslots"> | ||||||
|         <div align="center" class="discreet" style="margin-bottom: 3px"> |         <div align="center" class="discreet" style="margin-bottom: 3px"> | ||||||
|          <span>:_('event_span')</span> |          <span>:_('event_span')</span> | ||||||
|          <input type="text" size="3" name="eventSpan"/> |          <input type="text" size="3" name="eventSpan"/> | ||||||
|         </div> |         </div> | ||||||
|  |        </x> | ||||||
|        <input type="button" |        <input type="button" | ||||||
|               value=":_('object_save')" |               value=":_('object_save')" | ||||||
|               onclick=":'triggerCalendarEvent(%s, %s, %s, %s, \ |               onclick=":'triggerCalendarEvent(%s, %s, %s, %s, \ | ||||||
|  | @ -241,9 +227,10 @@ class Calendar(Field): | ||||||
|               value=":_('object_cancel')" |               value=":_('object_cancel')" | ||||||
|               onclick=":'closePopup(%s)' % q(popupId)"/> |               onclick=":'closePopup(%s)' % q(popupId)"/> | ||||||
|       </form> |       </form> | ||||||
|       </div> |      </div>''') | ||||||
| 
 | 
 | ||||||
|       <!-- Popup for deleting a calendar event --> |     # Popup for removing events in the month view | ||||||
|  |     pxDelEvent = Px(''' | ||||||
|      <div var="prefix='%s_delEvent' % field.name; |      <div var="prefix='%s_delEvent' % field.name; | ||||||
|                popupId=prefix + 'Popup'" |                popupId=prefix + 'Popup'" | ||||||
|           id=":popupId" class="popup" align="center"> |           id=":popupId" class="popup" align="center"> | ||||||
|  | @ -253,6 +240,7 @@ class Calendar(Field): | ||||||
|        <input type="hidden" name="name" value=":field.name"/> |        <input type="hidden" name="name" value=":field.name"/> | ||||||
|        <input type="hidden" name="action" value="process"/> |        <input type="hidden" name="action" value="process"/> | ||||||
|        <input type="hidden" name="actionType" value="deleteEvent"/> |        <input type="hidden" name="actionType" value="deleteEvent"/> | ||||||
|  |        <input type="hidden" name="timeslot" value="main"/> | ||||||
|        <input type="hidden" name="day"/> |        <input type="hidden" name="day"/> | ||||||
|        <div align="center" |        <div align="center" | ||||||
|             style="margin-bottom: 5px">:_('action_confirm')</div> |             style="margin-bottom: 5px">:_('action_confirm')</div> | ||||||
|  | @ -275,6 +263,84 @@ class Calendar(Field): | ||||||
|       </form> |       </form> | ||||||
|      </div>''') |      </div>''') | ||||||
| 
 | 
 | ||||||
|  |     # Month view for a calendar | ||||||
|  |     pxViewMonth = Px(''' | ||||||
|  |       <table cellpadding="0" cellspacing="0" width="100%" class="list" | ||||||
|  |              style="font-size: 95%" | ||||||
|  |              var="rowHeight=int(field.height/float(len(grid)))"> | ||||||
|  |        <!-- 1st row: names of days --> | ||||||
|  |        <tr height="22px"> | ||||||
|  |         <th for="dayId in field.weekDays" | ||||||
|  |             width="14%">:namesOfDays[dayId].short</th> | ||||||
|  |        </tr> | ||||||
|  |        <!-- The calendar in itself --> | ||||||
|  |        <tr for="row in grid" valign="top" height=":rowHeight"> | ||||||
|  |         <x for="date in row" | ||||||
|  |            var2="inRange=field.dateInRange(date, startDate, endDate); | ||||||
|  |                  cssClasses=field.getCellClass(zobj, date, render, today)"> | ||||||
|  |          <!-- Dump an empty cell if we are out of the supported date range --> | ||||||
|  |          <td if="not inRange" class=":cssClasses"></td> | ||||||
|  |          <!-- Dump a normal cell if we are in range --> | ||||||
|  |          <td if="inRange" | ||||||
|  |              var2="events=field.getEventsAt(zobj, date); | ||||||
|  |                    single=events and (len(events) == 1); | ||||||
|  |                    spansDays=field.hasEventsAt(zobj, date+1, events); | ||||||
|  |                    mayCreate=mayEdit and not field.dayIsFull(date, events); | ||||||
|  |                    mayDelete=mayEdit and events and field.mayDelete(obj,events); | ||||||
|  |                    day=date.day(); | ||||||
|  |                    dayString=date.strftime('%Y/%m/%d'); | ||||||
|  |                    js=mayEdit and 'toggleVisibility(this, %s)' % q('img') \ | ||||||
|  |                       or ''" | ||||||
|  |              style=":date.isCurrentDay() and 'font-weight:bold' or \ | ||||||
|  |                                              'font-weight:normal'" | ||||||
|  |              class=":cssClasses" onmouseover=":js" onmouseout=":js"> | ||||||
|  |           <span>:day</span> | ||||||
|  |           <span if="day == 1">:_('month_%s_short' % date.aMonth())</span> | ||||||
|  |           <!-- Icon for adding an event --> | ||||||
|  |           <x if="mayCreate"> | ||||||
|  |            <img class="clickable" style="visibility:hidden" | ||||||
|  |                 var="info=field.getApplicableEventsTypesAt(zobj, date, \ | ||||||
|  |                            eventTypes, preComputed, True)" | ||||||
|  |                 if="info and info.eventTypes" src=":url('plus')" | ||||||
|  |                 onclick=":'openEventPopup(%s, %s, %s, null, null, %s, %s)' % \ | ||||||
|  |                  (q('new'), q(field.name), q(dayString), q(info.eventTypes),\ | ||||||
|  |                   q(info.message))"/> | ||||||
|  |           </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, null, null)' % \ | ||||||
|  |              (q('del'), q(field.name), q(dayString), q('main'), q(spansDays))"/> | ||||||
|  |           <!-- Events --> | ||||||
|  |           <x if="events"> | ||||||
|  |           <div for="event in events" style="color: grey"> | ||||||
|  |            <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, null, null, null)'% \ | ||||||
|  |                    (q('del'), q(field.name), q(dayString), q(event.timeslot))"/> | ||||||
|  |           </div> | ||||||
|  |           </x> | ||||||
|  |           <!-- Events from other calendars --> | ||||||
|  |           <x if="others" | ||||||
|  |              var2="otherEvents=field.getOtherEventsAt(zobj, date, \ | ||||||
|  |                                 others, allEventNames, render, colors)"> | ||||||
|  |            <div style=":'color: %s; font-style: italic' % event.color" | ||||||
|  |                 for="event in otherEvents">:event.name</div> | ||||||
|  |           </x> | ||||||
|  |           <!-- Additional info --> | ||||||
|  |           <x var="info=field.getAdditionalInfoAt(zobj, date, preComputed)" | ||||||
|  |              if="info">::info</x> | ||||||
|  |          </td> | ||||||
|  |         </x> | ||||||
|  |        </tr> | ||||||
|  |       </table> | ||||||
|  | 
 | ||||||
|  |       <!-- Popups for creating and deleting a calendar event --> | ||||||
|  |       <x if="mayEdit and eventTypes"> | ||||||
|  |        <x>:field.pxAddEvent</x><x>:field.pxDelEvent</x></x>''') | ||||||
|  | 
 | ||||||
|     pxView = pxCell = Px(''' |     pxView = pxCell = Px(''' | ||||||
|      <div var="defaultDate=field.getDefaultDate(zobj); |      <div var="defaultDate=field.getDefaultDate(zobj); | ||||||
|                defaultDateMonth=defaultDate.strftime('%Y/%m'); |                defaultDateMonth=defaultDate.strftime('%Y/%m'); | ||||||
|  | @ -341,9 +407,9 @@ class Calendar(Field): | ||||||
|                  colspan=1, master=None, masterValue=None, focus=False, |                  colspan=1, master=None, masterValue=None, focus=False, | ||||||
|                  mapping=None, label=None, maxEventLength=50, render='month', |                  mapping=None, label=None, maxEventLength=50, render='month', | ||||||
|                  others=None, timelineName=None, additionalInfo=None, |                  others=None, timelineName=None, additionalInfo=None, | ||||||
|                  startDate=None, endDate=None, defaultDate=None, colors=None, |                  startDate=None, endDate=None, defaultDate=None, timeslots=None, | ||||||
|                  showUncolored=False, preCompute=None, applicableEvents=None, |                  colors=None, showUncolored=False, preCompute=None, | ||||||
|                  view=None, xml=None, delete=True): |                  applicableEvents=None, view=None, xml=None, delete=True): | ||||||
|         Field.__init__(self, validator, (0,1), default, show, page, group, |         Field.__init__(self, validator, (0,1), default, show, page, group, | ||||||
|                        layouts, move, False, True, False, specificReadPermission, |                        layouts, move, False, True, False, specificReadPermission, | ||||||
|                        specificWritePermission, width, height, None, colspan, |                        specificWritePermission, width, height, None, colspan, | ||||||
|  | @ -418,6 +484,14 @@ class Calendar(Field): | ||||||
|         # date is specified, it will be 'now' at the moment the calendar is |         # date is specified, it will be 'now' at the moment the calendar is | ||||||
|         # shown. |         # shown. | ||||||
|         self.defaultDate = defaultDate |         self.defaultDate = defaultDate | ||||||
|  |         # "timeslots" are a way to define, within a single day, time ranges. It | ||||||
|  |         # must be a list of Timeslot instances (see above). If you define | ||||||
|  |         # timeslots, the first one must be the one representing the whole day | ||||||
|  |         # and must have id "main". | ||||||
|  |         if not timeslots: self.timeslots = [Timeslot('main')] | ||||||
|  |         else: | ||||||
|  |             self.timeslots = timeslots | ||||||
|  |             self.checkTimeslots() | ||||||
|         # "colors" must be or return a dict ~{s_eventType: s_color}~ giving a |         # "colors" must be or return a dict ~{s_eventType: s_color}~ giving a | ||||||
|         # color to every event type defined in this calendar or in any calendar |         # color to every event type defined in this calendar or in any calendar | ||||||
|         # from "others". In a timeline, cells are too small to display |         # from "others". In a timeline, cells are too small to display | ||||||
|  | @ -442,6 +516,13 @@ class Calendar(Field): | ||||||
|         # it must accept an event type as single arg. |         # it must accept an event type as single arg. | ||||||
|         self.delete = delete |         self.delete = delete | ||||||
| 
 | 
 | ||||||
|  |     def checkTimeslots(self): | ||||||
|  |         '''Checks whether self.timeslots defines corect timeslots.''' | ||||||
|  |         # The first timeslot must be the global one, named 'main' | ||||||
|  |         if self.timeslots[0].id != 'main': | ||||||
|  |             raise Exception('The first timeslot must have id "main" and is ' \ | ||||||
|  |                             'the one representing the whole day.') | ||||||
|  | 
 | ||||||
|     def getPreComputedInfo(self, obj, monthDayOne, grid): |     def getPreComputedInfo(self, obj, monthDayOne, grid): | ||||||
|         '''Returns the result of calling self.preComputed, or None if no such |         '''Returns the result of calling self.preComputed, or None if no such | ||||||
|            method exists.''' |            method exists.''' | ||||||
|  | @ -561,6 +642,15 @@ class Calendar(Field): | ||||||
|         if callable(self.colors): return self.colors(obj) |         if callable(self.colors): return self.colors(obj) | ||||||
|         return self.colors |         return self.colors | ||||||
| 
 | 
 | ||||||
|  |     def dayIsFull(self, date, events): | ||||||
|  |         '''In the calendar full at p_date? Defined events at this p_date are in | ||||||
|  |            p_events. We check here if the main timeslot is used or if all | ||||||
|  |            others are used.''' | ||||||
|  |         if not events: return | ||||||
|  |         for e in events: | ||||||
|  |             if e.timeslot == 'main': return True | ||||||
|  |         return len(events) == len(self.timeslots)-1 | ||||||
|  | 
 | ||||||
|     def dateInRange(self, date, startDate, endDate): |     def dateInRange(self, date, startDate, endDate): | ||||||
|         '''Is p_date within the range (possibly) defined for this calendar by |         '''Is p_date within the range (possibly) defined for this calendar by | ||||||
|            p_startDate and p_endDate ?''' |            p_startDate and p_endDate ?''' | ||||||
|  | @ -613,6 +703,28 @@ class Calendar(Field): | ||||||
|         if not events: return |         if not events: return | ||||||
|         return events[0].eventType |         return events[0].eventType | ||||||
| 
 | 
 | ||||||
|  |     def walkEvents(self, obj, callback): | ||||||
|  |         '''Walks on p_obj, the calendar value for this field and calls | ||||||
|  |            p_callback for every day containing events. The callback must accept | ||||||
|  |            3 args: p_obj, the current day (as a DateTime instance) and the list | ||||||
|  |            of events at that day (the database-stored PersistentList | ||||||
|  |            instance). If the callback returns True we stop the walk.''' | ||||||
|  |         obj = obj.o | ||||||
|  |         if not hasattr(obj, self.name): return | ||||||
|  |         # Browse years | ||||||
|  |         years = getattr(obj, self.name) | ||||||
|  |         if not years: return | ||||||
|  |         for year in years.keys(): | ||||||
|  |             # Browse this year's months | ||||||
|  |             months = years[year] | ||||||
|  |             for month in months.keys(): | ||||||
|  |                 # Browse this month's days | ||||||
|  |                 days = months[month] | ||||||
|  |                 for day in days.keys(): | ||||||
|  |                     date = DateTime('%d/%d/%d UTC' % (year, month, day)) | ||||||
|  |                     stop = callback(obj, date, days[day]) | ||||||
|  |                     if stop: return | ||||||
|  | 
 | ||||||
|     def getEventsByType(self, obj, eventType, minDate=None, maxDate=None, |     def getEventsByType(self, obj, eventType, minDate=None, maxDate=None, | ||||||
|                         sorted=True, groupSpanned=False): |                         sorted=True, groupSpanned=False): | ||||||
|         '''Returns all the events of a given p_eventType. If p_eventType is |         '''Returns all the events of a given p_eventType. If p_eventType is | ||||||
|  | @ -669,7 +781,7 @@ class Calendar(Field): | ||||||
|                         # Filter unwanted events |                         # Filter unwanted events | ||||||
|                         if eventType and (event.eventType != eventType): |                         if eventType and (event.eventType != eventType): | ||||||
|                             continue |                             continue | ||||||
|                         # We have found a event. |                         # We have found a event | ||||||
|                         date = DateTime('%d/%d/%d UTC' % (year, month, day)) |                         date = DateTime('%d/%d/%d UTC' % (year, month, day)) | ||||||
|                         if groupSpanned: |                         if groupSpanned: | ||||||
|                             singleRes = [date, None, event] |                             singleRes = [date, None, event] | ||||||
|  | @ -699,9 +811,9 @@ class Calendar(Field): | ||||||
|     def hasEventsAt(self, obj, date, otherEvents): |     def hasEventsAt(self, obj, date, otherEvents): | ||||||
|         '''Returns True if, at p_date, an event is found of the same type as |         '''Returns True if, at p_date, an event is found of the same type as | ||||||
|            p_otherEvents.''' |            p_otherEvents.''' | ||||||
|         if not otherEvents: return False |         if not otherEvents: return | ||||||
|         events = self.getEventsAt(obj, date) |         events = self.getEventsAt(obj, date) | ||||||
|         if not events: return False |         if not events: return | ||||||
|         return events[0].eventType == otherEvents[0].eventType |         return events[0].eventType == otherEvents[0].eventType | ||||||
| 
 | 
 | ||||||
|     def getOtherEventsAt(self, obj, date, others, eventNames, render, colors): |     def getOtherEventsAt(self, obj, date, others, eventNames, render, colors): | ||||||
|  | @ -756,7 +868,7 @@ class Calendar(Field): | ||||||
|         '''Get the end date for this calendar if defined''' |         '''Get the end date for this calendar if defined''' | ||||||
|         if self.endDate: |         if self.endDate: | ||||||
|             d = self.endDate(obj.appy()) |             d = self.endDate(obj.appy()) | ||||||
|             # Return the end date without hour, in UTC. |             # Return the end date without hour, in UTC | ||||||
|             return DateTime('%d/%d/%d UTC' % (d.year(), d.month(), d.day())) |             return DateTime('%d/%d/%d UTC' % (d.year(), d.month(), d.day())) | ||||||
| 
 | 
 | ||||||
|     def getDefaultDate(self, obj): |     def getDefaultDate(self, obj): | ||||||
|  | @ -767,14 +879,14 @@ class Calendar(Field): | ||||||
|         else: |         else: | ||||||
|             return DateTime() # Now |             return DateTime() # Now | ||||||
| 
 | 
 | ||||||
|     def createEvent(self, obj, date, eventType=None, eventSpan=None, |     def createEvent(self, obj, date, timeslot, eventType=None, eventSpan=None, | ||||||
|                     handleEventSpan=True): |                     handleEventSpan=True): | ||||||
|         '''Create a new event in the calendar, at some p_date (day). |         '''Create a new event in the calendar, at some p_date (day). | ||||||
|            If p_eventType is given, it is used; else, rq['eventType'] is used. |            If p_eventType is given, it is used; else, rq['eventType'] is used. | ||||||
|            If p_handleEventSpan is True, we will use p_eventSpan (or |            If p_handleEventSpan is True, we will use p_eventSpan (or | ||||||
|            rq["eventSpan"] if p_eventSpan is not given) and also |            rq["eventSpan"] if p_eventSpan is not given) and also | ||||||
|            create the same event for successive days.''' |            create the same event for successive days.''' | ||||||
|         obj = obj.o # Ensure p_obj is not a wrapper. |         obj = obj.o # Ensure p_obj is not a wrapper | ||||||
|         rq = obj.REQUEST |         rq = obj.REQUEST | ||||||
|         # Get values from parameters |         # Get values from parameters | ||||||
|         if not eventType: eventType = rq['eventType'] |         if not eventType: eventType = rq['eventType'] | ||||||
|  | @ -802,16 +914,27 @@ class Calendar(Field): | ||||||
|             events = daysDict[day] |             events = daysDict[day] | ||||||
|         else: |         else: | ||||||
|             daysDict[day] = events = PersistentList() |             daysDict[day] = events = PersistentList() | ||||||
|         # Create and store the event, excepted if an event already exists |         # Return an error if the creation cannot occur | ||||||
|         if not events: |         for e in events: | ||||||
|             event = Object(eventType=eventType) |             if e.timeslot == timeslot: | ||||||
|             events.append(event) |                 return 'An event for this timeslot already exist' | ||||||
|  |             elif e.timeslot == 'main': | ||||||
|  |                 return 'No more place for adding this event' | ||||||
|  |         if events and (timeslot == 'main'): | ||||||
|  |             return 'No more place (2) for adding this event' | ||||||
|  |         # Create and store the event | ||||||
|  |         events.append(Event(eventType, timeslot)) | ||||||
|  |         # Sort events in the order of timeslots | ||||||
|  |         timeslots = [timeslot.id for timeslot in self.timeslots] | ||||||
|  |         if len(events) > 1: | ||||||
|  |             events.data.sort(key=lambda e: timeslots.index(e.timeslot)) | ||||||
|  |             events._p_changed = 1 | ||||||
|         # Span the event on the successive days if required |         # 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) |             nbOfDays = min(int(eventSpan), self.maxEventLength) | ||||||
|             for i in range(nbOfDays): |             for i in range(nbOfDays): | ||||||
|                 date = date + 1 |                 date = date + 1 | ||||||
|                 self.createEvent(obj, date, handleEventSpan=False) |                 self.createEvent(obj, date, timeslot, handleEventSpan=False) | ||||||
| 
 | 
 | ||||||
|     def mayDelete(self, obj, events): |     def mayDelete(self, obj, events): | ||||||
|         '''May the user delete p_events?''' |         '''May the user delete p_events?''' | ||||||
|  | @ -819,15 +942,19 @@ class Calendar(Field): | ||||||
|         if callable(self.delete): return self.delete(obj, events[0].eventType) |         if callable(self.delete): return self.delete(obj, events[0].eventType) | ||||||
|         return True |         return True | ||||||
| 
 | 
 | ||||||
|     def deleteEvent(self, obj, date, handleEventSpan=True): |     def deleteEvent(self, obj, date, timeslot, handleEventSpan=True): | ||||||
|         '''Deletes an event. It actually deletes all events at p_date. |         '''Deletes an event. If t_timeslot is "main", it deletes all events at | ||||||
|            If p_handleEventSpan is True, we will use rq["deleteNext"] to |            p_date, be there a single event on the main timeslot or several | ||||||
|            delete successive events, too.''' |            events on other timeslots. Else, it only deletes the event at | ||||||
|         obj = obj.o # Ensure p_obj is not a wrapper. |            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 |         if not self.getEventsAt(obj, date): return | ||||||
|         daysDict = getattr(obj, self.name)[date.year()][date.month()] |         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) |         events = self.getEventsAt(obj, date) | ||||||
|  |         if timeslot == 'main': | ||||||
|  |             # Delete all events; delete them also in the following days when | ||||||
|  |             # relevant. | ||||||
|             del daysDict[date.day()] |             del daysDict[date.day()] | ||||||
|             rq = obj.REQUEST |             rq = obj.REQUEST | ||||||
|             if handleEventSpan and rq.has_key('deleteNext') and \ |             if handleEventSpan and rq.has_key('deleteNext') and \ | ||||||
|  | @ -835,9 +962,18 @@ class Calendar(Field): | ||||||
|                 while True: |                 while True: | ||||||
|                     date = date + 1 |                     date = date + 1 | ||||||
|                     if self.hasEventsAt(obj, date, events): |                     if self.hasEventsAt(obj, date, events): | ||||||
|                     self.deleteEvent(obj, date, handleEventSpan=False) |                         self.deleteEvent(obj, date, timeslot, | ||||||
|  |                                          handleEventSpan=False) | ||||||
|                     else: |                     else: | ||||||
|                         break |                         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): |     def process(self, obj): | ||||||
|         '''Processes an action coming from the calendar widget, ie, the creation |         '''Processes an action coming from the calendar widget, ie, the creation | ||||||
|  | @ -846,11 +982,13 @@ class Calendar(Field): | ||||||
|         action = rq['actionType'] |         action = rq['actionType'] | ||||||
|         # Security check |         # Security check | ||||||
|         obj.mayEdit(self.writePermission, raiseError=True) |         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': |         if action == 'createEvent': | ||||||
|             return self.createEvent(obj, DateTime(rq['day'])) |             return self.createEvent(obj, date, timeslot) | ||||||
|         elif action == 'deleteEvent': |         elif action == 'deleteEvent': | ||||||
|             return self.deleteEvent(obj, DateTime(rq['day'])) |             return self.deleteEvent(obj, date, timeslot) | ||||||
| 
 | 
 | ||||||
|     def getColumnStyle(self, obj, date, render, today): |     def getColumnStyle(self, obj, date, render, today): | ||||||
|         '''What style(s) must apply to the table column representing p_date |         '''What style(s) must apply to the table column representing p_date | ||||||
|  |  | ||||||
|  | @ -715,6 +715,14 @@ msgstr "" | ||||||
| msgid "del_next_events" | msgid "del_next_events" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | #. Default: "Timeslot" | ||||||
|  | msgid "timeslot" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
|  | #. Default: "All day" | ||||||
|  | msgid "timeslot_main" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| #. Default: "Inserted by ${userName}" | #. Default: "Inserted by ${userName}" | ||||||
| msgid "history_insert" | msgid "history_insert" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | @ -715,6 +715,14 @@ msgstr "" | ||||||
| msgid "del_next_events" | msgid "del_next_events" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | #. Default: "Timeslot" | ||||||
|  | msgid "timeslot" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
|  | #. Default: "All day" | ||||||
|  | msgid "timeslot_main" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| #. Default: "Inserted by ${userName}" | #. Default: "Inserted by ${userName}" | ||||||
| msgid "history_insert" | msgid "history_insert" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | @ -715,6 +715,14 @@ msgstr "" | ||||||
| msgid "del_next_events" | msgid "del_next_events" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | #. Default: "Timeslot" | ||||||
|  | msgid "timeslot" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
|  | #. Default: "All day" | ||||||
|  | msgid "timeslot_main" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| #. Default: "Inserted by ${userName}" | #. Default: "Inserted by ${userName}" | ||||||
| msgid "history_insert" | msgid "history_insert" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | @ -716,6 +716,14 @@ msgstr "Extend the event on the following number of days (leave blank to create | ||||||
| msgid "del_next_events" | msgid "del_next_events" | ||||||
| msgstr "Also delete successive events of the same type." | 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}" | #. Default: "Inserted by ${userName}" | ||||||
| msgid "history_insert" | msgid "history_insert" | ||||||
| msgstr "Inserted by ${userName}" | msgstr "Inserted by ${userName}" | ||||||
|  |  | ||||||
|  | @ -715,6 +715,14 @@ msgstr "" | ||||||
| msgid "del_next_events" | msgid "del_next_events" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | #. Default: "Timeslot" | ||||||
|  | msgid "timeslot" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
|  | #. Default: "All day" | ||||||
|  | msgid "timeslot_main" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| #. Default: "Inserted by ${userName}" | #. Default: "Inserted by ${userName}" | ||||||
| msgid "history_insert" | msgid "history_insert" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | @ -716,6 +716,14 @@ msgstr "Étendre l'événement sur le nombre de jours suivants (laissez vide pou | ||||||
| msgid "del_next_events" | msgid "del_next_events" | ||||||
| msgstr "Supprimer aussi les événements successifs de même type" | 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}" | #. Default: "Inserted by ${userName}" | ||||||
| msgid "history_insert" | msgid "history_insert" | ||||||
| msgstr "Inséré par ${userName}" | msgstr "Inséré par ${userName}" | ||||||
|  |  | ||||||
|  | @ -715,6 +715,14 @@ msgstr "" | ||||||
| msgid "del_next_events" | msgid "del_next_events" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | #. Default: "Timeslot" | ||||||
|  | msgid "timeslot" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
|  | #. Default: "All day" | ||||||
|  | msgid "timeslot_main" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| #. Default: "Inserted by ${userName}" | #. Default: "Inserted by ${userName}" | ||||||
| msgid "history_insert" | msgid "history_insert" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | @ -715,6 +715,14 @@ msgstr "Het event uitbreiden naar de volgende dagen (leeg laten om een event aan | ||||||
| msgid "del_next_events" | msgid "del_next_events" | ||||||
| msgstr "Verwijder ook alle opeenvolgende events van hetzelfde type" | 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}" | #. Default: "Inserted by ${userName}" | ||||||
| msgid "history_insert" | msgid "history_insert" | ||||||
| msgstr "Ingevuld door ${userName}" | msgstr "Ingevuld door ${userName}" | ||||||
|  |  | ||||||
|  | @ -14,26 +14,28 @@ function askCalendar(hookId, objectUrl, render, fieldName, month) { | ||||||
|   askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxView', params); |   askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxView', params); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function openEventPopup(action, fieldName, day, spansDays, | function openEventPopup(action, fieldName, day, timeslot, spansDays, | ||||||
|                         applicableEventTypes, message) { |                         applicableEventTypes, message) { | ||||||
|   /* Opens the popup for creating (or deleting, depending on p_action) a |   /* 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 |      calendar event at some p_day. When action is "del", we need to know the | ||||||
|      (from p_spansDays) if the event spans more days, in order to propose a |      p_timeslot where the event is assigned and if the event spans more days | ||||||
|      checkbox allowing to delete events for those successive days. When action |      (from p_spansDays), in order to propose a checkbox allowing to delete | ||||||
|      is "new", a possibly restricted list of applicable event types for this |      events for those successive days. When action is "new", a possibly | ||||||
|      day is given in p_applicableEventTypes; p_message contains an optional |      restricted list of applicable event types for this day is given in | ||||||
|      message explaining why not applicable types are not applicable. */ |      p_applicableEventTypes; p_message contains an optional message explaining | ||||||
|  |      why not applicable types are not applicable. */ | ||||||
|   var prefix = fieldName + '_' + action + 'Event'; |   var prefix = fieldName + '_' + action + 'Event'; | ||||||
|   var f = document.getElementById(prefix + 'Form'); |   var f = document.getElementById(prefix + 'Form'); | ||||||
|   f.day.value = day; |   f.day.value = day; | ||||||
|   if (action == 'del') { |   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 elem = document.getElementById(prefix + 'DelNextEvent'); | ||||||
|     var cb = elem.getElementsByTagName('input'); |     var cb = elem.getElementsByTagName('input'); | ||||||
|     cb[0].checked = false; |     cb[0].checked = false; | ||||||
|     cb[1].value = 'False'; |     cb[1].value = 'False'; | ||||||
|     if (spansDays == 'True') { elem.style.display = 'block' } |     if (spansDays == 'True') elem.style.display = 'block'; | ||||||
|     else { elem.style.display = 'none' } |     else elem.style.display = 'none'; | ||||||
|   } |   } | ||||||
|   else if (action == 'new') { |   else if (action == 'new') { | ||||||
|     // First: reinitialise input fields
 |     // First: reinitialise input fields
 | ||||||
|  | @ -42,8 +44,8 @@ function openEventPopup(action, fieldName, day, spansDays, | ||||||
|     for (var i=0; i < allOptions.length; i++) { |     for (var i=0; i < allOptions.length; i++) { | ||||||
|       allOptions[i].selected = false; |       allOptions[i].selected = false; | ||||||
|     } |     } | ||||||
|     f.eventSpan.style.background = ''; |     if (f.eventSpan) f.eventSpan.style.background = ''; | ||||||
|     // Among all event types, show applicable ones and hide the others.
 |     // Among all event types, show applicable ones and hide the others
 | ||||||
|     var applicable = applicableEventTypes.split(','); |     var applicable = applicableEventTypes.split(','); | ||||||
|     var applicableDict = {}; |     var applicableDict = {}; | ||||||
|     for (var i=0; i < applicable.length; i++) { |     for (var i=0; i < applicable.length; i++) { | ||||||
|  | @ -76,6 +78,7 @@ function triggerCalendarEvent(action, hookId, fieldName, objectUrl, | ||||||
|       f.eventType.style.background = wrongTextInput; |       f.eventType.style.background = wrongTextInput; | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |     if (f.eventSpan) { | ||||||
|       // Check that eventSpan is empty or contains a valid number
 |       // Check that eventSpan is empty or contains a valid number
 | ||||||
|       var spanNumber = f.eventSpan.value.replace(' ', ''); |       var spanNumber = f.eventSpan.value.replace(' ', ''); | ||||||
|       if (spanNumber) { |       if (spanNumber) { | ||||||
|  | @ -86,6 +89,7 @@ function triggerCalendarEvent(action, hookId, fieldName, objectUrl, | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |   } | ||||||
|   var elems = f.elements; |   var elems = f.elements; | ||||||
|   var params = {}; |   var params = {}; | ||||||
|   // Put form elements into "params"
 |   // Put form elements into "params"
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Gaetan Delannay
						Gaetan Delannay