# 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"_=unicodefromdatetimeimportdatetime,date,timedeltafromlogilab.mtconverterimportxml_escapefromlogilab.common.dateimportONEDAY,strptime,date_range,todate,todatetimefromcubicweb.interfacesimportICalendarablefromcubicweb.selectorsimportimplements,adaptablefromcubicweb.viewimportEntityView,EntityAdapter,implements_adapter_compatclassICalendarableAdapter(EntityAdapter):__regid__='ICalendarable'__select__=implements(ICalendarable,warn=False)# XXX for bw compat, should be abstract@property@implements_adapter_compat('ICalendarable')defstart(self):"""return start date"""raiseNotImplementedError@property@implements_adapter_compat('ICalendarable')defstop(self):"""return stop state"""raiseNotImplementedError# 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:fromvobjectimportiCalendarclassiCalView(EntityView):"""A calendar view that generates a iCalendar file (RFC 2445) Does apply to ICalendarable compatible entities """__select__=adaptable('ICalendarable')paginable=Falsecontent_type='text/calendar'title=_('iCalendar')templatable=False__regid__='ical'defcall(self):ical=iCalendar()foriinrange(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')ificalendarable.start:event.add('dtstart').value=icalendarable.startificalendarable.stop:event.add('dtend').value=icalendarable.stopbuff=ical.serialize()ifnotisinstance(buff,unicode):buff=unicode(buff,self._cw.encoding)self.w(buff)exceptImportError:passclasshCalView(EntityView):"""A calendar view that generates a hCalendar file Does apply to ICalendarable compatible entities """__regid__='hcal'__select__=adaptable('ICalendarable')paginable=Falsetitle=_('hCalendar')#templatable = Falsedefcall(self):self.w(u'<div class="hcalendar">')foriinrange(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')ificalendarable.start:self.w(u'<abbr class="dtstart" title="%s">%s</abbr>'%(icalendarable.start.isoformat(),self._cw.format_date(icalendarable.start)))ificalendarable.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>')classCalendarItemView(EntityView):__regid__='calendaritem'defcell_call(self,row,col,dates=False):task=self.cw_rset.complete_entity(row,0)task.view('oneline',w=self.w)ifdates:icalendarable=task.cw_adapt_to('ICalendarable')ificalendarable.startandicalendarable.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.startoricalendarable.stop))classCalendarLargeItemView(CalendarItemView):__regid__='calendarlargeitem'class_TaskEntry(object):def__init__(self,task,color,index=0):self.task=taskself.color=colorself.index=indexself.length=1icalendarable=task.cw_adapt_to('ICalendarable')self.start=icalendarable.startself.stop=icalendarable.stopdefin_working_hours(self):"""predicate returning True is the task is in working hours"""iftodatetime(self.start).hour>7andtodatetime(self.stop).hour<20:returnTruereturnFalsedefis_one_day_task(self):returnself.startandself.stopandself.start.isocalendar()==self.stop.isocalendar()classOneMonthCal(EntityView):"""At some point, this view will probably replace ampm calendars"""__regid__='onemonthcal'__select__=adaptable('ICalendarable')paginable=Falsetitle=_('one month')defcall(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'inself._cw.form:year=int(self._cw.form['year'])else:year=_today.yearif'month'inself._cw.form:month=int(self._cw.form['month'])else:month=_today.monthfirst_day_of_month=date(year,month,1)firstday=first_day_of_month-timedelta(first_day_of_month.weekday())ifmonth>=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 7lastday=last_day_of_month+timedelta(7-last_day_of_month.weekday())month_dates=list(date_range(firstday,lastday))dates={}task_max=0forrowinxrange(self.cw_rset.rowcount):task=self.cw_rset.get_entity(row,0)iflen(self.cw_rset[row])>1andself.cw_rset.description[row][1]=='CWUser':user=self.cw_rset.get_entity(row,1)else:user=Nonethe_dates=[]icalendarable=task.cw_adapt_to('ICalendarable')tstart=icalendarable.startiftstart:tstart=todate(icalendarable.start)iftstart>lastday:continuethe_dates=[tstart]tstop=icalendarable.stopiftstop:tstop=todate(tstop)iftstop<firstday:continuethe_dates=[tstop]iftstartandtstop:iftstart.isocalendar()==tstop.isocalendar():iffirstday<=tstart<=lastday:the_dates=[tstart]else:the_dates=date_range(max(tstart,firstday),min(tstop+ONEDAY,lastday))ifnotthe_dates:continuefordinthe_dates:d_tasks=dates.setdefault((d.year,d.month,d.day),{})t_users=d_tasks.setdefault(task,set())t_users.add(user)iflen(d_tasks)>task_max:task_max=len(d_tasks)days=[]nrows=max(3,task_max)# colors here are class names defined in cubicweb.csscolors=["col%x"%iforiinrange(12)]next_color_index=0visited_tasks={}# holds a description of a tasktask_colors={}# remember a color assigned to a taskformdateinmonth_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=[]fortaskind_tasks:iftaskinvisited_tasks:task_descr=visited_tasks[task]rows[task_descr.index]=task_descrelse:postpone.append(task)fortaskinpostpone:# to every 'new' task we must affect a color# (which must be the same for every user concerned# by the task)fori,tinenumerate(rows):iftisNone:iftaskintask_colors:color=task_colors[task]else:color=colors[next_color_index]next_color_index=(next_color_index+1)%len(colors)task_colors[task]=colortask_descr=_TaskEntry(task,color,i)rows[i]=task_descrvisited_tasks[task]=task_descrbreakelse:raiseRuntimeError("is it possible we got it wrong?")days.append(rows)curdate=first_day_of_monthself.w(u'<div id="onemonthcalid">')# build scheduleself.w(u'<table class="omcalendar">')prevlink,nextlink=self._prevnext_links(curdate)# XXXself.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 headerself.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)fordayinWEEKDAYS))# build calendarformdate,task_rowsinzip(month_dates,days):ifmdate.weekday()==0:self.w(u'<tr>')self._build_calendar_cell(mdate,task_rows,curdate)ifmdate.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)returnprevlink,nextlinkdef_build_calendar_cell(self,celldate,rows,curdate):curmonth=curdate.monthclasses=""ifcelldate.month!=curmonth:classes+=" outOfRange"ifcelldate==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)iflen(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">')fortask_descrinrows:iftask_descr:task=task_descr.taskself.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>')classOneWeekCal(EntityView):"""At some point, this view will probably replace ampm calendars"""__regid__='oneweekcal'__select__=adaptable('ICalendarable')paginable=Falsetitle=_('one week')defcall(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'inself._cw.form:year=int(self._cw.form['year'])else:year=_today.yearif'week'inself._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 0first_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_weekdates=[[]foriinrange(7)]task_colors={}# remember a color assigned to a task# colors here are class names defined in cubicweb.csscolors=["col%x"%iforiinrange(12)]next_color_index=0done_tasks=set()forrowinxrange(self.cw_rset.rowcount):task=self.cw_rset.get_entity(row,0)iftask.eidindone_tasks:continuedone_tasks.add(task.eid)the_dates=[]icalendarable=task.cw_adapt_to('ICalendarable')tstart=icalendarable.starttstop=icalendarable.stopiftstart:tstart=todate(tstart)iftstart>lastday:continuethe_dates=[tstart]iftstop:tstop=todate(tstop)iftstop<firstday:continuethe_dates=[tstop]iftstartandtstop:the_dates=date_range(max(tstart,firstday),min(tstop+ONEDAY,lastday))ifnotthe_dates:continueiftasknotintask_colors:task_colors[task]=colors[next_color_index]next_color_index=(next_color_index+1)%len(colors)fordinthe_dates:day=d.weekday()task_descr=_TaskEntry(task,task_colors[task])dates[day].append(task_descr)self.w(u'<div id="oneweekcalid">')# build scheduleself.w(u'<table class="omcalendar" id="week">')prevlink,nextlink=self._prevnext_links(first_day_of_week)# XXXself.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 headerself.w(u'<tr>')self.w(u'<th class="transparent"></th>')# column for hours_today=date.today()fori,dayinenumerate(WEEKDAYS):wdate=first_day_of_week+timedelta(i)ifwdate.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 calendarself.w(u'<tr>')self.w(u'<td style="width:5em;">')# column for hoursextra=""forhinrange(8,20):self.w(u'<div class="hour" %s>'%extra)self.w(u'%02d:00'%h)self.w(u'</div>')self.w(u'</td>')fori,dayinenumerate(WEEKDAYS):wdate=first_day_of_week+timedelta(i)classes=""ifwdate.isocalendar()==_today.isocalendar():classes=" today"self.w(u'<td class="column %s" id="%s">'%(classes,day))iflen(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)forhinrange(8,20):self.w(u'<div class="hourline" style="top:%sex;">'%((h-7)*8))self.w(u'</div>')ifdates[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=[tfortintask_descrsift.is_one_day_task()andt.in_working_hours()]wholeday_tasks=[tfortintask_descrsifnott.is_one_day_task()]inday_tasks.sort(key=lambdat:t.start)sorted_tasks=[]fori,tinenumerate(wholeday_tasks):t.index=incols=len(wholeday_tasks)whileinday_tasks:t=inday_tasks.pop(0)fori,cinenumerate(sorted_tasks):ifnotcorc[-1].stop<=t.start:c.append(t)t.index=i+ncolsbreakelse:t.index=len(sorted_tasks)+ncolssorted_tasks.append([t])ncols+=len(sorted_tasks)ifncols==0:returninday_tasks=[]fortasklistinsorted_tasks:inday_tasks+=tasklistwidth=100.0/ncolsfortask_descinwholeday_tasks+inday_tasks:task=task_desc.taskstart_hour=8start_min=0stop_hour=20stop_min=0iftask_desc.start:ifdate<todate(task_desc.start)<date+ONEDAY:start_hour=max(8,task_desc.start.hour)start_min=task_desc.start.minuteiftask_desc.stop:ifdate<todate(task_desc.stop)<date+ONEDAY:stop_hour=min(20,task_desc.stop.hour)ifstop_hour<20:stop_min=task_desc.stop.minuteheight=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.indexstyle="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>')iftask_desc.startisNone: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.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])returnprevlink,nextlink