# HG changeset patch # User Paul Boddie # Date 1428353843 -7200 # Node ID 54c8876a02c48503bb1dedfe4320ecf648263141 # Parent edc4e54a5dbf0e787198fe6b41fe8f170f3d8986 Introduced abstractions for form dates and periods, making the recurrence period details specified in the form separate from the stored details, and providing conversions from and to a suitable representation for updating the storage. diff -r edc4e54a5dbf -r 54c8876a02c4 imipweb/data.py --- a/imipweb/data.py Mon Apr 06 16:39:52 2015 +0200 +++ b/imipweb/data.py Mon Apr 06 22:57:23 2015 +0200 @@ -20,93 +20,275 @@ """ from datetime import datetime, timedelta -from imiptools.dates import get_datetime, get_start_of_day +from imiptools.dates import format_datetime, get_datetime, get_start_of_day, to_date from imiptools.period import Period +class PeriodError(Exception): + pass + class EventPeriod(Period): - "A simple period plus attribute details, compatible with RecurringPeriod." + """ + A simple period plus attribute details, compatible with RecurringPeriod, and + intended to represent information obtained from an iCalendar resource. + """ - def __init__(self, start, end, start_attr=None, end_attr=None): + def __init__(self, start, end, start_attr=None, end_attr=None, form_start=None, form_end=None): Period.__init__(self, start, end) self.start_attr = start_attr self.end_attr = end_attr + self.form_start = form_start + self.form_end = form_end + + def as_tuple(self): + return self.start, self.end, self.start_attr, self.end_attr, self.form_start, self.form_end + + def __repr__(self): + return "EventPeriod(%r, %r, %r, %r, %r, %r)" % self.as_tuple() + + def get_start(self): + return self.start + + def get_end(self): + return self.end + + def as_event_period(self): + return self + + def get_form_start(self): + if not self.form_start: + self.form_start = self.get_form_date(self.start, self.start_attr) + return self.form_start + + def get_form_end(self): + if not self.form_end: + self.form_end = self.get_form_date(self.end, self.end_attr) + return self.form_end + + def as_form_period(self): + return FormPeriod( + self.get_form_date(self.start, self.start_attr), + self.get_form_date(self.end, self.end_attr), + isinstance(self.end, datetime) or self.start != self.end - timedelta(1), + isinstance(self.start, datetime) or isinstance(self.end, datetime) + ) + + def get_form_date(self, dt, attr=None): + return FormDate( + format_datetime(to_date(dt)), + isinstance(dt, datetime) and str(dt.hour) or None, + isinstance(dt, datetime) and str(dt.minute) or None, + isinstance(dt, datetime) and str(dt.second) or None, + attr and attr.get("TZID") or None, + dt, attr + ) + +def event_period_from_recurrence_period(period): + return EventPeriod(period.start, period.end, period.start_attr, period.end_attr) + +class FormPeriod: + + "A period whose information originates from a form." + + def __init__(self, start, end, end_enabled=True, times_enabled=True): + self.start = start + self.end = end + self.end_enabled = end_enabled + self.times_enabled = times_enabled def as_tuple(self): - return self.start, self.end, self.start_attr, self.end_attr + return self.start, self.end, self.end_enabled, self.times_enabled def __repr__(self): - return "EventPeriod(%r, %r, %r, %r)" % self.as_tuple() + return "FormPeriod(%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 + + def _get_end(self): + + # Handle specified end datetimes. + + if self.end_enabled: + t = self.end.as_datetime_item(self.times_enabled) + if t: + dtend, dtend_attr = t + else: + return 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 + 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 dtend, dtend_attr + + def get_start(self): + t = self._get_start() + if t: + dtstart, dtstart_attr = t + return dtstart + else: + return None + + def get_end(self): + t = self._get_end() + if t: + dtend, dtend_attr = t + return dtend + else: + return None + + def as_event_period(self, index=None): + t = self._get_start() + if t: + dtstart, dtstart_attr = t + else: + raise PeriodError(*[index is not None and ("dtstart", index) or "dtstart"]) + + t = self._get_end() + if t: + dtend, dtend_attr = t + else: + raise PeriodError(*[index is not None and ("dtend", index) or "dtend"]) + + if dtstart > dtend: + raise PeriodError(*[ + index is not None and ("dtstart", index) or "dtstart", + index is not None and ("dtend", index) or "dtend" + ]) + + return EventPeriod(dtstart, dtend, dtstart_attr, dtend_attr, self.start, self.end) + + def get_form_start(self): + return self.start + + def get_form_end(self): + return self.end + + def as_form_period(self): + return self -def handle_date_control_values(values, with_time=True): +class FormDate: + + "Date information originating from form information." + + def __init__(self, date=None, hour=None, minute=None, second=None, tzid=None, dt=None, attr=None): + self.date = date + self.hour = hour + self.minute = minute + self.second = second + self.tzid = tzid + self.dt = dt + self.attr = attr + + def as_tuple(self): + return self.date, self.hour, self.minute, self.second, self.tzid, self.dt, self.attr + + def __repr__(self): + return "FormDate(%r, %r, %r, %r, %r, %r, %r)" % self.as_tuple() + + def get_component(self, value): + return (value or "").rjust(2, "0")[:2] + + def get_hour(self): + return self.get_component(self.hour) + + def get_minute(self): + return self.get_component(self.minute) + + def get_second(self): + return self.get_component(self.second) + + def get_date_string(self): + return self.date or "" + + def get_datetime_string(self): + if not self.date: + return "" + + hour = self.hour; minute = self.minute; second = self.second + + if hour or minute or second: + time = "T%s%s%s" % tuple(map(self.get_component, (hour, minute, second))) + else: + time = "" + + return "%s%s" % (self.date, time) + + def get_tzid(self): + return self.tzid + + def as_datetime_item(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 any original datetime details. + + if self.dt: + return self.dt, self.attr + + # Otherwise, construct a datetime and attributes. + + 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) + else: + dt = None + + # Interpret incomplete datetimes as dates. + + if not dt: + attr = {"VALUE" : "DATE"} + dt = get_datetime(self.get_date_string(), attr) + + if dt: + return dt, attr + + return None + +def end_date_to_calendar(dt): """ - Handle date control information for the given 'values', returning a - (datetime, attr) tuple, or None if the fields cannot be used to - construct a datetime object. - """ - - if not values or not values["date"]: - return None - elif with_time: - value = "%s%s" % (values["date"], values["time"]) - attr = {"TZID" : values["tzid"], "VALUE" : "DATE-TIME"} - dt = get_datetime(value, attr) - else: - attr = {"VALUE" : "DATE"} - dt = get_datetime(values["date"]) - - if dt: - return dt, attr - - return None - -def handle_period_controls(start_values, end_values, dtend_enabled, dttimes_enabled, index=None): - - """ - Handle datetime controls for a particular period, described by the given - 'start_values' and 'end_values', with 'dtend_enabled' and - 'dttimes_enabled' affecting the usage of the provided values. - - If 'index' is specified, incorporate it into any error indicator. + Change end dates to refer to the actual dates, not the iCalendar "next day" + dates. """ - t = handle_date_control_values(start_values, dttimes_enabled) - if t: - dtstart, dtstart_attr = t + if not isinstance(dt, datetime): + return dt + timedelta(1) else: - return None, [index is not None and ("dtstart", index) or "dtstart"] + return dt - # Handle specified end datetimes. - - if dtend_enabled: - t = handle_date_control_values(end_values, dttimes_enabled) - if t: - dtend, dtend_attr = t - - # Convert end dates to iCalendar "next day" dates. +def end_date_from_calendar(dt): - if not isinstance(dtend, datetime): - dtend += timedelta(1) - else: - return None, [index is not None and ("dtend", index) or "dtend"] + """ + Change end dates to refer to the actual dates, not the iCalendar "next day" + dates. + """ - # Otherwise, treat the end date as the start date. Datetimes are - # handled by making the event occupy the rest of the day. - + if not isinstance(dt, datetime): + return dt - timedelta(1) else: - dtend = dtstart + timedelta(1) - dtend_attr = dtstart_attr - - if isinstance(dtstart, datetime): - dtend = get_start_of_day(dtend, attr["TZID"]) - - if dtstart > dtend: - return None, [ - index is not None and ("dtstart", index) or "dtstart", - index is not None and ("dtend", index) or "dtend" - ] - - return EventPeriod(dtstart, dtend, dtstart_attr, dtend_attr), None + return dt # vim: tabstop=4 expandtab shiftwidth=4 diff -r edc4e54a5dbf -r 54c8876a02c4 imipweb/event.py --- a/imipweb/event.py Mon Apr 06 16:39:52 2015 +0200 +++ b/imipweb/event.py Mon Apr 06 22:57:23 2015 +0200 @@ -19,7 +19,7 @@ this program. If not, see . """ -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from imiptools.client import update_attendees, update_participation from imiptools.data import get_uri, uri_dict, uri_values from imiptools.dates import format_datetime, to_date, get_datetime, \ @@ -27,8 +27,10 @@ to_timezone from imiptools.mail import Messenger from imiptools.period import have_conflict -from imipweb.data import EventPeriod, handle_date_control_values, \ - handle_period_controls +from imipweb.data import EventPeriod, \ + end_date_from_calendar, end_date_to_calendar, \ + event_period_from_recurrence_period, \ + FormDate, FormPeriod, PeriodError from imipweb.handler import ManagerHandler from imipweb.resource import Resource import pytz @@ -109,13 +111,15 @@ # Update time periods (main and recurring). - period, errors = self.handle_main_period() - if errors: - return errors + try: + period = self.handle_main_period() + except PeriodError, exc: + return exc.args - periods, errors = self.handle_recurrence_periods() - if errors: - return errors + try: + periods = self.handle_recurrence_periods() + except PeriodError, exc: + return exc.args self.set_period_in_object(obj, period) self.set_periods_in_object(obj, periods) @@ -180,49 +184,62 @@ return None + def set_period_in_object(self, obj, period): + + "Set in the given 'obj' the given 'period' as the main start and end." + + p = period.as_event_period() + result = self.set_datetime_in_object(p.start, p.start_attr and p.start_attr.get("TZID"), "DTSTART", obj) + result = self.set_datetime_in_object(end_date_to_calendar(p.end), p.end_attr and p.end_attr.get("TZID"), "DTEND", obj) or result + return result + + def set_periods_in_object(self, obj, periods): + + "Set in the given 'obj' the given 'periods'." + + update = False + + old_values = obj.get_values("RDATE") + new_rdates = [] + + if obj.has_key("RDATE"): + del obj["RDATE"] + + for period in periods: + p = period.as_event_period() + tzid = p.start_attr and p.start_attr.get("TZID") or p.end_attr and p.end_attr.get("TZID") + new_rdates.append(get_period_item(p.start, end_date_to_calendar(p.end), tzid)) + + obj["RDATE"] = new_rdates + + # NOTE: To do: calculate the update status. + return update + + def set_datetime_in_object(self, dt, tzid, property, obj): + + """ + Set 'dt' and 'tzid' for the given 'property' in 'obj', returning whether + an update has occurred. + """ + + if dt: + old_value = obj.get_value(property) + obj[property] = [get_datetime_item(dt, tzid)] + return format_datetime(dt) != old_value + + return False + def handle_main_period(self): "Return period details for the main start/end period in an event." - args = self.env.get_args() - - dtend_enabled = args.get("dtend-control", [None])[0] - dttimes_enabled = args.get("dttimes-control", [None])[0] - start_values = self.get_date_control_values("dtstart") - end_values = self.get_date_control_values("dtend") - - period, errors = handle_period_controls(start_values, end_values, dtend_enabled, dttimes_enabled) - - if errors: - return None, errors - else: - return period, errors + return self.get_main_period().as_event_period() def handle_recurrence_periods(self): "Return period details for the recurrences specified for an event." - args = self.env.get_args() - - all_dtend_enabled = args.get("dtend-control-recur", []) - all_dttimes_enabled = args.get("dttimes-control-recur", []) - all_start_values = self.get_date_control_values("dtstart-recur", multiple=True) - all_end_values = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur") - - periods = [] - errors = [] - - for index, (start_values, end_values, dtend_enabled, dttimes_enabled) in \ - enumerate(map(None, all_start_values, all_end_values, all_dtend_enabled, all_dttimes_enabled)): - - dtend_enabled = str(index) in all_dtend_enabled - dttimes_enabled = str(index) in all_dttimes_enabled - period, _errors = handle_period_controls(start_values, end_values, dtend_enabled, dttimes_enabled, index) - - periods.append(period) - errors += _errors - - return periods, errors + return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences())] def get_date_control_values(self, name, multiple=False, tzid_name=None): @@ -245,90 +262,40 @@ # Handle absent values by employing None values. field_values = map(None, dates, hours, minutes, seconds, tzids) - if not field_values and not multiple: - field_values = [(None, None, None, None, None)] - - all_values = [] - - for date, hour, minute, second, tzid in field_values: - - # Construct a usable dictionary of values. - if hour or minute or second: - hour = (hour or "").rjust(2, "0")[:2] - minute = (minute or "").rjust(2, "0")[:2] - second = (second or "").rjust(2, "0")[:2] - time = "T%s%s%s" % (hour, minute, second) - else: - hour = minute = second = time = "" + if not field_values and not multiple: + all_values = FormDate() + else: + all_values = [] + for date, hour, minute, second, tzid in field_values: + value = FormDate(date, hour, minute, second, tzid or self.get_tzid()) - value = { - "date" : date, "time" : time, - "tzid" : tzid or self.get_tzid(), - "hour" : hour, "minute" : minute, "second" : second - } + # Return a single value or append to a collection of all values. - # Return a single value or append to a collection of all values. - - if not multiple: - return value - else: - all_values.append(value) + if not multiple: + return value + else: + all_values.append(value) return all_values - def set_period_in_object(self, obj, period): - - "Set in the given 'obj' the given 'period' as the main start and end." - - p = period - result = self.set_datetime_in_object(p.start, p.start_attr and p.start_attr.get("TZID"), "DTSTART", obj) - result = self.set_datetime_in_object(p.end, p.end_attr and p.end_attr.get("TZID"), "DTEND", obj) or result - return result - - def set_periods_in_object(self, obj, periods): - - "Set in the given 'obj' the given 'periods'." - - update = False + def get_current_main_period(self, obj): + args = self.env.get_args() + initial_load = not args.has_key("editing") - old_values = obj.get_values("RDATE") - new_rdates = [] - - if obj.has_key("RDATE"): - del obj["RDATE"] + if initial_load or not self.is_organiser(obj): + return self.get_existing_main_period(obj) + else: + return self.get_main_period() - for p in periods: - tzid = p.start_attr and p.start_attr.get("TZID") or p.end_attr and p.end_attr.get("TZID") - new_rdates.append(get_period_item(p.start, p.end, tzid)) - - obj["RDATE"] = new_rdates - - # NOTE: To do: calculate the update status. - return update - - def set_datetime_in_object(self, dt, tzid, property, obj): + def get_existing_main_period(self, obj): """ - Set 'dt' and 'tzid' for the given 'property' in 'obj', returning whether - an update has occurred. - """ - - if dt: - old_value = obj.get_value(property) - obj[property] = [get_datetime_item(dt, tzid)] - return format_datetime(dt) != old_value - - return False - - def get_event_period(self, obj): - - """ - Return (dtstart, dtstart attributes), (dtend, dtend attributes) for - 'obj'. + Return the main event period for the given 'obj'. """ dtstart, dtstart_attr = obj.get_datetime_item("DTSTART") + if obj.has_key("DTEND"): dtend, dtend_attr = obj.get_datetime_item("DTEND") elif obj.has_key("DURATION"): @@ -337,7 +304,60 @@ dtend_attr = dtstart_attr else: dtend, dtend_attr = dtstart, dtstart_attr - return (dtstart, dtstart_attr), (dtend, dtend_attr) + + return EventPeriod(dtstart, end_date_from_calendar(dtend), dtstart_attr, dtend_attr) + + def get_main_period(self): + + "Return the main period defined in the event form." + + args = self.env.get_args() + + dtend_enabled = args.get("dtend-control", [None])[0] + dttimes_enabled = args.get("dttimes-control", [None])[0] + start = self.get_date_control_values("dtstart") + end = self.get_date_control_values("dtend") + + return FormPeriod(start, end, dtend_enabled, dttimes_enabled) + + def get_current_recurrences(self, obj): + args = self.env.get_args() + initial_load = not args.has_key("editing") + + if initial_load or not self.is_organiser(obj): + return self.get_existing_recurrences(obj) + else: + return self.get_recurrences() + + def get_existing_recurrences(self, obj): + recurrences = [] + for period in obj.get_periods(self.get_tzid(), self.get_window_end()): + if period.origin == "RDATE": + recurrences.append(event_period_from_recurrence_period(period)) + return recurrences + + def get_recurrences(self): + + "Return the recurrences defined in the event form." + + args = self.env.get_args() + + all_dtend_enabled = args.get("dtend-control-recur", []) + all_dttimes_enabled = args.get("dttimes-control-recur", []) + all_starts = self.get_date_control_values("dtstart-recur", multiple=True) + all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur") + + periods = [] + + for index, (start, end, dtend_enabled, dttimes_enabled) in \ + enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled)): + + dtend_enabled = str(index) in all_dtend_enabled + dttimes_enabled = str(index) in all_dttimes_enabled + period = FormPeriod(start, end, dtend_enabled, dttimes_enabled) + periods.append(period) + + return periods def get_current_attendees(self, obj): @@ -486,14 +506,14 @@ else: attendees = self.update_attendees(obj) - (dtstart, dtstart_attr), (dtend, dtend_attr) = self.get_event_period(obj) - self.show_object_datetime_controls(dtstart, dtend) + p = self.get_current_main_period(obj) + self.show_object_datetime_controls(p) # Obtain any separate recurrences for this event. recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID")) recurrenceids = self._get_recurrences(uid) - start_utc = format_datetime(to_timezone(dtstart, "UTC")) + start_utc = format_datetime(to_timezone(p.get_start(), "UTC")) replaced = not recurrenceid and recurrenceids and start_utc in recurrenceids # Provide a summary of the object. @@ -527,17 +547,13 @@ # Obtain the datetime. - if name == "DTSTART": - dt, attr = dtstart, dtstart_attr + is_start = name == "DTSTART" # Where no end datetime exists, use the start datetime as the # basis of any potential datetime specified if dt-control is # set. - else: - dt, attr = dtend or dtstart, dtend_attr or dtstart_attr - - self.show_datetime_controls(obj, dt, attr, name == "DTSTART") + self.show_datetime_controls(obj, is_start and p.get_form_start() or p.get_form_end(), is_start) elif name == "DTSTART": page.td(class_="objectvalue %s replaced" % field, rowspan=2) @@ -694,33 +710,27 @@ # Obtain the periods associated with the event in the user's time zone. - periods = obj.get_periods(self.get_tzid(), self.get_window_end()) - recurrenceids = self._get_recurrences(uid) + periods = map(event_period_from_recurrence_period, obj.get_periods(self.get_tzid(), self.get_window_end())) + recurrences = self.get_current_recurrences(obj) - if len(periods) == 1: + if len(periods) <= 1: return - if self.is_organiser(obj): - page.p("This event recurs on the following occasions within the next %d days:" % self.get_window_size()) - else: - page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size()) - - # Determine whether any periods are explicitly created or are part of a - # rule. - - explicit_periods = filter(lambda p: p.origin != "RRULE", periods) + recurrenceids = self._get_recurrences(uid) # Show each recurrence in a separate table if editable. - if self.is_organiser(obj) and explicit_periods: + if self.is_organiser(obj) and recurrences: - for index, p in enumerate(periods[1:]): + page.p("The following occurrences are editable:") + + for index, p in enumerate(recurrences): # Isolate the controls from neighbouring tables. page.div() - self.show_object_datetime_controls(p.start, p.end, index) + self.show_object_datetime_controls(p, index) page.table(cellspacing=5, cellpadding=5, class_="recurrence") page.caption("Occurrence") @@ -742,28 +752,26 @@ # Otherwise, use a compact single table. - else: - page.table(cellspacing=5, cellpadding=5, class_="recurrence") - page.caption("Occurrences") - page.thead() - page.tr() - page.th("Start", class_="objectheading start") - page.th("End", class_="objectheading end") - page.tr.close() - page.thead.close() - page.tbody() + page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size()) - # Show only subsequent periods if organiser, since the principal - # period will be the start and end datetimes. + page.table(cellspacing=5, cellpadding=5, class_="recurrence") + page.caption("Occurrences") + page.thead() + page.tr() + page.th("Start", class_="objectheading start") + page.th("End", class_="objectheading end") + page.tr.close() + page.thead.close() + page.tbody() - for index, p in enumerate(self.is_organiser(obj) and periods[1:] or periods): - page.tr() - self.show_recurrence_controls(obj, index, p, recurrenceid, recurrenceids, True) - self.show_recurrence_controls(obj, index, p, recurrenceid, recurrenceids, False) - page.tr.close() + for index, p in enumerate(periods): + page.tr() + self.show_recurrence_label(p, recurrenceid, recurrenceids, True) + self.show_recurrence_label(p, recurrenceid, recurrenceids, False) + page.tr.close() - page.tbody.close() - page.table.close() + page.tbody.close() + page.table.close() def show_conflicting_events(self, uid, obj): @@ -849,14 +857,16 @@ # Generation of controls within page fragments. - def show_object_datetime_controls(self, start, end, index=None): + def show_object_datetime_controls(self, period, index=None): """ Show datetime-related controls if already active or if an object needs - them for the given 'start' to 'end' period. The given 'index' is used to - parameterise individual controls for dynamic manipulation. + them for the given 'period'. The given 'index' is used to parameterise + individual controls for dynamic manipulation. """ + p = period.as_form_period() + page = self.page args = self.env.get_args() sn = self._suffixed_name @@ -884,46 +894,28 @@ page.style.close() - dtend_control = args.get(ssn("dtend-control", "recur", index), []) - dttimes_control = args.get(ssn("dttimes-control", "recur", index), []) - - dtend_enabled = index is not None and str(index) in dtend_control or index is None and dtend_control - dttimes_enabled = index is not None and str(index) in dttimes_control or index is None and dttimes_control - - initial_load = not args.has_key("editing") - - dtend_enabled = dtend_enabled or initial_load and (isinstance(end, datetime) or start != end - timedelta(1)) - dttimes_enabled = dttimes_enabled or initial_load and (isinstance(start, datetime) or isinstance(end, datetime)) - self._control( ssn("dtend-control", "recur", index), "checkbox", - index is not None and str(index) or "enable", dtend_enabled, + index is not None and str(index) or "enable", p.end_enabled, id=sn("dtend-enable", index) ) self._control( ssn("dttimes-control", "recur", index), "checkbox", - index is not None and str(index) or "enable", dttimes_enabled, + index is not None and str(index) or "enable", p.times_enabled, id=sn("dttimes-enable", index) ) - def show_datetime_controls(self, obj, dt, attr, show_start): + def show_datetime_controls(self, obj, formdate, show_start): """ - Show datetime details from the given 'obj' for the datetime 'dt' and - attributes 'attr', showing start details if 'show_start' is set - to a true value. Details will appear as controls for organisers and - labels for attendees. + Show datetime details from the given 'obj' for the 'formdate', showing + start details if 'show_start' is set to a true value. Details will + appear as controls for organisers and labels for attendees. """ page = self.page - # Change end dates to refer to the actual dates, not the iCalendar - # "next day" dates. - - if not show_start and not isinstance(dt, datetime): - dt -= timedelta(1) - # Show controls for editing as organiser. if self.is_organiser(obj): @@ -931,7 +923,7 @@ if show_start: page.div(class_="dt enabled") - self._show_date_controls("dtstart", dt, attr.get("TZID")) + self._show_date_controls("dtstart", formdate) page.br() page.label("Specify times", for_="dttimes-enable", class_="time disabled enable") page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable") @@ -942,7 +934,7 @@ page.label("Specify end date", for_="dtend-enable", class_="enable") page.div.close() page.div(class_="dt enabled") - self._show_date_controls("dtend", dt, attr.get("TZID")) + self._show_date_controls("dtend", formdate) page.br() page.label("End on same day", for_="dtend-enable", class_="disable") page.div.close() @@ -952,7 +944,12 @@ # Show a label as attendee. else: - page.td(self.format_datetime(dt, "full")) + t = formdate.as_datetime_item() + if t: + dt, attr = t + page.td(self.format_datetime(dt, "full")) + else: + page.td("(Unrecognised date)") def show_recurrence_controls(self, obj, index, period, recurrenceid, recurrenceids, show_start): @@ -972,13 +969,7 @@ ssn = self._simple_suffixed_name p = period - # Change end dates to refer to the actual dates, not the iCalendar - # "next day" dates. - - if not isinstance(p.end, datetime): - p.end -= timedelta(1) - - start_utc = format_datetime(to_timezone(p.start, "UTC")) + start_utc = format_datetime(to_timezone(p.get_start(), "UTC")) replaced = recurrenceids and start_utc in recurrenceids and "replaced" or "" css = " ".join([ replaced, @@ -987,12 +978,12 @@ # Show controls for editing as organiser. - if self.is_organiser(obj) and not replaced and p.origin != "RRULE": + if self.is_organiser(obj) and not replaced: page.td(class_="objectvalue dt%s" % (show_start and "start" or "end")) if show_start: page.div(class_="dt enabled") - self._show_date_controls(ssn("dtstart", "recur", index), p.start, p.start_attr.get("TZID"), index=index) + self._show_date_controls(ssn("dtstart", "recur", index), p.get_form_start(), index=index) page.br() page.label("Specify times", for_=sn("dttimes-enable", index), class_="time disabled enable") page.label("Specify dates only", for_=sn("dttimes-enable", index), class_="time enabled disable") @@ -1003,7 +994,7 @@ page.label("Specify end date", for_=sn("dtend-enable", index), class_="enable") page.div.close() page.div(class_="dt enabled") - self._show_date_controls(ssn("dtend", "recur", index), p.end, index=index, show_tzid=False) + self._show_date_controls(ssn("dtend", "recur", index), p.get_form_end(), index=index, show_tzid=False) page.br() page.label("End on same day", for_=sn("dtend-enable", index), class_="disable") page.div.close() @@ -1013,7 +1004,35 @@ # Show label as attendee. else: - page.td(self.format_datetime(show_start and p.start or p.end, "long"), class_=css) + self.show_recurrence_label(p, recurrenceid, recurrenceids, show_start) + + def show_recurrence_label(self, p, recurrenceid, recurrenceids, show_start): + + """ + Show datetime details for the given period 'p', employing any + 'recurrenceid' and 'recurrenceids' for the object to configure the + displayed information. + + If 'show_start' is set to a true value, the start details will be shown; + otherwise, the end details will be shown. + """ + + page = self.page + + 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 "" + ]) + + formdate = show_start and p.get_form_start() or p.get_form_end() + t = formdate.as_datetime_item() + if t: + dt, attr = t + page.td(self.format_datetime(dt, "long"), class_=css) + else: + page.td("(Unrecognised date)") # Full page output methods. @@ -1077,25 +1096,32 @@ page.option(label, value=v) page.select.close() - def _show_date_controls(self, name, default, tzid=None, index=None, show_tzid=True): + def _show_date_controls(self, name, default, index=None, show_tzid=True): """ - Show date controls for a field with the given 'name' and 'default' value - and 'tzid'. If 'index' is specified, default field values will be - overridden by the element from a collection of existing form values with - the specified index; otherwise, field values will be overridden by a - single form value. + Show date controls for a field with the given 'name' and 'default' form + date value. + + If 'index' is specified, default field values will be overridden by the + element from a collection of existing form values with the specified + index; otherwise, field values will be overridden by a single form + value. If 'show_tzid' is set to a false value, the time zone menu will not be provided. """ page = self.page - args = self.env.get_args() # Show dates for up to one week around the current date. - base = to_date(default) + t = default.as_datetime_item() + if t: + dt, attr = t + else: + dt = date.today() + + base = to_date(dt) items = [] for i in range(-7, 8): d = base + timedelta(i) @@ -1105,25 +1131,18 @@ # Show time details. - default_time = isinstance(default, datetime) and default or None - - hour = args.get("%s-hour" % name, [])[index or 0:] - hour = hour and hour[0] or "%02d" % (default_time and default_time.hour or 0) - minute = args.get("%s-minute" % name, [])[index or 0:] - minute = minute and minute[0] or "%02d" % (default_time and default_time.minute or 0) - second = args.get("%s-second" % name, [])[index or 0:] - second = second and second[0] or "%02d" % (default_time and default_time.second or 0) + page.span(class_="time enabled") + page.input(name="%s-hour" % name, type="text", value=default.get_hour(), maxlength=2, size=2) + page.add(":") + page.input(name="%s-minute" % name, type="text", value=default.get_minute(), maxlength=2, size=2) + page.add(":") + page.input(name="%s-second" % name, type="text", value=default.get_second(), maxlength=2, size=2) - page.span(class_="time enabled") - page.input(name="%s-hour" % name, type="text", value=hour, maxlength=2, size=2) - page.add(":") - page.input(name="%s-minute" % name, type="text", value=minute, maxlength=2, size=2) - page.add(":") - page.input(name="%s-second" % name, type="text", value=second, maxlength=2, size=2) if show_tzid: - tzid = tzid or self.get_tzid() page.add(" ") + tzid = default.get_tzid() or self.get_tzid() self._show_timezone_menu("%s-tzid" % name, tzid, index) + page.span.close() def _show_timezone_menu(self, name, default, index=None):