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