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