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