imip-agent

Changeset

809:e6600c59ed01
2015-10-05 Paul Boddie raw files shortlog changelog graph Moved SEQUENCE and DTSTAMP update methods to Object, adjusting method names. Introduced support for declining counter-proposals in the Web interface. Changed the handlers to explicitly remove counter-proposals alongside dequeuing requests, making some minor fixes. Use the stored attributes to present attendee details when editing events.
imiptools/client.py (file) imiptools/data.py (file) imiptools/handlers/person.py (file) imiptools/handlers/person_outgoing.py (file) imipweb/event.py (file) imipweb/resource.py (file) tests/test_handle.py (file)
     1.1 --- a/imiptools/client.py	Mon Oct 05 23:53:38 2015 +0200
     1.2 +++ b/imiptools/client.py	Mon Oct 05 23:56:48 2015 +0200
     1.3 @@ -25,7 +25,7 @@
     1.4                             is_new_object, make_freebusy, to_part, \
     1.5                             uri_dict, uri_items, uri_values
     1.6  from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \
     1.7 -                            get_duration, get_time, get_timestamp
     1.8 +                            get_duration, get_timestamp
     1.9  from imiptools.period import can_schedule, remove_period, \
    1.10                               remove_additional_periods, remove_affected_period, \
    1.11                               update_freebusy
    1.12 @@ -370,21 +370,19 @@
    1.13          self.obj["RECURRENCE-ID"] = [self.obj.get_item("DTSTART")]
    1.14          self.recurrenceid = self.obj.get_recurrenceid()
    1.15  
    1.16 -    def update_dtstamp(self):
    1.17 +    def update_dtstamp(self, obj=None):
    1.18  
    1.19 -        "Update the DTSTAMP in the current object."
    1.20 +        "Update the DTSTAMP in the current object or any given object 'obj'."
    1.21 +
    1.22 +        obj = obj or self.obj
    1.23 +        self.dtstamp = obj.update_dtstamp()
    1.24  
    1.25 -        dtstamp = self.obj.get_utc_datetime("DTSTAMP")
    1.26 -        utcnow = get_time()
    1.27 -        self.dtstamp = format_datetime(dtstamp and dtstamp > utcnow and dtstamp or utcnow)
    1.28 -        self.obj["DTSTAMP"] = [(self.dtstamp, {})]
    1.29 +    def update_sequence(self, increment=False, obj=None):
    1.30  
    1.31 -    def set_sequence(self, increment=False):
    1.32 +        "Update the SEQUENCE in the current object or any given object 'obj'."
    1.33  
    1.34 -        "Update the SEQUENCE in the current object."
    1.35 -
    1.36 -        sequence = self.obj.get_value("SEQUENCE") or "0"
    1.37 -        self.obj["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})]
    1.38 +        obj = obj or self.obj
    1.39 +        obj.update_sequence(increment)
    1.40  
    1.41      def merge_attendance(self, attendees):
    1.42  
    1.43 @@ -866,4 +864,17 @@
    1.44  
    1.45          return True
    1.46  
    1.47 +    # Convenience methods for removing counter-proposals and updating the
    1.48 +    # request queue.
    1.49 +
    1.50 +    def remove_counter(self, attendee):
    1.51 +        self.remove_counters([attendee])
    1.52 +
    1.53 +    def remove_counters(self, attendees):
    1.54 +        for attendee in attendees:
    1.55 +            self.store.remove_counter(self.user, attendee, self.uid, self.recurrenceid)
    1.56 +
    1.57 +        if not self.store.get_counters(self.user, self.uid, self.recurrenceid):
    1.58 +            self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
    1.59 +
    1.60  # vim: tabstop=4 expandtab shiftwidth=4
     2.1 --- a/imiptools/data.py	Mon Oct 05 23:53:38 2015 +0200
     2.2 +++ b/imiptools/data.py	Mon Oct 05 23:56:48 2015 +0200
     2.3 @@ -28,7 +28,8 @@
     2.4                              get_datetime_tzid, \
     2.5                              get_duration, get_period, get_period_item, \
     2.6                              get_recurrence_start_point, \
     2.7 -                            get_tzid, to_datetime, to_timezone, to_utc_datetime
     2.8 +                            get_time, get_tzid, to_datetime, to_timezone, \
     2.9 +                            to_utc_datetime
    2.10  from imiptools.period import FreeBusyPeriod, Period, RecurringPeriod, period_overlaps
    2.11  from vCalendar import iterwrite, parse, ParseError, to_dict, to_node
    2.12  from vRecurrence import get_parameters, get_rule
    2.13 @@ -419,6 +420,24 @@
    2.14  
    2.15          return old_values != set(self.get_date_values("RDATE") or [])
    2.16  
    2.17 +    def update_dtstamp(self):
    2.18 +
    2.19 +        "Update the DTSTAMP in the object."
    2.20 +
    2.21 +        dtstamp = self.get_utc_datetime("DTSTAMP")
    2.22 +        utcnow = get_time()
    2.23 +        dtstamp = format_datetime(dtstamp and dtstamp > utcnow and dtstamp or utcnow)
    2.24 +        self["DTSTAMP"] = [(dtstamp, {})]
    2.25 +        return dtstamp
    2.26 +
    2.27 +    def update_sequence(self, increment=False):
    2.28 +
    2.29 +        "Set or update the SEQUENCE in the object."
    2.30 +
    2.31 +        sequence = self.get_value("SEQUENCE") or "0"
    2.32 +        self["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})]
    2.33 +        return sequence
    2.34 +
    2.35      def update_exceptions(self, excluded):
    2.36  
    2.37          """
     3.1 --- a/imiptools/handlers/person.py	Mon Oct 05 23:53:38 2015 +0200
     3.2 +++ b/imiptools/handlers/person.py	Mon Oct 05 23:56:48 2015 +0200
     3.3 @@ -164,11 +164,11 @@
     3.4  
     3.5              if cancel:
     3.6                  self.store.cancel_event(self.user, self.uid, self.recurrenceid)
     3.7 -                self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
     3.8  
     3.9                  # Remove any associated request.
    3.10  
    3.11                  self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
    3.12 +                self.store.remove_counters(self.user, self.uid, self.recurrenceid)
    3.13  
    3.14                  # No return message will occur to update the free/busy
    3.15                  # information, so this is done here using outgoing message
     4.1 --- a/imiptools/handlers/person_outgoing.py	Mon Oct 05 23:53:38 2015 +0200
     4.2 +++ b/imiptools/handlers/person_outgoing.py	Mon Oct 05 23:56:48 2015 +0200
     4.3 @@ -119,6 +119,7 @@
     4.4          # Remove any associated request.
     4.5  
     4.6          self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
     4.7 +        self.store.remove_counters(self.user, self.uid, self.recurrenceid)
     4.8  
     4.9          # Update free/busy information.
    4.10  
    4.11 @@ -197,11 +198,11 @@
    4.12  
    4.13          if cancel_entire_event:
    4.14              self.store.cancel_event(self.user, self.uid, self.recurrenceid)
    4.15 -            self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
    4.16  
    4.17          # Remove any associated request.
    4.18  
    4.19          self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
    4.20 +        self.store.remove_counters(self.user, self.uid, self.recurrenceid)
    4.21  
    4.22          return True
    4.23  
    4.24 @@ -217,7 +218,7 @@
    4.25          if not self.have_new_object():
    4.26              return False
    4.27  
    4.28 -        self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
    4.29 +        self.remove_counters(uri_values(self.obj.get_values("ATTENDEE")))
    4.30  
    4.31  class Event(PersonHandler):
    4.32  
     5.1 --- a/imipweb/event.py	Mon Oct 05 23:53:38 2015 +0200
     5.2 +++ b/imipweb/event.py	Mon Oct 05 23:56:48 2015 +0200
     5.3 @@ -534,6 +534,8 @@
     5.4          if not attendees:
     5.5              return
     5.6  
     5.7 +        attendees = self.get_verbose_attendees(attendees)
     5.8 +
     5.9          page.p("The following counter-proposals have been received for this event:")
    5.10  
    5.11          page.table(cellspacing=5, cellpadding=5, class_="counters")
    5.12 @@ -549,8 +551,9 @@
    5.13          page.thead.close()
    5.14          page.tbody()
    5.15  
    5.16 -        for attendee in attendees:
    5.17 -            obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee)
    5.18 +        for i, attendee in enumerate(attendees):
    5.19 +            attendee_uri = get_uri(attendee)
    5.20 +            obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee_uri)
    5.21              periods = self.get_periods(obj)
    5.22  
    5.23              first = True
    5.24 @@ -562,7 +565,6 @@
    5.25  
    5.26                  if first:
    5.27                      page.td(attendee, rowspan=len(periods))
    5.28 -                    first = False
    5.29  
    5.30                  start = self.format_datetime(to_timezone(p.get_start(), tzid), "long")
    5.31                  end = self.format_datetime(to_timezone(p.get_end(), tzid), "long")
    5.32 @@ -570,7 +572,14 @@
    5.33                  page.td(start)
    5.34                  page.td(end)
    5.35  
    5.36 +                if first:
    5.37 +                    page.td(rowspan=len(periods))
    5.38 +                    self.control("decline-%d" % i, "submit", "Decline")
    5.39 +                    self.control("decline", "hidden", attendee)
    5.40 +                    page.td.close()
    5.41 +
    5.42                  page.tr.close()
    5.43 +                first = False
    5.44  
    5.45          page.tbody.close()
    5.46          page.table.close()
    5.47 @@ -705,6 +714,25 @@
    5.48          cancel = args.has_key("cancel")
    5.49          ignore = args.has_key("ignore")
    5.50          save = args.has_key("save")
    5.51 +        decline = filter(None, [(arg.startswith("decline-") and arg[len("decline-"):]) for arg in args.keys()])
    5.52 +
    5.53 +        # Decline a counter-proposal.
    5.54 +
    5.55 +        if decline:
    5.56 +            for s in decline:
    5.57 +                try:
    5.58 +                    i = int(s)
    5.59 +                except (IndexError, ValueError):
    5.60 +                    pass
    5.61 +                else:
    5.62 +                    attendee_uri = get_uri(args.get("decline", [])[i])
    5.63 +                    self.process_declined_counter(attendee_uri)
    5.64 +
    5.65 +                    # Update the counter-proposals synchronously instead of
    5.66 +                    # assuming that the outgoing handler will have done so
    5.67 +                    # before the form is refreshed.
    5.68 +
    5.69 +                    self.remove_counter(attendee_uri)
    5.70  
    5.71          have_action = reply or discard or create or cancel or ignore or save
    5.72  
    5.73 @@ -923,9 +951,22 @@
    5.74  
    5.75      def get_attendees_from_page(self):
    5.76  
    5.77 -        "Return attendees from the request."
    5.78 +        """
    5.79 +        Return attendees from the request, using any stored attributes to obtain
    5.80 +        verbose details.
    5.81 +        """
    5.82 +
    5.83 +        return self.get_verbose_attendees(self.env.get_args().get("attendee", []))
    5.84  
    5.85 -        return self.env.get_args().get("attendee", [])
    5.86 +    def get_verbose_attendees(self, attendees):
    5.87 +
    5.88 +        """
    5.89 +        Use any stored attributes to obtain verbose details for the given
    5.90 +        'attendees'.
    5.91 +        """
    5.92 +
    5.93 +        attendee_map = self.obj.get_value_map("ATTENDEE")
    5.94 +        return [get_verbose_address(value, attendee_map.get(value)) for value in attendees]
    5.95  
    5.96      def update_attendees_from_page(self):
    5.97  
     6.1 --- a/imipweb/resource.py	Mon Oct 05 23:53:38 2015 +0200
     6.2 +++ b/imipweb/resource.py	Mon Oct 05 23:56:48 2015 +0200
     6.3 @@ -260,6 +260,10 @@
     6.4          if part:
     6.5              parts.append(part)
     6.6  
     6.7 +        self._send_message(sender, recipients, parts)
     6.8 +
     6.9 +    def _send_message(self, sender, recipients, parts):
    6.10 +
    6.11          # Explicitly specify the outgoing BCC recipient since we are sending as
    6.12          # the generic calendar user.
    6.13  
    6.14 @@ -268,6 +272,22 @@
    6.15  
    6.16      # Action methods.
    6.17  
    6.18 +    def process_declined_counter(self, attendee):
    6.19 +
    6.20 +        "Process a declined counter-proposal."
    6.21 +
    6.22 +        # Obtain the counter-proposal for the attendee.
    6.23 +
    6.24 +        obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee)
    6.25 +        if not obj:
    6.26 +            return False
    6.27 +
    6.28 +        method = "DECLINECOUNTER"
    6.29 +        obj.update_dtstamp()
    6.30 +        obj.update_sequence(False)
    6.31 +        self._send_message(get_address(self.user), [get_address(attendee)], parts=[obj.to_part(method)])
    6.32 +        return True
    6.33 +
    6.34      def process_received_request(self):
    6.35  
    6.36          """
    6.37 @@ -284,7 +304,7 @@
    6.38  
    6.39          self.obj["ATTENDEE"] = [(self.user, attendee_attr)]
    6.40          self.update_dtstamp()
    6.41 -        self.set_sequence(False)
    6.42 +        self.update_sequence(False)
    6.43          self.send_message("REPLY", get_address(self.user), from_organiser=False)
    6.44          return True
    6.45  
    6.46 @@ -307,7 +327,7 @@
    6.47  
    6.48          self.update_sender(organiser_attr)
    6.49          self.update_dtstamp()
    6.50 -        self.set_sequence(True)
    6.51 +        self.update_sequence(True)
    6.52  
    6.53          parts = [self.obj.to_part(method)]
    6.54  
     7.1 --- a/tests/test_handle.py	Mon Oct 05 23:53:38 2015 +0200
     7.2 +++ b/tests/test_handle.py	Mon Oct 05 23:56:48 2015 +0200
     7.3 @@ -79,7 +79,7 @@
     7.4          self.update_sender(attendee_attr)
     7.5          self.obj["ATTENDEE"] = [(self.user, attendee_attr)]
     7.6          self.update_dtstamp()
     7.7 -        self.set_sequence(False)
     7.8 +        self.update_sequence(False)
     7.9  
    7.10          message = self.messenger.make_outgoing_message(
    7.11              [self.obj.to_part(method)],