1 """html calendar views |
1 """html calendar views |
2 |
2 |
3 :organization: Logilab |
3 :organization: Logilab |
4 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
6 """ |
6 """ |
7 |
7 |
8 from datetime import datetime, date, time, timedelta |
8 from datetime import date, time, timedelta |
9 #from datetime import datetime, RelativeDateTime, date, time, Sunday |
|
10 |
9 |
11 from logilab.mtconverter import html_escape |
10 from logilab.mtconverter import html_escape |
12 |
11 |
13 from cubicweb.interfaces import ICalendarViews |
12 from cubicweb.interfaces import ICalendarViews |
14 from cubicweb.utils import date_range |
13 from cubicweb.utils import ONEDAY, ONEWEEK, date_range, previous_month, next_month |
15 from cubicweb.selectors import implements |
14 from cubicweb.selectors import implements |
16 from cubicweb.view import EntityView |
15 from cubicweb.view import EntityView |
17 |
16 |
18 # Define some useful constants |
17 # Define some useful constants |
19 #ONE_MONTH = RelativeDateTime(months=1) |
18 ONE_MONTH = timedelta(days=31) # XXX |
20 ONE_MONTH = timedelta(days=31) |
19 # used by i18n tools |
21 TODAY = date.today() |
|
22 THIS_MONTH = TODAY.month |
|
23 THIS_YEAR = TODAY.year |
|
24 # mx.DateTime and ustrftime could be used to build WEEKDAYS |
|
25 WEEKDAYS = [_("monday"), _("tuesday"), _("wednesday"), _("thursday"), |
20 WEEKDAYS = [_("monday"), _("tuesday"), _("wednesday"), _("thursday"), |
26 _("friday"), _("saturday"), _("sunday")] |
21 _("friday"), _("saturday"), _("sunday")] |
27 |
|
28 # used by i18n tools |
|
29 MONTHNAMES = [ _('january'), _('february'), _('march'), _('april'), _('may'), |
22 MONTHNAMES = [ _('january'), _('february'), _('march'), _('april'), _('may'), |
30 _('june'), _('july'), _('august'), _('september'), _('october'), |
23 _('june'), _('july'), _('august'), _('september'), _('october'), |
31 _('november'), _('december') |
24 _('november'), _('december') |
32 ] |
25 ] |
33 |
26 |
42 NEXT = u'<a href="%s">></a> <a href="%s">>></a>' |
35 NEXT = u'<a href="%s">></a> <a href="%s">>></a>' |
43 NAV_HEADER = u"""<table class="calendarPageHeader"> |
36 NAV_HEADER = u"""<table class="calendarPageHeader"> |
44 <tr><td class="prev">%s</td><td class="next">%s</td></tr> |
37 <tr><td class="prev">%s</td><td class="next">%s</td></tr> |
45 </table> |
38 </table> |
46 """ % (PREV, NEXT) |
39 """ % (PREV, NEXT) |
47 |
40 |
48 def nav_header(self, date, smallshift=3, bigshift=9): |
41 def nav_header(self, date, smallshift=3, bigshift=9): |
49 """prints shortcut links to go to previous/next steps (month|week)""" |
42 """prints shortcut links to go to previous/next steps (month|week)""" |
50 prev1 = date - RelativeDateTime(months=smallshift) |
43 prev1 = next1 = prev2 = nex2 = date |
51 prev2 = date - RelativeDateTime(months=bigshift) |
44 prev1 = previous_month(date, smallshift) |
52 next1 = date + RelativeDateTime(months=smallshift) |
45 next1 = next_month(date, smallshift) |
53 next2 = date + RelativeDateTime(months=bigshift) |
46 prev2 = previous_month(date, bigshift) |
54 rql, vid = self.rset.printable_rql(), self.id |
47 next2 = next_month(date, bigshift) |
|
48 rql = self.rset.printable_rql() |
55 return self.NAV_HEADER % ( |
49 return self.NAV_HEADER % ( |
56 html_escape(self.build_url(rql=rql, vid=vid, year=prev2.year, month=prev2.month)), |
50 html_escape(self.build_url(rql=rql, vid=self.id, year=prev2.year, |
57 html_escape(self.build_url(rql=rql, vid=vid, year=prev1.year, month=prev1.month)), |
51 month=prev2.month)), |
58 html_escape(self.build_url(rql=rql, vid=vid, year=next1.year, month=next1.month)), |
52 html_escape(self.build_url(rql=rql, vid=self.id, year=prev1.year, |
59 html_escape(self.build_url(rql=rql, vid=vid, year=next2.year, month=next2.month))) |
53 month=prev1.month)), |
60 |
54 html_escape(self.build_url(rql=rql, vid=self.id, year=next1.year, |
61 |
55 month=next1.month)), |
|
56 html_escape(self.build_url(rql=rql, vid=self.id, year=next2.year, |
|
57 month=next2.month))) |
|
58 |
|
59 |
62 # Calendar building methods ############################################## |
60 # Calendar building methods ############################################## |
63 |
61 |
64 def build_calendars(self, schedule, begin, end): |
62 def build_calendars(self, schedule, begin, end): |
65 """build several HTML calendars at once, one for each month |
63 """build several HTML calendars at once, one for each month |
66 between begin and end |
64 between begin and end |
67 """ |
65 """ |
68 return [self.build_calendar(schedule, date) |
66 return [self.build_calendar(schedule, date) |
69 for date in date_range(begin, end, incr=ONE_MONTH)] |
67 for date in date_range(begin, end, incr=ONE_MONTH)] |
70 |
68 |
71 def build_calendar(self, schedule, first_day): |
69 def build_calendar(self, schedule, first_day): |
72 """method responsible for building *one* HTML calendar""" |
70 """method responsible for building *one* HTML calendar""" |
73 # FIXME iterates between [first_day-first_day.day_of_week ; |
71 # FIXME iterates between [first_day-first_day.day_of_week ; |
74 # last_day+6-last_day.day_of_week] |
72 # last_day+6-last_day.day_of_week] |
75 umonth = self.format_date(first_day, '%B %Y') # localized month name |
73 umonth = self.format_date(first_day, '%B %Y') # localized month name |
106 :param itemvid: which view to call to render elements in cells |
104 :param itemvid: which view to call to render elements in cells |
107 |
105 |
108 returns { day1 : { hour : [views] }, |
106 returns { day1 : { hour : [views] }, |
109 day2 : { hour : [views] } ... } |
107 day2 : { hour : [views] } ... } |
110 """ |
108 """ |
111 # put this here since all sub views are calling this method |
109 # put this here since all sub views are calling this method |
112 self.req.add_css('cubicweb.calendar.css') |
110 self.req.add_css('cubicweb.calendar.css') |
113 schedule = {} |
111 schedule = {} |
114 for row in xrange(len(self.rset.rows)): |
112 for row in xrange(len(self.rset.rows)): |
115 entity = self.entity(row) |
113 entity = self.entity(row) |
116 infos = u'<div class="event">' |
114 infos = u'<div class="event">' |
117 infos += self.view(itemvid, self.rset, row=row) |
115 infos += self.view(itemvid, self.rset, row=row) |
118 infos += u'</div>' |
116 infos += u'</div>' |
119 for date in entity.matching_dates(begin, end): |
117 for date_ in entity.matching_dates(begin, end): |
120 day = Date(date.year, date.month, date.day) |
118 day = date(date_.year, date_.month, date_.day) |
121 time = Time(date.hour, date.minute, date.second) |
119 try: |
|
120 dt = time(date_.hour, date_.minute, date_.second) |
|
121 except AttributeError: |
|
122 # date instance |
|
123 dt = time(0, 0, 0) |
122 schedule.setdefault(day, {}) |
124 schedule.setdefault(day, {}) |
123 schedule[day].setdefault(time, []).append(infos) |
125 schedule[day].setdefault(dt, []).append(infos) |
124 return schedule |
126 return schedule |
125 |
127 |
126 |
128 |
127 @staticmethod |
129 @staticmethod |
128 def get_date_range(day=TODAY, shift=4): |
130 def get_date_range(day, shift=4): |
129 """returns a couple (begin, end) |
131 """returns a couple (begin, end) |
130 |
132 |
131 <begin> is the first day of current_month - shift |
133 <begin> is the first day of current_month - shift |
132 <end> is the last day of current_month + (shift+1) |
134 <end> is the last day of current_month + (shift+1) |
133 """ |
135 """ |
134 first_day_in_month = DateTime(day.year, day.month, 1) |
136 begin = first_day(previous_month(day, shift)) |
135 begin = first_day_in_month - RelativeDateTime(months=shift) |
137 end = last_day(next_month(day, shift)) |
136 end = (first_day_in_month + RelativeDateTime(months=shift+1)) - 1 |
|
137 return begin, end |
138 return begin, end |
138 |
|
139 |
139 |
140 def _build_ampm_cells(self, daynum, events): |
140 def _build_ampm_cells(self, daynum, events): |
141 """create a view without any hourly details. |
141 """create a view without any hourly details. |
142 |
142 |
143 :param daynum: day of the built cell |
143 :param daynum: day of the built cell |
164 |
164 |
165 class YearCalendarView(_CalendarView): |
165 class YearCalendarView(_CalendarView): |
166 id = 'calendaryear' |
166 id = 'calendaryear' |
167 title = _('calendar (year)') |
167 title = _('calendar (year)') |
168 |
168 |
169 def call(self, year=THIS_YEAR, month=THIS_MONTH): |
169 def call(self, year=None, month=None): |
170 """this view renders a 3x3 calendars' table""" |
170 """this view renders a 3x3 calendars' table""" |
171 year = int(self.req.form.get('year', year)) |
171 year = year or int(self.req.form.get('year', date.today().year)) |
172 month = int(self.req.form.get('month', month)) |
172 month = month or int(self.req.form.get('month', date.today().month)) |
173 center_date = DateTime(year, month) |
173 center_date = date(year, month, 1) |
174 begin, end = self.get_date_range(day=center_date) |
174 begin, end = self.get_date_range(day=center_date) |
175 schedule = self._mk_schedule(begin, end) |
175 schedule = self._mk_schedule(begin, end) |
176 self.w(self.nav_header(center_date)) |
176 self.w(self.nav_header(center_date)) |
177 calendars = tuple(self.build_calendars(schedule, begin, end)) |
177 calendars = tuple(self.build_calendars(schedule, begin, end)) |
178 self.w(SMALL_CALENDARS_PAGE % calendars) |
178 self.w(SMALL_CALENDARS_PAGE % calendars) |
183 one column per month |
183 one column per month |
184 """ |
184 """ |
185 id = 'calendarsemester' |
185 id = 'calendarsemester' |
186 title = _('calendar (semester)') |
186 title = _('calendar (semester)') |
187 |
187 |
188 def call(self, year=THIS_YEAR, month=THIS_MONTH): |
188 def call(self, year=None, month=None): |
189 year = int(self.req.form.get('year', year)) |
189 year = year or int(self.req.form.get('year', date.today().year)) |
190 month = int(self.req.form.get('month', month)) |
190 month = month or int(self.req.form.get('month', date.today().month)) |
191 begin = DateTime(year, month) - RelativeDateTime(months=2) |
191 begin = previous_month(date(year, month, 1), 2) |
192 end = DateTime(year, month) + RelativeDateTime(months=3) |
192 end = next_month(date(year, month, 1), 3) |
193 schedule = self._mk_schedule(begin, end) |
193 schedule = self._mk_schedule(begin, end) |
194 self.w(self.nav_header(DateTime(year, month), 1, 6)) |
194 self.w(self.nav_header(date(year, month, 1), 1, 6)) |
195 self.w(u'<table class="semesterCalendar">') |
195 self.w(u'<table class="semesterCalendar">') |
196 self.build_calendars(schedule, begin, end) |
196 self.build_calendars(schedule, begin, end) |
197 self.w(u'</table>') |
197 self.w(u'</table>') |
198 self.w(self.nav_header(DateTime(year, month), 1, 6)) |
198 self.w(self.nav_header(date(year, month, 1), 1, 6)) |
199 |
199 |
200 def build_calendars(self, schedule, begin, end): |
200 def build_calendars(self, schedule, begin, end): |
201 self.w(u'<tr>') |
201 self.w(u'<tr>') |
202 rql = self.rset.printable_rql() |
202 rql = self.rset.printable_rql() |
203 for cur_month in date_range(begin, end, incr=ONE_MONTH): |
203 for cur_month in date_range(begin, end, incr=ONE_MONTH): |
212 self.w(u'<tr>') |
212 self.w(u'<tr>') |
213 for cur_month in date_range(begin, end, incr=ONE_MONTH): |
213 for cur_month in date_range(begin, end, incr=ONE_MONTH): |
214 if day_num >= cur_month.days_in_month: |
214 if day_num >= cur_month.days_in_month: |
215 self.w(u'%s%s' % (NO_CELL, NO_CELL)) |
215 self.w(u'%s%s' % (NO_CELL, NO_CELL)) |
216 else: |
216 else: |
217 day = DateTime(cur_month.year, cur_month.month, day_num+1) |
217 day = date(cur_month.year, cur_month.month, day_num+1) |
218 events = schedule.get(day) |
218 events = schedule.get(day) |
219 self.w(u'<td>%s %s</td>\n' % (_(WEEKDAYS[day.day_of_week])[0].upper(), day_num+1)) |
219 self.w(u'<td>%s %s</td>\n' % (_(WEEKDAYS[day.day_of_week])[0].upper(), day_num+1)) |
220 self.format_day_events(day, events) |
220 self.format_day_events(day, events) |
221 self.w(u'</tr>') |
221 self.w(u'</tr>') |
222 |
222 |
223 def format_day_events(self, day, events): |
223 def format_day_events(self, day, events): |
224 if events: |
224 if events: |
225 events = ['\n'.join(event) for event in events.values()] |
225 events = ['\n'.join(event) for event in events.values()] |
226 self.w(WEEK_CELL % '\n'.join(events)) |
226 self.w(WEEK_CELL % '\n'.join(events)) |
227 else: |
227 else: |
228 self.w(WEEK_EMPTY_CELL) |
228 self.w(WEEK_EMPTY_CELL) |
229 |
229 |
230 |
230 |
231 class MonthCalendarView(_CalendarView): |
231 class MonthCalendarView(_CalendarView): |
232 """this view renders a 3x1 calendars' table""" |
232 """this view renders a 3x1 calendars' table""" |
233 id = 'calendarmonth' |
233 id = 'calendarmonth' |
234 title = _('calendar (month)') |
234 title = _('calendar (month)') |
235 |
235 |
236 def call(self, year=THIS_YEAR, month=THIS_MONTH): |
236 def call(self, year=None, month=None): |
237 year = int(self.req.form.get('year', year)) |
237 year = year or int(self.req.form.get('year', date.today().year)) |
238 month = int(self.req.form.get('month', month)) |
238 month = month or int(self.req.form.get('month', date.today().month)) |
239 center_date = DateTime(year, month) |
239 center_date = date(year, month, 1) |
240 begin, end = self.get_date_range(day=center_date, shift=1) |
240 begin, end = self.get_date_range(day=center_date, shift=1) |
241 schedule = self._mk_schedule(begin, end) |
241 schedule = self._mk_schedule(begin, end) |
242 calendars = self.build_calendars(schedule, begin, end) |
242 calendars = self.build_calendars(schedule, begin, end) |
243 self.w(self.nav_header(center_date, 1, 3)) |
243 self.w(self.nav_header(center_date, 1, 3)) |
244 self.w(BIG_CALENDARS_PAGE % tuple(calendars)) |
244 self.w(BIG_CALENDARS_PAGE % tuple(calendars)) |
245 self.w(self.nav_header(center_date, 1, 3)) |
245 self.w(self.nav_header(center_date, 1, 3)) |
246 |
246 |
247 |
247 |
248 class WeekCalendarView(_CalendarView): |
248 class WeekCalendarView(_CalendarView): |
249 """this view renders a calendar for week events""" |
249 """this view renders a calendar for week events""" |
250 id = 'calendarweek' |
250 id = 'calendarweek' |
251 title = _('calendar (week)') |
251 title = _('calendar (week)') |
252 |
252 |
253 def call(self, year=THIS_YEAR, week=TODAY.isocalendar()[1]): |
253 def call(self, year=None, week=None): |
254 year = int(self.req.form.get('year', year)) |
254 year = year or int(self.req.form.get('year', date.today().year)) |
255 week = int(self.req.form.get('week', week)) |
255 week = week or int(self.req.form.get('week', week, |
256 day0 = DateTime(year) |
256 date.today().isocalendar()[1])) |
257 first_day_of_week = (day0-day0.day_of_week) + 7*week |
257 day0 = date(year, 1, 1) |
258 begin, end = first_day_of_week-7, first_day_of_week+14 |
258 first_day_of_week = day0 - day0.day_of_week*ONEDAY + ONEWEEK |
|
259 begin, end = first_day_of_week- ONEWEEK, first_day_of_week + 2*ONEWEEK |
259 schedule = self._mk_schedule(begin, end, itemvid='calendarlargeitem') |
260 schedule = self._mk_schedule(begin, end, itemvid='calendarlargeitem') |
260 self.w(self.nav_header(first_day_of_week)) |
261 self.w(self.nav_header(first_day_of_week)) |
261 self.w(u'<table class="weekCalendar">') |
262 self.w(u'<table class="weekCalendar">') |
262 _weeks = [(first_day_of_week-7, first_day_of_week-1), |
263 _weeks = [(first_day_of_week-ONEWEEK, first_day_of_week-ONEDAY), |
263 (first_day_of_week, first_day_of_week+6), |
264 (first_day_of_week, first_day_of_week+6*ONEDAY), |
264 (first_day_of_week+7, first_day_of_week+13)] |
265 (first_day_of_week+ONEWEEK, first_day_of_week+13*ONEDAY)] |
265 self.build_calendar(schedule, _weeks) |
266 self.build_calendar(schedule, _weeks) |
266 self.w(u'</table>') |
267 self.w(u'</table>') |
267 self.w(self.nav_header(first_day_of_week)) |
268 self.w(self.nav_header(first_day_of_week)) |
268 |
269 |
269 def build_calendar(self, schedule, weeks): |
270 def build_calendar(self, schedule, weeks): |
270 rql = self.rset.printable_rql() |
271 rql = self.rset.printable_rql() |
271 _ = self.req._ |
272 _ = self.req._ |
272 for monday, sunday in weeks: |
273 for monday, sunday in weeks: |
273 umonth = self.format_date(monday, '%B %Y') |
274 umonth = self.format_date(monday, '%B %Y') |
274 url = self.build_url(rql=rql, vid='calendarmonth', |
275 url = self.build_url(rql=rql, vid='calendarmonth', |
275 year=monday.year, month=monday.month) |
276 year=monday.year, month=monday.month) |
276 monthlink = '<a href="%s">%s</a>' % (html_escape(url), umonth) |
277 monthlink = '<a href="%s">%s</a>' % (html_escape(url), umonth) |
277 self.w(u'<tr><th colspan="3">%s %s (%s)</th></tr>' \ |
278 self.w(u'<tr><th colspan="3">%s %s (%s)</th></tr>' \ |
285 events = ['\n'.join(event) for event in events.values()] |
286 events = ['\n'.join(event) for event in events.values()] |
286 self.w(WEEK_CELL % '\n'.join(events)) |
287 self.w(WEEK_CELL % '\n'.join(events)) |
287 else: |
288 else: |
288 self.w(WEEK_EMPTY_CELL) |
289 self.w(WEEK_EMPTY_CELL) |
289 self.w(u'</tr>') |
290 self.w(u'</tr>') |
290 |
291 |
291 def nav_header(self, date, smallshift=1, bigshift=3): |
292 def nav_header(self, date, smallshift=1, bigshift=3): |
292 """prints shortcut links to go to previous/next steps (month|week)""" |
293 """prints shortcut links to go to previous/next steps (month|week)""" |
293 prev1 = date - RelativeDateTime(weeks=smallshift) |
294 prev1 = date - ONEWEEK * smallshift |
294 prev2 = date - RelativeDateTime(weeks=bigshift) |
295 prev2 = date - ONEWEEK * bigshift |
295 next1 = date + RelativeDateTime(weeks=smallshift) |
296 next1 = date + ONEWEEK * smallshift |
296 next2 = date + RelativeDateTime(weeks=bigshift) |
297 next2 = date + ONEWEEK * bigshift |
297 rql, vid = self.rset.printable_rql(), self.id |
298 rql = self.rset.printable_rql() |
298 return self.NAV_HEADER % ( |
299 return self.NAV_HEADER % ( |
299 html_escape(self.build_url(rql=rql, vid=vid, year=prev2.year, week=prev2.isocalendar()[1])), |
300 html_escape(self.build_url(rql=rql, vid=self.id, year=prev2.year, week=prev2.isocalendar()[1])), |
300 html_escape(self.build_url(rql=rql, vid=vid, year=prev1.year, week=prev1.isocalendar()[1])), |
301 html_escape(self.build_url(rql=rql, vid=self.id, year=prev1.year, week=prev1.isocalendar()[1])), |
301 html_escape(self.build_url(rql=rql, vid=vid, year=next1.year, week=next1.isocalendar()[1])), |
302 html_escape(self.build_url(rql=rql, vid=self.id, year=next1.year, week=next1.isocalendar()[1])), |
302 html_escape(self.build_url(rql=rql, vid=vid, year=next2.year, week=next2.isocalendar()[1]))) |
303 html_escape(self.build_url(rql=rql, vid=self.id, year=next2.year, week=next2.isocalendar()[1]))) |
303 |
304 |
304 |
305 |
305 |
306 |
306 class AMPMYearCalendarView(YearCalendarView): |
307 class AMPMYearCalendarView(YearCalendarView): |
307 id = 'ampmcalendaryear' |
308 id = 'ampmcalendaryear' |
308 title = _('am/pm calendar (year)') |
309 title = _('am/pm calendar (year)') |
309 |
310 |
310 def build_calendar(self, schedule, first_day): |
311 def build_calendar(self, schedule, first_day): |
311 """method responsible for building *one* HTML calendar""" |
312 """method responsible for building *one* HTML calendar""" |
312 umonth = self.format_date(first_day, '%B %Y') # localized month name |
313 umonth = self.format_date(first_day, '%B %Y') # localized month name |
313 rows = [] # each row is: (am,pm), (am,pm) ... week_title |
314 rows = [] # each row is: (am,pm), (am,pm) ... week_title |
314 current_row = [(NO_CELL, NO_CELL, NO_CELL)] * first_day.day_of_week |
315 current_row = [(NO_CELL, NO_CELL, NO_CELL)] * first_day.day_of_week |
376 self.w(u'<tr>') |
377 self.w(u'<tr>') |
377 for cur_month in date_range(begin, end, incr=ONE_MONTH): |
378 for cur_month in date_range(begin, end, incr=ONE_MONTH): |
378 if day_num >= cur_month.days_in_month: |
379 if day_num >= cur_month.days_in_month: |
379 self.w(u'%s%s%s' % (NO_CELL, NO_CELL, NO_CELL)) |
380 self.w(u'%s%s%s' % (NO_CELL, NO_CELL, NO_CELL)) |
380 else: |
381 else: |
381 day = DateTime(cur_month.year, cur_month.month, day_num+1) |
382 day = date(cur_month.year, cur_month.month, day_num+1) |
382 events = schedule.get(day) |
383 events = schedule.get(day) |
383 self.w(u'<td>%s %s</td>\n' % (_(WEEKDAYS[day.day_of_week])[0].upper(), |
384 self.w(u'<td>%s %s</td>\n' % (_(WEEKDAYS[day.day_of_week])[0].upper(), |
384 day_num+1)) |
385 day_num+1)) |
385 self.format_day_events(day, events) |
386 self.format_day_events(day, events) |
386 self.w(u'</tr>') |
387 self.w(u'</tr>') |
387 |
388 |
388 def format_day_events(self, day, events): |
389 def format_day_events(self, day, events): |
389 if events: |
390 if events: |
390 self.w(u'\n'.join(self._build_ampm_cells(day, events))) |
391 self.w(u'\n'.join(self._build_ampm_cells(day, events))) |
391 else: |
392 else: |
392 self.w(u'%s %s'% (AMPM_EMPTY % ("amCell", "am"), |
393 self.w(u'%s %s'% (AMPM_EMPTY % ("amCell", "am"), |
393 AMPM_EMPTY % ("pmCell", "pm"))) |
394 AMPM_EMPTY % ("pmCell", "pm"))) |
394 |
395 |
395 |
396 |
396 class AMPMMonthCalendarView(MonthCalendarView): |
397 class AMPMMonthCalendarView(MonthCalendarView): |
397 """this view renders a 3x1 calendars' table""" |
398 """this view renders a 3x1 calendars' table""" |