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