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