1 #!/usr/bin/env python 2 3 """ 4 Web interface data abstractions. 5 6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from datetime import datetime, timedelta 23 from imiptools.data import get_tzid 24 from imiptools.dates import end_date_from_calendar, end_date_to_calendar, \ 25 format_datetime, get_datetime, get_start_of_day, \ 26 to_date 27 from imiptools.period import Period 28 29 class PeriodError(Exception): 30 pass 31 32 class EventPeriod(Period): 33 34 """ 35 A simple period plus attribute details, compatible with RecurringPeriod, and 36 intended to represent information obtained from an iCalendar resource. 37 """ 38 39 def __init__(self, start, end, start_attr=None, end_attr=None, form_start=None, form_end=None, origin=None): 40 41 """ 42 Initialise a period with the given 'start' and 'end' datetimes, together 43 with optional 'start_attr' and 'end_attr' metadata, 'form_start' and 44 'form_end' values provided as textual input, and with an optional 45 'origin' indicating the kind of period this object describes. 46 """ 47 48 Period.__init__(self, start, end, origin) 49 self.start_attr = start_attr 50 self.end_attr = end_attr 51 self.form_start = form_start 52 self.form_end = form_end 53 54 def as_tuple(self): 55 return self.start, self.end, self.start_attr, self.end_attr, self.form_start, self.form_end, self.origin 56 57 def __repr__(self): 58 return "EventPeriod(%r, %r, %r, %r, %r, %r, %r)" % self.as_tuple() 59 60 def as_event_period(self): 61 return self 62 63 # Period data methods. 64 65 # get_start inherited 66 67 def get_end(self): 68 return end_date_from_calendar(Period.get_end(self)) 69 70 def get_tzid(self): 71 return get_tzid(self.start_attr, self.end_attr) 72 73 def get_start_item(self): 74 return self.start, self.start_attr 75 76 def get_end_item(self): 77 return self.end, self.end_attr 78 79 # Form data compatibility methods. 80 81 def get_form_start(self): 82 if not self.form_start: 83 self.form_start = self.get_form_date(self.get_start(), self.start_attr) 84 return self.form_start 85 86 def get_form_end(self): 87 if not self.form_end: 88 self.form_end = self.get_form_date(self.get_end(), self.end_attr) 89 return self.form_end 90 91 def as_form_period(self): 92 return FormPeriod( 93 self.get_form_start(), 94 self.get_form_end(), 95 isinstance(self.end, datetime) or self.get_start() != self.get_end(), 96 isinstance(self.start, datetime) or isinstance(self.end, datetime), 97 self.origin 98 ) 99 100 def get_form_date(self, dt, attr=None): 101 return FormDate( 102 format_datetime(to_date(dt)), 103 isinstance(dt, datetime) and str(dt.hour) or None, 104 isinstance(dt, datetime) and str(dt.minute) or None, 105 isinstance(dt, datetime) and str(dt.second) or None, 106 attr and attr.get("TZID") or None, 107 dt, attr 108 ) 109 110 class FormPeriod: 111 112 "A period whose information originates from a form." 113 114 def __init__(self, start, end, end_enabled=True, times_enabled=True, origin=None): 115 self.start = start 116 self.end = end 117 self.end_enabled = end_enabled 118 self.times_enabled = times_enabled 119 self.origin = origin 120 121 def as_tuple(self): 122 return self.start, self.end, self.end_enabled, self.times_enabled, self.origin 123 124 def __repr__(self): 125 return "FormPeriod(%r, %r, %r, %r, %r)" % self.as_tuple() 126 127 def _get_start(self): 128 return self.start.as_datetime(self.times_enabled), self.start.get_attributes(self.times_enabled) 129 130 def _get_end(self, adjust=False): 131 132 # Handle specified end datetimes. 133 134 if self.end_enabled: 135 dtend = self.end.as_datetime(self.times_enabled) 136 dtend_attr = self.end.get_attributes(self.times_enabled) 137 if dtend: 138 dtend = adjust and end_date_to_calendar(dtend) or dtend 139 else: 140 return None, None 141 142 # Otherwise, treat the end date as the start date. Datetimes are 143 # handled by making the event occupy the rest of the day. 144 145 else: 146 dtstart, dtstart_attr = self._get_start() 147 if dtstart: 148 dtend = dtstart + timedelta(1) 149 dtend_attr = dtstart_attr 150 151 if isinstance(dtstart, datetime): 152 dtend = get_start_of_day(dtend, dtend_attr["TZID"]) 153 else: 154 return None, None 155 156 return dtend, dtend_attr 157 158 def as_event_period(self, index=None): 159 160 """ 161 Return a converted version of this object as an event period suitable 162 for iCalendar usage. If 'index' is indicated, include it in any error 163 raised in the conversion process. 164 """ 165 166 dtstart, dtstart_attr = self._get_start() 167 if not dtstart: 168 raise PeriodError(*[index is not None and ("dtstart", index) or "dtstart"]) 169 170 dtend, dtend_attr = self._get_end(adjust=True) 171 if not dtend: 172 raise PeriodError(*[index is not None and ("dtend", index) or "dtend"]) 173 174 if dtstart > dtend: 175 raise PeriodError(*[ 176 index is not None and ("dtstart", index) or "dtstart", 177 index is not None and ("dtend", index) or "dtend" 178 ]) 179 180 return EventPeriod(dtstart, dtend, dtstart_attr, dtend_attr, self.start, self.end, self.origin) 181 182 # Period data methods. 183 184 def get_start(self): 185 dtstart, dtstart_attr = self._get_start() 186 return dtstart 187 188 def get_end(self): 189 dtend, dtend_attr = self._get_end() 190 return dtend 191 192 def get_start_item(self): 193 return self._get_start() 194 195 def get_end_item(self): 196 return self._get_end() 197 198 # Form data methods. 199 200 def get_form_start(self): 201 return self.start 202 203 def get_form_end(self): 204 return self.end 205 206 def as_form_period(self): 207 return self 208 209 class FormDate: 210 211 "Date information originating from form information." 212 213 def __init__(self, date=None, hour=None, minute=None, second=None, tzid=None, dt=None, attr=None): 214 self.date = date 215 self.hour = hour 216 self.minute = minute 217 self.second = second 218 self.tzid = tzid 219 self.dt = dt 220 self.attr = attr 221 222 def as_tuple(self): 223 return self.date, self.hour, self.minute, self.second, self.tzid, self.dt, self.attr 224 225 def __repr__(self): 226 return "FormDate(%r, %r, %r, %r, %r, %r, %r)" % self.as_tuple() 227 228 def get_component(self, value): 229 return (value or "").rjust(2, "0")[:2] 230 231 def get_hour(self): 232 return self.get_component(self.hour) 233 234 def get_minute(self): 235 return self.get_component(self.minute) 236 237 def get_second(self): 238 return self.get_component(self.second) 239 240 def get_date_string(self): 241 return self.date or "" 242 243 def get_datetime_string(self): 244 if not self.date: 245 return "" 246 247 hour = self.hour; minute = self.minute; second = self.second 248 249 if hour or minute or second: 250 time = "T%s%s%s" % tuple(map(self.get_component, (hour, minute, second))) 251 else: 252 time = "" 253 254 return "%s%s" % (self.date, time) 255 256 def get_tzid(self): 257 return self.tzid 258 259 def as_datetime(self, with_time=True): 260 261 "Return a datetime for this object." 262 263 # Return any original datetime details. 264 265 if self.dt: 266 return self.dt 267 268 # Otherwise, construct a datetime. 269 270 s, attr = self.as_datetime_item(with_time) 271 if s: 272 return get_datetime(s, attr) 273 else: 274 return None 275 276 def as_datetime_item(self, with_time=True): 277 278 """ 279 Return a (datetime string, attr) tuple for the datetime information 280 provided by this object, where both tuple elements will be None if no 281 suitable date or datetime information exists. 282 """ 283 284 s = None 285 if with_time: 286 s = self.get_datetime_string() 287 attr = self.get_attributes(True) 288 if not s: 289 s = self.get_date_string() 290 attr = self.get_attributes(False) 291 if not s: 292 return None, None 293 return s, attr 294 295 def get_attributes(self, with_time=True): 296 297 "Return attributes for the date or datetime represented by this object." 298 299 if with_time: 300 return {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"} 301 else: 302 return {"VALUE" : "DATE"} 303 304 def event_period_from_period(period): 305 if isinstance(period, EventPeriod): 306 return period 307 elif isinstance(period, FormPeriod): 308 return period.as_event_period() 309 else: 310 dtstart, dtstart_attr = period.get_start_item() 311 dtend, dtend_attr = period.get_end_item() 312 return EventPeriod(dtstart, dtend, dtstart_attr, dtend_attr, origin=period.origin) 313 314 def form_period_from_period(period): 315 if isinstance(period, EventPeriod): 316 return period.as_form_period() 317 elif isinstance(period, FormPeriod): 318 return period 319 else: 320 return event_period_from_period(period).as_form_period() 321 322 # vim: tabstop=4 expandtab shiftwidth=4