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 format_datetime, get_datetime, get_start_of_day, to_date 24 from imiptools.period import Period 25 26 class PeriodError(Exception): 27 pass 28 29 class EventPeriod(Period): 30 31 """ 32 A simple period plus attribute details, compatible with RecurringPeriod, and 33 intended to represent information obtained from an iCalendar resource. 34 """ 35 36 def __init__(self, start, end, start_attr=None, end_attr=None, form_start=None, form_end=None): 37 Period.__init__(self, start, end) 38 self.start_attr = start_attr 39 self.end_attr = end_attr 40 self.form_start = form_start 41 self.form_end = form_end 42 43 def as_tuple(self): 44 return self.start, self.end, self.start_attr, self.end_attr, self.form_start, self.form_end 45 46 def __repr__(self): 47 return "EventPeriod(%r, %r, %r, %r, %r, %r)" % self.as_tuple() 48 49 def get_start(self): 50 return self.start 51 52 def get_end(self): 53 return self.end 54 55 def as_event_period(self): 56 return self 57 58 def get_form_start(self): 59 if not self.form_start: 60 self.form_start = self.get_form_date(self.start, self.start_attr) 61 return self.form_start 62 63 def get_form_end(self): 64 if not self.form_end: 65 self.form_end = self.get_form_date(self.end, self.end_attr) 66 return self.form_end 67 68 def as_form_period(self): 69 return FormPeriod( 70 self.get_form_date(self.start, self.start_attr), 71 self.get_form_date(self.end, self.end_attr), 72 isinstance(self.end, datetime) or self.start != self.end - timedelta(1), 73 isinstance(self.start, datetime) or isinstance(self.end, datetime) 74 ) 75 76 def get_form_date(self, dt, attr=None): 77 return FormDate( 78 format_datetime(to_date(dt)), 79 isinstance(dt, datetime) and str(dt.hour) or None, 80 isinstance(dt, datetime) and str(dt.minute) or None, 81 isinstance(dt, datetime) and str(dt.second) or None, 82 attr and attr.get("TZID") or None, 83 dt, attr 84 ) 85 86 def event_period_from_recurrence_period(period): 87 return EventPeriod(period.start, period.end, period.start_attr, period.end_attr) 88 89 class FormPeriod: 90 91 "A period whose information originates from a form." 92 93 def __init__(self, start, end, end_enabled=True, times_enabled=True): 94 self.start = start 95 self.end = end 96 self.end_enabled = end_enabled 97 self.times_enabled = times_enabled 98 99 def as_tuple(self): 100 return self.start, self.end, self.end_enabled, self.times_enabled 101 102 def __repr__(self): 103 return "FormPeriod(%r, %r, %r, %r)" % self.as_tuple() 104 105 def _get_start(self): 106 t = self.start.as_datetime_item(self.times_enabled) 107 if t: 108 return t 109 else: 110 return None 111 112 def _get_end(self): 113 114 # Handle specified end datetimes. 115 116 if self.end_enabled: 117 t = self.end.as_datetime_item(self.times_enabled) 118 if t: 119 dtend, dtend_attr = t 120 else: 121 return 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 t = self._get_start() 128 if t: 129 dtstart, dtstart_attr = t 130 dtend = dtstart + timedelta(1) 131 dtend_attr = dtstart_attr 132 133 if isinstance(dtstart, datetime): 134 dtend = get_start_of_day(dtend, dtend_attr["TZID"]) 135 else: 136 return None 137 138 return dtend, dtend_attr 139 140 def get_start(self): 141 t = self._get_start() 142 if t: 143 dtstart, dtstart_attr = t 144 return dtstart 145 else: 146 return None 147 148 def get_end(self): 149 t = self._get_end() 150 if t: 151 dtend, dtend_attr = t 152 return dtend 153 else: 154 return None 155 156 def as_event_period(self, index=None): 157 t = self._get_start() 158 if t: 159 dtstart, dtstart_attr = t 160 else: 161 raise PeriodError(*[index is not None and ("dtstart", index) or "dtstart"]) 162 163 t = self._get_end() 164 if t: 165 dtend, dtend_attr = t 166 else: 167 raise PeriodError(*[index is not None and ("dtend", index) or "dtend"]) 168 169 if dtstart > dtend: 170 raise PeriodError(*[ 171 index is not None and ("dtstart", index) or "dtstart", 172 index is not None and ("dtend", index) or "dtend" 173 ]) 174 175 return EventPeriod(dtstart, dtend, dtstart_attr, dtend_attr, self.start, self.end) 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, %r, %r, %r, %r, %r, %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_item(self, with_time=True): 237 238 """ 239 Return a (datetime, attr) tuple for the datetime information provided by 240 this object, or None if the fields cannot be used to construct a 241 datetime object. 242 """ 243 244 # Return any original datetime details. 245 246 if self.dt: 247 return self.dt, self.attr 248 249 # Otherwise, construct a datetime and attributes. 250 251 if not self.date: 252 return None 253 elif with_time: 254 attr = {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"} 255 dt = get_datetime(self.get_datetime_string(), attr) 256 else: 257 dt = None 258 259 # Interpret incomplete datetimes as dates. 260 261 if not dt: 262 attr = {"VALUE" : "DATE"} 263 dt = get_datetime(self.get_date_string(), attr) 264 265 if dt: 266 return dt, attr 267 268 return None 269 270 def end_date_to_calendar(dt): 271 272 """ 273 Change end dates to refer to the actual dates, not the iCalendar "next day" 274 dates. 275 """ 276 277 if not isinstance(dt, datetime): 278 return dt + timedelta(1) 279 else: 280 return dt 281 282 def end_date_from_calendar(dt): 283 284 """ 285 Change end dates to refer to the actual dates, not the iCalendar "next day" 286 dates. 287 """ 288 289 if not isinstance(dt, datetime): 290 return dt - timedelta(1) 291 else: 292 return dt 293 294 # vim: tabstop=4 expandtab shiftwidth=4