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