1.1 --- a/imipweb/data.py Mon Apr 06 16:39:52 2015 +0200
1.2 +++ b/imipweb/data.py Mon Apr 06 22:57:23 2015 +0200
1.3 @@ -20,93 +20,275 @@
1.4 """
1.5
1.6 from datetime import datetime, timedelta
1.7 -from imiptools.dates import get_datetime, get_start_of_day
1.8 +from imiptools.dates import format_datetime, get_datetime, get_start_of_day, to_date
1.9 from imiptools.period import Period
1.10
1.11 +class PeriodError(Exception):
1.12 + pass
1.13 +
1.14 class EventPeriod(Period):
1.15
1.16 - "A simple period plus attribute details, compatible with RecurringPeriod."
1.17 + """
1.18 + A simple period plus attribute details, compatible with RecurringPeriod, and
1.19 + intended to represent information obtained from an iCalendar resource.
1.20 + """
1.21
1.22 - def __init__(self, start, end, start_attr=None, end_attr=None):
1.23 + def __init__(self, start, end, start_attr=None, end_attr=None, form_start=None, form_end=None):
1.24 Period.__init__(self, start, end)
1.25 self.start_attr = start_attr
1.26 self.end_attr = end_attr
1.27 + self.form_start = form_start
1.28 + self.form_end = form_end
1.29 +
1.30 + def as_tuple(self):
1.31 + return self.start, self.end, self.start_attr, self.end_attr, self.form_start, self.form_end
1.32 +
1.33 + def __repr__(self):
1.34 + return "EventPeriod(%r, %r, %r, %r, %r, %r)" % self.as_tuple()
1.35 +
1.36 + def get_start(self):
1.37 + return self.start
1.38 +
1.39 + def get_end(self):
1.40 + return self.end
1.41 +
1.42 + def as_event_period(self):
1.43 + return self
1.44 +
1.45 + def get_form_start(self):
1.46 + if not self.form_start:
1.47 + self.form_start = self.get_form_date(self.start, self.start_attr)
1.48 + return self.form_start
1.49 +
1.50 + def get_form_end(self):
1.51 + if not self.form_end:
1.52 + self.form_end = self.get_form_date(self.end, self.end_attr)
1.53 + return self.form_end
1.54 +
1.55 + def as_form_period(self):
1.56 + return FormPeriod(
1.57 + self.get_form_date(self.start, self.start_attr),
1.58 + self.get_form_date(self.end, self.end_attr),
1.59 + isinstance(self.end, datetime) or self.start != self.end - timedelta(1),
1.60 + isinstance(self.start, datetime) or isinstance(self.end, datetime)
1.61 + )
1.62 +
1.63 + def get_form_date(self, dt, attr=None):
1.64 + return FormDate(
1.65 + format_datetime(to_date(dt)),
1.66 + isinstance(dt, datetime) and str(dt.hour) or None,
1.67 + isinstance(dt, datetime) and str(dt.minute) or None,
1.68 + isinstance(dt, datetime) and str(dt.second) or None,
1.69 + attr and attr.get("TZID") or None,
1.70 + dt, attr
1.71 + )
1.72 +
1.73 +def event_period_from_recurrence_period(period):
1.74 + return EventPeriod(period.start, period.end, period.start_attr, period.end_attr)
1.75 +
1.76 +class FormPeriod:
1.77 +
1.78 + "A period whose information originates from a form."
1.79 +
1.80 + def __init__(self, start, end, end_enabled=True, times_enabled=True):
1.81 + self.start = start
1.82 + self.end = end
1.83 + self.end_enabled = end_enabled
1.84 + self.times_enabled = times_enabled
1.85
1.86 def as_tuple(self):
1.87 - return self.start, self.end, self.start_attr, self.end_attr
1.88 + return self.start, self.end, self.end_enabled, self.times_enabled
1.89
1.90 def __repr__(self):
1.91 - return "EventPeriod(%r, %r, %r, %r)" % self.as_tuple()
1.92 + return "FormPeriod(%r, %r, %r, %r)" % self.as_tuple()
1.93 +
1.94 + def _get_start(self):
1.95 + t = self.start.as_datetime_item(self.times_enabled)
1.96 + if t:
1.97 + return t
1.98 + else:
1.99 + return None
1.100 +
1.101 + def _get_end(self):
1.102 +
1.103 + # Handle specified end datetimes.
1.104 +
1.105 + if self.end_enabled:
1.106 + t = self.end.as_datetime_item(self.times_enabled)
1.107 + if t:
1.108 + dtend, dtend_attr = t
1.109 + else:
1.110 + return None
1.111 +
1.112 + # Otherwise, treat the end date as the start date. Datetimes are
1.113 + # handled by making the event occupy the rest of the day.
1.114 +
1.115 + else:
1.116 + t = self._get_start()
1.117 + if t:
1.118 + dtstart, dtstart_attr = t
1.119 + dtend = dtstart + timedelta(1)
1.120 + dtend_attr = dtstart_attr
1.121 +
1.122 + if isinstance(dtstart, datetime):
1.123 + dtend = get_start_of_day(dtend, dtend_attr["TZID"])
1.124 + else:
1.125 + return None
1.126 +
1.127 + return dtend, dtend_attr
1.128 +
1.129 + def get_start(self):
1.130 + t = self._get_start()
1.131 + if t:
1.132 + dtstart, dtstart_attr = t
1.133 + return dtstart
1.134 + else:
1.135 + return None
1.136 +
1.137 + def get_end(self):
1.138 + t = self._get_end()
1.139 + if t:
1.140 + dtend, dtend_attr = t
1.141 + return dtend
1.142 + else:
1.143 + return None
1.144 +
1.145 + def as_event_period(self, index=None):
1.146 + t = self._get_start()
1.147 + if t:
1.148 + dtstart, dtstart_attr = t
1.149 + else:
1.150 + raise PeriodError(*[index is not None and ("dtstart", index) or "dtstart"])
1.151 +
1.152 + t = self._get_end()
1.153 + if t:
1.154 + dtend, dtend_attr = t
1.155 + else:
1.156 + raise PeriodError(*[index is not None and ("dtend", index) or "dtend"])
1.157 +
1.158 + if dtstart > dtend:
1.159 + raise PeriodError(*[
1.160 + index is not None and ("dtstart", index) or "dtstart",
1.161 + index is not None and ("dtend", index) or "dtend"
1.162 + ])
1.163 +
1.164 + return EventPeriod(dtstart, dtend, dtstart_attr, dtend_attr, self.start, self.end)
1.165 +
1.166 + def get_form_start(self):
1.167 + return self.start
1.168 +
1.169 + def get_form_end(self):
1.170 + return self.end
1.171 +
1.172 + def as_form_period(self):
1.173 + return self
1.174
1.175 -def handle_date_control_values(values, with_time=True):
1.176 +class FormDate:
1.177 +
1.178 + "Date information originating from form information."
1.179 +
1.180 + def __init__(self, date=None, hour=None, minute=None, second=None, tzid=None, dt=None, attr=None):
1.181 + self.date = date
1.182 + self.hour = hour
1.183 + self.minute = minute
1.184 + self.second = second
1.185 + self.tzid = tzid
1.186 + self.dt = dt
1.187 + self.attr = attr
1.188 +
1.189 + def as_tuple(self):
1.190 + return self.date, self.hour, self.minute, self.second, self.tzid, self.dt, self.attr
1.191 +
1.192 + def __repr__(self):
1.193 + return "FormDate(%r, %r, %r, %r, %r, %r, %r)" % self.as_tuple()
1.194 +
1.195 + def get_component(self, value):
1.196 + return (value or "").rjust(2, "0")[:2]
1.197 +
1.198 + def get_hour(self):
1.199 + return self.get_component(self.hour)
1.200 +
1.201 + def get_minute(self):
1.202 + return self.get_component(self.minute)
1.203 +
1.204 + def get_second(self):
1.205 + return self.get_component(self.second)
1.206 +
1.207 + def get_date_string(self):
1.208 + return self.date or ""
1.209 +
1.210 + def get_datetime_string(self):
1.211 + if not self.date:
1.212 + return ""
1.213 +
1.214 + hour = self.hour; minute = self.minute; second = self.second
1.215 +
1.216 + if hour or minute or second:
1.217 + time = "T%s%s%s" % tuple(map(self.get_component, (hour, minute, second)))
1.218 + else:
1.219 + time = ""
1.220 +
1.221 + return "%s%s" % (self.date, time)
1.222 +
1.223 + def get_tzid(self):
1.224 + return self.tzid
1.225 +
1.226 + def as_datetime_item(self, with_time=True):
1.227 +
1.228 + """
1.229 + Return a (datetime, attr) tuple for the datetime information provided by
1.230 + this object, or None if the fields cannot be used to construct a
1.231 + datetime object.
1.232 + """
1.233 +
1.234 + # Return any original datetime details.
1.235 +
1.236 + if self.dt:
1.237 + return self.dt, self.attr
1.238 +
1.239 + # Otherwise, construct a datetime and attributes.
1.240 +
1.241 + if not self.date:
1.242 + return None
1.243 + elif with_time:
1.244 + attr = {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"}
1.245 + dt = get_datetime(self.get_datetime_string(), attr)
1.246 + else:
1.247 + dt = None
1.248 +
1.249 + # Interpret incomplete datetimes as dates.
1.250 +
1.251 + if not dt:
1.252 + attr = {"VALUE" : "DATE"}
1.253 + dt = get_datetime(self.get_date_string(), attr)
1.254 +
1.255 + if dt:
1.256 + return dt, attr
1.257 +
1.258 + return None
1.259 +
1.260 +def end_date_to_calendar(dt):
1.261
1.262 """
1.263 - Handle date control information for the given 'values', returning a
1.264 - (datetime, attr) tuple, or None if the fields cannot be used to
1.265 - construct a datetime object.
1.266 - """
1.267 -
1.268 - if not values or not values["date"]:
1.269 - return None
1.270 - elif with_time:
1.271 - value = "%s%s" % (values["date"], values["time"])
1.272 - attr = {"TZID" : values["tzid"], "VALUE" : "DATE-TIME"}
1.273 - dt = get_datetime(value, attr)
1.274 - else:
1.275 - attr = {"VALUE" : "DATE"}
1.276 - dt = get_datetime(values["date"])
1.277 -
1.278 - if dt:
1.279 - return dt, attr
1.280 -
1.281 - return None
1.282 -
1.283 -def handle_period_controls(start_values, end_values, dtend_enabled, dttimes_enabled, index=None):
1.284 -
1.285 - """
1.286 - Handle datetime controls for a particular period, described by the given
1.287 - 'start_values' and 'end_values', with 'dtend_enabled' and
1.288 - 'dttimes_enabled' affecting the usage of the provided values.
1.289 -
1.290 - If 'index' is specified, incorporate it into any error indicator.
1.291 + Change end dates to refer to the actual dates, not the iCalendar "next day"
1.292 + dates.
1.293 """
1.294
1.295 - t = handle_date_control_values(start_values, dttimes_enabled)
1.296 - if t:
1.297 - dtstart, dtstart_attr = t
1.298 + if not isinstance(dt, datetime):
1.299 + return dt + timedelta(1)
1.300 else:
1.301 - return None, [index is not None and ("dtstart", index) or "dtstart"]
1.302 + return dt
1.303
1.304 - # Handle specified end datetimes.
1.305 -
1.306 - if dtend_enabled:
1.307 - t = handle_date_control_values(end_values, dttimes_enabled)
1.308 - if t:
1.309 - dtend, dtend_attr = t
1.310 -
1.311 - # Convert end dates to iCalendar "next day" dates.
1.312 +def end_date_from_calendar(dt):
1.313
1.314 - if not isinstance(dtend, datetime):
1.315 - dtend += timedelta(1)
1.316 - else:
1.317 - return None, [index is not None and ("dtend", index) or "dtend"]
1.318 + """
1.319 + Change end dates to refer to the actual dates, not the iCalendar "next day"
1.320 + dates.
1.321 + """
1.322
1.323 - # Otherwise, treat the end date as the start date. Datetimes are
1.324 - # handled by making the event occupy the rest of the day.
1.325 -
1.326 + if not isinstance(dt, datetime):
1.327 + return dt - timedelta(1)
1.328 else:
1.329 - dtend = dtstart + timedelta(1)
1.330 - dtend_attr = dtstart_attr
1.331 -
1.332 - if isinstance(dtstart, datetime):
1.333 - dtend = get_start_of_day(dtend, attr["TZID"])
1.334 -
1.335 - if dtstart > dtend:
1.336 - return None, [
1.337 - index is not None and ("dtstart", index) or "dtstart",
1.338 - index is not None and ("dtend", index) or "dtend"
1.339 - ]
1.340 -
1.341 - return EventPeriod(dtstart, dtend, dtstart_attr, dtend_attr), None
1.342 + return dt
1.343
1.344 # vim: tabstop=4 expandtab shiftwidth=4
2.1 --- a/imipweb/event.py Mon Apr 06 16:39:52 2015 +0200
2.2 +++ b/imipweb/event.py Mon Apr 06 22:57:23 2015 +0200
2.3 @@ -19,7 +19,7 @@
2.4 this program. If not, see <http://www.gnu.org/licenses/>.
2.5 """
2.6
2.7 -from datetime import datetime, timedelta
2.8 +from datetime import date, datetime, timedelta
2.9 from imiptools.client import update_attendees, update_participation
2.10 from imiptools.data import get_uri, uri_dict, uri_values
2.11 from imiptools.dates import format_datetime, to_date, get_datetime, \
2.12 @@ -27,8 +27,10 @@
2.13 to_timezone
2.14 from imiptools.mail import Messenger
2.15 from imiptools.period import have_conflict
2.16 -from imipweb.data import EventPeriod, handle_date_control_values, \
2.17 - handle_period_controls
2.18 +from imipweb.data import EventPeriod, \
2.19 + end_date_from_calendar, end_date_to_calendar, \
2.20 + event_period_from_recurrence_period, \
2.21 + FormDate, FormPeriod, PeriodError
2.22 from imipweb.handler import ManagerHandler
2.23 from imipweb.resource import Resource
2.24 import pytz
2.25 @@ -109,13 +111,15 @@
2.26
2.27 # Update time periods (main and recurring).
2.28
2.29 - period, errors = self.handle_main_period()
2.30 - if errors:
2.31 - return errors
2.32 + try:
2.33 + period = self.handle_main_period()
2.34 + except PeriodError, exc:
2.35 + return exc.args
2.36
2.37 - periods, errors = self.handle_recurrence_periods()
2.38 - if errors:
2.39 - return errors
2.40 + try:
2.41 + periods = self.handle_recurrence_periods()
2.42 + except PeriodError, exc:
2.43 + return exc.args
2.44
2.45 self.set_period_in_object(obj, period)
2.46 self.set_periods_in_object(obj, periods)
2.47 @@ -180,49 +184,62 @@
2.48
2.49 return None
2.50
2.51 + def set_period_in_object(self, obj, period):
2.52 +
2.53 + "Set in the given 'obj' the given 'period' as the main start and end."
2.54 +
2.55 + p = period.as_event_period()
2.56 + result = self.set_datetime_in_object(p.start, p.start_attr and p.start_attr.get("TZID"), "DTSTART", obj)
2.57 + 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
2.58 + return result
2.59 +
2.60 + def set_periods_in_object(self, obj, periods):
2.61 +
2.62 + "Set in the given 'obj' the given 'periods'."
2.63 +
2.64 + update = False
2.65 +
2.66 + old_values = obj.get_values("RDATE")
2.67 + new_rdates = []
2.68 +
2.69 + if obj.has_key("RDATE"):
2.70 + del obj["RDATE"]
2.71 +
2.72 + for period in periods:
2.73 + p = period.as_event_period()
2.74 + tzid = p.start_attr and p.start_attr.get("TZID") or p.end_attr and p.end_attr.get("TZID")
2.75 + new_rdates.append(get_period_item(p.start, end_date_to_calendar(p.end), tzid))
2.76 +
2.77 + obj["RDATE"] = new_rdates
2.78 +
2.79 + # NOTE: To do: calculate the update status.
2.80 + return update
2.81 +
2.82 + def set_datetime_in_object(self, dt, tzid, property, obj):
2.83 +
2.84 + """
2.85 + Set 'dt' and 'tzid' for the given 'property' in 'obj', returning whether
2.86 + an update has occurred.
2.87 + """
2.88 +
2.89 + if dt:
2.90 + old_value = obj.get_value(property)
2.91 + obj[property] = [get_datetime_item(dt, tzid)]
2.92 + return format_datetime(dt) != old_value
2.93 +
2.94 + return False
2.95 +
2.96 def handle_main_period(self):
2.97
2.98 "Return period details for the main start/end period in an event."
2.99
2.100 - args = self.env.get_args()
2.101 -
2.102 - dtend_enabled = args.get("dtend-control", [None])[0]
2.103 - dttimes_enabled = args.get("dttimes-control", [None])[0]
2.104 - start_values = self.get_date_control_values("dtstart")
2.105 - end_values = self.get_date_control_values("dtend")
2.106 -
2.107 - period, errors = handle_period_controls(start_values, end_values, dtend_enabled, dttimes_enabled)
2.108 -
2.109 - if errors:
2.110 - return None, errors
2.111 - else:
2.112 - return period, errors
2.113 + return self.get_main_period().as_event_period()
2.114
2.115 def handle_recurrence_periods(self):
2.116
2.117 "Return period details for the recurrences specified for an event."
2.118
2.119 - args = self.env.get_args()
2.120 -
2.121 - all_dtend_enabled = args.get("dtend-control-recur", [])
2.122 - all_dttimes_enabled = args.get("dttimes-control-recur", [])
2.123 - all_start_values = self.get_date_control_values("dtstart-recur", multiple=True)
2.124 - all_end_values = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur")
2.125 -
2.126 - periods = []
2.127 - errors = []
2.128 -
2.129 - for index, (start_values, end_values, dtend_enabled, dttimes_enabled) in \
2.130 - enumerate(map(None, all_start_values, all_end_values, all_dtend_enabled, all_dttimes_enabled)):
2.131 -
2.132 - dtend_enabled = str(index) in all_dtend_enabled
2.133 - dttimes_enabled = str(index) in all_dttimes_enabled
2.134 - period, _errors = handle_period_controls(start_values, end_values, dtend_enabled, dttimes_enabled, index)
2.135 -
2.136 - periods.append(period)
2.137 - errors += _errors
2.138 -
2.139 - return periods, errors
2.140 + return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences())]
2.141
2.142 def get_date_control_values(self, name, multiple=False, tzid_name=None):
2.143
2.144 @@ -245,90 +262,40 @@
2.145 # Handle absent values by employing None values.
2.146
2.147 field_values = map(None, dates, hours, minutes, seconds, tzids)
2.148 - if not field_values and not multiple:
2.149 - field_values = [(None, None, None, None, None)]
2.150 -
2.151 - all_values = []
2.152 -
2.153 - for date, hour, minute, second, tzid in field_values:
2.154 -
2.155 - # Construct a usable dictionary of values.
2.156
2.157 - if hour or minute or second:
2.158 - hour = (hour or "").rjust(2, "0")[:2]
2.159 - minute = (minute or "").rjust(2, "0")[:2]
2.160 - second = (second or "").rjust(2, "0")[:2]
2.161 - time = "T%s%s%s" % (hour, minute, second)
2.162 - else:
2.163 - hour = minute = second = time = ""
2.164 + if not field_values and not multiple:
2.165 + all_values = FormDate()
2.166 + else:
2.167 + all_values = []
2.168 + for date, hour, minute, second, tzid in field_values:
2.169 + value = FormDate(date, hour, minute, second, tzid or self.get_tzid())
2.170
2.171 - value = {
2.172 - "date" : date, "time" : time,
2.173 - "tzid" : tzid or self.get_tzid(),
2.174 - "hour" : hour, "minute" : minute, "second" : second
2.175 - }
2.176 + # Return a single value or append to a collection of all values.
2.177
2.178 - # Return a single value or append to a collection of all values.
2.179 -
2.180 - if not multiple:
2.181 - return value
2.182 - else:
2.183 - all_values.append(value)
2.184 + if not multiple:
2.185 + return value
2.186 + else:
2.187 + all_values.append(value)
2.188
2.189 return all_values
2.190
2.191 - def set_period_in_object(self, obj, period):
2.192 -
2.193 - "Set in the given 'obj' the given 'period' as the main start and end."
2.194 -
2.195 - p = period
2.196 - result = self.set_datetime_in_object(p.start, p.start_attr and p.start_attr.get("TZID"), "DTSTART", obj)
2.197 - result = self.set_datetime_in_object(p.end, p.end_attr and p.end_attr.get("TZID"), "DTEND", obj) or result
2.198 - return result
2.199 -
2.200 - def set_periods_in_object(self, obj, periods):
2.201 -
2.202 - "Set in the given 'obj' the given 'periods'."
2.203 -
2.204 - update = False
2.205 + def get_current_main_period(self, obj):
2.206 + args = self.env.get_args()
2.207 + initial_load = not args.has_key("editing")
2.208
2.209 - old_values = obj.get_values("RDATE")
2.210 - new_rdates = []
2.211 -
2.212 - if obj.has_key("RDATE"):
2.213 - del obj["RDATE"]
2.214 + if initial_load or not self.is_organiser(obj):
2.215 + return self.get_existing_main_period(obj)
2.216 + else:
2.217 + return self.get_main_period()
2.218
2.219 - for p in periods:
2.220 - tzid = p.start_attr and p.start_attr.get("TZID") or p.end_attr and p.end_attr.get("TZID")
2.221 - new_rdates.append(get_period_item(p.start, p.end, tzid))
2.222 -
2.223 - obj["RDATE"] = new_rdates
2.224 -
2.225 - # NOTE: To do: calculate the update status.
2.226 - return update
2.227 -
2.228 - def set_datetime_in_object(self, dt, tzid, property, obj):
2.229 + def get_existing_main_period(self, obj):
2.230
2.231 """
2.232 - Set 'dt' and 'tzid' for the given 'property' in 'obj', returning whether
2.233 - an update has occurred.
2.234 - """
2.235 -
2.236 - if dt:
2.237 - old_value = obj.get_value(property)
2.238 - obj[property] = [get_datetime_item(dt, tzid)]
2.239 - return format_datetime(dt) != old_value
2.240 -
2.241 - return False
2.242 -
2.243 - def get_event_period(self, obj):
2.244 -
2.245 - """
2.246 - Return (dtstart, dtstart attributes), (dtend, dtend attributes) for
2.247 - 'obj'.
2.248 + Return the main event period for the given 'obj'.
2.249 """
2.250
2.251 dtstart, dtstart_attr = obj.get_datetime_item("DTSTART")
2.252 +
2.253 if obj.has_key("DTEND"):
2.254 dtend, dtend_attr = obj.get_datetime_item("DTEND")
2.255 elif obj.has_key("DURATION"):
2.256 @@ -337,7 +304,60 @@
2.257 dtend_attr = dtstart_attr
2.258 else:
2.259 dtend, dtend_attr = dtstart, dtstart_attr
2.260 - return (dtstart, dtstart_attr), (dtend, dtend_attr)
2.261 +
2.262 + return EventPeriod(dtstart, end_date_from_calendar(dtend), dtstart_attr, dtend_attr)
2.263 +
2.264 + def get_main_period(self):
2.265 +
2.266 + "Return the main period defined in the event form."
2.267 +
2.268 + args = self.env.get_args()
2.269 +
2.270 + dtend_enabled = args.get("dtend-control", [None])[0]
2.271 + dttimes_enabled = args.get("dttimes-control", [None])[0]
2.272 + start = self.get_date_control_values("dtstart")
2.273 + end = self.get_date_control_values("dtend")
2.274 +
2.275 + return FormPeriod(start, end, dtend_enabled, dttimes_enabled)
2.276 +
2.277 + def get_current_recurrences(self, obj):
2.278 + args = self.env.get_args()
2.279 + initial_load = not args.has_key("editing")
2.280 +
2.281 + if initial_load or not self.is_organiser(obj):
2.282 + return self.get_existing_recurrences(obj)
2.283 + else:
2.284 + return self.get_recurrences()
2.285 +
2.286 + def get_existing_recurrences(self, obj):
2.287 + recurrences = []
2.288 + for period in obj.get_periods(self.get_tzid(), self.get_window_end()):
2.289 + if period.origin == "RDATE":
2.290 + recurrences.append(event_period_from_recurrence_period(period))
2.291 + return recurrences
2.292 +
2.293 + def get_recurrences(self):
2.294 +
2.295 + "Return the recurrences defined in the event form."
2.296 +
2.297 + args = self.env.get_args()
2.298 +
2.299 + all_dtend_enabled = args.get("dtend-control-recur", [])
2.300 + all_dttimes_enabled = args.get("dttimes-control-recur", [])
2.301 + all_starts = self.get_date_control_values("dtstart-recur", multiple=True)
2.302 + all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur")
2.303 +
2.304 + periods = []
2.305 +
2.306 + for index, (start, end, dtend_enabled, dttimes_enabled) in \
2.307 + enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled)):
2.308 +
2.309 + dtend_enabled = str(index) in all_dtend_enabled
2.310 + dttimes_enabled = str(index) in all_dttimes_enabled
2.311 + period = FormPeriod(start, end, dtend_enabled, dttimes_enabled)
2.312 + periods.append(period)
2.313 +
2.314 + return periods
2.315
2.316 def get_current_attendees(self, obj):
2.317
2.318 @@ -486,14 +506,14 @@
2.319 else:
2.320 attendees = self.update_attendees(obj)
2.321
2.322 - (dtstart, dtstart_attr), (dtend, dtend_attr) = self.get_event_period(obj)
2.323 - self.show_object_datetime_controls(dtstart, dtend)
2.324 + p = self.get_current_main_period(obj)
2.325 + self.show_object_datetime_controls(p)
2.326
2.327 # Obtain any separate recurrences for this event.
2.328
2.329 recurrenceid = format_datetime(obj.get_utc_datetime("RECURRENCE-ID"))
2.330 recurrenceids = self._get_recurrences(uid)
2.331 - start_utc = format_datetime(to_timezone(dtstart, "UTC"))
2.332 + start_utc = format_datetime(to_timezone(p.get_start(), "UTC"))
2.333 replaced = not recurrenceid and recurrenceids and start_utc in recurrenceids
2.334
2.335 # Provide a summary of the object.
2.336 @@ -527,17 +547,13 @@
2.337
2.338 # Obtain the datetime.
2.339
2.340 - if name == "DTSTART":
2.341 - dt, attr = dtstart, dtstart_attr
2.342 + is_start = name == "DTSTART"
2.343
2.344 # Where no end datetime exists, use the start datetime as the
2.345 # basis of any potential datetime specified if dt-control is
2.346 # set.
2.347
2.348 - else:
2.349 - dt, attr = dtend or dtstart, dtend_attr or dtstart_attr
2.350 -
2.351 - self.show_datetime_controls(obj, dt, attr, name == "DTSTART")
2.352 + self.show_datetime_controls(obj, is_start and p.get_form_start() or p.get_form_end(), is_start)
2.353
2.354 elif name == "DTSTART":
2.355 page.td(class_="objectvalue %s replaced" % field, rowspan=2)
2.356 @@ -694,33 +710,27 @@
2.357
2.358 # Obtain the periods associated with the event in the user's time zone.
2.359
2.360 - periods = obj.get_periods(self.get_tzid(), self.get_window_end())
2.361 - recurrenceids = self._get_recurrences(uid)
2.362 + periods = map(event_period_from_recurrence_period, obj.get_periods(self.get_tzid(), self.get_window_end()))
2.363 + recurrences = self.get_current_recurrences(obj)
2.364
2.365 - if len(periods) == 1:
2.366 + if len(periods) <= 1:
2.367 return
2.368
2.369 - if self.is_organiser(obj):
2.370 - page.p("This event recurs on the following occasions within the next %d days:" % self.get_window_size())
2.371 - else:
2.372 - page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size())
2.373 -
2.374 - # Determine whether any periods are explicitly created or are part of a
2.375 - # rule.
2.376 -
2.377 - explicit_periods = filter(lambda p: p.origin != "RRULE", periods)
2.378 + recurrenceids = self._get_recurrences(uid)
2.379
2.380 # Show each recurrence in a separate table if editable.
2.381
2.382 - if self.is_organiser(obj) and explicit_periods:
2.383 + if self.is_organiser(obj) and recurrences:
2.384
2.385 - for index, p in enumerate(periods[1:]):
2.386 + page.p("The following occurrences are editable:")
2.387 +
2.388 + for index, p in enumerate(recurrences):
2.389
2.390 # Isolate the controls from neighbouring tables.
2.391
2.392 page.div()
2.393
2.394 - self.show_object_datetime_controls(p.start, p.end, index)
2.395 + self.show_object_datetime_controls(p, index)
2.396
2.397 page.table(cellspacing=5, cellpadding=5, class_="recurrence")
2.398 page.caption("Occurrence")
2.399 @@ -742,28 +752,26 @@
2.400
2.401 # Otherwise, use a compact single table.
2.402
2.403 - else:
2.404 - page.table(cellspacing=5, cellpadding=5, class_="recurrence")
2.405 - page.caption("Occurrences")
2.406 - page.thead()
2.407 - page.tr()
2.408 - page.th("Start", class_="objectheading start")
2.409 - page.th("End", class_="objectheading end")
2.410 - page.tr.close()
2.411 - page.thead.close()
2.412 - page.tbody()
2.413 + page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size())
2.414
2.415 - # Show only subsequent periods if organiser, since the principal
2.416 - # period will be the start and end datetimes.
2.417 + page.table(cellspacing=5, cellpadding=5, class_="recurrence")
2.418 + page.caption("Occurrences")
2.419 + page.thead()
2.420 + page.tr()
2.421 + page.th("Start", class_="objectheading start")
2.422 + page.th("End", class_="objectheading end")
2.423 + page.tr.close()
2.424 + page.thead.close()
2.425 + page.tbody()
2.426
2.427 - for index, p in enumerate(self.is_organiser(obj) and periods[1:] or periods):
2.428 - page.tr()
2.429 - self.show_recurrence_controls(obj, index, p, recurrenceid, recurrenceids, True)
2.430 - self.show_recurrence_controls(obj, index, p, recurrenceid, recurrenceids, False)
2.431 - page.tr.close()
2.432 + for index, p in enumerate(periods):
2.433 + page.tr()
2.434 + self.show_recurrence_label(p, recurrenceid, recurrenceids, True)
2.435 + self.show_recurrence_label(p, recurrenceid, recurrenceids, False)
2.436 + page.tr.close()
2.437
2.438 - page.tbody.close()
2.439 - page.table.close()
2.440 + page.tbody.close()
2.441 + page.table.close()
2.442
2.443 def show_conflicting_events(self, uid, obj):
2.444
2.445 @@ -849,14 +857,16 @@
2.446
2.447 # Generation of controls within page fragments.
2.448
2.449 - def show_object_datetime_controls(self, start, end, index=None):
2.450 + def show_object_datetime_controls(self, period, index=None):
2.451
2.452 """
2.453 Show datetime-related controls if already active or if an object needs
2.454 - them for the given 'start' to 'end' period. The given 'index' is used to
2.455 - parameterise individual controls for dynamic manipulation.
2.456 + them for the given 'period'. The given 'index' is used to parameterise
2.457 + individual controls for dynamic manipulation.
2.458 """
2.459
2.460 + p = period.as_form_period()
2.461 +
2.462 page = self.page
2.463 args = self.env.get_args()
2.464 sn = self._suffixed_name
2.465 @@ -884,46 +894,28 @@
2.466
2.467 page.style.close()
2.468
2.469 - dtend_control = args.get(ssn("dtend-control", "recur", index), [])
2.470 - dttimes_control = args.get(ssn("dttimes-control", "recur", index), [])
2.471 -
2.472 - dtend_enabled = index is not None and str(index) in dtend_control or index is None and dtend_control
2.473 - dttimes_enabled = index is not None and str(index) in dttimes_control or index is None and dttimes_control
2.474 -
2.475 - initial_load = not args.has_key("editing")
2.476 -
2.477 - dtend_enabled = dtend_enabled or initial_load and (isinstance(end, datetime) or start != end - timedelta(1))
2.478 - dttimes_enabled = dttimes_enabled or initial_load and (isinstance(start, datetime) or isinstance(end, datetime))
2.479 -
2.480 self._control(
2.481 ssn("dtend-control", "recur", index), "checkbox",
2.482 - index is not None and str(index) or "enable", dtend_enabled,
2.483 + index is not None and str(index) or "enable", p.end_enabled,
2.484 id=sn("dtend-enable", index)
2.485 )
2.486
2.487 self._control(
2.488 ssn("dttimes-control", "recur", index), "checkbox",
2.489 - index is not None and str(index) or "enable", dttimes_enabled,
2.490 + index is not None and str(index) or "enable", p.times_enabled,
2.491 id=sn("dttimes-enable", index)
2.492 )
2.493
2.494 - def show_datetime_controls(self, obj, dt, attr, show_start):
2.495 + def show_datetime_controls(self, obj, formdate, show_start):
2.496
2.497 """
2.498 - Show datetime details from the given 'obj' for the datetime 'dt' and
2.499 - attributes 'attr', showing start details if 'show_start' is set
2.500 - to a true value. Details will appear as controls for organisers and
2.501 - labels for attendees.
2.502 + Show datetime details from the given 'obj' for the 'formdate', showing
2.503 + start details if 'show_start' is set to a true value. Details will
2.504 + appear as controls for organisers and labels for attendees.
2.505 """
2.506
2.507 page = self.page
2.508
2.509 - # Change end dates to refer to the actual dates, not the iCalendar
2.510 - # "next day" dates.
2.511 -
2.512 - if not show_start and not isinstance(dt, datetime):
2.513 - dt -= timedelta(1)
2.514 -
2.515 # Show controls for editing as organiser.
2.516
2.517 if self.is_organiser(obj):
2.518 @@ -931,7 +923,7 @@
2.519
2.520 if show_start:
2.521 page.div(class_="dt enabled")
2.522 - self._show_date_controls("dtstart", dt, attr.get("TZID"))
2.523 + self._show_date_controls("dtstart", formdate)
2.524 page.br()
2.525 page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")
2.526 page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable")
2.527 @@ -942,7 +934,7 @@
2.528 page.label("Specify end date", for_="dtend-enable", class_="enable")
2.529 page.div.close()
2.530 page.div(class_="dt enabled")
2.531 - self._show_date_controls("dtend", dt, attr.get("TZID"))
2.532 + self._show_date_controls("dtend", formdate)
2.533 page.br()
2.534 page.label("End on same day", for_="dtend-enable", class_="disable")
2.535 page.div.close()
2.536 @@ -952,7 +944,12 @@
2.537 # Show a label as attendee.
2.538
2.539 else:
2.540 - page.td(self.format_datetime(dt, "full"))
2.541 + t = formdate.as_datetime_item()
2.542 + if t:
2.543 + dt, attr = t
2.544 + page.td(self.format_datetime(dt, "full"))
2.545 + else:
2.546 + page.td("(Unrecognised date)")
2.547
2.548 def show_recurrence_controls(self, obj, index, period, recurrenceid, recurrenceids, show_start):
2.549
2.550 @@ -972,13 +969,7 @@
2.551 ssn = self._simple_suffixed_name
2.552 p = period
2.553
2.554 - # Change end dates to refer to the actual dates, not the iCalendar
2.555 - # "next day" dates.
2.556 -
2.557 - if not isinstance(p.end, datetime):
2.558 - p.end -= timedelta(1)
2.559 -
2.560 - start_utc = format_datetime(to_timezone(p.start, "UTC"))
2.561 + start_utc = format_datetime(to_timezone(p.get_start(), "UTC"))
2.562 replaced = recurrenceids and start_utc in recurrenceids and "replaced" or ""
2.563 css = " ".join([
2.564 replaced,
2.565 @@ -987,12 +978,12 @@
2.566
2.567 # Show controls for editing as organiser.
2.568
2.569 - if self.is_organiser(obj) and not replaced and p.origin != "RRULE":
2.570 + if self.is_organiser(obj) and not replaced:
2.571 page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
2.572
2.573 if show_start:
2.574 page.div(class_="dt enabled")
2.575 - self._show_date_controls(ssn("dtstart", "recur", index), p.start, p.start_attr.get("TZID"), index=index)
2.576 + self._show_date_controls(ssn("dtstart", "recur", index), p.get_form_start(), index=index)
2.577 page.br()
2.578 page.label("Specify times", for_=sn("dttimes-enable", index), class_="time disabled enable")
2.579 page.label("Specify dates only", for_=sn("dttimes-enable", index), class_="time enabled disable")
2.580 @@ -1003,7 +994,7 @@
2.581 page.label("Specify end date", for_=sn("dtend-enable", index), class_="enable")
2.582 page.div.close()
2.583 page.div(class_="dt enabled")
2.584 - self._show_date_controls(ssn("dtend", "recur", index), p.end, index=index, show_tzid=False)
2.585 + self._show_date_controls(ssn("dtend", "recur", index), p.get_form_end(), index=index, show_tzid=False)
2.586 page.br()
2.587 page.label("End on same day", for_=sn("dtend-enable", index), class_="disable")
2.588 page.div.close()
2.589 @@ -1013,7 +1004,35 @@
2.590 # Show label as attendee.
2.591
2.592 else:
2.593 - page.td(self.format_datetime(show_start and p.start or p.end, "long"), class_=css)
2.594 + self.show_recurrence_label(p, recurrenceid, recurrenceids, show_start)
2.595 +
2.596 + def show_recurrence_label(self, p, recurrenceid, recurrenceids, show_start):
2.597 +
2.598 + """
2.599 + Show datetime details for the given period 'p', employing any
2.600 + 'recurrenceid' and 'recurrenceids' for the object to configure the
2.601 + displayed information.
2.602 +
2.603 + If 'show_start' is set to a true value, the start details will be shown;
2.604 + otherwise, the end details will be shown.
2.605 + """
2.606 +
2.607 + page = self.page
2.608 +
2.609 + start_utc = format_datetime(to_timezone(p.get_start(), "UTC"))
2.610 + replaced = recurrenceids and start_utc in recurrenceids and "replaced" or ""
2.611 + css = " ".join([
2.612 + replaced,
2.613 + recurrenceid and start_utc == recurrenceid and "affected" or ""
2.614 + ])
2.615 +
2.616 + formdate = show_start and p.get_form_start() or p.get_form_end()
2.617 + t = formdate.as_datetime_item()
2.618 + if t:
2.619 + dt, attr = t
2.620 + page.td(self.format_datetime(dt, "long"), class_=css)
2.621 + else:
2.622 + page.td("(Unrecognised date)")
2.623
2.624 # Full page output methods.
2.625
2.626 @@ -1077,25 +1096,32 @@
2.627 page.option(label, value=v)
2.628 page.select.close()
2.629
2.630 - def _show_date_controls(self, name, default, tzid=None, index=None, show_tzid=True):
2.631 + def _show_date_controls(self, name, default, index=None, show_tzid=True):
2.632
2.633 """
2.634 - Show date controls for a field with the given 'name' and 'default' value
2.635 - and 'tzid'. If 'index' is specified, default field values will be
2.636 - overridden by the element from a collection of existing form values with
2.637 - the specified index; otherwise, field values will be overridden by a
2.638 - single form value.
2.639 + Show date controls for a field with the given 'name' and 'default' form
2.640 + date value.
2.641 +
2.642 + If 'index' is specified, default field values will be overridden by the
2.643 + element from a collection of existing form values with the specified
2.644 + index; otherwise, field values will be overridden by a single form
2.645 + value.
2.646
2.647 If 'show_tzid' is set to a false value, the time zone menu will not be
2.648 provided.
2.649 """
2.650
2.651 page = self.page
2.652 - args = self.env.get_args()
2.653
2.654 # Show dates for up to one week around the current date.
2.655
2.656 - base = to_date(default)
2.657 + t = default.as_datetime_item()
2.658 + if t:
2.659 + dt, attr = t
2.660 + else:
2.661 + dt = date.today()
2.662 +
2.663 + base = to_date(dt)
2.664 items = []
2.665 for i in range(-7, 8):
2.666 d = base + timedelta(i)
2.667 @@ -1105,25 +1131,18 @@
2.668
2.669 # Show time details.
2.670
2.671 - default_time = isinstance(default, datetime) and default or None
2.672 -
2.673 - hour = args.get("%s-hour" % name, [])[index or 0:]
2.674 - hour = hour and hour[0] or "%02d" % (default_time and default_time.hour or 0)
2.675 - minute = args.get("%s-minute" % name, [])[index or 0:]
2.676 - minute = minute and minute[0] or "%02d" % (default_time and default_time.minute or 0)
2.677 - second = args.get("%s-second" % name, [])[index or 0:]
2.678 - second = second and second[0] or "%02d" % (default_time and default_time.second or 0)
2.679 + page.span(class_="time enabled")
2.680 + page.input(name="%s-hour" % name, type="text", value=default.get_hour(), maxlength=2, size=2)
2.681 + page.add(":")
2.682 + page.input(name="%s-minute" % name, type="text", value=default.get_minute(), maxlength=2, size=2)
2.683 + page.add(":")
2.684 + page.input(name="%s-second" % name, type="text", value=default.get_second(), maxlength=2, size=2)
2.685
2.686 - page.span(class_="time enabled")
2.687 - page.input(name="%s-hour" % name, type="text", value=hour, maxlength=2, size=2)
2.688 - page.add(":")
2.689 - page.input(name="%s-minute" % name, type="text", value=minute, maxlength=2, size=2)
2.690 - page.add(":")
2.691 - page.input(name="%s-second" % name, type="text", value=second, maxlength=2, size=2)
2.692 if show_tzid:
2.693 - tzid = tzid or self.get_tzid()
2.694 page.add(" ")
2.695 + tzid = default.get_tzid() or self.get_tzid()
2.696 self._show_timezone_menu("%s-tzid" % name, tzid, index)
2.697 +
2.698 page.span.close()
2.699
2.700 def _show_timezone_menu(self, name, default, index=None):