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