1.1 --- a/imiptools/data.py Fri May 15 22:09:02 2015 +0200
1.2 +++ b/imiptools/data.py Sat May 16 01:03:51 2015 +0200
1.3 @@ -338,6 +338,8 @@
1.4 value, attr = t
1.5 return get_datetime(value, attr), attr
1.6
1.7 +# Conversion functions.
1.8 +
1.9 def get_addresses(values):
1.10 return [address for name, address in email.utils.getaddresses(values)]
1.11
1.12 @@ -400,11 +402,16 @@
1.13 "A period with origin information from the object."
1.14
1.15 def __init__(self, start, end, origin, start_attr=None, end_attr=None):
1.16 - Period.__init__(self, start, end)
1.17 - self.origin = origin
1.18 + Period.__init__(self, start, end, origin)
1.19 self.start_attr = start_attr
1.20 self.end_attr = end_attr
1.21
1.22 + def get_start_item(self):
1.23 + return self.start, self.start_attr
1.24 +
1.25 + def get_end_item(self):
1.26 + return self.end, self.end_attr
1.27 +
1.28 def get_tzid(self):
1.29 return get_tzid(self.start_attr, self.end_attr)
1.30
2.1 --- a/imiptools/period.py Fri May 15 22:09:02 2015 +0200
2.2 +++ b/imiptools/period.py Sat May 16 01:03:51 2015 +0200
2.3 @@ -21,15 +21,17 @@
2.4
2.5 from bisect import bisect_left, bisect_right, insort_left
2.6 from datetime import datetime, timedelta
2.7 -from imiptools.dates import get_datetime, get_start_of_day, to_timezone
2.8 +from imiptools.dates import get_datetime, get_datetime_attributes, \
2.9 + get_start_of_day, to_timezone
2.10
2.11 class Period:
2.12
2.13 "A basic period abstraction."
2.14
2.15 - def __init__(self, start, end=None):
2.16 + def __init__(self, start, end=None, origin=None):
2.17 self.start = start
2.18 self.end = end
2.19 + self.origin = origin
2.20
2.21 def as_tuple(self):
2.22 return self.start, self.end
2.23 @@ -49,6 +51,20 @@
2.24 def __repr__(self):
2.25 return "Period(%r, %r)" % (self.start, self.end)
2.26
2.27 + # Datetime metadata methods.
2.28 +
2.29 + def get_start(self):
2.30 + return self.start
2.31 +
2.32 + def get_end(self):
2.33 + return self.end
2.34 +
2.35 + def get_start_item(self):
2.36 + return self.start, get_datetime_attributes(self.start)
2.37 +
2.38 + def get_end_item(self):
2.39 + return self.end, get_datetime_attributes(self.end)
2.40 +
2.41 class FreeBusyPeriod(Period):
2.42
2.43 "A free/busy record abstraction."
3.1 --- a/imipweb/data.py Fri May 15 22:09:02 2015 +0200
3.2 +++ b/imipweb/data.py Sat May 16 01:03:51 2015 +0200
3.3 @@ -35,12 +35,19 @@
3.4 """
3.5
3.6 def __init__(self, start, end, start_attr=None, end_attr=None, form_start=None, form_end=None, origin=None):
3.7 - Period.__init__(self, start, end)
3.8 +
3.9 + """
3.10 + Initialise a period with the given 'start' and 'end' datetimes, together
3.11 + with optional 'start_attr' and 'end_attr' metadata, 'form_start' and
3.12 + 'form_end' values provided as textual input, and with an optional
3.13 + 'origin' indicating the kind of period this object describes.
3.14 + """
3.15 +
3.16 + Period.__init__(self, start, end, origin)
3.17 self.start_attr = start_attr
3.18 self.end_attr = end_attr
3.19 self.form_start = form_start
3.20 self.form_end = form_end
3.21 - self.origin = origin
3.22
3.23 def as_tuple(self):
3.24 return self.start, self.end, self.start_attr, self.end_attr, self.form_start, self.form_end, self.origin
3.25 @@ -62,6 +69,12 @@
3.26 def get_tzid(self):
3.27 return get_tzid(self.start_attr, self.end_attr)
3.28
3.29 + def get_start_item(self):
3.30 + return self.start, self.start_attr
3.31 +
3.32 + def get_end_item(self):
3.33 + return self.end, self.end_attr
3.34 +
3.35 # Form data compatibility methods.
3.36
3.37 def get_form_start(self):
3.38 @@ -110,52 +123,50 @@
3.39 return "FormPeriod(%r, %r, %r, %r, %r)" % self.as_tuple()
3.40
3.41 def _get_start(self):
3.42 - t = self.start.as_datetime_item(self.times_enabled)
3.43 - if t:
3.44 - return t
3.45 - else:
3.46 - return None
3.47 + return self.start.as_datetime(self.times_enabled), self.start.get_attributes(self.times_enabled)
3.48
3.49 def _get_end(self, adjust=False):
3.50
3.51 # Handle specified end datetimes.
3.52
3.53 if self.end_enabled:
3.54 - t = self.end.as_datetime_item(self.times_enabled)
3.55 - if t:
3.56 - dtend, dtend_attr = t
3.57 + dtend = self.end.as_datetime(self.times_enabled)
3.58 + dtend_attr = self.end.get_attributes(self.times_enabled)
3.59 + if dtend:
3.60 dtend = adjust and end_date_to_calendar(dtend) or dtend
3.61 else:
3.62 - return None
3.63 + return None, None
3.64
3.65 # Otherwise, treat the end date as the start date. Datetimes are
3.66 # handled by making the event occupy the rest of the day.
3.67
3.68 else:
3.69 - t = self._get_start()
3.70 - if t:
3.71 - dtstart, dtstart_attr = t
3.72 + dtstart, dtstart_attr = self._get_start()
3.73 + if dtstart:
3.74 dtend = dtstart + timedelta(1)
3.75 dtend_attr = dtstart_attr
3.76
3.77 if isinstance(dtstart, datetime):
3.78 dtend = get_start_of_day(dtend, dtend_attr["TZID"])
3.79 else:
3.80 - return None
3.81 + return None, None
3.82
3.83 return dtend, dtend_attr
3.84
3.85 def as_event_period(self, index=None):
3.86 - t = self._get_start()
3.87 - if t:
3.88 - dtstart, dtstart_attr = t
3.89 - else:
3.90 +
3.91 + """
3.92 + Return a converted version of this object as an event period suitable
3.93 + for iCalendar usage. If 'index' is indicated, include it in any error
3.94 + raised in the conversion process.
3.95 + """
3.96 +
3.97 + dtstart, dtstart_attr = self._get_start()
3.98 + if not dtstart:
3.99 raise PeriodError(*[index is not None and ("dtstart", index) or "dtstart"])
3.100
3.101 - t = self._get_end(adjust=True)
3.102 - if t:
3.103 - dtend, dtend_attr = t
3.104 - else:
3.105 + dtend, dtend_attr = self._get_end(adjust=True)
3.106 + if not dtend:
3.107 raise PeriodError(*[index is not None and ("dtend", index) or "dtend"])
3.108
3.109 if dtstart > dtend:
3.110 @@ -169,20 +180,18 @@
3.111 # Period data methods.
3.112
3.113 def get_start(self):
3.114 - t = self._get_start()
3.115 - if t:
3.116 - dtstart, dtstart_attr = t
3.117 - return dtstart
3.118 - else:
3.119 - return None
3.120 + dtstart, dtstart_attr = self._get_start()
3.121 + return dtstart
3.122
3.123 def get_end(self):
3.124 - t = self._get_end()
3.125 - if t:
3.126 - dtend, dtend_attr = t
3.127 - return dtend
3.128 - else:
3.129 - return None
3.130 + dtend, dtend_attr = self._get_end()
3.131 + return dtend
3.132 +
3.133 + def get_start_item(self):
3.134 + return self._get_start()
3.135 +
3.136 + def get_end_item(self):
3.137 + return self._get_end()
3.138
3.139 # Form data methods.
3.140
3.141 @@ -245,39 +254,50 @@
3.142 def get_tzid(self):
3.143 return self.tzid
3.144
3.145 - def as_datetime_item(self, with_time=True):
3.146 + def as_datetime(self, with_time=True):
3.147
3.148 - """
3.149 - Return a (datetime, attr) tuple for the datetime information provided by
3.150 - this object, or None if the fields cannot be used to construct a
3.151 - datetime object.
3.152 - """
3.153 + "Return a datetime for this object."
3.154
3.155 # Return any original datetime details.
3.156
3.157 if self.dt:
3.158 - return self.dt, self.attr
3.159 + return self.dt
3.160
3.161 - # Otherwise, construct a datetime and attributes.
3.162 + # Otherwise, construct a datetime.
3.163
3.164 - if not self.date:
3.165 - return None
3.166 - elif with_time:
3.167 - attr = {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"}
3.168 - dt = get_datetime(self.get_datetime_string(), attr)
3.169 + s, attr = self.as_datetime_item(with_time)
3.170 + if s:
3.171 + return get_datetime(s, attr)
3.172 else:
3.173 - dt = None
3.174 + return None
3.175 +
3.176 + def as_datetime_item(self, with_time=True):
3.177
3.178 - # Interpret incomplete datetimes as dates.
3.179 + """
3.180 + Return a (datetime string, attr) tuple for the datetime information
3.181 + provided by this object, where both tuple elements will be None if no
3.182 + suitable date or datetime information exists.
3.183 + """
3.184
3.185 - if not dt:
3.186 - attr = {"VALUE" : "DATE"}
3.187 - dt = get_datetime(self.get_date_string(), attr)
3.188 + s = None
3.189 + if with_time:
3.190 + s = self.get_datetime_string()
3.191 + attr = self.get_attributes(True)
3.192 + if not s:
3.193 + s = self.get_date_string()
3.194 + attr = self.get_attributes(False)
3.195 + if not s:
3.196 + return None, None
3.197 + return s, attr
3.198
3.199 - if dt:
3.200 - return dt, attr
3.201 + def get_attributes(self, with_time=True):
3.202 +
3.203 + "Return attributes for the date or datetime represented by this object."
3.204
3.205 - return None
3.206 + if with_time:
3.207 + return {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"}
3.208 + else:
3.209 + return {"VALUE" : "DATE"}
3.210
3.211 def end_date_to_calendar(dt):
3.212
3.213 @@ -309,7 +329,9 @@
3.214 elif isinstance(period, FormPeriod):
3.215 return period.as_event_period()
3.216 else:
3.217 - return EventPeriod(period.start, period.end, period.start_attr, period.end_attr, origin=period.origin)
3.218 + dtstart, dtstart_attr = period.get_start_item()
3.219 + dtend, dtend_attr = period.get_end_item()
3.220 + return EventPeriod(dtstart, dtend, dtstart_attr, dtend_attr, origin=period.origin)
3.221
3.222 def form_period_from_period(period):
3.223 if isinstance(period, EventPeriod):
4.1 --- a/imipweb/event.py Fri May 15 22:09:02 2015 +0200
4.2 +++ b/imipweb/event.py Sat May 16 01:03:51 2015 +0200
4.3 @@ -24,9 +24,9 @@
4.4 from imiptools.data import get_uri, uri_dict, uri_values
4.5 from imiptools.dates import format_datetime, to_date, get_datetime, \
4.6 get_datetime_item, get_period_item, \
4.7 - to_timezone
4.8 + to_timezone, to_utc_datetime
4.9 from imiptools.mail import Messenger
4.10 -from imiptools.period import have_conflict
4.11 +from imiptools.period import convert_periods, have_conflict
4.12 from imipweb.data import EventPeriod, \
4.13 event_period_from_period, form_period_from_period, \
4.14 FormDate, FormPeriod, PeriodError
4.15 @@ -64,6 +64,17 @@
4.16 def is_organiser(self, obj):
4.17 return get_uri(obj.get_value("ORGANIZER")) == self.user
4.18
4.19 + def get_recurrence_key(self, period):
4.20 + return format_datetime(to_utc_datetime(period.get_start(), self.get_tzid()))
4.21 +
4.22 + def is_replaced(self, period, recurrenceids):
4.23 + start_utc = self.get_recurrence_key(period)
4.24 + return recurrenceids and start_utc in recurrenceids and "replaced" or ""
4.25 +
4.26 + def is_affected(self, period, recurrenceid):
4.27 + start_utc = self.get_recurrence_key(period)
4.28 + return recurrenceid and start_utc == recurrenceid and "affected" or ""
4.29 +
4.30 # Request logic methods.
4.31
4.32 def handle_request(self, uid, recurrenceid, obj):
4.33 @@ -526,8 +537,7 @@
4.34
4.35 recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID"))
4.36 recurrenceids = self._get_recurrences(uid)
4.37 - start_utc = format_datetime(to_timezone(p.get_start(), "UTC"))
4.38 - replaced = not recurrenceid and recurrenceids and start_utc in recurrenceids
4.39 + replaced = self.is_replaced(p, recurrenceids)
4.40
4.41 # Provide a summary of the object.
4.42
4.43 @@ -570,7 +580,7 @@
4.44
4.45 elif name == "DTSTART":
4.46 page.td(class_="objectvalue %s replaced" % field, rowspan=2)
4.47 - page.a("First occurrence replaced by a separate event", href=self.link_to(uid, start_utc))
4.48 + page.a("First occurrence replaced by a separate event", href=self.link_to(uid, self.get_recurrence_key(p)))
4.49 page.td.close()
4.50
4.51 page.tr.close()
4.52 @@ -778,6 +788,9 @@
4.53
4.54 sequence = obj.get_value("SEQUENCE")
4.55
4.56 + p = event_period_from_period(period)
4.57 + replaced = self.is_replaced(p, recurrenceids)
4.58 +
4.59 # Isolate the controls from neighbouring tables.
4.60
4.61 page.div()
4.62 @@ -801,18 +814,21 @@
4.63
4.64 # Permit the removal of recurrences.
4.65
4.66 - page.tr()
4.67 - page.th("")
4.68 - page.td()
4.69 + if not replaced:
4.70 + page.tr()
4.71 + page.th("")
4.72 + page.td()
4.73
4.74 - remove_type = sequence is None or not period.origin and "submit" or "checkbox"
4.75 - self._control("recur-remove", remove_type, str(index), str(index) in args.get("recur-remove", []), id="recur-remove-%d" % index, class_="remove")
4.76 + remove_type = sequence is None or not period.origin and "submit" or "checkbox"
4.77 + self._control("recur-remove", remove_type, str(index),
4.78 + str(index) in args.get("recur-remove", []),
4.79 + id="recur-remove-%d" % index, class_="remove")
4.80
4.81 - page.label("Remove", for_="recur-remove-%d" % index, class_="remove")
4.82 - page.label("Removed", for_="recur-remove-%d" % index, class_="removed")
4.83 + page.label("Remove", for_="recur-remove-%d" % index, class_="remove")
4.84 + page.label("Removed", for_="recur-remove-%d" % index, class_="removed")
4.85
4.86 - page.td.close()
4.87 - page.tr.close()
4.88 + page.td.close()
4.89 + page.tr.close()
4.90
4.91 page.tbody.close()
4.92 page.table.close()
4.93 @@ -828,6 +844,7 @@
4.94
4.95 page = self.page
4.96 recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID"))
4.97 + recurrenceids = self._get_recurrences(uid)
4.98
4.99 # Obtain the user's timezone.
4.100
4.101 @@ -855,6 +872,10 @@
4.102 # Show any conflicts with periods of actual attendance.
4.103
4.104 for p in have_conflict(freebusy, periods, True):
4.105 + period = event_period_from_period(p)
4.106 + convert_periods([period], tzid)
4.107 + if self.is_replaced(period, recurrenceids):
4.108 + continue
4.109 if (p.uid != uid or (recurrenceid and p.recurrenceid) and p.recurrenceid != recurrenceid) and p.transp != "ORG":
4.110 conflicts.append(p)
4.111
4.112 @@ -990,9 +1011,8 @@
4.113 # Show a label as attendee.
4.114
4.115 else:
4.116 - t = formdate.as_datetime_item()
4.117 - if t:
4.118 - dt, attr = t
4.119 + dt = formdate.as_datetime()
4.120 + if dt:
4.121 page.td(self.format_datetime(dt, "full"))
4.122 else:
4.123 page.td("(Unrecognised date)")
4.124 @@ -1015,9 +1035,7 @@
4.125 ssn = self._simple_suffixed_name
4.126
4.127 p = event_period_from_period(period)
4.128 -
4.129 - start_utc = format_datetime(to_timezone(p.get_start(), "UTC"))
4.130 - replaced = recurrenceids and start_utc in recurrenceids and "replaced" or ""
4.131 + replaced = self.is_replaced(p, recurrenceids)
4.132
4.133 # Show controls for editing as organiser.
4.134
4.135 @@ -1072,18 +1090,16 @@
4.136 page = self.page
4.137
4.138 p = event_period_from_period(period)
4.139 + replaced = self.is_replaced(p, recurrenceids)
4.140
4.141 - start_utc = format_datetime(to_timezone(p.get_start(), "UTC"))
4.142 - replaced = recurrenceids and start_utc in recurrenceids and "replaced" or ""
4.143 css = " ".join([
4.144 replaced,
4.145 - recurrenceid and start_utc == recurrenceid and "affected" or ""
4.146 + self.is_affected(p, recurrenceid)
4.147 ])
4.148
4.149 formdate = show_start and p.get_form_start() or p.get_form_end()
4.150 - t = formdate.as_datetime_item()
4.151 - if t:
4.152 - dt, attr = t
4.153 + dt = formdate.as_datetime()
4.154 + if dt:
4.155 page.td(self.format_datetime(dt, "long"), class_=css)
4.156 else:
4.157 page.td("(Unrecognised date)")
4.158 @@ -1172,10 +1188,8 @@
4.159
4.160 # Show dates for up to one week around the current date.
4.161
4.162 - t = default.as_datetime_item()
4.163 - if t:
4.164 - dt, attr = t
4.165 - else:
4.166 + dt = default.as_datetime()
4.167 + if not dt:
4.168 dt = date.today()
4.169
4.170 base = to_date(dt)