1.1 --- a/imiptools/client.py Thu Oct 12 23:14:06 2017 +0200
1.2 +++ b/imiptools/client.py Fri Oct 13 15:07:43 2017 +0200
1.3 @@ -19,13 +19,14 @@
1.4 this program. If not, see <http://www.gnu.org/licenses/>.
1.5 """
1.6
1.7 +from collections import OrderedDict
1.8 from datetime import datetime, timedelta
1.9 from imiptools.config import settings
1.10 from imiptools.data import Object, check_delegation, get_address, get_uri, \
1.11 - get_recurrence_periods, \
1.12 + get_main_period, get_recurrence_periods, \
1.13 get_window_end, is_new_object, make_freebusy, \
1.14 - make_uid, to_part, uri_dict, uri_item, uri_items, \
1.15 - uri_parts, uri_values
1.16 + make_uid, new_object, to_part, uri_dict, uri_item, \
1.17 + uri_items, uri_parts, uri_values
1.18 from imiptools.dates import check_permitted_values, format_datetime, \
1.19 get_datetime, get_default_timezone, \
1.20 get_duration, get_time, get_timestamp, \
1.21 @@ -261,6 +262,74 @@
1.22 start=(future_only and self.get_window_start() or None),
1.23 end=(not explicit_only and self.get_window_end() or None))
1.24
1.25 + def get_updated_periods(self, obj):
1.26 +
1.27 + """
1.28 + Return the periods provided by 'obj' and associated recurrence
1.29 + instances. Each original period is returned in a tuple with a
1.30 + corresponding updated period which may be the same or which may be None
1.31 + if the period is cancelled. A list of these tuples is returned.
1.32 + """
1.33 +
1.34 + uid = obj.get_uid()
1.35 + recurrenceid = obj.get_recurrenceid()
1.36 +
1.37 + updated = []
1.38 +
1.39 + # Consider separate recurrences in isolation from the parent if
1.40 + # specified.
1.41 +
1.42 + if recurrenceid:
1.43 + for period in self.get_periods(obj):
1.44 + updated.append((period, period))
1.45 + return updated
1.46 +
1.47 + # For parent events, identify retained and replaced periods.
1.48 +
1.49 + recurrenceids = self.get_recurrences(uid)
1.50 +
1.51 + for period in self.get_periods(obj):
1.52 + recurrenceid = period.is_replaced(recurrenceids)
1.53 +
1.54 + # For parent event periods, obtain any replacement instead of the
1.55 + # replaced period.
1.56 +
1.57 + if recurrenceid:
1.58 + recurrence = self.get_stored_object(uid, recurrenceid)
1.59 + periods = recurrence and self.get_periods(recurrence)
1.60 +
1.61 + # Active periods are obtained.
1.62 +
1.63 + if periods:
1.64 +
1.65 + # Recurrence instances are assumed to provide only one
1.66 + # period.
1.67 +
1.68 + replacement = periods[0]
1.69 +
1.70 + # Redefine the origin of periods replacing recurrences and
1.71 + # not the main period, leaving DTSTART as the means of
1.72 + # identifying the main period.
1.73 +
1.74 + if replacement.origin == "DTSTART" and \
1.75 + period.origin != "DTSTART":
1.76 +
1.77 + replacement.origin = "DTSTART-RECUR"
1.78 +
1.79 + updated.append((period, replacement))
1.80 +
1.81 + # Cancelled periods yield None.
1.82 +
1.83 + else:
1.84 + updated.append((period, None))
1.85 +
1.86 + # Otherwise, retain the known period.
1.87 +
1.88 + else:
1.89 + updated.append((period, period))
1.90 +
1.91 + return updated
1.92 +
1.93 def get_main_period(self, obj):
1.94
1.95 "Return the main period defined by 'obj'."
1.96 @@ -423,6 +492,13 @@
1.97 self.sequence = obj and self.obj.get_value("SEQUENCE")
1.98 self.dtstamp = obj and self.obj.get_value("DTSTAMP")
1.99
1.100 + def new_object(self, objtype):
1.101 +
1.102 + "Initialise a new object for the client with the given 'objtype'."
1.103 +
1.104 + self.set_object(new_object(objtype, self.user, self.get_user_attributes()))
1.105 + return self.obj
1.106 +
1.107 def load_object(self, uid, recurrenceid):
1.108
1.109 "Load the object with the given 'uid' and 'recurrenceid'."
1.110 @@ -446,6 +522,12 @@
1.111
1.112 return True
1.113
1.114 + def is_attendee(self):
1.115 +
1.116 + "Return whether the current user is an attendee in the current object."
1.117 +
1.118 + return self.obj.get_value_map("ATTENDEE").has_key(self.user)
1.119 +
1.120 def is_organiser(self):
1.121
1.122 """
1.123 @@ -461,11 +543,11 @@
1.124 parent = self.get_parent_object()
1.125 return parent and parent.has_recurrence(self.get_tzid(), self.obj.get_recurrenceid())
1.126
1.127 - def get_recurrences(self):
1.128 + def get_recurrences(self, uid=None):
1.129
1.130 "Return the current object's recurrence identifiers."
1.131
1.132 - return self.store.get_recurrences(self.user, self.uid)
1.133 + return self.store.get_recurrences(self.user, uid or self.uid)
1.134
1.135 def get_periods(self, obj=None, explicit_only=False, future_only=False):
1.136
1.137 @@ -473,43 +555,14 @@
1.138
1.139 return Client.get_periods(self, obj or self.obj, explicit_only, future_only)
1.140
1.141 - def get_updated_periods(self):
1.142 + def get_updated_periods(self, obj=None):
1.143
1.144 """
1.145 Return the periods provided by the current object and associated
1.146 - recurrence instances. Each original period is returned in a tuple with
1.147 - a corresponding updated period which may be the same or which may be
1.148 - None if the period is cancelled. A list of these tuples is returned.
1.149 + recurrence instances.
1.150 """
1.151
1.152 - updated = []
1.153 - recurrenceids = self.get_recurrences()
1.154 -
1.155 - for period in self.get_periods():
1.156 - recurrenceid = period.is_replaced(recurrenceids)
1.157 -
1.158 - # Obtain any replacement instead of the replaced period.
1.159 -
1.160 - if recurrenceid:
1.161 - obj = self.get_stored_object(self.uid, recurrenceid)
1.162 - periods = obj and Client.get_periods(self, obj)
1.163 -
1.164 - # Active periods are obtained. Cancelled periods yield None.
1.165 -
1.166 - if periods:
1.167 - p = periods[0]
1.168 - if p.origin == "DTSTART" and period.origin != "DTSTART":
1.169 - p.origin = "DTSTART-RECUR"
1.170 - updated.append((period, p))
1.171 - else:
1.172 - updated.append((period, None))
1.173 -
1.174 - # Otherwise, retain the known period.
1.175 -
1.176 - else:
1.177 - updated.append((period, period))
1.178 -
1.179 - return updated
1.180 + return Client.get_updated_periods(self, obj or self.obj)
1.181
1.182 def get_main_period(self, obj=None):
1.183
1.184 @@ -547,12 +600,17 @@
1.185 obj = obj or self.obj
1.186 calendar_uri = self.messenger and get_uri(self.messenger.sender)
1.187
1.188 - for attendee, attendee_attr in uri_items(obj.get_items("ATTENDEE")):
1.189 - if attendee != self.user:
1.190 - if attendee_attr.get("SENT-BY") == calendar_uri:
1.191 - del attendee_attr["SENT-BY"]
1.192 - else:
1.193 - attendee_attr["SENT-BY"] = calendar_uri
1.194 + for attendee, attendee_attr in uri_items(obj.get_items("ATTENDEE") or []):
1.195 +
1.196 + # Fix up the SENT-BY attribute for this user.
1.197 +
1.198 + if attendee == self.user:
1.199 + self.update_sender_attr(attendee_attr)
1.200 +
1.201 + # Remove any conflicting SENT-BY attributes for other users.
1.202 +
1.203 + elif attendee_attr.get("SENT-BY") == calendar_uri:
1.204 + del attendee_attr["SENT-BY"]
1.205
1.206 def get_sending_attendee(self):
1.207
1.208 @@ -562,7 +620,7 @@
1.209
1.210 senders = self.senders or self.messenger and [self.messenger.sender] or []
1.211
1.212 - for attendee, attendee_attr in uri_items(self.obj.get_items("ATTENDEE")):
1.213 + for attendee, attendee_attr in uri_items(self.obj.get_items("ATTENDEE") or []):
1.214 if get_address(attendee) in senders or \
1.215 get_address(attendee_attr.get("SENT-BY")) in senders:
1.216 return get_uri(attendee)
1.217 @@ -641,86 +699,58 @@
1.218
1.219 return self.store.set_event(self.user, self.uid, self.recurrenceid, obj.to_node())
1.220
1.221 - def update_attendees(self, attendees, removed):
1.222 + def update_attendees(self, to_invite, to_cancel, to_modify):
1.223
1.224 """
1.225 - Update the attendees in the current object with the given 'attendees'
1.226 - and 'removed' attendee lists.
1.227 -
1.228 - A tuple is returned containing two items: a list of the attendees whose
1.229 - attendance is being proposed (in a counter-proposal), a list of the
1.230 - attendees whose attendance should be cancelled.
1.231 + Update the attendees in the current object with the given 'to_invite',
1.232 + 'to_cancel' and 'to_modify' attendee mappings.
1.233 """
1.234
1.235 - to_cancel = []
1.236 -
1.237 - existing_attendees = uri_items(self.obj.get_items("ATTENDEE") or [])
1.238 - existing_attendees_map = dict(existing_attendees)
1.239 + attendees = uri_items(self.obj.get_items("ATTENDEE") or [])
1.240 + attendee_map = OrderedDict(attendees)
1.241
1.242 - # Added attendees are those from the supplied collection not already
1.243 - # present in the object.
1.244 + # Normalise the identities.
1.245
1.246 - added = set(uri_values(attendees)).difference([uri for uri, attr in existing_attendees])
1.247 - removed = uri_values(removed)
1.248 -
1.249 - if added or removed:
1.250 -
1.251 - # The organiser can remove existing attendees.
1.252 + to_invite = uri_dict(to_invite)
1.253 + to_cancel = uri_dict(to_cancel)
1.254 + to_modify = uri_dict(to_modify)
1.255
1.256 - if removed and self.is_organiser():
1.257 - remaining = []
1.258 + if self.is_organiser():
1.259
1.260 - for attendee, attendee_attr in existing_attendees:
1.261 - if attendee in removed:
1.262 -
1.263 - # Only when an event has not been published can
1.264 - # attendees be silently removed.
1.265 + # Remove uninvited attendees.
1.266
1.267 - if self.obj.is_shared():
1.268 - to_cancel.append((attendee, attendee_attr))
1.269 - else:
1.270 - remaining.append((attendee, attendee_attr))
1.271 + for attendee in to_cancel.keys():
1.272 + if attendee_map.has_key(attendee):
1.273 + del attendee_map[attendee]
1.274
1.275 - existing_attendees = remaining
1.276 -
1.277 - # Attendees (when countering) must only include the current user and
1.278 - # any added attendees.
1.279 + # Attendees (when countering) must only include the current user and
1.280 + # any added attendees.
1.281
1.282 - elif not self.is_organiser():
1.283 - existing_attendees = []
1.284 -
1.285 - # Both organisers and attendees (when countering) can add attendees.
1.286 -
1.287 - if added:
1.288 + else:
1.289 + attr = attendee_map.get(self.user) or self.get_user_attributes()
1.290 + attendee_map = {self.user : attr}
1.291
1.292 - # Obtain a mapping from URIs to name details.
1.293 -
1.294 - attendee_map = dict([(attendee_uri, cn) for cn, attendee_uri in uri_parts(attendees)])
1.295 + # Update modified attendees.
1.296
1.297 - for attendee in added:
1.298 - attendee = attendee.strip()
1.299 - if attendee:
1.300 - cn = attendee_map.get(attendee)
1.301 - attendee_attr = {"CN" : cn} or {}
1.302 + for attendee, attr in to_modify.items():
1.303 + existing_attr = attendee_map.get(attendee)
1.304 + if existing_attr:
1.305 + existing_attr.update(attr)
1.306
1.307 - # Only the organiser can reset the participation attributes.
1.308 + # Add newly-invited attendees, applicable for organisers and attendees
1.309 + # (when countering).
1.310
1.311 - if self.is_organiser():
1.312 - attendee_attr.update({"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"})
1.313 -
1.314 - existing_attendees.append((attendee, attendee_attr))
1.315 + for attendee, attr in to_invite.items():
1.316 + if not attendee_map.has_key(attendee):
1.317
1.318 - # Attendees (when countering) must only include the current user and
1.319 - # any added attendees.
1.320 + # Only the organiser can reset the participation attributes.
1.321
1.322 - if not self.is_organiser() and self.user not in existing_attendees:
1.323 - user_attr = self.get_user_attributes()
1.324 - user_attr.update(existing_attendees_map.get(self.user) or {})
1.325 - existing_attendees.append((self.user, user_attr))
1.326 + if self.is_organiser():
1.327 + attr.update({"PARTSTAT" : "NEEDS-ACTION", "RSVP" : "TRUE"})
1.328
1.329 - self.obj["ATTENDEE"] = existing_attendees
1.330 + attendee_map[attendee] = attr
1.331
1.332 - return added, to_cancel
1.333 + self.obj["ATTENDEE"] = attendee_map.items()
1.334
1.335 def update_participation(self, partstat=None):
1.336
1.337 @@ -743,7 +773,7 @@
1.338
1.339 return attendee_attr
1.340
1.341 - def update_event(self, changed=False):
1.342 + def update_event_version(self, changed=False):
1.343
1.344 """
1.345 Update the event version information and details for sending. Where
1.346 @@ -754,16 +784,6 @@
1.347
1.348 if self.is_organiser():
1.349 self.update_sender()
1.350 - else:
1.351 - # Reply only on behalf of this user.
1.352 -
1.353 - attendee_attr = self.update_participation()
1.354 -
1.355 - if not attendee_attr:
1.356 - return False
1.357 -
1.358 - if not changed:
1.359 - self.obj["ATTENDEE"] = [(self.user, attendee_attr)]
1.360
1.361 # Process attendee SENT-BY usage, timestamp and sequence details
1.362 # appropriately for the sender's role.
1.363 @@ -774,6 +794,23 @@
1.364
1.365 return True
1.366
1.367 + def update_event_from_periods(self, to_set, to_exclude):
1.368 +
1.369 + """
1.370 + Set the periods in any redefined event from the 'to_set' list, excluding
1.371 + the main period if it appears in 'to_exclude'.
1.372 + """
1.373 +
1.374 + if to_set:
1.375 + self.obj.set_periods(to_set)
1.376 +
1.377 + # Exclude only the main period, if appropriate.
1.378 +
1.379 + if to_exclude:
1.380 + main = get_main_period(to_exclude)
1.381 + if main:
1.382 + self.obj.update_exceptions([main], [])
1.383 +
1.384 # General message generation methods.
1.385
1.386 def get_recipients(self, obj=None):
1.387 @@ -908,25 +945,40 @@
1.388
1.389 return rescheduled_parts
1.390
1.391 - def make_update_message(self, recipients, to_unschedule=None, to_reschedule=None):
1.392 + def make_update_message(self, recipients, update_parent=False,
1.393 + to_unschedule=None, to_reschedule=None,
1.394 + all_unscheduled=None, all_rescheduled=None,
1.395 + to_add=None):
1.396
1.397 """
1.398 Prepare event updates from the organiser of an event for the given
1.399 - 'recipients', using the period collections 'to_unschedule' and
1.400 - 'to_reschedule'.
1.401 + 'recipients', including the parent event if 'update_parent' is set to a
1.402 + true value.
1.403 +
1.404 + Additional parts are provided by the 'to_unschedule' and 'to_reschedule'
1.405 + collections. Alternatively, where the parent event is being updated, the
1.406 + 'all_unscheduled' and 'all_rescheduled' period collections are included.
1.407 +
1.408 + The 'to_add' period collection augments the existing periods.
1.409 """
1.410
1.411 - # Start with the parent object and augment it with the given
1.412 - # amendments providing cancelled and modified occurrence information.
1.413 + parts = []
1.414
1.415 - parts = [self.object_to_part("REQUEST", self.obj)]
1.416 - unscheduled_parts = self.get_rescheduled_parts(to_unschedule, "CANCEL")
1.417 - rescheduled_parts = self.get_rescheduled_parts(to_reschedule, "REQUEST")
1.418 + if update_parent:
1.419 + parts.append(self.object_to_part("REQUEST", self.obj))
1.420 + unscheduled = all_unscheduled
1.421 + rescheduled = all_rescheduled
1.422 + else:
1.423 + unscheduled = to_unschedule
1.424 + rescheduled = to_reschedule
1.425
1.426 - return self.make_message(parts + unscheduled_parts + rescheduled_parts,
1.427 - recipients)
1.428 + parts += self.get_rescheduled_parts(unscheduled, "CANCEL")
1.429 + parts += self.get_rescheduled_parts(rescheduled, "REQUEST")
1.430 + parts += self.get_rescheduled_parts(to_add, "ADD")
1.431 + return self.make_message(parts, recipients)
1.432
1.433 - def make_self_update_message(self, to_unschedule=None, to_reschedule=None):
1.434 + def make_self_update_message(self, all_unscheduled=None, all_rescheduled=None,
1.435 + to_add=None):
1.436
1.437 """
1.438 Prepare event updates to be sent from the organiser of an event to
1.439 @@ -934,34 +986,51 @@
1.440 """
1.441
1.442 parts = [self.object_to_part("PUBLISH", self.obj)]
1.443 - unscheduled_parts = self.get_rescheduled_parts(to_unschedule, "CANCEL")
1.444 - rescheduled_parts = self.get_rescheduled_parts(to_reschedule, "PUBLISH")
1.445 - return self.make_message_for_self(parts + unscheduled_parts + rescheduled_parts)
1.446 + parts += self.get_rescheduled_parts(all_unscheduled, "CANCEL")
1.447 + parts += self.get_rescheduled_parts(all_rescheduled, "PUBLISH")
1.448 + parts += self.get_rescheduled_parts(to_add, "ADD")
1.449 + return self.make_message_for_self(parts)
1.450
1.451 - def make_cancel_object(self, to_cancel=None):
1.452 + def make_response_message(self, recipients, update_parent=False,
1.453 + all_rescheduled=None, to_reschedule=None):
1.454
1.455 """
1.456 - Prepare an event cancellation object involving the participants in the
1.457 - 'to_cancel' list.
1.458 + Prepare a response to 'recipients', including the parent event if
1.459 + 'update_parent' is set to a true value, incorporating 'all_rescheduled'
1.460 + periods, of which there may be indicated periods 'to_reschedule'.
1.461 """
1.462
1.463 - if to_cancel:
1.464 - obj = self.obj.copy()
1.465 - obj["ATTENDEE"] = to_cancel
1.466 - else:
1.467 - obj = self.obj
1.468 + parts = [self.object_to_part(update_parent and "COUNTER" or "REPLY", self.obj)]
1.469 +
1.470 + # Determine existing replaced periods that are not newly rescheduled.
1.471 +
1.472 + rescheduled_unmodified = set(all_rescheduled or []).difference(to_reschedule or [])
1.473 +
1.474 + if rescheduled_unmodified:
1.475 + parts += self.get_rescheduled_parts(rescheduled_unmodified, update_parent and "COUNTER" or "REPLY")
1.476
1.477 - return obj
1.478 + # Suggest details for newly rescheduled periods.
1.479
1.480 - def make_cancel_message(self, recipients, obj):
1.481 + if to_reschedule:
1.482 + parts += self.get_rescheduled_parts(to_reschedule, "COUNTER")
1.483 +
1.484 + return self.make_message(parts, recipients, bcc_sender=True)
1.485 +
1.486 + def make_cancel_message(self, to_cancel=None):
1.487
1.488 """
1.489 - Prepare an event cancellation message to 'recipients' using the details
1.490 - in 'obj'.
1.491 + Prepare an event cancellation message involving the participants in the
1.492 + 'to_cancel' mapping.
1.493 """
1.494
1.495 + if not to_cancel:
1.496 + return None
1.497 +
1.498 + obj = self.obj.copy()
1.499 + obj["ATTENDEE"] = to_cancel.items()
1.500 +
1.501 parts = [self.object_to_part("CANCEL", obj)]
1.502 - return self.make_message(parts, recipients)
1.503 + return self.make_message(parts, to_cancel.keys())
1.504
1.505 def make_cancel_message_for_self(self, obj):
1.506
1.507 @@ -970,36 +1039,23 @@
1.508 parts = [self.object_to_part("CANCEL", obj)]
1.509 return self.make_message_for_self(parts)
1.510
1.511 - def make_response_message(self, recipients, changed=False):
1.512 -
1.513 - """
1.514 - Prepare a response to 'recipients' for the current object with the
1.515 - indicated 'changed' state.
1.516 - """
1.517 -
1.518 - # NOTE: Might need updating to include rescheduled objects.
1.519 -
1.520 - parts = [self.object_to_part(changed and "COUNTER" or "REPLY", self.obj)]
1.521 - return self.make_message(parts, recipients, bcc_sender=True)
1.522 -
1.523 # Action methods.
1.524
1.525 - def process_declined_counter(self, attendee):
1.526 + def send_declined_counter_to_attendee(self, attendee):
1.527
1.528 - "Process a declined counter-proposal."
1.529 + "Send a declined counter-proposal to 'attendee'."
1.530
1.531 # Obtain the counter-proposal for the attendee.
1.532
1.533 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee)
1.534 if not obj:
1.535 - return False
1.536 + return
1.537
1.538 - method = "DECLINECOUNTER"
1.539 self.update_senders(obj)
1.540 obj.update_dtstamp()
1.541 obj.update_sequence()
1.542
1.543 - parts = [self.object_to_part(method, obj)]
1.544 + parts = [self.object_to_part("DECLINECOUNTER", obj)]
1.545
1.546 # Create and send the response.
1.547
1.548 @@ -1007,86 +1063,67 @@
1.549 message = self.make_message(parts, recipients, bcc_sender=True)
1.550 self.send_message(message, recipients, bcc_sender=True)
1.551
1.552 - return True
1.553 -
1.554 - def process_received_request(self, changed=False):
1.555 + def send_response_to_organiser(self, all_rescheduled=None, to_reschedule=None,
1.556 + changed=False):
1.557
1.558 """
1.559 - Process the current request for the current user. Return whether any
1.560 - action was taken. If 'changed' is set to a true value, or if 'attendees'
1.561 - is specified and differs from the stored attendees, a counter-proposal
1.562 - will be sent instead of a reply.
1.563 + Send a response to the organiser describing attendance and proposed
1.564 + amendments to the event.
1.565 +
1.566 + If 'all_rescheduled' is specified, it provides details of separate
1.567 + recurrence instances for which a response needs to be generated.
1.568 +
1.569 + If 'to_reschedule' provides rescheduled periods, these will be sent as
1.570 + counter-proposals.
1.571 +
1.572 + If 'changed' is set to a true value, a counter-proposal will be sent for
1.573 + the entire event instead of a reply.
1.574 """
1.575
1.576 - if not self.update_event(changed):
1.577 - return False
1.578 -
1.579 - # Create and send the response.
1.580 -
1.581 recipients = self.get_recipients()
1.582 - message = self.make_response_message(recipients, changed)
1.583 + message = self.make_response_message(recipients, all_rescheduled,
1.584 + to_reschedule, changed)
1.585 self.send_message(message, recipients, bcc_sender=True)
1.586
1.587 - return True
1.588 -
1.589 - def process_created_request(self, method, to_cancel=None,
1.590 - to_unschedule=None, to_reschedule=None):
1.591 + def send_update_to_recipients(self, to_unschedule=None, to_reschedule=None):
1.592
1.593 """
1.594 - Process the current request, sending a created request of the given
1.595 - 'method' to attendees. Return whether any action was taken.
1.596 -
1.597 - If 'to_cancel' is specified, a list of participants to be sent cancel
1.598 - messages is provided.
1.599 + Send cancellations for each of the recurrences 'to_unschedule' along
1.600 + with modifications for each of the recurrences 'to_reschedule'.
1.601 + """
1.602
1.603 - If 'to_unschedule' is specified, a list of periods to be unscheduled is
1.604 - provided.
1.605 + recipients = self.get_recipients()
1.606 + message = self.make_update_message(recipients, to_unschedule, to_reschedule)
1.607 + self.send_message(message, recipients)
1.608
1.609 - If 'to_reschedule' is specified, a list of periods to be rescheduled is
1.610 - provided.
1.611 + def send_publish_to_self(self, all_unscheduled=None, all_rescheduled=None):
1.612
1.613 - Note that this method, although similar to get_message_parts, processes
1.614 - the core object and the explicitly-specified objects, not the separate
1.615 - recurrence instances that are already stored.
1.616 + """
1.617 + Send published event details incorporating 'all_unscheduled' and
1.618 + 'all_rescheduled' periods.
1.619 """
1.620
1.621 - self.update_event()
1.622 -
1.623 - if method == "REQUEST":
1.624 -
1.625 - # Send the updated event, along with a cancellation for each of the
1.626 - # unscheduled occurrences.
1.627 + # Since the organiser can update the SEQUENCE but this can leave any
1.628 + # mail/calendar client lagging, issue a PUBLISH message to the
1.629 + # user's address.
1.630
1.631 - recipients = self.get_recipients()
1.632 - message = self.make_update_message(recipients, to_unschedule, to_reschedule)
1.633 - self.send_message(message, recipients)
1.634 + recipients = self.get_recipients()
1.635 + message = self.make_self_update_message(all_unscheduled, all_rescheduled)
1.636 + self.send_message_to_self(message)
1.637
1.638 - # Since the organiser can update the SEQUENCE but this can leave any
1.639 - # mail/calendar client lagging, issue a PUBLISH message to the
1.640 - # user's address.
1.641 -
1.642 - message = self.make_self_update_message(to_unschedule, to_reschedule)
1.643 - self.send_message_to_self(message)
1.644 + def send_cancel_to_recipients(self, to_cancel=None):
1.645
1.646 - # When cancelling, replace the attendees with those for whom the event
1.647 - # is now cancelled.
1.648 + "Send a cancellation to all uninvited attendees in 'to_cancel'."
1.649
1.650 - if method == "CANCEL" or to_cancel:
1.651 -
1.652 - # Send a cancellation to all uninvited attendees.
1.653 + message = self.make_cancel_message(to_cancel)
1.654 + self.send_message(message, to_cancel.keys())
1.655
1.656 - obj = self.make_cancel_object(to_cancel)
1.657 - recipients = self.get_recipients(obj)
1.658 - message = self.make_cancel_message(recipients, obj)
1.659 - self.send_message(message, recipients)
1.660 + def send_cancel_to_self(self):
1.661
1.662 - # Issue a CANCEL message to the user's address.
1.663 + "Issue a CANCEL message to the user's address."
1.664
1.665 - if method == "CANCEL":
1.666 - message = self.make_cancel_message_for_self(obj)
1.667 - self.send_message_to_self(message)
1.668 -
1.669 - return True
1.670 + message = self.make_cancel_message_for_self(self.obj)
1.671 + self.send_message_to_self(message)
1.672
1.673 # Object-related tests.
1.674