1.1 --- a/imipweb/event.py Fri Sep 15 00:03:38 2017 +0200
1.2 +++ b/imipweb/event.py Mon Sep 18 20:34:43 2017 +0200
1.3 @@ -23,10 +23,11 @@
1.4 uri_parts, uri_values
1.5 from imiptools.dates import format_datetime, to_timezone
1.6 from imiptools.mail import Messenger
1.7 -from imipweb.data import EventPeriod, event_period_from_period, \
1.8 - form_period_from_period, \
1.9 - classify_periods, filter_duplicates, \
1.10 - remove_from_collection, \
1.11 +from imipweb.data import event_periods_from_periods, \
1.12 + form_period_from_period, form_periods_from_updated_periods, \
1.13 + classify_operations, classify_periods, combine_periods, \
1.14 + get_recurrence_periods, \
1.15 + filter_duplicates, remove_from_collection, \
1.16 get_period_control_values, \
1.17 PeriodError
1.18 from imipweb.resource import DateTimeFormUtilities, FormUtilities, ResourceClientForObject
1.19 @@ -111,33 +112,16 @@
1.20 def get_stored_attendees(self):
1.21 return [get_verbose_address(value, attr) for value, attr in self.obj.get_items("ATTENDEE") or []]
1.22
1.23 - def get_stored_main_period(self):
1.24 -
1.25 - "Return the main event period for the current object."
1.26 -
1.27 - period = self.obj.get_main_period(self.get_tzid())
1.28 - return event_period_from_period(period)
1.29 -
1.30 - def get_stored_recurrences(self):
1.31 -
1.32 - "Return recurrences computed using the current object."
1.33 -
1.34 - recurrenceids = self._get_recurrences(self.uid)
1.35 - recurrences = []
1.36 - for period in self.get_periods(self.obj):
1.37 - period = event_period_from_period(period)
1.38 - period.replaced = period.is_replaced(recurrenceids)
1.39 - if period.origin != "DTSTART":
1.40 - recurrences.append(period)
1.41 - return recurrences
1.42 -
1.43 # Access to current object information.
1.44
1.45 - def get_current_main_period(self):
1.46 - return self.get_stored_main_period()
1.47 + def get_stored_main_period(self):
1.48 + return form_period_from_period(self.get_main_period())
1.49
1.50 - def get_current_recurrences(self):
1.51 - return self.get_stored_recurrences()
1.52 + def get_stored_recurrence_periods(self):
1.53 + return get_recurrence_periods(form_periods_from_updated_periods(self.get_updated_periods()))
1.54 +
1.55 + get_current_main_period = get_stored_main_period
1.56 + get_current_recurrence_periods = get_stored_recurrence_periods
1.57
1.58 def get_current_attendees(self):
1.59 return self.get_stored_attendees()
1.60 @@ -216,15 +200,8 @@
1.61
1.62 attendees = self.get_current_attendees()
1.63 period = self.get_current_main_period()
1.64 - stored_period = self.get_stored_main_period()
1.65 self.show_object_datetime_controls(period)
1.66
1.67 - # Obtain any separate recurrences for this event.
1.68 -
1.69 - recurrenceids = self._get_recurrences(self.uid)
1.70 - replaced = not self.recurrenceid and period.is_replaced(recurrenceids)
1.71 - excluded = period == stored_period and period not in self.get_periods(self.obj)
1.72 -
1.73 # Provide a summary of the object.
1.74
1.75 page.table(class_="object", cellspacing=5, cellpadding=5)
1.76 @@ -259,41 +236,23 @@
1.77 # Handle datetimes specially.
1.78
1.79 if name in ("DTSTART", "DTEND"):
1.80 - if not replaced and not excluded:
1.81 -
1.82 - # Obtain the datetime.
1.83 -
1.84 - is_start = name == "DTSTART"
1.85 + is_start = name == "DTSTART"
1.86
1.87 - # Where no end datetime exists, use the start datetime as the
1.88 - # basis of any potential datetime specified if dt-control is
1.89 - # set.
1.90 -
1.91 - self.show_datetime_controls(is_start and period.get_form_start() or period.get_form_end(), is_start)
1.92 -
1.93 - elif name == "DTSTART":
1.94 + # Obtain the datetime.
1.95
1.96 - # Replaced occurrences link to their replacements.
1.97 -
1.98 - if replaced:
1.99 - page.td(class_="objectvalue %s replaced" % field, rowspan=2, colspan=2)
1.100 - page.a(_("First occurrence replaced by a separate event"), href=self.link_to(self.uid, replaced))
1.101 - page.td.close()
1.102 + # Where no end datetime exists, use the start datetime as the
1.103 + # basis of any potential datetime specified if dt-control is
1.104 + # set.
1.105
1.106 - # NOTE: Should provide a way of editing recurrences when the
1.107 - # NOTE: first occurrence is excluded, plus a way of
1.108 - # NOTE: reinstating the occurrence.
1.109 -
1.110 - elif excluded:
1.111 - page.td(class_="objectvalue %s excluded" % field, rowspan=2, colspan=2)
1.112 - page.add(_("First occurrence excluded"))
1.113 - page.td.close()
1.114 + self.show_datetime_controls(is_start and period.get_form_start() or period.get_form_end(), is_start)
1.115 + if is_start:
1.116 + self.show_period_state(None, period)
1.117
1.118 page.tr.close()
1.119
1.120 # After the end datetime, show a control to add recurrences.
1.121
1.122 - if name == "DTEND":
1.123 + if not is_start:
1.124 page.tr()
1.125 page.td(colspan=2)
1.126 self.control("recur-add", "submit", "add", id="recur-add", class_="add")
1.127 @@ -452,7 +411,7 @@
1.128
1.129 # Obtain the periods associated with the event.
1.130
1.131 - recurrences = self.get_current_recurrences()
1.132 + recurrences = self.get_current_recurrence_periods()
1.133
1.134 if len(recurrences) < 1:
1.135 return
1.136 @@ -501,13 +460,32 @@
1.137 self.show_recurrence_controls(index, period, recurrenceid, False)
1.138 page.tr.close()
1.139
1.140 + # Actions.
1.141 +
1.142 + page.tr()
1.143 + page.th("")
1.144 + page.td()
1.145 +
1.146 + # Permit the restoration of cancelled recurrences.
1.147 +
1.148 + if period.cancelled:
1.149 + self.control("recur-restore", "checkbox", str(index),
1.150 + period in self.get_state("recur-restore", list),
1.151 + id="recur-restore-%d" % index, class_="restore")
1.152 +
1.153 + page.label(for_="recur-restore-%d" % index, class_="restore")
1.154 + page.add(_("(Removed)"))
1.155 + page.span(_("Restore"), class_="action")
1.156 + page.label.close()
1.157 +
1.158 + page.label(for_="recur-restore-%d" % index, class_="restored")
1.159 + page.add(_("(Restored)"))
1.160 + page.span(_("Remove"), class_="action")
1.161 + page.label.close()
1.162 +
1.163 # Permit the removal of recurrences.
1.164
1.165 - if not period.replaced:
1.166 - page.tr()
1.167 - page.th("")
1.168 - page.td()
1.169 -
1.170 + else:
1.171 # Attendees can instantly remove recurrences and thus produce a
1.172 # counter-proposal. Organisers may need to unschedule recurrences
1.173 # instead.
1.174 @@ -524,8 +502,8 @@
1.175 page.span(_("Re-add"), class_="action")
1.176 page.label.close()
1.177
1.178 - page.td.close()
1.179 - page.tr.close()
1.180 + page.td.close()
1.181 + page.tr.close()
1.182
1.183 page.tbody.close()
1.184 page.table.close()
1.185 @@ -628,15 +606,12 @@
1.186 page.thead.close()
1.187 page.tbody()
1.188
1.189 - recurrenceids = self._get_recurrences(self.uid)
1.190 -
1.191 suggested_periods = list(suggested_periods.items())
1.192 suggested_periods.sort()
1.193
1.194 for attendee, periods in suggested_periods:
1.195 first = True
1.196 for p in periods:
1.197 - replaced = not self.recurrenceid and p.is_replaced(recurrenceids)
1.198 identifier = "%s-%s" % (format_datetime(p.get_start_point()), format_datetime(p.get_end_point()))
1.199 css = identifier == counter and "selected" or ""
1.200
1.201 @@ -647,9 +622,8 @@
1.202
1.203 # Show each period.
1.204
1.205 - css = replaced and "replaced" or ""
1.206 - page.td(start, class_=css)
1.207 - page.td(end, class_=css)
1.208 + page.td(start)
1.209 + page.td(end)
1.210
1.211 # Show attendees and controls alongside the first period in each
1.212 # attendee's collection.
1.213 @@ -838,42 +812,57 @@
1.214
1.215 single_user = False
1.216 changed = False
1.217 + to_cancel = []
1.218 + to_unschedule = []
1.219 + to_reschedule = []
1.220
1.221 - if reply or create or cancel or save:
1.222 + if reply or create or save:
1.223
1.224 - # Update time periods (main and recurring).
1.225 + # Update time periods.
1.226
1.227 try:
1.228 - period = self.handle_main_period()
1.229 - except PeriodError, exc:
1.230 - return exc.args
1.231 -
1.232 - try:
1.233 - periods = self.handle_recurrence_periods()
1.234 + periods = self.handle_periods()
1.235 except PeriodError, exc:
1.236 return exc.args
1.237
1.238 - # Set the periods in the object, first obtaining removed and
1.239 - # modified period information.
1.240 # NOTE: Currently, rules are not updated.
1.241
1.242 - editable_periods, to_change, to_remove = self.classify_periods(periods)
1.243 + # Obtain removed and modified period information.
1.244 +
1.245 + new, to_change, unchanged, removed = self.classify_periods(periods)
1.246 +
1.247 + # Determine the modifications required to represent the edits.
1.248
1.249 - active_periods = editable_periods + to_change
1.250 - to_unschedule = self.is_organiser() and to_remove or []
1.251 - to_exclude = not self.is_organiser() and to_remove or []
1.252 + to_unschedule, to_reschedule, to_exclude, to_set = \
1.253 + classify_operations(new, to_change, unchanged, removed,
1.254 + self.is_organiser(), self.obj.is_shared())
1.255 +
1.256 + # Set the periods in any redefined event.
1.257 +
1.258 + if to_set:
1.259 + self.obj.set_periods(to_set)
1.260 +
1.261 + # Add and remove exceptions.
1.262
1.263 - periods = set(periods)
1.264 - changed = self.obj.set_period(period) or changed
1.265 - changed = self.obj.set_periods(periods) or changed
1.266 + if to_exclude:
1.267 + self.obj.update_exceptions(to_exclude, to_set)
1.268 +
1.269 + # Obtain any new participants and those to be removed.
1.270
1.271 - # Add and remove exceptions.
1.272 + attendees = self.get_current_attendees()
1.273 + removed = self.get_removed_attendees()
1.274 +
1.275 + added, to_cancel = self.update_attendees(attendees, removed)
1.276
1.277 - changed = self.obj.update_exceptions(to_exclude, active_periods) or changed
1.278 + # Determine the properties of the event for subsequent actions.
1.279 +
1.280 + single_user = not attendees or uri_values(attendees) == [self.user]
1.281 + changed = added or to_set or to_change or removed
1.282
1.283 - # Assert periods restored after cancellation.
1.284 + # Update attendee participation for the current user.
1.285
1.286 - changed = self.revert_cancellations(active_periods) or changed
1.287 + if args.has_key("partstat"):
1.288 + self.update_participation(args["partstat"][0])
1.289
1.290 # Organiser-only changes...
1.291
1.292 @@ -884,20 +873,6 @@
1.293 if args.has_key("summary"):
1.294 self.obj["SUMMARY"] = [(args["summary"][0], {})]
1.295
1.296 - # Obtain any new participants and those to be removed.
1.297 -
1.298 - attendees = self.get_current_attendees()
1.299 - removed = self.get_removed_attendees()
1.300 -
1.301 - added, to_cancel = self.update_attendees(attendees, removed)
1.302 - single_user = not attendees or uri_values(attendees) == [self.user]
1.303 - changed = added or changed
1.304 -
1.305 - # Update attendee participation for the current user.
1.306 -
1.307 - if args.has_key("partstat"):
1.308 - self.update_participation(args["partstat"][0])
1.309 -
1.310 # Process any action.
1.311
1.312 invite = not save and create and not single_user
1.313 @@ -915,10 +890,11 @@
1.314
1.315 elif self.is_organiser() and (invite or cancel):
1.316
1.317 - # Invitation, uninvitation and unscheduling...
1.318 + # Invitation, uninvitation and unscheduling, rescheduling...
1.319
1.320 if self.process_created_request(
1.321 - invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule):
1.322 + invite and "REQUEST" or "CANCEL", to_cancel,
1.323 + to_unschedule, to_reschedule):
1.324
1.325 self.remove_request()
1.326
1.327 @@ -996,19 +972,16 @@
1.328
1.329 return None
1.330
1.331 - def handle_main_period(self):
1.332 -
1.333 - "Return period details for the main start/end period in an event."
1.334 + def handle_periods(self):
1.335
1.336 - return self.get_current_main_period().as_event_period()
1.337 -
1.338 - def handle_recurrence_periods(self):
1.339 -
1.340 - "Return period details for the recurrences specified for an event."
1.341 + "Return period details for the periods specified for an event."
1.342
1.343 periods = []
1.344 - for i, p in enumerate(self.get_current_recurrences()):
1.345 + periods.append(self.get_current_main_period().as_event_period())
1.346 +
1.347 + for i, p in enumerate(self.get_current_recurrence_periods()):
1.348 periods.append(p.as_event_period(i))
1.349 +
1.350 return periods
1.351
1.352 # Access to form-originating object information.
1.353 @@ -1021,6 +994,9 @@
1.354 "dtstart", "dtend",
1.355 "dtend-control", "dttimes-control",
1.356 origin="DTSTART",
1.357 + replacement_name="main-replacement",
1.358 + cancelled_name="main-cancelled",
1.359 + recurrenceid_name="main-id",
1.360 tzid=self.get_tzid())
1.361
1.362 # Handle absent main period details.
1.363 @@ -1037,7 +1013,9 @@
1.364 return get_period_control_values(self.env.get_args(),
1.365 "dtstart-recur", "dtend-recur",
1.366 "dtend-control-recur", "dttimes-control-recur",
1.367 - origin_name="recur-origin", replaced_name="recur-replaced",
1.368 + origin_name="recur-origin",
1.369 + replacement_name="recur-replacement",
1.370 + cancelled_name="recur-cancelled",
1.371 recurrenceid_name="recur-id",
1.372 tzid=self.get_tzid())
1.373
1.374 @@ -1045,16 +1023,17 @@
1.375
1.376 """
1.377 From the recurrence 'periods' and information provided in the request,
1.378 - return a tuple containing the new and unchanged periods, the changed
1.379 + return a tuple containing the new periods, unchanged periods, changed
1.380 periods, and the periods to be removed.
1.381 """
1.382
1.383 - # Get remaining periods and those whose removal is deferred.
1.384 + # Combine original recurrences with the edited, updated recurrences.
1.385
1.386 - new, changed, unchanged, replaced, to_remove = classify_periods(periods,
1.387 - self.get_state("recur-remove", list))
1.388 + stored = event_periods_from_periods(self.get_recurrence_periods())
1.389 + current = get_recurrence_periods(periods)
1.390
1.391 - return new + unchanged, changed, to_remove
1.392 + updated = combine_periods(stored, current)
1.393 + return classify_periods(updated)
1.394
1.395 def get_attendees_from_page(self):
1.396
1.397 @@ -1109,11 +1088,11 @@
1.398 # Only actually remove attendees if the event is unsent, if the attendee
1.399 # is new, or if it is the current user being removed.
1.400
1.401 - remove = args.has_key("remove")
1.402 + remove = args.get("remove")
1.403
1.404 if remove:
1.405 - still_to_remove = remove_from_collection(attendees,
1.406 - args["remove"], self.can_remove_attendee)
1.407 + still_to_remove = remove_from_collection(attendees, remove,
1.408 + self.can_remove_attendee)
1.409 self.set_state("remove", still_to_remove)
1.410
1.411 if add or add_suggested or remove:
1.412 @@ -1129,22 +1108,36 @@
1.413
1.414 recurrences = self.get_recurrences_from_page()
1.415
1.416 + # Add new recurrences by copying the main period.
1.417 +
1.418 add = args.has_key("recur-add")
1.419
1.420 if add:
1.421 - period = self.get_current_main_period().as_form_period()
1.422 + period = self.get_current_main_period()
1.423 period.origin = "RDATE"
1.424 + period.replacement = False
1.425 + period.cancelled = False
1.426 + period.recurrenceid = None
1.427 recurrences.append(period)
1.428
1.429 # Only actually remove recurrences if the event is unsent, or if the
1.430 # recurrence is new, but only for explicit recurrences.
1.431
1.432 - remove = args.has_key("recur-remove")
1.433 + remove = args.get("recur-remove")
1.434
1.435 if remove:
1.436 - still_to_remove = remove_from_collection(recurrences,
1.437 - args["recur-remove"], self.can_remove_recurrence)
1.438 - self.set_state("recur-remove", still_to_remove)
1.439 + for p in remove_from_collection(recurrences, remove,
1.440 + self.can_remove_recurrence):
1.441 + p.replacement = True
1.442 + p.cancelled = True
1.443 +
1.444 + # Restore previously-cancelled recurrences.
1.445 +
1.446 + restore = args.get("recur-restore")
1.447 +
1.448 + if restore:
1.449 + for index in restore:
1.450 + recurrences[int(index)].cancelled = False
1.451
1.452 return recurrences
1.453
1.454 @@ -1182,7 +1175,7 @@
1.455 return self.get_state("main", self.is_initial_load() and
1.456 self.get_stored_main_period or self.get_main_period_from_page)
1.457
1.458 - def get_current_recurrences(self):
1.459 + def get_current_recurrence_periods(self):
1.460
1.461 """
1.462 Return recurrences for the current object using the original object
1.463 @@ -1190,14 +1183,14 @@
1.464 """
1.465
1.466 return self.get_state("recurrences", self.is_initial_load() and
1.467 - self.get_stored_recurrences or self.get_recurrences_from_page)
1.468 + self.get_stored_recurrence_periods or self.get_recurrences_from_page)
1.469
1.470 def update_current_recurrences(self):
1.471
1.472 "Return an updated collection of recurrences for the current object."
1.473
1.474 return self.get_state("recurrences", self.is_initial_load() and
1.475 - self.get_stored_recurrences or self.update_recurrences_from_page,
1.476 + self.get_stored_recurrence_periods or self.update_recurrences_from_page,
1.477 overwrite=True)
1.478
1.479 def get_current_attendees(self):