[entity] introduce a new 'adapters' registry
This changeset introduces the notion in adapters (as in Zope Component Architecture)
in a cubicweb way, eg using a specific registry of appobjects.
This allows nicer code structure, by avoid clutering entity classes and moving
code usually specific to a place of the ui (or something else) together with the
code that use the interface.
We don't use actual interface anymore, they are implied by adapters (which
may be abstract), whose reg id is an interface name.
Appobjects that used to 'implements(IFace)' should now be rewritten by:
* coding an IFaceAdapter(EntityAdapter) defining (implementing if desired)
the interface, usually with __regid__ = 'IFace'
* use "adaptable('IFace')" as selector instead
Also, the implements_adapter_compat decorator eases backward compatibility
with adapter's methods that may still be found on entities implementing
the interface.
Notice that unlike ZCA, we don't support automatic adapters chain (yagni?).
All interfaces defined in cubicweb have been turned into adapters, also
some new ones have been introduced to cleanup Entity / AnyEntity classes
namespace. At the end, the pluggable mixins mecanism should disappear in
favor of adapters as well.
# 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"""
from datetime import date, time, timedelta
from logilab.mtconverter import xml_escape
from logilab.common.date import (ONEDAY, ONEWEEK, days_in_month, previous_month,
next_month, first_day, last_day, date_range)
from cubicweb.interfaces import ICalendarViews
from cubicweb.selectors import implements, adaptable
from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
class ICalendarViewsAdapter(EntityAdapter):
"""calendar views interface"""
__regid__ = 'ICalendarViews'
__select__ = implements(ICalendarViews) # XXX for bw compat, should be abstract
@implements_adapter_compat('ICalendarViews')
def matching_dates(self, begin, end):
"""
:param begin: day considered as begin of the range (`DateTime`)
:param end: day considered as end of the range (`DateTime`)
:return:
a list of dates (`DateTime`) in the range [`begin`, `end`] on which
this entity apply
"""
raise NotImplementedError
# used by i18n tools
WEEKDAYS = [_("monday"), _("tuesday"), _("wednesday"), _("thursday"),
_("friday"), _("saturday"), _("sunday")]
MONTHNAMES = [ _('january'), _('february'), _('march'), _('april'), _('may'),
_('june'), _('july'), _('august'), _('september'), _('october'),
_('november'), _('december')
]
class _CalendarView(EntityView):
"""base calendar view containing helpful methods to build calendar views"""
__select__ = adaptable('ICalendarViews')
paginable = False
# Navigation building methods / views ####################################
PREV = u'<a href="%s"><<</a>  <a href="%s"><</a>'
NEXT = u'<a href="%s">></a>  <a href="%s">>></a>'
NAV_HEADER = u"""<table class="calendarPageHeader">
<tr><td class="prev">%s</td><td class="next">%s</td></tr>
</table>
""" % (PREV, NEXT)
def nav_header(self, date, smallshift=3, bigshift=9):
"""prints shortcut links to go to previous/next steps (month|week)"""
prev1 = previous_month(date, smallshift)
next1 = next_month(date, smallshift)
prev2 = previous_month(date, bigshift)
next2 = next_month(date, bigshift)
rql = self.cw_rset.printable_rql()
return self.NAV_HEADER % (
xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=prev2.year,
month=prev2.month)),
xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=prev1.year,
month=prev1.month)),
xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=next1.year,
month=next1.month)),
xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=next2.year,
month=next2.month)))
# Calendar building methods ##############################################
def build_calendars(self, schedule, begin, end):
"""build several HTML calendars at once, one for each month
between begin and end
"""
return [self.build_calendar(schedule, date)
for date in date_range(begin, end, incmonth=1)]
def build_calendar(self, schedule, first_day):
"""method responsible for building *one* HTML calendar"""
# FIXME iterates between [first_day-first_day.day_of_week ;
# last_day+6-last_day.day_of_week]
umonth = self._cw.format_date(first_day, '%B %Y') # localized month name
rows = []
current_row = [NO_CELL] * first_day.weekday()
for daynum in xrange(0, days_in_month(first_day)):
# build cell day
day = first_day + timedelta(daynum)
events = schedule.get(day)
if events:
events = [u'\n'.join(event) for event in events.values()]
current_row.append(CELL % (daynum+1, '\n'.join(events)))
else:
current_row.append(EMPTY_CELL % (daynum+1))
# store & reset current row on Sundays
if day.weekday() == 6:
rows.append(u'<tr>%s%s</tr>' % (WEEKNUM_CELL % day.isocalendar()[1], ''.join(current_row)))
current_row = []
current_row.extend([NO_CELL] * (6-day.weekday()))
rql = self.cw_rset.printable_rql()
if day.weekday() != 6:
rows.append(u'<tr>%s%s</tr>' % (WEEKNUM_CELL % day.isocalendar()[1], ''.join(current_row)))
url = self._cw.build_url(rql=rql, vid='calendarmonth',
year=first_day.year, month=first_day.month)
monthlink = u'<a href="%s">%s</a>' % (xml_escape(url), umonth)
return CALENDAR(self._cw) % (monthlink, '\n'.join(rows))
def _mk_schedule(self, begin, end, itemvid='calendaritem'):
"""private method that gathers information from resultset
and builds calendars according to it
:param begin: begin of date range
:param end: end of date rangs
:param itemvid: which view to call to render elements in cells
returns { day1 : { hour : [views] },
day2 : { hour : [views] } ... }
"""
# put this here since all sub views are calling this method
self._cw.add_css('cubicweb.calendar.css')
schedule = {}
for row in xrange(len(self.cw_rset.rows)):
entity = self.cw_rset.get_entity(row, 0)
infos = u'<div class="event">'
infos += self._cw.view(itemvid, self.cw_rset, row=row)
infos += u'</div>'
for date_ in entity.cw_adapt_to('ICalendarViews').matching_dates(begin, end):
day = date(date_.year, date_.month, date_.day)
try:
dt = time(date_.hour, date_.minute, date_.second)
except AttributeError:
# date instance
dt = time(0, 0, 0)
schedule.setdefault(day, {})
schedule[day].setdefault(dt, []).append(infos)
return schedule
@staticmethod
def get_date_range(day, shift=4):
"""returns a couple (begin, end)
<begin> is the first day of current_month - shift
<end> is the last day of current_month + (shift+1)
"""
begin = first_day(previous_month(day, shift))
end = last_day(next_month(day, shift))
return begin, end
def _build_ampm_cells(self, events):
"""create a view without any hourly details.
:param events: dictionnary with all events classified by hours
"""
# split events according am/pm
am_events = [event for e_time, e_list in events.iteritems()
if 0 <= e_time.hour < 12
for event in e_list]
pm_events = [event for e_time, e_list in events.iteritems()
if 12 <= e_time.hour < 24
for event in e_list]
# format each am/pm cell
if am_events:
am_content = AMPM_CONTENT % ("amCell", "am", '\n'.join(am_events))
else:
am_content = AMPM_EMPTY % ("amCell", "am")
if pm_events:
pm_content = AMPM_CONTENT % ("pmCell", "pm", '\n'.join(pm_events))
else:
pm_content = AMPM_EMPTY % ("pmCell", "pm")
return am_content, pm_content
class YearCalendarView(_CalendarView):
__regid__ = 'calendaryear'
title = _('calendar (year)')
def call(self, year=None, month=None):
"""this view renders a 3x3 calendars' table"""
year = year or int(self._cw.form.get('year', date.today().year))
month = month or int(self._cw.form.get('month', date.today().month))
center_date = date(year, month, 1)
begin, end = self.get_date_range(day=center_date)
schedule = self._mk_schedule(begin, end)
self.w(self.nav_header(center_date))
calendars = tuple(self.build_calendars(schedule, begin, end))
self.w(SMALL_CALENDARS_PAGE % calendars)
class SemesterCalendarView(_CalendarView):
"""this view renders three semesters as three rows of six columns,
one column per month
"""
__regid__ = 'calendarsemester'
title = _('calendar (semester)')
def call(self, year=None, month=None):
year = year or int(self._cw.form.get('year', date.today().year))
month = month or int(self._cw.form.get('month', date.today().month))
begin = previous_month(date(year, month, 1), 2)
end = next_month(date(year, month, 1), 3)
schedule = self._mk_schedule(begin, end)
self.w(self.nav_header(date(year, month, 1), 1, 6))
self.w(u'<table class="semesterCalendar">')
self.build_calendars(schedule, begin, end)
self.w(u'</table>')
self.w(self.nav_header(date(year, month, 1), 1, 6))
def build_calendars(self, schedule, begin, end):
self.w(u'<tr>')
rql = self.cw_rset.printable_rql()
for cur_month in date_range(begin, end, incmonth=1):
umonth = u'%s %s' % (self._cw.format_date(cur_month, '%B'), cur_month.year)
url = self._cw.build_url(rql=rql, vid=self.__regid__,
year=cur_month.year, month=cur_month.month)
self.w(u'<th colspan="2"><a href="%s">%s</a></th>' % (xml_escape(url),
umonth))
self.w(u'</tr>')
_ = self._cw._
for day_num in xrange(31):
self.w(u'<tr>')
for cur_month in date_range(begin, end, incmonth=1):
if day_num >= days_in_month(cur_month):
self.w(u'%s%s' % (NO_CELL, NO_CELL))
else:
day = date(cur_month.year, cur_month.month, day_num+1)
events = schedule.get(day)
self.w(u'<td>%s %s</td>\n' % (_(WEEKDAYS[day.weekday()])[0].upper(), day_num+1))
self.format_day_events(day, events)
self.w(u'</tr>')
def format_day_events(self, day, events):
if events:
events = ['\n'.join(event) for event in events.values()]
self.w(WEEK_CELL % '\n'.join(events))
else:
self.w(WEEK_EMPTY_CELL)
class MonthCalendarView(_CalendarView):
"""this view renders a 3x1 calendars' table"""
__regid__ = 'calendarmonth'
title = _('calendar (month)')
def call(self, year=None, month=None):
year = year or int(self._cw.form.get('year', date.today().year))
month = month or int(self._cw.form.get('month', date.today().month))
center_date = date(year, month, 1)
begin, end = self.get_date_range(day=center_date, shift=1)
schedule = self._mk_schedule(begin, end)
calendars = self.build_calendars(schedule, begin, end)
self.w(self.nav_header(center_date, 1, 3))
self.w(BIG_CALENDARS_PAGE % tuple(calendars))
self.w(self.nav_header(center_date, 1, 3))
class WeekCalendarView(_CalendarView):
"""this view renders a calendar for week events"""
__regid__ = 'calendarweek'
title = _('calendar (week)')
def call(self, year=None, week=None):
year = year or int(self._cw.form.get('year', date.today().year))
week = week or int(self._cw.form.get('week', date.today().isocalendar()[1]))
day0 = date(year, 1, 1)
first_day_of_week = day0 - day0.weekday()*ONEDAY + ONEWEEK
begin, end = first_day_of_week- ONEWEEK, first_day_of_week + 2*ONEWEEK
schedule = self._mk_schedule(begin, end, itemvid='calendarlargeitem')
self.w(self.nav_header(first_day_of_week))
self.w(u'<table class="weekCalendar">')
_weeks = [(first_day_of_week-ONEWEEK, first_day_of_week-ONEDAY),
(first_day_of_week, first_day_of_week+6*ONEDAY),
(first_day_of_week+ONEWEEK, first_day_of_week+13*ONEDAY)]
self.build_calendar(schedule, _weeks)
self.w(u'</table>')
self.w(self.nav_header(first_day_of_week))
def build_calendar(self, schedule, weeks):
rql = self.cw_rset.printable_rql()
_ = self._cw._
for monday, sunday in weeks:
umonth = self._cw.format_date(monday, '%B %Y')
url = self._cw.build_url(rql=rql, vid='calendarmonth',
year=monday.year, month=monday.month)
monthlink = '<a href="%s">%s</a>' % (xml_escape(url), umonth)
self.w(u'<tr><th colspan="3">%s %s (%s)</th></tr>' \
% (_('week'), monday.isocalendar()[1], monthlink))
for day in date_range(monday, sunday+ONEDAY):
self.w(u'<tr>')
self.w(u'<td>%s</td>' % _(WEEKDAYS[day.weekday()]))
self.w(u'<td>%s</td>' % (day.strftime('%Y-%m-%d')))
events = schedule.get(day)
if events:
events = ['\n'.join(event) for event in events.values()]
self.w(WEEK_CELL % '\n'.join(events))
else:
self.w(WEEK_EMPTY_CELL)
self.w(u'</tr>')
def nav_header(self, date, smallshift=1, bigshift=3):
"""prints shortcut links to go to previous/next steps (month|week)"""
prev1 = date - ONEWEEK * smallshift
prev2 = date - ONEWEEK * bigshift
next1 = date + ONEWEEK * smallshift
next2 = date + ONEWEEK * bigshift
rql = self.cw_rset.printable_rql()
return self.NAV_HEADER % (
xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=prev2.year, week=prev2.isocalendar()[1])),
xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=prev1.year, week=prev1.isocalendar()[1])),
xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=next1.year, week=next1.isocalendar()[1])),
xml_escape(self._cw.build_url(rql=rql, vid=self.__regid__, year=next2.year, week=next2.isocalendar()[1])))
class AMPMYearCalendarView(YearCalendarView):
__regid__ = 'ampmcalendaryear'
title = _('am/pm calendar (year)')
def build_calendar(self, schedule, first_day):
"""method responsible for building *one* HTML calendar"""
umonth = self._cw.format_date(first_day, '%B %Y') # localized month name
rows = [] # each row is: (am,pm), (am,pm) ... week_title
current_row = [(NO_CELL, NO_CELL, NO_CELL)] * first_day.weekday()
rql = self.cw_rset.printable_rql()
for daynum in xrange(0, days_in_month(first_day)):
# build cells day
day = first_day + timedelta(daynum)
events = schedule.get(day)
if events:
current_row.append((AMPM_DAY % (daynum+1),) + self._build_ampm_cells(events))
else:
current_row.append((AMPM_DAY % (daynum+1),
AMPM_EMPTY % ("amCell", "am"),
AMPM_EMPTY % ("pmCell", "pm")))
# store & reset current row on Sundays
if day.weekday() == 6:
url = self._cw.build_url(rql=rql, vid='ampmcalendarweek',
year=day.year, week=day.isocalendar()[1])
weeklink = '<a href="%s">%s</a>' % (xml_escape(url),
day.isocalendar()[1])
current_row.append(WEEKNUM_CELL % weeklink)
rows.append(current_row)
current_row = []
current_row.extend([(NO_CELL, NO_CELL, NO_CELL)] * (6-day.weekday()))
url = self._cw.build_url(rql=rql, vid='ampmcalendarweek',
year=day.year, week=day.isocalendar()[1])
weeklink = '<a href="%s">%s</a>' % (xml_escape(url), day.isocalendar()[1])
current_row.append(WEEKNUM_CELL % weeklink)
rows.append(current_row)
# build two rows for each week: am & pm
formatted_rows = []
for row in rows:
week_title = row.pop()
day_row = [day for day, am, pm in row]
am_row = [am for day, am, pm in row]
pm_row = [pm for day, am, pm in row]
formatted_rows.append('<tr>%s%s</tr>'% (week_title, '\n'.join(day_row)))
formatted_rows.append('<tr class="amRow"><td> </td>%s</tr>'% '\n'.join(am_row))
formatted_rows.append('<tr class="pmRow"><td> </td>%s</tr>'% '\n'.join(pm_row))
# tigh everything together
url = self._cw.build_url(rql=rql, vid='ampmcalendarmonth',
year=first_day.year, month=first_day.month)
monthlink = '<a href="%s">%s</a>' % (xml_escape(url), umonth)
return CALENDAR(self._cw) % (monthlink, '\n'.join(formatted_rows))
class AMPMSemesterCalendarView(SemesterCalendarView):
"""this view renders a 3x1 calendars' table"""
__regid__ = 'ampmcalendarsemester'
title = _('am/pm calendar (semester)')
def build_calendars(self, schedule, begin, end):
self.w(u'<tr>')
rql = self.cw_rset.printable_rql()
for cur_month in date_range(begin, end, incmonth=1):
umonth = u'%s %s' % (self._cw.format_date(cur_month, '%B'), cur_month.year)
url = self._cw.build_url(rql=rql, vid=self.__regid__,
year=cur_month.year, month=cur_month.month)
self.w(u'<th colspan="3"><a href="%s">%s</a></th>' % (xml_escape(url),
umonth))
self.w(u'</tr>')
_ = self._cw._
for day_num in xrange(31):
self.w(u'<tr>')
for cur_month in date_range(begin, end, incmonth=1):
if day_num >= days_in_month(cur_month):
self.w(u'%s%s%s' % (NO_CELL, NO_CELL, NO_CELL))
else:
day = date(cur_month.year, cur_month.month, day_num+1)
events = schedule.get(day)
self.w(u'<td>%s %s</td>\n' % (_(WEEKDAYS[day.weekday()])[0].upper(),
day_num+1))
self.format_day_events(day, events)
self.w(u'</tr>')
def format_day_events(self, day, events):
if events:
self.w(u'\n'.join(self._build_ampm_cells(events)))
else:
self.w(u'%s %s'% (AMPM_EMPTY % ("amCell", "am"),
AMPM_EMPTY % ("pmCell", "pm")))
class AMPMMonthCalendarView(MonthCalendarView):
"""this view renders a 3x1 calendars' table"""
__regid__ = 'ampmcalendarmonth'
title = _('am/pm calendar (month)')
def build_calendar(self, schedule, first_day):
"""method responsible for building *one* HTML calendar"""
umonth = self._cw.format_date(first_day, '%B %Y') # localized month name
rows = [] # each row is: (am,pm), (am,pm) ... week_title
current_row = [(NO_CELL, NO_CELL, NO_CELL)] * first_day.weekday()
rql = self.cw_rset.printable_rql()
for daynum in xrange(0, days_in_month(first_day)):
# build cells day
day = first_day + timedelta(daynum)
events = schedule.get(day)
if events:
current_row.append((AMPM_DAY % (daynum+1),) + self._build_ampm_cells(events))
else:
current_row.append((AMPM_DAY % (daynum+1),
AMPM_EMPTY % ("amCell", "am"),
AMPM_EMPTY % ("pmCell", "pm")))
# store & reset current row on Sundays
if day.weekday() == 6:
url = self._cw.build_url(rql=rql, vid='ampmcalendarweek',
year=day.year, week=day.isocalendar()[1])
weeklink = '<a href="%s">%s</a>' % (xml_escape(url),
day.isocalendar()[1])
current_row.append(WEEKNUM_CELL % weeklink)
rows.append(current_row)
current_row = []
current_row.extend([(NO_CELL, NO_CELL, NO_CELL)] * (6-day.weekday()))
url = self._cw.build_url(rql=rql, vid='ampmcalendarweek',
year=day.year, week=day.isocalendar()[1])
weeklink = '<a href="%s">%s</a>' % (xml_escape(url),
day.isocalendar()[1])
current_row.append(WEEKNUM_CELL % weeklink)
rows.append(current_row)
# build two rows for each week: am & pm
formatted_rows = []
for row in rows:
week_title = row.pop()
day_row = [day for day, am, pm in row]
am_row = [am for day, am, pm in row]
pm_row = [pm for day, am, pm in row]
formatted_rows.append('<tr>%s%s</tr>'% (week_title, '\n'.join(day_row)))
formatted_rows.append('<tr class="amRow"><td> </td>%s</tr>'% '\n'.join(am_row))
formatted_rows.append('<tr class="pmRow"><td> </td>%s</tr>'% '\n'.join(pm_row))
# tigh everything together
url = self._cw.build_url(rql=rql, vid='ampmcalendarmonth',
year=first_day.year, month=first_day.month)
monthlink = '<a href="%s">%s</a>' % (xml_escape(url),
umonth)
return CALENDAR(self._cw) % (monthlink, '\n'.join(formatted_rows))
class AMPMWeekCalendarView(WeekCalendarView):
"""this view renders a 3x1 calendars' table"""
__regid__ = 'ampmcalendarweek'
title = _('am/pm calendar (week)')
def build_calendar(self, schedule, weeks):
rql = self.cw_rset.printable_rql()
w = self.w
_ = self._cw._
for monday, sunday in weeks:
umonth = self._cw.format_date(monday, '%B %Y')
url = self._cw.build_url(rql=rql, vid='ampmcalendarmonth',
year=monday.year, month=monday.month)
monthlink = '<a href="%s">%s</a>' % (xml_escape(url), umonth)
w(u'<tr>%s</tr>' % (
WEEK_TITLE % (_('week'), monday.isocalendar()[1], monthlink)))
w(u'<tr><th>%s</th><th> </th></tr>'% _(u'Date'))
for day in date_range(monday, sunday+ONEDAY):
events = schedule.get(day)
style = day.weekday() % 2 and "even" or "odd"
w(u'<tr class="%s">' % style)
if events:
hours = events.keys()
hours.sort()
w(AMPM_DAYWEEK % (
len(hours), _(WEEKDAYS[day.weekday()]),
self._cw.format_date(day)))
w(AMPM_WEEK_CELL % (
hours[0].hour, hours[0].minute,
'\n'.join(events[hours[0]])))
w(u'</tr>')
for hour in hours[1:]:
w(u'<tr class="%s">%s</tr>'% (
style, AMPM_WEEK_CELL % (hour.hour, hour.minute,
'\n'.join(events[hour]))))
else:
w(AMPM_DAYWEEK_EMPTY % (
_(WEEKDAYS[day.weekday()]),
self._cw.format_date(day)))
w(WEEK_EMPTY_CELL)
w(u'</tr>')
SMALL_CALENDARS_PAGE = u"""<table class="smallCalendars">
<tr><td class="calendar">%s</td><td class="calendar">%s</td><td class="calendar">%s</td></tr>
<tr><td class="calendar">%s</td><td class="calendar">%s</td><td class="calendar">%s</td></tr>
<tr><td class="calendar">%s</td><td class="calendar">%s</td><td class="calendar">%s</td></tr>
</table>
"""
BIG_CALENDARS_PAGE = u"""<table class="bigCalendars">
<tr><td class="calendar">%s</td></tr>
<tr><td class="calendar">%s</td></tr>
<tr><td class="calendar">%s</td></tr>
</table>
"""
WEEKNUM_CELL = u'<td class="weeknum">%s</td>'
def CALENDAR(req):
_ = req._
WEEKNUM_HEADER = u'<th class="weeknum">%s</th>' % _('week')
CAL_HEADER = WEEKNUM_HEADER + u' \n'.join([u'<th class="weekday">%s</th>' % _(day)[0].upper()
for day in WEEKDAYS])
return u"""<table>
<tr><th class="month" colspan="8">%%s</th></tr>
<tr>
%s
</tr>
%%s
</table>
""" % (CAL_HEADER,)
DAY_TEMPLATE = """<tr><td class="weekday">%(daylabel)s</td><td>%(dmydate)s</td><td>%(dayschedule)s</td>
"""
NO_CELL = u'<td class="noday"></td>'
EMPTY_CELL = u'<td class="cellEmpty"><span class="cellTitle">%s</span></td>'
CELL = u'<td class="cell"><span class="cellTitle">%s</span><div class="cellContent">%s</div></td>'
AMPM_DAY = u'<td class="cellDay">%d</td>'
AMPM_EMPTY = u'<td class="%sEmpty"><span class="cellTitle">%s</span></td>'
AMPM_CONTENT = u'<td class="%s"><span class="cellTitle">%s</span><div class="cellContent">%s</div></td>'
WEEK_TITLE = u'<th class="weekTitle" colspan="2">%s %s (%s)</th>'
WEEK_EMPTY_CELL = u'<td class="weekEmptyCell"> </td>'
WEEK_CELL = u'<td class="weekCell"><div class="cellContent">%s</div></td>'
AMPM_DAYWEEK_EMPTY = u'<td>%s %s</td>'
AMPM_DAYWEEK = u'<td rowspan="%d">%s %s</td>'
AMPM_WEEK_CELL = u'<td class="ampmWeekCell"><div class="cellContent">%02d:%02d - %s</div></td>'