--- a/web/views/calendar.py Thu Mar 10 15:18:22 2011 +0100
+++ b/web/views/calendar.py Fri Mar 11 09:47:09 2011 +0100
@@ -20,15 +20,28 @@
__docformat__ = "restructuredtext en"
_ = unicode
-from datetime import datetime, date, timedelta
+import copy
+from datetime import timedelta
from logilab.mtconverter import xml_escape
-from logilab.common.date import ONEDAY, strptime, date_range, todate, todatetime
+from logilab.common.date import todatetime
+from cubicweb.utils import json_dumps
from cubicweb.interfaces import ICalendarable
from cubicweb.selectors import implements, adaptable
from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+# useful constants & functions ################################################
+
+ONEDAY = timedelta(1)
+
+WEEKDAYS = (_("monday"), _("tuesday"), _("wednesday"), _("thursday"),
+ _("friday"), _("saturday"), _("sunday"))
+MONTHNAMES = ( _('january'), _('february'), _('march'), _('april'), _('may'),
+ _('june'), _('july'), _('august'), _('september'), _('october'),
+ _('november'), _('december')
+ )
+
class ICalendarableAdapter(EntityAdapter):
__needs_bw_compat__ = True
@@ -44,21 +57,10 @@
@property
@implements_adapter_compat('ICalendarable')
def stop(self):
- """return stop state"""
+ """return stop date"""
raise NotImplementedError
-# useful constants & functions ################################################
-
-ONEDAY = timedelta(1)
-
-WEEKDAYS = (_("monday"), _("tuesday"), _("wednesday"), _("thursday"),
- _("friday"), _("saturday"), _("sunday"))
-MONTHNAMES = ( _('january'), _('february'), _('march'), _('april'), _('may'),
- _('june'), _('july'), _('august'), _('september'), _('october'),
- _('november'), _('december')
- )
-
# Calendar views ##############################################################
try:
@@ -146,9 +148,6 @@
self.w('<br/>%s'%self._cw.format_date(icalendarable.start
or icalendarable.stop))
-class CalendarLargeItemView(CalendarItemView):
- __regid__ = 'calendarlargeitem'
-
class _TaskEntry(object):
def __init__(self, task, color, index=0):
@@ -170,413 +169,91 @@
return self.start and self.stop and self.start.isocalendar() == self.stop.isocalendar()
-class OneMonthCal(EntityView):
- """At some point, this view will probably replace ampm calendars"""
- __regid__ = 'onemonthcal'
- __select__ = adaptable('ICalendarable')
-
- paginable = False
- title = _('one month')
-
- def call(self):
- self._cw.add_js('cubicweb.ajax.js')
- self._cw.add_css('cubicweb.calendar.css')
- # XXX: restrict courses directy with RQL
- _today = datetime.today()
-
- if 'year' in self._cw.form:
- year = int(self._cw.form['year'])
- else:
- year = _today.year
- if 'month' in self._cw.form:
- month = int(self._cw.form['month'])
- else:
- month = _today.month
-
- first_day_of_month = date(year, month, 1)
- firstday = first_day_of_month - timedelta(first_day_of_month.weekday())
- if month >= 12:
- last_day_of_month = date(year + 1, 1, 1) - timedelta(1)
- else:
- last_day_of_month = date(year, month + 1, 1) - timedelta(1)
- # date range exclude last day so we should at least add one day, hence
- # the 7
- lastday = last_day_of_month + timedelta(7 - last_day_of_month.weekday())
- month_dates = list(date_range(firstday, lastday))
- dates = {}
- task_max = 0
- for row in xrange(self.cw_rset.rowcount):
- task = self.cw_rset.get_entity(row, 0)
- if len(self.cw_rset[row]) > 1 and self.cw_rset.description[row][1] == 'CWUser':
- user = self.cw_rset.get_entity(row, 1)
- else:
- user = None
- the_dates = []
- icalendarable = task.cw_adapt_to('ICalendarable')
- tstart = icalendarable.start
- if tstart:
- tstart = todate(icalendarable.start)
- if tstart > lastday:
- continue
- the_dates = [tstart]
- tstop = icalendarable.stop
- if tstop:
- tstop = todate(tstop)
- if tstop < firstday:
- continue
- the_dates = [tstop]
- if tstart and tstop:
- if tstart.isocalendar() == tstop.isocalendar():
- if firstday <= tstart <= lastday:
- the_dates = [tstart]
- else:
- the_dates = date_range(max(tstart, firstday),
- min(tstop + ONEDAY, lastday))
- if not the_dates:
- continue
-
- for d in the_dates:
- d_tasks = dates.setdefault((d.year, d.month, d.day), {})
- t_users = d_tasks.setdefault(task, set())
- t_users.add( user )
- if len(d_tasks) > task_max:
- task_max = len(d_tasks)
-
- days = []
- nrows = max(3, task_max)
- # colors here are class names defined in cubicweb.css
- colors = [ "col%x" % i for i in range(12) ]
- next_color_index = 0
-
- visited_tasks = {} # holds a description of a task
- task_colors = {} # remember a color assigned to a task
- for mdate in month_dates:
- d_tasks = dates.get((mdate.year, mdate.month, mdate.day), {})
- rows = [None] * nrows
- # every task that is "visited" for the first time
- # require a special treatment, so we put them in
- # 'postpone'
- postpone = []
- for task in d_tasks:
- if task in visited_tasks:
- task_descr = visited_tasks[ task ]
- rows[task_descr.index] = task_descr
- else:
- postpone.append(task)
- for task in postpone:
- # to every 'new' task we must affect a color
- # (which must be the same for every user concerned
- # by the task)
- for i, t in enumerate(rows):
- if t is None:
- if task in task_colors:
- color = task_colors[task]
- else:
- color = colors[next_color_index]
- next_color_index = (next_color_index+1)%len(colors)
- task_colors[task] = color
- task_descr = _TaskEntry(task, color, i)
- rows[i] = task_descr
- visited_tasks[task] = task_descr
- break
- else:
- raise RuntimeError("is it possible we got it wrong?")
-
- days.append( rows )
-
- curdate = first_day_of_month
- self.w(u'<div id="onemonthcalid">')
- # build schedule
- self.w(u'<table class="omcalendar">')
- prevlink, nextlink = self._prevnext_links(curdate) # XXX
- self.w(u'<tr><th><a href="%s"><<</a></th><th colspan="5">%s %s</th>'
- u'<th><a href="%s">>></a></th></tr>' %
- (xml_escape(prevlink), self._cw._(curdate.strftime('%B').lower()),
- curdate.year, xml_escape(nextlink)))
-
- # output header
- self.w(u'<tr><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th><th>%s</th></tr>' %
- tuple(self._cw._(day) for day in WEEKDAYS))
- # build calendar
- for mdate, task_rows in zip(month_dates, days):
- if mdate.weekday() == 0:
- self.w(u'<tr>')
- self._build_calendar_cell(mdate, task_rows, curdate)
- if mdate.weekday() == 6:
- self.w(u'</tr>')
- self.w(u'</table></div>')
-
- def _prevnext_links(self, curdate):
- prevdate = curdate - timedelta(31)
- nextdate = curdate + timedelta(31)
- rql = self.cw_rset.printable_rql()
- prevlink = self._cw.ajax_replace_url('onemonthcalid', rql=rql,
- vid='onemonthcal',
- year=prevdate.year,
- month=prevdate.month)
- nextlink = self._cw.ajax_replace_url('onemonthcalid', rql=rql,
- vid='onemonthcal',
- year=nextdate.year,
- month=nextdate.month)
- return prevlink, nextlink
-
- def _build_calendar_cell(self, celldate, rows, curdate):
- curmonth = curdate.month
- classes = ""
- if celldate.month != curmonth:
- classes += " outOfRange"
- if celldate == date.today():
- classes += " today"
- self.w(u'<td class="cell%s">' % classes)
- self.w(u'<div class="calCellTitle%s">' % classes)
- self.w(u'<div class="day">%s</div>' % celldate.day)
-
- if len(self.cw_rset.column_types(0)) == 1:
- etype = list(self.cw_rset.column_types(0))[0]
- url = self._cw.build_url(vid='creation', etype=etype,
- schedule=True,
- start=self._cw.format_date(celldate), stop=self._cw.format_date(celldate),
- __redirectrql=self.cw_rset.printable_rql(),
- __redirectparams=self._cw.build_url_params(year=curdate.year, month=curmonth),
- __redirectvid=self.__regid__
- )
- self.w(u'<div class="cmd"><a href="%s">%s</a></div>' % (xml_escape(url), self._cw._(u'add')))
- self.w(u' ')
- self.w(u'</div>')
- self.w(u'<div class="cellContent">')
- for task_descr in rows:
- if task_descr:
- task = task_descr.task
- self.w(u'<div class="task %s">' % task_descr.color)
- task.view('calendaritem', w=self.w )
- url = task.absolute_url(vid='edition',
- __redirectrql=self.cw_rset.printable_rql(),
- __redirectparams=self._cw.build_url_params(year=curdate.year, month=curmonth),
- __redirectvid=self.__regid__
- )
-
- self.w(u'<div class="tooltip" ondblclick="stopPropagation(event); window.location.assign(\'%s\'); return false;">' % xml_escape(url))
- task.view('tooltip', w=self.w )
- self.w(u'</div>')
- else:
- self.w(u'<div class="task">')
- self.w(u" ")
- self.w(u'</div>')
- self.w(u'</div>')
- self.w(u'</td>')
-
-
-class OneWeekCal(EntityView):
- """At some point, this view will probably replace ampm calendars"""
- __regid__ = 'oneweekcal'
+class CalendarView(EntityView):
+ __regid__ = 'calendar'
__select__ = adaptable('ICalendarable')
paginable = False
- title = _('one week')
+ title = _('calendar')
+
+ fullcalendar_options = {
+ 'firstDay': 1,
+ 'header': {'left': 'prev,next today',
+ 'center': 'title',
+ 'right': 'month,agendaWeek,agendaDay',
+ },
+ 'editable': True,
+ 'defaultView': 'month',
+ 'timeFormat': {'month': '',
+ '': 'H:mm'},
+ 'firstHour': 8,
+ 'axisFormat': 'H:mm',
+ 'columnFormat': {'month': 'dddd',
+ 'agendaWeek': 'dddd yyyy/M/dd',
+ 'agendaDay': 'dddd yyyy/M/dd'}
+ }
+
def call(self):
- self._cw.add_js( ('cubicweb.ajax.js', 'cubicweb.calendar.js') )
- self._cw.add_css('cubicweb.calendar.css')
- # XXX: restrict directly with RQL
- _today = datetime.today()
- if 'year' in self._cw.form:
- year = int(self._cw.form['year'])
- else:
- year = _today.year
- if 'week' in self._cw.form:
- week = int(self._cw.form['week'])
- else:
- week = _today.isocalendar()[1]
- # week - 1 since we get week number > 0 while we want it to start from 0
- first_day_of_week = todate(strptime('%s-%s-1' % (year, week - 1), '%Y-%U-%w'))
- lastday = first_day_of_week + timedelta(6)
- firstday = first_day_of_week
- dates = [[] for i in range(7)]
- task_colors = {} # remember a color assigned to a task
- # colors here are class names defined in cubicweb.css
- colors = [ "col%x" % i for i in range(12) ]
- next_color_index = 0
- done_tasks = set()
- for row in xrange(self.cw_rset.rowcount):
- task = self.cw_rset.get_entity(row, 0)
- if task.eid in done_tasks:
- continue
- done_tasks.add(task.eid)
- the_dates = []
- icalendarable = task.cw_adapt_to('ICalendarable')
- tstart = icalendarable.start
- tstop = icalendarable.stop
- if tstart:
- tstart = todate(tstart)
- if tstart > lastday:
- continue
- the_dates = [tstart]
- if tstop:
- tstop = todate(tstop)
- if tstop < firstday:
- continue
- the_dates = [tstop]
- if tstart and tstop:
- the_dates = date_range(max(tstart, firstday),
- min(tstop + ONEDAY, lastday))
- if not the_dates:
- continue
+ self._cw.demote_to_html()
+ self._cw.add_css(('fullcalendar.css', 'cubicweb.calendar.css'))
+ self._cw.add_js(('jquery.ui.js', 'fullcalendar.min.js', 'jquery.qtip.min.js'))
+ self.add_onload()
+ # write calendar div to load jquery fullcalendar object
+ self.w(u'<div id="calendar"></div>')
- if task not in task_colors:
- task_colors[task] = colors[next_color_index]
- next_color_index = (next_color_index+1) % len(colors)
-
- for d in the_dates:
- day = d.weekday()
- task_descr = _TaskEntry(task, task_colors[task])
- dates[day].append(task_descr)
-
- self.w(u'<div id="oneweekcalid">')
- # build schedule
- self.w(u'<table class="omcalendar" id="week">')
- prevlink, nextlink = self._prevnext_links(first_day_of_week) # XXX
- self.w(u'<tr><th class="transparent"></th>')
- self.w(u'<th><a href="%s"><<</a></th><th colspan="5">%s %s %s</th>'
- u'<th><a href="%s">>></a></th></tr>' %
- (xml_escape(prevlink), first_day_of_week.year,
- self._cw._(u'week'), first_day_of_week.isocalendar()[1],
- xml_escape(nextlink)))
-
- # output header
- self.w(u'<tr>')
- self.w(u'<th class="transparent"></th>') # column for hours
- _today = date.today()
- for i, day in enumerate(WEEKDAYS):
- wdate = first_day_of_week + timedelta(i)
- if wdate.isocalendar() == _today.isocalendar():
- self.w(u'<th class="today">%s<br/>%s</th>' % (self._cw._(day), self._cw.format_date(wdate)))
- else:
- self.w(u'<th>%s<br/>%s</th>' % (self._cw._(day), self._cw.format_date(wdate)))
- self.w(u'</tr>')
-
- # build week calendar
- self.w(u'<tr>')
- self.w(u'<td style="width:5em;">') # column for hours
- extra = ""
- for h in range(8, 20):
- self.w(u'<div class="hour" %s>'%extra)
- self.w(u'%02d:00'%h)
- self.w(u'</div>')
- self.w(u'</td>')
- for i, day in enumerate(WEEKDAYS):
- wdate = first_day_of_week + timedelta(i)
- classes = ""
- if wdate.isocalendar() == _today.isocalendar():
- classes = " today"
- self.w(u'<td class="column %s" id="%s">' % (classes, day))
- if len(self.cw_rset.column_types(0)) == 1:
- etype = list(self.cw_rset.column_types(0))[0]
- url = self._cw.build_url(vid='creation', etype=etype,
- schedule=True,
- __redirectrql=self.cw_rset.printable_rql(),
- __redirectparams=self._cw.build_url_params(year=year, week=week),
- __redirectvid=self.__regid__
- )
- extra = ' ondblclick="addCalendarItem(event, hmin=8, hmax=20, year=%s, month=%s, day=%s, duration=2, baseurl=\'%s\')"' % (
- wdate.year, wdate.month, wdate.day, xml_escape(url))
- else:
- extra = ""
- self.w(u'<div class="columndiv"%s>'% extra)
- for h in range(8, 20):
- self.w(u'<div class="hourline" style="top:%sex;">'%((h-7)*8))
- self.w(u'</div>')
- if dates[i]:
- self._build_calendar_cell(wdate, dates[i])
- self.w(u'</div>')
- self.w(u'</td>')
- self.w(u'</tr>')
- self.w(u'</table></div>')
- self.w(u'<div id="coord"></div>')
- self.w(u'<div id="debug"> </div>')
-
- def _build_calendar_cell(self, date, task_descrs):
- inday_tasks = [t for t in task_descrs if t.is_one_day_task() and t.in_working_hours()]
- wholeday_tasks = [t for t in task_descrs if not t.is_one_day_task()]
- inday_tasks.sort(key=lambda t:t.start)
- sorted_tasks = []
- for i, t in enumerate(wholeday_tasks):
- t.index = i
- ncols = len(wholeday_tasks)
- while inday_tasks:
- t = inday_tasks.pop(0)
- for i, c in enumerate(sorted_tasks):
- if not c or c[-1].stop <= t.start:
- c.append(t)
- t.index = i+ncols
- break
- else:
- t.index = len(sorted_tasks) + ncols
- sorted_tasks.append([t])
- ncols += len(sorted_tasks)
- if ncols == 0:
- return
-
- inday_tasks = []
- for tasklist in sorted_tasks:
- inday_tasks += tasklist
- width = 100.0/ncols
- for task_desc in wholeday_tasks + inday_tasks:
- task = task_desc.task
- start_hour = 8
- start_min = 0
- stop_hour = 20
- stop_min = 0
- if task_desc.start:
- if date < todate(task_desc.start) < date + ONEDAY:
- start_hour = max(8, task_desc.start.hour)
- start_min = task_desc.start.minute
- if task_desc.stop:
- if date < todate(task_desc.stop) < date + ONEDAY:
- stop_hour = min(20, task_desc.stop.hour)
- if stop_hour < 20:
- stop_min = task_desc.stop.minute
-
- height = 100.0*(stop_hour+stop_min/60.0-start_hour-start_min/60.0)/(20-8)
- top = 100.0*(start_hour+start_min/60.0-8)/(20-8)
- left = width*task_desc.index
- style = "height: %s%%; width: %s%%; top: %s%%; left: %s%%; " % \
- (height, width, top, left)
- self.w(u'<div class="task %s" style="%s">' % \
- (task_desc.color, style))
- task.view('calendaritem', dates=False, w=self.w)
- url = task.absolute_url(vid='edition',
- __redirectrql=self.cw_rset.printable_rql(),
- __redirectparams=self._cw.build_url_params(year=date.year, week=date.isocalendar()[1]),
- __redirectvid=self.__regid__
- )
-
- self.w(u'<div class="tooltip" ondblclick="stopPropagation(event); window.location.assign(\'%s\'); return false;">' % xml_escape(url))
- task.view('tooltip', w=self.w)
- self.w(u'</div>')
- if task_desc.start is None:
- self.w(u'<div class="bottommarker">')
- self.w(u'<div class="bottommarkerline" style="margin: 0px 3px 0px 3px; height: 1px;">')
- self.w(u'</div>')
- self.w(u'<div class="bottommarkerline" style="margin: 0px 2px 0px 2px; height: 1px;">')
- self.w(u'</div>')
- self.w(u'<div class="bottommarkerline" style="margin: 0px 1px 0px 1px; height: 3ex; color: white; font-size: x-small; vertical-align: center; text-align: center;">')
- self.w(u'end')
- self.w(u'</div>')
- self.w(u'</div>')
- self.w(u'</div>')
+ def add_onload(self):
+ fullcalendar_options = self.fullcalendar_options.copy()
+ fullcalendar_options['events'] = self.get_events()
+ fullcalendar_options['buttonText'] = {'today': self._cw._('today'),
+ 'month': self._cw._('month'),
+ 'week': self._cw._('week'),
+ 'day': self._cw._('day')}
+ # js callback to add a tooltip and to put html in event's title
+ js = """
+ var options = %s;
+ options.eventRender = function(event, $element) {
+ // add a tooltip for each event
+ var div = '<div class="tooltip">'+ event.description+ '</div>';
+ $element.append(div);
+ // allow to have html tags in event's title
+ $element.find('span.fc-event-title').html($element.find('span.fc-event-title').text());
+ };
+ $("#calendar").fullCalendar(options);
+ """ #"
+ self._cw.add_onload(js % json_dumps(fullcalendar_options))
- def _prevnext_links(self, curdate):
- prevdate = curdate - timedelta(7)
- nextdate = curdate + timedelta(7)
- rql = self.cw_rset.printable_rql()
- prevlink = self._cw.ajax_replace_url('oneweekcalid', rql=rql,
- vid='oneweekcal',
- year=prevdate.year,
- week=prevdate.isocalendar()[1])
- nextlink = self._cw.ajax_replace_url('oneweekcalid', rql=rql,
- vid='oneweekcal',
- year=nextdate.year,
- week=nextdate.isocalendar()[1])
- return prevlink, nextlink
+ def get_events(self):
+ events = []
+ for entity in self.cw_rset.entities():
+ icalendarable = entity.cw_adapt_to('ICalendarable')
+ event = {'eid': entity.eid,
+ 'title': entity.view('calendaritem'),
+ 'url': xml_escape(entity.absolute_url()),
+ 'className': 'calevent',
+ 'description': entity.view('tooltip'),
+ }
+ start_date = icalendarable.start
+ if not start_date:
+ start_date = icalendarable.stop
+ event['start'] = start_date.strftime('%Y-%m-%dT%H:%M')
+ event['allDay'] = True
+ if icalendarable.stop:
+ event['end'] = icalendarable.stop.strftime('%Y-%m-%dT%H:%M')
+ event['allDay'] = False
+ events.append(event)
+ return events
+
+class OneMonthCal(CalendarView):
+ __regid__ = 'onemonthcal'
+
+ title = _('one month')
+
+class OneWeekCal(CalendarView):
+ __regid__ = 'oneweekcal'
+
+ title = _('one week')
+ fullcalendar_options = CalendarView.fullcalendar_options.copy()
+ fullcalendar_options['defaultView'] = 'agendaWeek'