[rset] Add a line break the first result in repr of multiple rows rset
Improve readability of rset while debugging by aligning the first value with the
others if the rset have multiple row. This first line was usually not read as
it' was on the same line that the first part of the repr ("<rset <RQL>, (%i
rows) ...")
# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""html calendar views"""
__docformat__ = "restructuredtext en"
_ = unicode
from datetime import datetime, date, timedelta
from logilab.mtconverter import xml_escape
from logilab.common.date import ONEDAY, strptime, date_range, todate, todatetime
from cubicweb.interfaces import ICalendarable
from cubicweb.selectors import implements, adaptable
from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
class ICalendarableAdapter(EntityAdapter):
__regid__ = 'ICalendarable'
__select__ = implements(ICalendarable) # XXX for bw compat, should be abstract
@property
@implements_adapter_compat('ICalendarable')
def start(self):
"""return start date"""
raise NotImplementedError
@property
@implements_adapter_compat('ICalendarable')
def stop(self):
"""return stop state"""
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:
from vobject import iCalendar
class iCalView(EntityView):
"""A calendar view that generates a iCalendar file (RFC 2445)
Does apply to ICalendarable compatible entities
"""
__select__ = adaptable('ICalendarable')
paginable = False
content_type = 'text/calendar'
title = _('iCalendar')
templatable = False
__regid__ = 'ical'
def call(self):
ical = iCalendar()
for i in range(len(self.cw_rset.rows)):
task = self.cw_rset.complete_entity(i, 0)
event = ical.add('vevent')
event.add('summary').value = task.dc_title()
event.add('description').value = task.dc_description()
icalendarable = task.cw_adapt_to('ICalendarable')
if icalendarable.start:
event.add('dtstart').value = icalendarable.start
if icalendarable.stop:
event.add('dtend').value = icalendarable.stop
buff = ical.serialize()
if not isinstance(buff, unicode):
buff = unicode(buff, self._cw.encoding)
self.w(buff)
except ImportError:
pass
class hCalView(EntityView):
"""A calendar view that generates a hCalendar file
Does apply to ICalendarable compatible entities
"""
__regid__ = 'hcal'
__select__ = adaptable('ICalendarable')
paginable = False
title = _('hCalendar')
#templatable = False
def call(self):
self.w(u'<div class="hcalendar">')
for i in range(len(self.cw_rset.rows)):
task = self.cw_rset.complete_entity(i, 0)
self.w(u'<div class="vevent">')
self.w(u'<h3 class="summary">%s</h3>' % xml_escape(task.dc_title()))
self.w(u'<div class="description">%s</div>'
% task.dc_description(format='text/html'))
icalendarable = task.cw_adapt_to('ICalendarable')
if icalendarable.start:
self.w(u'<abbr class="dtstart" title="%s">%s</abbr>'
% (icalendarable.start.isoformat(),
self._cw.format_date(icalendarable.start)))
if icalendarable.stop:
self.w(u'<abbr class="dtstop" title="%s">%s</abbr>'
% (icalendarable.stop.isoformat(),
self._cw.format_date(icalendarable.stop)))
self.w(u'</div>')
self.w(u'</div>')
class CalendarItemView(EntityView):
__regid__ = 'calendaritem'
def cell_call(self, row, col, dates=False):
task = self.cw_rset.complete_entity(row, 0)
task.view('oneline', w=self.w)
if dates:
icalendarable = task.cw_adapt_to('ICalendarable')
if icalendarable.start and icalendarable.stop:
self.w('<br/> %s' % self._cw._('from %(date)s')
% {'date': self._cw.format_date(icalendarable.start)})
self.w('<br/> %s' % self._cw._('to %(date)s')
% {'date': self._cw.format_date(icalendarable.stop)})
else:
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):
self.task = task
self.color = color
self.index = index
self.length = 1
icalendarable = task.cw_adapt_to('ICalendarable')
self.start = icalendarable.start
self.stop = icalendarable.stop
def in_working_hours(self):
"""predicate returning True is the task is in working hours"""
if todatetime(self.start).hour > 7 and todatetime(self.stop).hour < 20:
return True
return False
def is_one_day_task(self):
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.build_ajax_replace_url('onemonthcalid', rql, 'onemonthcal',
year=prevdate.year,
month=prevdate.month)
nextlink = self._cw.build_ajax_replace_url('onemonthcalid', rql, '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'
__select__ = adaptable('ICalendarable')
paginable = False
title = _('one week')
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 = []
for row in xrange(self.cw_rset.rowcount):
task = self.cw_rset.get_entity(row, 0)
if task in done_tasks:
continue
done_tasks.append(task)
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
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 _prevnext_links(self, curdate):
prevdate = curdate - timedelta(7)
nextdate = curdate + timedelta(7)
rql = self.cw_rset.printable_rql()
prevlink = self._cw.build_ajax_replace_url('oneweekcalid', rql, 'oneweekcal',
year=prevdate.year,
week=prevdate.isocalendar()[1])
nextlink = self._cw.build_ajax_replace_url('oneweekcalid', rql, 'oneweekcal',
year=nextdate.year,
week=nextdate.isocalendar()[1])
return prevlink, nextlink