# HG changeset patch # User Paul Boddie # Date 1431731031 -7200 # Node ID 07ab41a7f8cf77ecbc59ef4268cf9ec6d5c9d1c3 # Parent cc917b4539c40b661a0ca263e9d2f67ff9481c93 Made period abstractions more consistent. Indicate cancelled recurrences and make sure that they are not considered as conflicting with other event periods. diff -r cc917b4539c4 -r 07ab41a7f8cf imiptools/data.py --- a/imiptools/data.py Fri May 15 22:09:02 2015 +0200 +++ b/imiptools/data.py Sat May 16 01:03:51 2015 +0200 @@ -338,6 +338,8 @@ value, attr = t return get_datetime(value, attr), attr +# Conversion functions. + def get_addresses(values): return [address for name, address in email.utils.getaddresses(values)] @@ -400,11 +402,16 @@ "A period with origin information from the object." def __init__(self, start, end, origin, start_attr=None, end_attr=None): - Period.__init__(self, start, end) - self.origin = origin + Period.__init__(self, start, end, origin) self.start_attr = start_attr self.end_attr = end_attr + def get_start_item(self): + return self.start, self.start_attr + + def get_end_item(self): + return self.end, self.end_attr + def get_tzid(self): return get_tzid(self.start_attr, self.end_attr) diff -r cc917b4539c4 -r 07ab41a7f8cf imiptools/period.py --- a/imiptools/period.py Fri May 15 22:09:02 2015 +0200 +++ b/imiptools/period.py Sat May 16 01:03:51 2015 +0200 @@ -21,15 +21,17 @@ from bisect import bisect_left, bisect_right, insort_left from datetime import datetime, timedelta -from imiptools.dates import get_datetime, get_start_of_day, to_timezone +from imiptools.dates import get_datetime, get_datetime_attributes, \ + get_start_of_day, to_timezone class Period: "A basic period abstraction." - def __init__(self, start, end=None): + def __init__(self, start, end=None, origin=None): self.start = start self.end = end + self.origin = origin def as_tuple(self): return self.start, self.end @@ -49,6 +51,20 @@ def __repr__(self): return "Period(%r, %r)" % (self.start, self.end) + # Datetime metadata methods. + + def get_start(self): + return self.start + + def get_end(self): + return self.end + + def get_start_item(self): + return self.start, get_datetime_attributes(self.start) + + def get_end_item(self): + return self.end, get_datetime_attributes(self.end) + class FreeBusyPeriod(Period): "A free/busy record abstraction." diff -r cc917b4539c4 -r 07ab41a7f8cf imipweb/data.py --- a/imipweb/data.py Fri May 15 22:09:02 2015 +0200 +++ b/imipweb/data.py Sat May 16 01:03:51 2015 +0200 @@ -35,12 +35,19 @@ """ def __init__(self, start, end, start_attr=None, end_attr=None, form_start=None, form_end=None, origin=None): - Period.__init__(self, start, end) + + """ + Initialise a period with the given 'start' and 'end' datetimes, together + with optional 'start_attr' and 'end_attr' metadata, 'form_start' and + 'form_end' values provided as textual input, and with an optional + 'origin' indicating the kind of period this object describes. + """ + + Period.__init__(self, start, end, origin) self.start_attr = start_attr self.end_attr = end_attr self.form_start = form_start self.form_end = form_end - self.origin = origin def as_tuple(self): return self.start, self.end, self.start_attr, self.end_attr, self.form_start, self.form_end, self.origin @@ -62,6 +69,12 @@ def get_tzid(self): return get_tzid(self.start_attr, self.end_attr) + def get_start_item(self): + return self.start, self.start_attr + + def get_end_item(self): + return self.end, self.end_attr + # Form data compatibility methods. def get_form_start(self): @@ -110,52 +123,50 @@ return "FormPeriod(%r, %r, %r, %r, %r)" % self.as_tuple() def _get_start(self): - t = self.start.as_datetime_item(self.times_enabled) - if t: - return t - else: - return None + return self.start.as_datetime(self.times_enabled), self.start.get_attributes(self.times_enabled) def _get_end(self, adjust=False): # Handle specified end datetimes. if self.end_enabled: - t = self.end.as_datetime_item(self.times_enabled) - if t: - dtend, dtend_attr = t + dtend = self.end.as_datetime(self.times_enabled) + dtend_attr = self.end.get_attributes(self.times_enabled) + if dtend: dtend = adjust and end_date_to_calendar(dtend) or dtend else: - return None + return None, None # Otherwise, treat the end date as the start date. Datetimes are # handled by making the event occupy the rest of the day. else: - t = self._get_start() - if t: - dtstart, dtstart_attr = t + dtstart, dtstart_attr = self._get_start() + if dtstart: dtend = dtstart + timedelta(1) dtend_attr = dtstart_attr if isinstance(dtstart, datetime): dtend = get_start_of_day(dtend, dtend_attr["TZID"]) else: - return None + return None, None return dtend, dtend_attr def as_event_period(self, index=None): - t = self._get_start() - if t: - dtstart, dtstart_attr = t - else: + + """ + Return a converted version of this object as an event period suitable + for iCalendar usage. If 'index' is indicated, include it in any error + raised in the conversion process. + """ + + dtstart, dtstart_attr = self._get_start() + if not dtstart: raise PeriodError(*[index is not None and ("dtstart", index) or "dtstart"]) - t = self._get_end(adjust=True) - if t: - dtend, dtend_attr = t - else: + dtend, dtend_attr = self._get_end(adjust=True) + if not dtend: raise PeriodError(*[index is not None and ("dtend", index) or "dtend"]) if dtstart > dtend: @@ -169,20 +180,18 @@ # Period data methods. def get_start(self): - t = self._get_start() - if t: - dtstart, dtstart_attr = t - return dtstart - else: - return None + dtstart, dtstart_attr = self._get_start() + return dtstart def get_end(self): - t = self._get_end() - if t: - dtend, dtend_attr = t - return dtend - else: - return None + dtend, dtend_attr = self._get_end() + return dtend + + def get_start_item(self): + return self._get_start() + + def get_end_item(self): + return self._get_end() # Form data methods. @@ -245,39 +254,50 @@ def get_tzid(self): return self.tzid - def as_datetime_item(self, with_time=True): + def as_datetime(self, with_time=True): - """ - Return a (datetime, attr) tuple for the datetime information provided by - this object, or None if the fields cannot be used to construct a - datetime object. - """ + "Return a datetime for this object." # Return any original datetime details. if self.dt: - return self.dt, self.attr + return self.dt - # Otherwise, construct a datetime and attributes. + # Otherwise, construct a datetime. - if not self.date: - return None - elif with_time: - attr = {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"} - dt = get_datetime(self.get_datetime_string(), attr) + s, attr = self.as_datetime_item(with_time) + if s: + return get_datetime(s, attr) else: - dt = None + return None + + def as_datetime_item(self, with_time=True): - # Interpret incomplete datetimes as dates. + """ + Return a (datetime string, attr) tuple for the datetime information + provided by this object, where both tuple elements will be None if no + suitable date or datetime information exists. + """ - if not dt: - attr = {"VALUE" : "DATE"} - dt = get_datetime(self.get_date_string(), attr) + s = None + if with_time: + s = self.get_datetime_string() + attr = self.get_attributes(True) + if not s: + s = self.get_date_string() + attr = self.get_attributes(False) + if not s: + return None, None + return s, attr - if dt: - return dt, attr + def get_attributes(self, with_time=True): + + "Return attributes for the date or datetime represented by this object." - return None + if with_time: + return {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"} + else: + return {"VALUE" : "DATE"} def end_date_to_calendar(dt): @@ -309,7 +329,9 @@ elif isinstance(period, FormPeriod): return period.as_event_period() else: - return EventPeriod(period.start, period.end, period.start_attr, period.end_attr, origin=period.origin) + dtstart, dtstart_attr = period.get_start_item() + dtend, dtend_attr = period.get_end_item() + return EventPeriod(dtstart, dtend, dtstart_attr, dtend_attr, origin=period.origin) def form_period_from_period(period): if isinstance(period, EventPeriod): diff -r cc917b4539c4 -r 07ab41a7f8cf imipweb/event.py --- a/imipweb/event.py Fri May 15 22:09:02 2015 +0200 +++ b/imipweb/event.py Sat May 16 01:03:51 2015 +0200 @@ -24,9 +24,9 @@ from imiptools.data import get_uri, uri_dict, uri_values from imiptools.dates import format_datetime, to_date, get_datetime, \ get_datetime_item, get_period_item, \ - to_timezone + to_timezone, to_utc_datetime from imiptools.mail import Messenger -from imiptools.period import have_conflict +from imiptools.period import convert_periods, have_conflict from imipweb.data import EventPeriod, \ event_period_from_period, form_period_from_period, \ FormDate, FormPeriod, PeriodError @@ -64,6 +64,17 @@ def is_organiser(self, obj): return get_uri(obj.get_value("ORGANIZER")) == self.user + def get_recurrence_key(self, period): + return format_datetime(to_utc_datetime(period.get_start(), self.get_tzid())) + + def is_replaced(self, period, recurrenceids): + start_utc = self.get_recurrence_key(period) + return recurrenceids and start_utc in recurrenceids and "replaced" or "" + + def is_affected(self, period, recurrenceid): + start_utc = self.get_recurrence_key(period) + return recurrenceid and start_utc == recurrenceid and "affected" or "" + # Request logic methods. def handle_request(self, uid, recurrenceid, obj): @@ -526,8 +537,7 @@ recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID")) recurrenceids = self._get_recurrences(uid) - start_utc = format_datetime(to_timezone(p.get_start(), "UTC")) - replaced = not recurrenceid and recurrenceids and start_utc in recurrenceids + replaced = self.is_replaced(p, recurrenceids) # Provide a summary of the object. @@ -570,7 +580,7 @@ elif name == "DTSTART": page.td(class_="objectvalue %s replaced" % field, rowspan=2) - page.a("First occurrence replaced by a separate event", href=self.link_to(uid, start_utc)) + page.a("First occurrence replaced by a separate event", href=self.link_to(uid, self.get_recurrence_key(p))) page.td.close() page.tr.close() @@ -778,6 +788,9 @@ sequence = obj.get_value("SEQUENCE") + p = event_period_from_period(period) + replaced = self.is_replaced(p, recurrenceids) + # Isolate the controls from neighbouring tables. page.div() @@ -801,18 +814,21 @@ # Permit the removal of recurrences. - page.tr() - page.th("") - page.td() + if not replaced: + page.tr() + page.th("") + page.td() - remove_type = sequence is None or not period.origin and "submit" or "checkbox" - self._control("recur-remove", remove_type, str(index), str(index) in args.get("recur-remove", []), id="recur-remove-%d" % index, class_="remove") + remove_type = sequence is None or not period.origin and "submit" or "checkbox" + self._control("recur-remove", remove_type, str(index), + str(index) in args.get("recur-remove", []), + id="recur-remove-%d" % index, class_="remove") - page.label("Remove", for_="recur-remove-%d" % index, class_="remove") - page.label("Removed", for_="recur-remove-%d" % index, class_="removed") + page.label("Remove", for_="recur-remove-%d" % index, class_="remove") + page.label("Removed", for_="recur-remove-%d" % index, class_="removed") - page.td.close() - page.tr.close() + page.td.close() + page.tr.close() page.tbody.close() page.table.close() @@ -828,6 +844,7 @@ page = self.page recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID")) + recurrenceids = self._get_recurrences(uid) # Obtain the user's timezone. @@ -855,6 +872,10 @@ # Show any conflicts with periods of actual attendance. for p in have_conflict(freebusy, periods, True): + period = event_period_from_period(p) + convert_periods([period], tzid) + if self.is_replaced(period, recurrenceids): + continue if (p.uid != uid or (recurrenceid and p.recurrenceid) and p.recurrenceid != recurrenceid) and p.transp != "ORG": conflicts.append(p) @@ -990,9 +1011,8 @@ # Show a label as attendee. else: - t = formdate.as_datetime_item() - if t: - dt, attr = t + dt = formdate.as_datetime() + if dt: page.td(self.format_datetime(dt, "full")) else: page.td("(Unrecognised date)") @@ -1015,9 +1035,7 @@ ssn = self._simple_suffixed_name p = event_period_from_period(period) - - start_utc = format_datetime(to_timezone(p.get_start(), "UTC")) - replaced = recurrenceids and start_utc in recurrenceids and "replaced" or "" + replaced = self.is_replaced(p, recurrenceids) # Show controls for editing as organiser. @@ -1072,18 +1090,16 @@ page = self.page p = event_period_from_period(period) + replaced = self.is_replaced(p, recurrenceids) - start_utc = format_datetime(to_timezone(p.get_start(), "UTC")) - replaced = recurrenceids and start_utc in recurrenceids and "replaced" or "" css = " ".join([ replaced, - recurrenceid and start_utc == recurrenceid and "affected" or "" + self.is_affected(p, recurrenceid) ]) formdate = show_start and p.get_form_start() or p.get_form_end() - t = formdate.as_datetime_item() - if t: - dt, attr = t + dt = formdate.as_datetime() + if dt: page.td(self.format_datetime(dt, "long"), class_=css) else: page.td("(Unrecognised date)") @@ -1172,10 +1188,8 @@ # Show dates for up to one week around the current date. - t = default.as_datetime_item() - if t: - dt, attr = t - else: + dt = default.as_datetime() + if not dt: dt = date.today() base = to_date(dt)