1.1 --- a/imiptools/client.py Sat Oct 17 19:08:30 2015 +0200
1.2 +++ b/imiptools/client.py Sat Oct 17 19:15:22 2015 +0200
1.3 @@ -23,7 +23,7 @@
1.4 from imiptools import config
1.5 from imiptools.data import Object, get_address, get_uri, get_window_end, \
1.6 is_new_object, make_freebusy, to_part, \
1.7 - uri_dict, uri_items, uri_parts, uri_values
1.8 + uri_dict, uri_item, uri_items, uri_parts, uri_values
1.9 from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \
1.10 get_duration, get_timestamp
1.11 from imiptools.period import can_schedule, remove_period, \
1.12 @@ -291,6 +291,79 @@
1.13
1.14 update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires)
1.15
1.16 + # Preparation of messages communicating the state of events.
1.17 +
1.18 + def get_message_parts(self, obj, method, attendee=None):
1.19 +
1.20 + """
1.21 + Return a tuple containing a list of methods and a list of message parts,
1.22 + with the parts collectively describing the given object 'obj' and its
1.23 + recurrences, using 'method' as the means of publishing details (with
1.24 + CANCEL being used to retract or remove details).
1.25 +
1.26 + If 'attendee' is indicated, the attendee's participation will be taken
1.27 + into account when generating the description.
1.28 + """
1.29 +
1.30 + # Assume that the outcome will be composed of requests and
1.31 + # cancellations. It would not seem completely bizarre to produce
1.32 + # publishing messages if a refresh message was unprovoked.
1.33 +
1.34 + responses = []
1.35 + methods = set()
1.36 +
1.37 + # Get the parent event, add SENT-BY details to the organiser.
1.38 +
1.39 + if not attendee or self.is_participating(attendee, obj=obj):
1.40 + organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
1.41 + self.update_sender(organiser_attr)
1.42 + responses.append(obj.to_part(method))
1.43 + methods.add(method)
1.44 +
1.45 + # Get recurrences for parent events.
1.46 +
1.47 + if not self.recurrenceid:
1.48 +
1.49 + # Collect active and cancelled recurrences.
1.50 +
1.51 + for rl, section, rmethod in [
1.52 + (self.store.get_active_recurrences(self.user, self.uid), None, method),
1.53 + (self.store.get_cancelled_recurrences(self.user, self.uid), "cancellations", "CANCEL"),
1.54 + ]:
1.55 +
1.56 + for recurrenceid in rl:
1.57 +
1.58 + # Get the recurrence, add SENT-BY details to the organiser.
1.59 +
1.60 + obj = self.get_stored_object(self.uid, recurrenceid, section)
1.61 +
1.62 + if not attendee or self.is_participating(attendee, obj=obj):
1.63 + organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
1.64 + self.update_sender(organiser_attr)
1.65 + responses.append(obj.to_part(rmethod))
1.66 + methods.add(rmethod)
1.67 +
1.68 + return methods, responses
1.69 +
1.70 + def get_unscheduled_parts(self, periods):
1.71 +
1.72 + "Return message parts describing unscheduled 'periods'."
1.73 +
1.74 + unscheduled_parts = []
1.75 +
1.76 + if periods:
1.77 + obj = self.obj.copy()
1.78 + obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"])
1.79 +
1.80 + for p in periods:
1.81 + if not p.origin:
1.82 + continue
1.83 + obj["RECURRENCE-ID"] = obj["DTSTART"] = [(format_datetime(p.get_start()), p.get_start_attr())]
1.84 + obj["DTEND"] = [(format_datetime(p.get_end()), p.get_end_attr())]
1.85 + unscheduled_parts.append(obj.to_part("CANCEL"))
1.86 +
1.87 + return unscheduled_parts
1.88 +
1.89 class ClientForObject(Client):
1.90
1.91 "A client maintaining a specific object."
1.92 @@ -668,6 +741,18 @@
1.93
1.94 return self.recurrenceid and self.get_stored_object(self.uid, None) or None
1.95
1.96 + def revert_cancellations(self, periods):
1.97 +
1.98 + """
1.99 + Restore cancelled recurrences corresponding to any of the given
1.100 + 'periods'.
1.101 + """
1.102 +
1.103 + for recurrenceid in self.store.get_cancelled_recurrences(self.user, self.uid):
1.104 + obj = self.get_stored_object(self.uid, recurrenceid, "cancellations")
1.105 + if set(self.get_periods(obj)).intersection(periods):
1.106 + self.store.remove_cancellation(self.user, self.uid, recurrenceid)
1.107 +
1.108 # Convenience methods for modifying free/busy collections.
1.109
1.110 def get_recurrence_start_point(self, recurrenceid):
2.1 --- a/imiptools/handlers/__init__.py Sat Oct 17 19:08:30 2015 +0200
2.2 +++ b/imiptools/handlers/__init__.py Sat Oct 17 19:15:22 2015 +0200
2.3 @@ -83,13 +83,25 @@
2.4
2.5 """
2.6 Record a result having the given 'method', 'outgoing_recipients' and
2.7 - message part.
2.8 + message 'part'.
2.9 """
2.10
2.11 if outgoing_recipients:
2.12 self.outgoing_methods.add(method)
2.13 self.results.append((outgoing_recipients, part))
2.14
2.15 + def add_results(self, methods, outgoing_recipients, parts):
2.16 +
2.17 + """
2.18 + Record results having the given 'methods', 'outgoing_recipients' and
2.19 + message 'parts'.
2.20 + """
2.21 +
2.22 + if outgoing_recipients:
2.23 + self.outgoing_methods.update(methods)
2.24 + for part in parts:
2.25 + self.results.append((outgoing_recipients, part))
2.26 +
2.27 def get_results(self):
2.28 return self.results
2.29
3.1 --- a/imiptools/handlers/person.py Sat Oct 17 19:08:30 2015 +0200
3.2 +++ b/imiptools/handlers/person.py Sat Oct 17 19:15:22 2015 +0200
3.3 @@ -19,7 +19,7 @@
3.4 this program. If not, see <http://www.gnu.org/licenses/>.
3.5 """
3.6
3.7 -from imiptools.data import get_address, to_part, uri_dict, uri_item
3.8 +from imiptools.data import get_address
3.9 from imiptools.handlers import Handler
3.10 from imiptools.handlers.common import CommonFreebusy, CommonEvent
3.11 from imiptools.period import FreeBusyPeriod, Period, replace_overlapping
3.12 @@ -103,6 +103,24 @@
3.13
3.14 return True
3.15
3.16 + def _cancel(self):
3.17 +
3.18 + "Record an event cancellation."
3.19 +
3.20 + # Handle an event being published by the sender to themself.
3.21 +
3.22 + organiser_item = self.require_organiser()
3.23 + if organiser_item:
3.24 + organiser, organiser_attr = organiser_item
3.25 + if self.user == organiser:
3.26 + self.store.cancel_event(self.user, self.uid, self.recurrenceid)
3.27 + self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
3.28 + self.store.remove_counters(self.user, self.uid, self.recurrenceid)
3.29 + self.remove_event_from_freebusy()
3.30 + return True
3.31 +
3.32 + return self._record(from_organiser=True, queue=False, cancel=True)
3.33 +
3.34 def _declinecounter(self):
3.35
3.36 "Revoke any counter-proposal recorded as a free/busy offer."
3.37 @@ -128,6 +146,7 @@
3.38 organiser, organiser_attr = organiser_item
3.39 if self.user == organiser:
3.40 self.store.set_event(self.user, self.uid, self.recurrenceid, self.obj.to_node())
3.41 + self.update_event_in_freebusy()
3.42 return True
3.43
3.44 return self._record(from_organiser=True, queue=False)
3.45 @@ -232,50 +251,11 @@
3.46 if not attendees:
3.47 return False
3.48
3.49 - # Assume that the outcome will be a request. It would not seem
3.50 - # completely bizarre to produce a publishing message instead if a
3.51 - # refresh message was unprovoked.
3.52 + # Produce REQUEST and CANCEL results.
3.53
3.54 for attendee in attendees:
3.55 - responses = []
3.56 - cancel_responses = []
3.57 -
3.58 - # Get the parent event, add SENT-BY details to the organiser.
3.59 -
3.60 - obj = self.get_stored_object_version()
3.61 -
3.62 - if self.is_participating(attendee, obj=obj):
3.63 - organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
3.64 - self.update_sender(organiser_attr)
3.65 - responses.append(obj.to_node())
3.66 -
3.67 - # Get recurrences for parent events.
3.68 -
3.69 - if not self.recurrenceid:
3.70 -
3.71 - # Collect active and cancelled recurrences.
3.72 -
3.73 - for l, rl, section in [
3.74 - (responses, self.store.get_active_recurrences(self.user, self.uid), None),
3.75 - (cancel_responses, self.store.get_cancelled_recurrences(self.user, self.uid), "cancellations"),
3.76 - ]:
3.77 - for recurrenceid in rl:
3.78 -
3.79 - # Get the recurrence, add SENT-BY details to the organiser.
3.80 -
3.81 - obj = self.get_stored_object(self.uid, recurrenceid, section)
3.82 -
3.83 - if self.is_participating(attendee, obj=obj):
3.84 - organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
3.85 - self.update_sender(organiser_attr)
3.86 - l.append(obj.to_node())
3.87 -
3.88 - method = "REQUEST"
3.89 - self.add_result(method, [get_address(attendee)], to_part(method, responses))
3.90 -
3.91 - if cancel_responses:
3.92 - method = "CANCEL"
3.93 - self.add_result(method, [get_address(attendee)], to_part(method, cancel_responses))
3.94 + methods, parts = self.get_message_parts(obj, "REQUEST", attendee)
3.95 + self.add_results(methods, [get_address(attendee)], parts)
3.96
3.97 return True
3.98
3.99 @@ -294,7 +274,7 @@
3.100
3.101 "Queue a cancellation of any active event."
3.102
3.103 - if self._record(from_organiser=True, queue=False, cancel=True):
3.104 + if self._cancel():
3.105 return self.wrap("An event cancellation has been received.", link=False)
3.106
3.107 def counter(self):
4.1 --- a/imipweb/event.py Sat Oct 17 19:08:30 2015 +0200
4.2 +++ b/imipweb/event.py Sat Oct 17 19:15:22 2015 +0200
4.3 @@ -866,6 +866,7 @@
4.4 changed = self.obj.set_period(period) or changed
4.5 changed = self.obj.set_periods(periods) or changed
4.6 changed = self.obj.update_exceptions(to_exclude) or changed
4.7 + changed = self.revert_cancellations(periods) or changed
4.8
4.9 # Organiser-only changes...
4.10
4.11 @@ -995,7 +996,7 @@
4.12
4.13 "Return period details for the recurrences specified for an event."
4.14
4.15 - return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())]
4.16 + return set([p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())])
4.17
4.18 # Access to form-originating object information.
4.19
5.1 --- a/imipweb/resource.py Sat Oct 17 19:08:30 2015 +0200
5.2 +++ b/imipweb/resource.py Sat Oct 17 19:15:22 2015 +0200
5.3 @@ -219,7 +219,7 @@
5.4
5.5 # Communication methods.
5.6
5.7 - def send_message(self, parts, sender, from_organiser):
5.8 + def send_message(self, parts, sender, from_organiser, bcc_sender):
5.9
5.10 """
5.11 Send the given 'parts' to the appropriate recipients, also sending a
5.12 @@ -254,19 +254,23 @@
5.13 if part:
5.14 parts.append(part)
5.15
5.16 - self._send_message(sender, recipients, parts)
5.17 + self._send_message(sender, recipients, parts, bcc_sender)
5.18
5.19 - def _send_message(self, sender, recipients, parts):
5.20 + def _send_message(self, sender, recipients, parts, bcc_sender):
5.21
5.22 """
5.23 Send a message, explicitly specifying the 'sender' as an outgoing BCC
5.24 recipient since the generic calendar user will be the actual sender.
5.25 """
5.26
5.27 - message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender)
5.28 - self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender)
5.29 + if bcc_sender:
5.30 + message = self.messenger.make_outgoing_message(parts, recipients)
5.31 + self.messenger.sendmail(recipients, message.as_string())
5.32 + else:
5.33 + message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender)
5.34 + self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender)
5.35
5.36 - def _send_message_to_self(self, parts):
5.37 + def send_message_to_self(self, parts):
5.38
5.39 "Send a message composed of the given 'parts' to the given user."
5.40
5.41 @@ -290,7 +294,7 @@
5.42 self.update_senders(obj=obj)
5.43 obj.update_dtstamp()
5.44 obj.update_sequence(False)
5.45 - self._send_message(get_address(self.user), [get_address(attendee)], parts=[obj.to_part(method)])
5.46 + self._send_message(get_address(self.user), [get_address(attendee)], [obj.to_part(method)], True)
5.47 return True
5.48
5.49 def process_received_request(self, changed=False):
5.50 @@ -316,7 +320,7 @@
5.51
5.52 self.update_dtstamp()
5.53 self.update_sequence(False)
5.54 - self.send_message([self.obj.to_part(changed and "COUNTER" or "REPLY")], get_address(self.user), from_organiser=False)
5.55 + self.send_message([self.obj.to_part(changed and "COUNTER" or "REPLY")], get_address(self.user), False, True)
5.56 return True
5.57
5.58 def process_created_request(self, method, to_cancel=None, to_unschedule=None):
5.59 @@ -341,42 +345,43 @@
5.60 self.update_dtstamp()
5.61 self.update_sequence(True)
5.62
5.63 - parts = [self.obj.to_part(method)]
5.64 + if method == "REQUEST":
5.65 + methods, parts = self.get_message_parts(self.obj, "REQUEST")
5.66
5.67 - # Add message parts with cancelled occurrence information.
5.68 - # NOTE: This could probably be merged with the updated event message.
5.69 + # Add message parts with cancelled occurrence information.
5.70
5.71 - if to_unschedule:
5.72 - obj = self.obj.copy()
5.73 - obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"])
5.74 + unscheduled_parts = self.get_unscheduled_parts(to_unschedule)
5.75
5.76 - for p in to_unschedule:
5.77 - if not p.origin:
5.78 - continue
5.79 - obj["RECURRENCE-ID"] = [(format_datetime(p.get_start()), p.get_start_attr())]
5.80 - parts.append(obj.to_part("CANCEL"))
5.81 + # Send the updated event, along with a cancellation for each of the
5.82 + # unscheduled occurrences.
5.83 +
5.84 + self.send_message(parts + unscheduled_parts, get_address(organiser), True, False)
5.85
5.86 - # Send the updated event, along with a cancellation for each of the
5.87 - # unscheduled occurrences.
5.88 + # Since the organiser can update the SEQUENCE but this can leave any
5.89 + # mail/calendar client lagging, issue a PUBLISH message to the
5.90 + # user's address.
5.91
5.92 - self.send_message(parts, get_address(organiser), from_organiser=True)
5.93 + methods, parts = self.get_message_parts(self.obj, "PUBLISH")
5.94 + self.send_message_to_self(parts + unscheduled_parts)
5.95
5.96 # When cancelling, replace the attendees with those for whom the event
5.97 # is now cancelled.
5.98
5.99 - if to_cancel:
5.100 - obj = self.obj.copy()
5.101 - obj["ATTENDEE"] = to_cancel
5.102 + if method == "CANCEL" or to_cancel:
5.103 + if to_cancel:
5.104 + obj = self.obj.copy()
5.105 + obj["ATTENDEE"] = to_cancel
5.106 + else:
5.107 + obj = self.obj
5.108
5.109 # Send a cancellation to all uninvited attendees.
5.110
5.111 - self.send_message([self.obj.to_part("CANCEL")], get_address(organiser), from_organiser=True)
5.112 + parts = [obj.to_part("CANCEL")]
5.113 + self.send_message(parts, get_address(organiser), True, False)
5.114
5.115 - # Since the organiser can update the SEQUENCE but this can leave any
5.116 - # mail/calendar client lagging, issue a PUBLISH message to the user's
5.117 - # address.
5.118 + # Issue a CANCEL message to the user's address.
5.119
5.120 - self._send_message_to_self([self.obj.to_part("PUBLISH")])
5.121 + self.send_message_to_self(parts)
5.122
5.123 return True
5.124
6.1 --- a/tests/test_person_invitation_refresh.sh Sat Oct 17 19:08:30 2015 +0200
6.2 +++ b/tests/test_person_invitation_refresh.sh Sat Oct 17 19:15:22 2015 +0200
6.3 @@ -113,6 +113,7 @@
6.4 # Test another request from an attendee for the event details to be refreshed.
6.5
6.6 "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/event-refresh-person-recurring.txt" 2>> $ERROR \
6.7 +| tee out6r.tmp \
6.8 | "$SHOWMAIL" \
6.9 > out6.tmp
6.10
6.11 @@ -124,7 +125,7 @@
6.12
6.13 # Process the resulting message.
6.14
6.15 - "$PERSON_SCRIPT" $ARGS < out6.tmp 2>> $ERROR \
6.16 + "$PERSON_SCRIPT" $ARGS < out6r.tmp 2>> $ERROR \
6.17 | "$SHOWMAIL" \
6.18 > out6a.tmp
6.19
6.20 @@ -211,3 +212,27 @@
6.21 grep -q "^20141010T080000Z${TAB}20141010T090000Z" "$FBFILE" \
6.22 && echo "Success" \
6.23 || echo "Failed"
6.24 +
6.25 +# Test yet another request from an attendee for the event details to be refreshed.
6.26 +
6.27 + "$PERSON_SCRIPT" $ARGS < "$TEMPLATES/event-refresh-person-recurring.txt" 2>> $ERROR \
6.28 +| tee out10r.tmp \
6.29 +| "$SHOWMAIL" \
6.30 +> out10.tmp
6.31 +
6.32 + grep -q 'METHOD:REQUEST' out10.tmp \
6.33 +&& grep -q 'RECURRENCE-ID' out10.tmp \
6.34 +&& [ `grep 'BEGIN:VEVENT' out10.tmp | wc -l` = '2' ] \
6.35 +&& echo "Success" \
6.36 +|| echo "Failed"
6.37 +
6.38 +# Process the resulting message.
6.39 +
6.40 + "$PERSON_SCRIPT" $ARGS < out10r.tmp 2>> $ERROR \
6.41 +| "$SHOWMAIL" \
6.42 +> out11.tmp
6.43 +
6.44 + [ -e "$STORE/$USER/objects/event8@example.com" ] \
6.45 +&& [ -e "$STORE/$USER/recurrences/event8@example.com/20141010T080000Z" ] \
6.46 +&& echo "Success" \
6.47 +|| echo "Failed"