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