web/views/calendar.py
changeset 5556 9ab2b4c74baf
parent 5424 8ecbcbff9777
child 5713 605f571198eb
equal deleted inserted replaced
5555:a64f48dd5fe4 5556:9ab2b4c74baf
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
    14 # details.
    14 # details.
    15 #
    15 #
    16 # You should have received a copy of the GNU Lesser General Public License along
    16 # You should have received a copy of the GNU Lesser General Public License along
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """html calendar views
    18 """html calendar views"""
    19 
    19 
    20 """
       
    21 __docformat__ = "restructuredtext en"
    20 __docformat__ = "restructuredtext en"
    22 _ = unicode
    21 _ = unicode
    23 
    22 
    24 from datetime import datetime, date, timedelta
    23 from datetime import datetime, date, timedelta
    25 
    24 
    26 from logilab.mtconverter import xml_escape
    25 from logilab.mtconverter import xml_escape
    27 from logilab.common.date import strptime, date_range, todate, todatetime
    26 from logilab.common.date import ONEDAY, strptime, date_range, todate, todatetime
    28 
    27 
    29 from cubicweb.interfaces import ICalendarable
    28 from cubicweb.interfaces import ICalendarable
    30 from cubicweb.selectors import implements
    29 from cubicweb.selectors import implements, adaptable
    31 from cubicweb.view import EntityView
    30 from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
       
    31 
       
    32 
       
    33 class ICalendarableAdapter(EntityAdapter):
       
    34     __regid__ = 'ICalendarable'
       
    35     __select__ = implements(ICalendarable) # XXX for bw compat, should be abstract
       
    36 
       
    37     @property
       
    38     @implements_adapter_compat('ICalendarable')
       
    39     def start(self):
       
    40         """return start date"""
       
    41         raise NotImplementedError
       
    42 
       
    43     @property
       
    44     @implements_adapter_compat('ICalendarable')
       
    45     def stop(self):
       
    46         """return stop state"""
       
    47         raise NotImplementedError
    32 
    48 
    33 
    49 
    34 # useful constants & functions ################################################
    50 # useful constants & functions ################################################
    35 
    51 
    36 ONEDAY = timedelta(1)
    52 ONEDAY = timedelta(1)
    50     class iCalView(EntityView):
    66     class iCalView(EntityView):
    51         """A calendar view that generates a iCalendar file (RFC 2445)
    67         """A calendar view that generates a iCalendar file (RFC 2445)
    52 
    68 
    53         Does apply to ICalendarable compatible entities
    69         Does apply to ICalendarable compatible entities
    54         """
    70         """
    55         __select__ = implements(ICalendarable)
    71         __select__ = adaptable('ICalendarable')
    56         paginable = False
    72         paginable = False
    57         content_type = 'text/calendar'
    73         content_type = 'text/calendar'
    58         title = _('iCalendar')
    74         title = _('iCalendar')
    59         templatable = False
    75         templatable = False
    60         __regid__ = 'ical'
    76         __regid__ = 'ical'
    64             for i in range(len(self.cw_rset.rows)):
    80             for i in range(len(self.cw_rset.rows)):
    65                 task = self.cw_rset.complete_entity(i, 0)
    81                 task = self.cw_rset.complete_entity(i, 0)
    66                 event = ical.add('vevent')
    82                 event = ical.add('vevent')
    67                 event.add('summary').value = task.dc_title()
    83                 event.add('summary').value = task.dc_title()
    68                 event.add('description').value = task.dc_description()
    84                 event.add('description').value = task.dc_description()
    69                 if task.start:
    85                 icalendarable = task.cw_adapt_to('ICalendarable')
    70                     event.add('dtstart').value = task.start
    86                 if icalendarable.start:
    71                 if task.stop:
    87                     event.add('dtstart').value = icalendarable.start
    72                     event.add('dtend').value = task.stop
    88                 if icalendarable.stop:
       
    89                     event.add('dtend').value = icalendarable.stop
    73 
    90 
    74             buff = ical.serialize()
    91             buff = ical.serialize()
    75             if not isinstance(buff, unicode):
    92             if not isinstance(buff, unicode):
    76                 buff = unicode(buff, self._cw.encoding)
    93                 buff = unicode(buff, self._cw.encoding)
    77             self.w(buff)
    94             self.w(buff)
    83     """A calendar view that generates a hCalendar file
   100     """A calendar view that generates a hCalendar file
    84 
   101 
    85     Does apply to ICalendarable compatible entities
   102     Does apply to ICalendarable compatible entities
    86     """
   103     """
    87     __regid__ = 'hcal'
   104     __regid__ = 'hcal'
    88     __select__ = implements(ICalendarable)
   105     __select__ = adaptable('ICalendarable')
    89     paginable = False
   106     paginable = False
    90     title = _('hCalendar')
   107     title = _('hCalendar')
    91     #templatable = False
   108     #templatable = False
    92 
   109 
    93     def call(self):
   110     def call(self):
    96             task = self.cw_rset.complete_entity(i, 0)
   113             task = self.cw_rset.complete_entity(i, 0)
    97             self.w(u'<div class="vevent">')
   114             self.w(u'<div class="vevent">')
    98             self.w(u'<h3 class="summary">%s</h3>' % xml_escape(task.dc_title()))
   115             self.w(u'<h3 class="summary">%s</h3>' % xml_escape(task.dc_title()))
    99             self.w(u'<div class="description">%s</div>'
   116             self.w(u'<div class="description">%s</div>'
   100                    % task.dc_description(format='text/html'))
   117                    % task.dc_description(format='text/html'))
   101             if task.start:
   118             icalendarable = task.cw_adapt_to('ICalendarable')
   102                 self.w(u'<abbr class="dtstart" title="%s">%s</abbr>' % (task.start.isoformat(), self._cw.format_date(task.start)))
   119             if icalendarable.start:
   103             if task.stop:
   120                 self.w(u'<abbr class="dtstart" title="%s">%s</abbr>'
   104                 self.w(u'<abbr class="dtstop" title="%s">%s</abbr>' % (task.stop.isoformat(), self._cw.format_date(task.stop)))
   121                        % (icalendarable.start.isoformat(),
       
   122                           self._cw.format_date(icalendarable.start)))
       
   123             if icalendarable.stop:
       
   124                 self.w(u'<abbr class="dtstop" title="%s">%s</abbr>'
       
   125                        % (icalendarable.stop.isoformat(),
       
   126                           self._cw.format_date(icalendarable.stop)))
   105             self.w(u'</div>')
   127             self.w(u'</div>')
   106         self.w(u'</div>')
   128         self.w(u'</div>')
   107 
   129 
   108 
   130 
   109 class CalendarItemView(EntityView):
   131 class CalendarItemView(EntityView):
   111 
   133 
   112     def cell_call(self, row, col, dates=False):
   134     def cell_call(self, row, col, dates=False):
   113         task = self.cw_rset.complete_entity(row, 0)
   135         task = self.cw_rset.complete_entity(row, 0)
   114         task.view('oneline', w=self.w)
   136         task.view('oneline', w=self.w)
   115         if dates:
   137         if dates:
   116             if task.start and task.stop:
   138             icalendarable = task.cw_adapt_to('ICalendarable')
   117                 self.w('<br/>' % self._cw._('from %(date)s' % {'date': self._cw.format_date(task.start)}))
   139             if icalendarable.start and icalendarable.stop:
   118                 self.w('<br/>' % self._cw._('to %(date)s' % {'date': self._cw.format_date(task.stop)}))
   140                 self.w('<br/> %s' % self._cw._('from %(date)s')
   119                 self.w('<br/>to %s'%self._cw.format_date(task.stop))
   141                        % {'date': self._cw.format_date(icalendarable.start)})
       
   142                 self.w('<br/> %s' % self._cw._('to %(date)s')
       
   143                        % {'date': self._cw.format_date(icalendarable.stop)})
       
   144             else:
       
   145                 self.w('<br/>%s'%self._cw.format_date(icalendarable.start
       
   146                                                       or icalendarable.stop))
   120 
   147 
   121 class CalendarLargeItemView(CalendarItemView):
   148 class CalendarLargeItemView(CalendarItemView):
   122     __regid__ = 'calendarlargeitem'
   149     __regid__ = 'calendarlargeitem'
   123 
   150 
   124 
   151 
   126     def __init__(self, task, color, index=0):
   153     def __init__(self, task, color, index=0):
   127         self.task = task
   154         self.task = task
   128         self.color = color
   155         self.color = color
   129         self.index = index
   156         self.index = index
   130         self.length = 1
   157         self.length = 1
       
   158         icalendarable = task.cw_adapt_to('ICalendarable')
       
   159         self.start = icalendarable.start
       
   160         self.stop = icalendarable.stop
   131 
   161 
   132     def in_working_hours(self):
   162     def in_working_hours(self):
   133         """predicate returning True is the task is in working hours"""
   163         """predicate returning True is the task is in working hours"""
   134         if todatetime(self.task.start).hour > 7 and todatetime(self.task.stop).hour < 20:
   164         if todatetime(self.start).hour > 7 and todatetime(self.stop).hour < 20:
   135             return True
   165             return True
   136         return False
   166         return False
   137 
   167 
   138     def is_one_day_task(self):
   168     def is_one_day_task(self):
   139         task = self.task
   169         return self.start and self.stop and self.start.isocalendar() == self.stop.isocalendar()
   140         return task.start and task.stop and task.start.isocalendar() ==  task.stop.isocalendar()
       
   141 
   170 
   142 
   171 
   143 class OneMonthCal(EntityView):
   172 class OneMonthCal(EntityView):
   144     """At some point, this view will probably replace ampm calendars"""
   173     """At some point, this view will probably replace ampm calendars"""
   145     __regid__ = 'onemonthcal'
   174     __regid__ = 'onemonthcal'
   146     __select__ = implements(ICalendarable)
   175     __select__ = adaptable('ICalendarable')
       
   176 
   147     paginable = False
   177     paginable = False
   148     title = _('one month')
   178     title = _('one month')
   149 
   179 
   150     def call(self):
   180     def call(self):
   151         self._cw.add_js('cubicweb.ajax.js')
   181         self._cw.add_js('cubicweb.ajax.js')
   179             if len(self.cw_rset[row]) > 1 and self.cw_rset.description[row][1] == 'CWUser':
   209             if len(self.cw_rset[row]) > 1 and self.cw_rset.description[row][1] == 'CWUser':
   180                 user = self.cw_rset.get_entity(row, 1)
   210                 user = self.cw_rset.get_entity(row, 1)
   181             else:
   211             else:
   182                 user = None
   212                 user = None
   183             the_dates = []
   213             the_dates = []
   184             tstart = task.start
   214             icalendarable = task.cw_adapt_to('ICalendarable')
       
   215             tstart = icalendarable.start
   185             if tstart:
   216             if tstart:
   186                 tstart = todate(task.start)
   217                 tstart = todate(icalendarable.start)
   187                 if tstart > lastday:
   218                 if tstart > lastday:
   188                     continue
   219                     continue
   189                 the_dates = [tstart]
   220                 the_dates = [tstart]
   190             tstop = task.stop
   221             tstop = icalendarable.stop
   191             if tstop:
   222             if tstop:
   192                 tstop = todate(tstop)
   223                 tstop = todate(tstop)
   193                 if tstop < firstday:
   224                 if tstop < firstday:
   194                     continue
   225                     continue
   195                 the_dates = [tstop]
   226                 the_dates = [tstop]
   197                 if tstart.isocalendar() == tstop.isocalendar():
   228                 if tstart.isocalendar() == tstop.isocalendar():
   198                     if firstday <= tstart <= lastday:
   229                     if firstday <= tstart <= lastday:
   199                         the_dates = [tstart]
   230                         the_dates = [tstart]
   200                 else:
   231                 else:
   201                     the_dates = date_range(max(tstart, firstday),
   232                     the_dates = date_range(max(tstart, firstday),
   202                                            min(tstop, lastday))
   233                                            min(tstop + ONEDAY, lastday))
   203             if not the_dates:
   234             if not the_dates:
   204                 continue
   235                 continue
   205 
   236 
   206             for d in the_dates:
   237             for d in the_dates:
   207                 d_tasks = dates.setdefault((d.year, d.month, d.day), {})
   238                 d_tasks = dates.setdefault((d.year, d.month, d.day), {})
   333 
   364 
   334 
   365 
   335 class OneWeekCal(EntityView):
   366 class OneWeekCal(EntityView):
   336     """At some point, this view will probably replace ampm calendars"""
   367     """At some point, this view will probably replace ampm calendars"""
   337     __regid__ = 'oneweekcal'
   368     __regid__ = 'oneweekcal'
   338     __select__ = implements(ICalendarable)
   369     __select__ = adaptable('ICalendarable')
       
   370 
   339     paginable = False
   371     paginable = False
   340     title = _('one week')
   372     title = _('one week')
   341 
   373 
   342     def call(self):
   374     def call(self):
   343         self._cw.add_js( ('cubicweb.ajax.js', 'cubicweb.calendar.js') )
   375         self._cw.add_js( ('cubicweb.ajax.js', 'cubicweb.calendar.js') )
   366             task = self.cw_rset.get_entity(row, 0)
   398             task = self.cw_rset.get_entity(row, 0)
   367             if task in done_tasks:
   399             if task in done_tasks:
   368                 continue
   400                 continue
   369             done_tasks.append(task)
   401             done_tasks.append(task)
   370             the_dates = []
   402             the_dates = []
   371             tstart = task.start
   403             icalendarable = task.cw_adapt_to('ICalendarable')
   372             tstop = task.stop
   404             tstart = icalendarable.start
       
   405             tstop = icalendarable.stop
   373             if tstart:
   406             if tstart:
   374                 tstart = todate(tstart)
   407                 tstart = todate(tstart)
   375                 if tstart > lastday:
   408                 if tstart > lastday:
   376                     continue
   409                     continue
   377                 the_dates = [tstart]
   410                 the_dates = [tstart]
   380                 if tstop < firstday:
   413                 if tstop < firstday:
   381                     continue
   414                     continue
   382                 the_dates = [tstop]
   415                 the_dates = [tstop]
   383             if tstart and tstop:
   416             if tstart and tstop:
   384                 the_dates = date_range(max(tstart, firstday),
   417                 the_dates = date_range(max(tstart, firstday),
   385                                        min(tstop, lastday))
   418                                        min(tstop + ONEDAY, lastday))
   386             if not the_dates:
   419             if not the_dates:
   387                 continue
   420                 continue
   388 
   421 
   389             if task not in task_colors:
   422             if task not in task_colors:
   390                 task_colors[task] = colors[next_color_index]
   423                 task_colors[task] = colors[next_color_index]
   460         self.w(u'<div id="debug">&#160;</div>')
   493         self.w(u'<div id="debug">&#160;</div>')
   461 
   494 
   462     def _build_calendar_cell(self, date, task_descrs):
   495     def _build_calendar_cell(self, date, task_descrs):
   463         inday_tasks = [t for t in task_descrs if t.is_one_day_task() and  t.in_working_hours()]
   496         inday_tasks = [t for t in task_descrs if t.is_one_day_task() and  t.in_working_hours()]
   464         wholeday_tasks = [t for t in task_descrs if not t.is_one_day_task()]
   497         wholeday_tasks = [t for t in task_descrs if not t.is_one_day_task()]
   465         inday_tasks.sort(key=lambda t:t.task.start)
   498         inday_tasks.sort(key=lambda t:t.start)
   466         sorted_tasks = []
   499         sorted_tasks = []
   467         for i, t in enumerate(wholeday_tasks):
   500         for i, t in enumerate(wholeday_tasks):
   468             t.index = i
   501             t.index = i
   469         ncols = len(wholeday_tasks)
   502         ncols = len(wholeday_tasks)
   470         while inday_tasks:
   503         while inday_tasks:
   471             t = inday_tasks.pop(0)
   504             t = inday_tasks.pop(0)
   472             for i, c in enumerate(sorted_tasks):
   505             for i, c in enumerate(sorted_tasks):
   473                 if not c or c[-1].task.stop <= t.task.start:
   506                 if not c or c[-1].stop <= t.start:
   474                     c.append(t)
   507                     c.append(t)
   475                     t.index = i+ncols
   508                     t.index = i+ncols
   476                     break
   509                     break
   477             else:
   510             else:
   478                 t.index = len(sorted_tasks) + ncols
   511                 t.index = len(sorted_tasks) + ncols
   489             task = task_desc.task
   522             task = task_desc.task
   490             start_hour = 8
   523             start_hour = 8
   491             start_min = 0
   524             start_min = 0
   492             stop_hour = 20
   525             stop_hour = 20
   493             stop_min = 0
   526             stop_min = 0
   494             if task.start:
   527             if task_desc.start:
   495                 if date < todate(task.start) < date + ONEDAY:
   528                 if date < todate(task_desc.start) < date + ONEDAY:
   496                     start_hour = max(8, task.start.hour)
   529                     start_hour = max(8, task_desc.start.hour)
   497                     start_min = task.start.minute
   530                     start_min = task_desc.start.minute
   498             if task.stop:
   531             if task_desc.stop:
   499                 if date < todate(task.stop) < date + ONEDAY:
   532                 if date < todate(task_desc.stop) < date + ONEDAY:
   500                     stop_hour = min(20, task.stop.hour)
   533                     stop_hour = min(20, task_desc.stop.hour)
   501                     if stop_hour < 20:
   534                     if stop_hour < 20:
   502                         stop_min = task.stop.minute
   535                         stop_min = task_desc.stop.minute
   503 
   536 
   504             height = 100.0*(stop_hour+stop_min/60.0-start_hour-start_min/60.0)/(20-8)
   537             height = 100.0*(stop_hour+stop_min/60.0-start_hour-start_min/60.0)/(20-8)
   505             top = 100.0*(start_hour+start_min/60.0-8)/(20-8)
   538             top = 100.0*(start_hour+start_min/60.0-8)/(20-8)
   506             left = width*task_desc.index
   539             left = width*task_desc.index
   507             style = "height: %s%%; width: %s%%; top: %s%%; left: %s%%; " % \
   540             style = "height: %s%%; width: %s%%; top: %s%%; left: %s%%; " % \
   516                                  )
   549                                  )
   517 
   550 
   518             self.w(u'<div class="tooltip" ondblclick="stopPropagation(event); window.location.assign(\'%s\'); return false;">' % xml_escape(url))
   551             self.w(u'<div class="tooltip" ondblclick="stopPropagation(event); window.location.assign(\'%s\'); return false;">' % xml_escape(url))
   519             task.view('tooltip', w=self.w)
   552             task.view('tooltip', w=self.w)
   520             self.w(u'</div>')
   553             self.w(u'</div>')
   521             if task.start is None:
   554             if task_desc.start is None:
   522                 self.w(u'<div class="bottommarker">')
   555                 self.w(u'<div class="bottommarker">')
   523                 self.w(u'<div class="bottommarkerline" style="margin: 0px 3px 0px 3px; height: 1px;">')
   556                 self.w(u'<div class="bottommarkerline" style="margin: 0px 3px 0px 3px; height: 1px;">')
   524                 self.w(u'</div>')
   557                 self.w(u'</div>')
   525                 self.w(u'<div class="bottommarkerline" style="margin: 0px 2px 0px 2px; height: 1px;">')
   558                 self.w(u'<div class="bottommarkerline" style="margin: 0px 2px 0px 2px; height: 1px;">')
   526                 self.w(u'</div>')
   559                 self.w(u'</div>')