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