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) |
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] |
460 self.w(u'<div id="debug"> </div>') |
493 self.w(u'<div id="debug"> </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>') |