# HG changeset patch # User Paul Boddie # Date 1445102122 -7200 # Node ID 9d9d2ab4734fbd12124601a58266a8dd92ab3985 # Parent 36b25bf2b7a9b6faa82a4ef76a45593c7a5d7849 Consolidated descriptions of events for both REFRESH requests and for messages sent by the manager, bundling the necessary REQUEST and CANCEL payloads to describe events and their recurrences. Changed organiser-sent messages in the manager to be also sent to the user's own address, not the people-outgoing version of that address, adding support for self-sent CANCEL messages in the person handler. Added the removal of cancelled recurrences to restore periods in events edited in the manager. Otherwise, such periods can never be restored. Expanded the REFRESH method tests. diff -r 36b25bf2b7a9 -r 9d9d2ab4734f imiptools/client.py --- a/imiptools/client.py Sat Oct 17 19:08:30 2015 +0200 +++ b/imiptools/client.py Sat Oct 17 19:15:22 2015 +0200 @@ -23,7 +23,7 @@ from imiptools import config from imiptools.data import Object, get_address, get_uri, get_window_end, \ is_new_object, make_freebusy, to_part, \ - uri_dict, uri_items, uri_parts, uri_values + uri_dict, uri_item, uri_items, uri_parts, uri_values from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \ get_duration, get_timestamp from imiptools.period import can_schedule, remove_period, \ @@ -291,6 +291,79 @@ update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires) + # Preparation of messages communicating the state of events. + + def get_message_parts(self, obj, method, attendee=None): + + """ + Return a tuple containing a list of methods and a list of message parts, + with the parts collectively describing the given object 'obj' and its + recurrences, using 'method' as the means of publishing details (with + CANCEL being used to retract or remove details). + + If 'attendee' is indicated, the attendee's participation will be taken + into account when generating the description. + """ + + # Assume that the outcome will be composed of requests and + # cancellations. It would not seem completely bizarre to produce + # publishing messages if a refresh message was unprovoked. + + responses = [] + methods = set() + + # Get the parent event, add SENT-BY details to the organiser. + + if not attendee or self.is_participating(attendee, obj=obj): + organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER")) + self.update_sender(organiser_attr) + responses.append(obj.to_part(method)) + methods.add(method) + + # Get recurrences for parent events. + + if not self.recurrenceid: + + # Collect active and cancelled recurrences. + + for rl, section, rmethod in [ + (self.store.get_active_recurrences(self.user, self.uid), None, method), + (self.store.get_cancelled_recurrences(self.user, self.uid), "cancellations", "CANCEL"), + ]: + + for recurrenceid in rl: + + # Get the recurrence, add SENT-BY details to the organiser. + + obj = self.get_stored_object(self.uid, recurrenceid, section) + + if not attendee or self.is_participating(attendee, obj=obj): + organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER")) + self.update_sender(organiser_attr) + responses.append(obj.to_part(rmethod)) + methods.add(rmethod) + + return methods, responses + + def get_unscheduled_parts(self, periods): + + "Return message parts describing unscheduled 'periods'." + + unscheduled_parts = [] + + if periods: + obj = self.obj.copy() + obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"]) + + for p in periods: + if not p.origin: + continue + obj["RECURRENCE-ID"] = obj["DTSTART"] = [(format_datetime(p.get_start()), p.get_start_attr())] + obj["DTEND"] = [(format_datetime(p.get_end()), p.get_end_attr())] + unscheduled_parts.append(obj.to_part("CANCEL")) + + return unscheduled_parts + class ClientForObject(Client): "A client maintaining a specific object." @@ -668,6 +741,18 @@ return self.recurrenceid and self.get_stored_object(self.uid, None) or None + def revert_cancellations(self, periods): + + """ + Restore cancelled recurrences corresponding to any of the given + 'periods'. + """ + + for recurrenceid in self.store.get_cancelled_recurrences(self.user, self.uid): + obj = self.get_stored_object(self.uid, recurrenceid, "cancellations") + if set(self.get_periods(obj)).intersection(periods): + self.store.remove_cancellation(self.user, self.uid, recurrenceid) + # Convenience methods for modifying free/busy collections. def get_recurrence_start_point(self, recurrenceid): diff -r 36b25bf2b7a9 -r 9d9d2ab4734f imiptools/handlers/__init__.py --- a/imiptools/handlers/__init__.py Sat Oct 17 19:08:30 2015 +0200 +++ b/imiptools/handlers/__init__.py Sat Oct 17 19:15:22 2015 +0200 @@ -83,13 +83,25 @@ """ Record a result having the given 'method', 'outgoing_recipients' and - message part. + message 'part'. """ if outgoing_recipients: self.outgoing_methods.add(method) self.results.append((outgoing_recipients, part)) + def add_results(self, methods, outgoing_recipients, parts): + + """ + Record results having the given 'methods', 'outgoing_recipients' and + message 'parts'. + """ + + if outgoing_recipients: + self.outgoing_methods.update(methods) + for part in parts: + self.results.append((outgoing_recipients, part)) + def get_results(self): return self.results diff -r 36b25bf2b7a9 -r 9d9d2ab4734f imiptools/handlers/person.py --- a/imiptools/handlers/person.py Sat Oct 17 19:08:30 2015 +0200 +++ b/imiptools/handlers/person.py Sat Oct 17 19:15:22 2015 +0200 @@ -19,7 +19,7 @@ this program. If not, see . """ -from imiptools.data import get_address, to_part, uri_dict, uri_item +from imiptools.data import get_address from imiptools.handlers import Handler from imiptools.handlers.common import CommonFreebusy, CommonEvent from imiptools.period import FreeBusyPeriod, Period, replace_overlapping @@ -103,6 +103,24 @@ return True + def _cancel(self): + + "Record an event cancellation." + + # Handle an event being published by the sender to themself. + + organiser_item = self.require_organiser() + if organiser_item: + organiser, organiser_attr = organiser_item + if self.user == organiser: + self.store.cancel_event(self.user, self.uid, self.recurrenceid) + self.store.dequeue_request(self.user, self.uid, self.recurrenceid) + self.store.remove_counters(self.user, self.uid, self.recurrenceid) + self.remove_event_from_freebusy() + return True + + return self._record(from_organiser=True, queue=False, cancel=True) + def _declinecounter(self): "Revoke any counter-proposal recorded as a free/busy offer." @@ -128,6 +146,7 @@ organiser, organiser_attr = organiser_item if self.user == organiser: self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node()) + self.update_event_in_freebusy() return True return self._record(from_organiser=True, queue=False) @@ -232,50 +251,11 @@ if not attendees: return False - # Assume that the outcome will be a request. It would not seem - # completely bizarre to produce a publishing message instead if a - # refresh message was unprovoked. + # Produce REQUEST and CANCEL results. for attendee in attendees: - responses = [] - cancel_responses = [] - - # Get the parent event, add SENT-BY details to the organiser. - - obj = self.get_stored_object_version() - - if self.is_participating(attendee, obj=obj): - organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER")) - self.update_sender(organiser_attr) - responses.append(obj.to_node()) - - # Get recurrences for parent events. - - if not self.recurrenceid: - - # Collect active and cancelled recurrences. - - for l, rl, section in [ - (responses, self.store.get_active_recurrences(self.user, self.uid), None), - (cancel_responses, self.store.get_cancelled_recurrences(self.user, self.uid), "cancellations"), - ]: - for recurrenceid in rl: - - # Get the recurrence, add SENT-BY details to the organiser. - - obj = self.get_stored_object(self.uid, recurrenceid, section) - - if self.is_participating(attendee, obj=obj): - organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER")) - self.update_sender(organiser_attr) - l.append(obj.to_node()) - - method = "REQUEST" - self.add_result(method, [get_address(attendee)], to_part(method, responses)) - - if cancel_responses: - method = "CANCEL" - self.add_result(method, [get_address(attendee)], to_part(method, cancel_responses)) + methods, parts = self.get_message_parts(obj, "REQUEST", attendee) + self.add_results(methods, [get_address(attendee)], parts) return True @@ -294,7 +274,7 @@ "Queue a cancellation of any active event." - if self._record(from_organiser=True, queue=False, cancel=True): + if self._cancel(): return self.wrap("An event cancellation has been received.", link=False) def counter(self): diff -r 36b25bf2b7a9 -r 9d9d2ab4734f imipweb/event.py --- a/imipweb/event.py Sat Oct 17 19:08:30 2015 +0200 +++ b/imipweb/event.py Sat Oct 17 19:15:22 2015 +0200 @@ -866,6 +866,7 @@ changed = self.obj.set_period(period) or changed changed = self.obj.set_periods(periods) or changed changed = self.obj.update_exceptions(to_exclude) or changed + changed = self.revert_cancellations(periods) or changed # Organiser-only changes... @@ -995,7 +996,7 @@ "Return period details for the recurrences specified for an event." - return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())] + return set([p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())]) # Access to form-originating object information. diff -r 36b25bf2b7a9 -r 9d9d2ab4734f imipweb/resource.py --- a/imipweb/resource.py Sat Oct 17 19:08:30 2015 +0200 +++ b/imipweb/resource.py Sat Oct 17 19:15:22 2015 +0200 @@ -219,7 +219,7 @@ # Communication methods. - def send_message(self, parts, sender, from_organiser): + def send_message(self, parts, sender, from_organiser, bcc_sender): """ Send the given 'parts' to the appropriate recipients, also sending a @@ -254,19 +254,23 @@ if part: parts.append(part) - self._send_message(sender, recipients, parts) + self._send_message(sender, recipients, parts, bcc_sender) - def _send_message(self, sender, recipients, parts): + def _send_message(self, sender, recipients, parts, bcc_sender): """ Send a message, explicitly specifying the 'sender' as an outgoing BCC recipient since the generic calendar user will be the actual sender. """ - message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender) - self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender) + if bcc_sender: + message = self.messenger.make_outgoing_message(parts, recipients) + self.messenger.sendmail(recipients, message.as_string()) + else: + message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender) + self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender) - def _send_message_to_self(self, parts): + def send_message_to_self(self, parts): "Send a message composed of the given 'parts' to the given user." @@ -290,7 +294,7 @@ self.update_senders(obj=obj) obj.update_dtstamp() obj.update_sequence(False) - self._send_message(get_address(self.user), [get_address(attendee)], parts=[obj.to_part(method)]) + self._send_message(get_address(self.user), [get_address(attendee)], [obj.to_part(method)], True) return True def process_received_request(self, changed=False): @@ -316,7 +320,7 @@ self.update_dtstamp() self.update_sequence(False) - self.send_message([self.obj.to_part(changed and "COUNTER" or "REPLY")], get_address(self.user), from_organiser=False) + self.send_message([self.obj.to_part(changed and "COUNTER" or "REPLY")], get_address(self.user), False, True) return True def process_created_request(self, method, to_cancel=None, to_unschedule=None): @@ -341,42 +345,43 @@ self.update_dtstamp() self.update_sequence(True) - parts = [self.obj.to_part(method)] + if method == "REQUEST": + methods, parts = self.get_message_parts(self.obj, "REQUEST") - # Add message parts with cancelled occurrence information. - # NOTE: This could probably be merged with the updated event message. + # Add message parts with cancelled occurrence information. - if to_unschedule: - obj = self.obj.copy() - obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"]) + unscheduled_parts = self.get_unscheduled_parts(to_unschedule) - for p in to_unschedule: - if not p.origin: - continue - obj["RECURRENCE-ID"] = [(format_datetime(p.get_start()), p.get_start_attr())] - parts.append(obj.to_part("CANCEL")) + # Send the updated event, along with a cancellation for each of the + # unscheduled occurrences. + + self.send_message(parts + unscheduled_parts, get_address(organiser), True, False) - # Send the updated event, along with a cancellation for each of the - # unscheduled occurrences. + # Since the organiser can update the SEQUENCE but this can leave any + # mail/calendar client lagging, issue a PUBLISH message to the + # user's address. - self.send_message(parts, get_address(organiser), from_organiser=True) + methods, parts = self.get_message_parts(self.obj, "PUBLISH") + self.send_message_to_self(parts + unscheduled_parts) # When cancelling, replace the attendees with those for whom the event # is now cancelled. - if to_cancel: - obj = self.obj.copy() - obj["ATTENDEE"] = to_cancel + if method == "CANCEL" or to_cancel: + if to_cancel: + obj = self.obj.copy() + obj["ATTENDEE"] = to_cancel + else: + obj = self.obj # Send a cancellation to all uninvited attendees. - self.send_message([self.obj.to_part("CANCEL")], get_address(organiser), from_organiser=True) + parts = [obj.to_part("CANCEL")] + self.send_message(parts, get_address(organiser), True, False) - # Since the organiser can update the SEQUENCE but this can leave any - # mail/calendar client lagging, issue a PUBLISH message to the user's - # address. + # Issue a CANCEL message to the user's address. - self._send_message_to_self([self.obj.to_part("PUBLISH")]) + self.send_message_to_self(parts) return True diff -r 36b25bf2b7a9 -r 9d9d2ab4734f tests/test_person_invitation_refresh.sh --- a/tests/test_person_invitation_refresh.sh Sat Oct 17 19:08:30 2015 +0200 +++ b/tests/test_person_invitation_refresh.sh Sat Oct 17 19:15:22 2015 +0200 @@ -113,6 +113,7 @@ # Test another request from an attendee for the event details to be refreshed. "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/event-refresh-person-recurring.txt" 2>> $ERROR \ +| tee out6r.tmp \ | "$SHOWMAIL" \ > out6.tmp @@ -124,7 +125,7 @@ # Process the resulting message. - "$PERSON_SCRIPT" $ARGS < out6.tmp 2>> $ERROR \ + "$PERSON_SCRIPT" $ARGS < out6r.tmp 2>> $ERROR \ | "$SHOWMAIL" \ > out6a.tmp @@ -211,3 +212,27 @@ grep -q "^20141010T080000Z${TAB}20141010T090000Z" "$FBFILE" \ && echo "Success" \ || echo "Failed" + +# Test yet another request from an attendee for the event details to be refreshed. + + "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/event-refresh-person-recurring.txt" 2>> $ERROR \ +| tee out10r.tmp \ +| "$SHOWMAIL" \ +> out10.tmp + + grep -q 'METHOD:REQUEST' out10.tmp \ +&& grep -q 'RECURRENCE-ID' out10.tmp \ +&& [ `grep 'BEGIN:VEVENT' out10.tmp | wc -l` = '2' ] \ +&& echo "Success" \ +|| echo "Failed" + +# Process the resulting message. + + "$PERSON_SCRIPT" $ARGS < out10r.tmp 2>> $ERROR \ +| "$SHOWMAIL" \ +> out11.tmp + + [ -e "$STORE/$USER/objects/event8@example.com" ] \ +&& [ -e "$STORE/$USER/recurrences/event8@example.com/20141010T080000Z" ] \ +&& echo "Success" \ +|| echo "Failed"