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 Period.__init__(self, start, end) 39 self.start_attr = start_attr 40 self.end_attr = end_attr 41 self.form_start = form_start 42 self.form_end = form_end 43 self.origin = origin 44 45 def as_tuple(self): 46 return self.start, self.end, self.start_attr, self.end_attr, self.form_start, self.form_end, self.origin 47 48 def __repr__(self): 49 return "EventPeriod(%r, %r, %r, %r, %r, %r, %r)" % self.as_tuple() 50 51 def as_event_period(self): 52 return self 53 54 # Period data methods. 55 56 def get_start(self): 57 return self.start 58 59 def get_end(self): 60 return end_date_from_calendar(self.end) 61 62 def get_tzid(self): 63 return get_tzid(self.start_attr, self.end_attr) 64 65 # Form data compatibility methods. 66 67 def get_form_start(self): 68 if not self.form_start: 69 self.form_start = self.get_form_date(self.get_start(), self.start_attr) 70 return self.form_start 71 72 def get_form_end(self): 73 if not self.form_end: 74 self.form_end = self.get_form_date(self.get_end(), self.end_attr) 75 return self.form_end 76 77 def as_form_period(self): 78 return FormPeriod( 79 self.get_form_start(), 80 self.get_form_end(), 81 isinstance(self.end, datetime) or self.get_start() != self.get_end(), 82 isinstance(self.start, datetime) or isinstance(self.end, datetime) 83 ) 84 85 def get_form_date(self, dt, attr=None): 86 return FormDate( 87 format_datetime(to_date(dt)), 88 isinstance(dt, datetime) and str(dt.hour) or None, 89 isinstance(dt, datetime) and str(dt.minute) or None, 90 isinstance(dt, datetime) and str(dt.second) or None, 91 attr and attr.get("TZID") or None, 92 dt, attr 93 ) 94 95 class FormPeriod: 96 97 "A period whose information originates from a form." 98 99 def __init__(self, start, end, end_enabled=True, times_enabled=True, origin=None): 100 self.start = start 101 self.end = end 102 self.end_enabled = end_enabled 103 self.times_enabled = times_enabled 104 self.origin = origin 105 106 def as_tuple(self): 107 return self.start, self.end, self.end_enabled, self.times_enabled, self.origin 108 109 def __repr__(self): 110 return "FormPeriod(%r, %r, %r, %r, %r)" % self.as_tuple() 111 112 def _get_start(self): 113 t = self.start.as_datetime_item(self.times_enabled) 114 if t: 115 return t 116 else: 117 return None 118 119 def _get_end(self, adjust=False): 120 121 # Handle specified end datetimes. 122 123 if self.end_enabled: 124 t = self.end.as_datetime_item(self.times_enabled) 125 if t: 126 dtend, dtend_attr = t 127 dtend = adjust and end_date_to_calendar(dtend) or dtend 128 else: 129 return None 130 131 # Otherwise, treat the end date as the start date. Datetimes are 132 # handled by making the event occupy the rest of the day. 133 134 else: 135 t = self._get_start() 136 if t: 137 dtstart, dtstart_attr = t 138 dtend = dtstart + timedelta(1) 139 dtend_attr = dtstart_attr 140 141 if isinstance(dtstart, datetime): 142 dtend = get_start_of_day(dtend, dtend_attr["TZID"]) 143 else: 144 return None 145 146 return dtend, dtend_attr 147 148 def as_event_period(self, index=None): 149 t = self._get_start() 150 if t: 151 dtstart, dtstart_attr = t 152 else: 153 raise PeriodError(*[index is not None and ("dtstart", index) or "dtstart"]) 154 155 t = self._get_end(adjust=True) 156 if t: 157 dtend, dtend_attr = t 158 else: 159 raise PeriodError(*[index is not None and ("dtend", index) or "dtend"]) 160 161 if dtstart > dtend: 162 raise PeriodError(*[ 163 index is not None and ("dtstart", index) or "dtstart", 164 index is not None and ("dtend", index) or "dtend" 165 ]) 166 167 return EventPeriod(dtstart, dtend, dtstart_attr, dtend_attr, self.start, self.end, self.origin) 168 169 # Period data methods. 170 171 def get_start(self): 172 t = self._get_start() 173 if t: 174 dtstart, dtstart_attr = t 175 return dtstart 176 else: 177 return None 178 179 def get_end(self): 180 t = self._get_end() 181 if t: 182 dtend, dtend_attr = t 183 return dtend 184 else: 185 return None 186 187 # Form data methods. 188 189 def get_form_start(self): 190 return self.start 191 192 def get_form_end(self): 193 return self.end 194 195 def as_form_period(self): 196 return self 197 198 class FormDate: 199 200 "Date information originating from form information." 201 202 def __init__(self, date=None, hour=None, minute=None, second=None, tzid=None, dt=None, attr=None): 203 self.date = date 204 self.hour = hour 205 self.minute = minute 206 self.second = second 207 self.tzid = tzid 208 self.dt = dt 209 self.attr = attr 210 211 def as_tuple(self): 212 return self.date, self.hour, self.minute, self.second, self.tzid, self.dt, self.attr 213 214 def __repr__(self): 215 return "FormDate(%r, %r, %r, %r, %r, %r, %r)" % self.as_tuple() 216 217 def get_component(self, value): 218 return (value or "").rjust(2, "0")[:2] 219 220 def get_hour(self): 221 return self.get_component(self.hour) 222 223 def get_minute(self): 224 return self.get_component(self.minute) 225 226 def get_second(self): 227 return self.get_component(self.second) 228 229 def get_date_string(self): 230 return self.date or "" 231 232 def get_datetime_string(self): 233 if not self.date: 234 return "" 235 236 hour = self.hour; minute = self.minute; second = self.second 237 238 if hour or minute or second: 239 time = "T%s%s%s" % tuple(map(self.get_component, (hour, minute, second))) 240 else: 241 time = "" 242 243 return "%s%s" % (self.date, time) 244 245 def get_tzid(self): 246 return self.tzid 247 248 def as_datetime_item(self, with_time=True): 249 250 """ 251 Return a (datetime, attr) tuple for the datetime information provided by 252 this object, or None if the fields cannot be used to construct a 253 datetime object. 254 """ 255 256 # Return any original datetime details. 257 258 if self.dt: 259 return self.dt, self.attr 260 261 # Otherwise, construct a datetime and attributes. 262 263 if not self.date: 264 return None 265 elif with_time: 266 attr = {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"} 267 dt = get_datetime(self.get_datetime_string(), attr) 268 else: 269 dt = None 270 271 # Interpret incomplete datetimes as dates. 272 273 if not dt: 274 attr = {"VALUE" : "DATE"} 275 dt = get_datetime(self.get_date_string(), attr) 276 277 if dt: 278 return dt, attr 279 280 return None 281 282 def end_date_to_calendar(dt): 283 284 """ 285 Change end dates to refer to the iCalendar "next day" dates, not the actual 286 dates. 287 """ 288 289 if not isinstance(dt, datetime): 290 return dt + timedelta(1) 291 else: 292 return dt 293 294 def end_date_from_calendar(dt): 295 296 """ 297 Change end dates to refer to the actual dates, not the iCalendar "next day" 298 dates. 299 """ 300 301 if not isinstance(dt, datetime): 302 return dt - timedelta(1) 303 else: 304 return dt 305 306 def event_period_from_period(period): 307 if isinstance(period, EventPeriod): 308 return period 309 elif isinstance(period, FormPeriod): 310 return period.as_event_period() 311 else: 312 return EventPeriod(period.start, period.end, period.start_attr, period.end_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