1.1 --- a/docs/preferences.txt Thu Sep 24 19:13:39 2015 +0200
1.2 +++ b/docs/preferences.txt Tue Sep 29 00:25:31 2015 +0200
1.3 @@ -70,15 +70,16 @@
1.4 supporting this setting when counter-proposals are made during event
1.5 scheduling.
1.6
1.7 -This setting requires a value of one of the following forms:
1.8 +This setting requires a value indicating a duration as described in the
1.9 +iCalendar format specification:
1.10
1.11 - <number of seconds>
1.12 - <number of days>d
1.13 +http://tools.ietf.org/html/rfc5545#section-3.3.6
1.14
1.15 For example:
1.16
1.17 - 600 extend scheduling offers for 10 minutes
1.18 - 1d extend offers for 1 day
1.19 + PT10M extend scheduling offers for 10 minutes
1.20 + PT600S extend scheduling offers for 600 seconds (10 minutes)
1.21 + PT1D extend offers for 1 day
1.22
1.23 freebusy_publishing
1.24 -------------------
2.1 --- a/htdocs/styles.css Thu Sep 24 19:13:39 2015 +0200
2.2 +++ b/htdocs/styles.css Tue Sep 29 00:25:31 2015 +0200
2.3 @@ -1,7 +1,7 @@
2.4 /* Table styling. */
2.5
2.6 -table.calendar,
2.7 table.conflicts,
2.8 +table.counters,
2.9 table.recurrence,
2.10 table.object {
2.11 border: 2px solid #000;
2.12 @@ -23,9 +23,10 @@
2.13 background-color: #faa;
2.14 }
2.15
2.16 -th.dayheading,
2.17 +caption.dayheading,
2.18 th.mainheading {
2.19 background-color: #f85;
2.20 + width: 100%;
2.21 }
2.22
2.23 th.timeslot,
2.24 @@ -50,6 +51,7 @@
2.25 td.event {
2.26 background-color: #ff8;
2.27 border: 2px solid #000;
2.28 + width: 10em;
2.29 }
2.30
2.31 td.event.only-organising {
2.32 @@ -111,10 +113,15 @@
2.33 text-decoration: line-through;
2.34 }
2.35
2.36 +.objectvalue.dtstart.excluded,
2.37 .objectvalue.dtstart.replaced {
2.38 vertical-align: top;
2.39 }
2.40
2.41 +table.counters tr.selected {
2.42 + background-color: #ee2;
2.43 +}
2.44 +
2.45 /* New event controls. */
2.46
2.47 .newevent-with-periods {
2.48 @@ -127,13 +134,17 @@
2.49 display: none;
2.50 }
2.51
2.52 +input.newevent.selector:checked ~ p.newevent-with-periods {
2.53 + display: block;
2.54 +}
2.55 +
2.56 th.container,
2.57 td.container {
2.58 padding: 0; /* for regions covered by labels */
2.59 }
2.60
2.61 -th.dayheading:hover,
2.62 -th.dayheading:focus,
2.63 +caption.dayheading:hover,
2.64 +caption.dayheading:focus,
2.65 th.timeslot:hover,
2.66 th.timeslot:focus,
2.67 td.container:hover,
2.68 @@ -174,9 +185,8 @@
2.69
2.70 /* Hide calendar rows depending on the selected controls. */
2.71
2.72 -input#hidebusy:checked ~ .calendar tr.slot.busy,
2.73 -input#showdays:not(:checked) ~ .calendar thead.separator.empty,
2.74 -input#showdays:not(:checked) ~ .calendar tbody.points.empty,
2.75 +input#hidebusy:checked ~ div.calendar tr.slot.busy,
2.76 +input#showdays:not(:checked) ~ div.calendar .calendar.empty,
2.77
2.78 /* Hiding/showing end datetimes and start/end times. */
2.79
2.80 @@ -206,10 +216,29 @@
2.81
2.82 /* Show slot endpoints when hiding adjacent busy periods. */
2.83
2.84 -input#hidebusy:checked ~ .calendar th.timeslot span.endpoint {
2.85 +input#hidebusy:checked ~ div.calendar th.timeslot span.endpoint {
2.86 display: block;
2.87 }
2.88
2.89 +/* Make calendar labels occupy cells completely.
2.90 + See: http://stackoverflow.com/questions/2841484/how-can-a-label-completely-fill-its-parent-td
2.91 +*/
2.92 +
2.93 +tr.slot {
2.94 + height: 0;
2.95 +}
2.96 +
2.97 +th.timeslot,
2.98 +td.empty {
2.99 + height: 100%;
2.100 +}
2.101 +
2.102 +label.timepoint,
2.103 +label.newevent {
2.104 + display: block;
2.105 + min-height: 100%;
2.106 +}
2.107 +
2.108 /* Style the labels. */
2.109
2.110 label.day,
3.1 --- a/imip_manager.py Thu Sep 24 19:13:39 2015 +0200
3.2 +++ b/imip_manager.py Tue Sep 29 00:25:31 2015 +0200
3.3 @@ -29,9 +29,9 @@
3.4
3.5 from imipweb.calendar import CalendarPage
3.6 from imipweb.event import EventPage
3.7 -from imipweb.resource import Resource
3.8 +from imipweb.resource import ResourceClient
3.9
3.10 -class Manager(Resource):
3.11 +class Manager(ResourceClient):
3.12
3.13 "A simple manager application."
3.14
4.1 --- a/imip_store.py Thu Sep 24 19:13:39 2015 +0200
4.2 +++ b/imip_store.py Tue Sep 29 00:25:31 2015 +0200
4.3 @@ -367,7 +367,7 @@
4.4 indicated 'uid'. Only cancelled recurrences are returned.
4.5 """
4.6
4.7 - filename = self.get_object_in_store(user, "cancelled", "recurrences", uid)
4.8 + filename = self.get_object_in_store(user, "cancellations", "recurrences", uid)
4.9 if not filename or not exists(filename):
4.10 return []
4.11
4.12 @@ -757,29 +757,27 @@
4.13 """
4.14
4.15 for request in requests:
4.16 - if request[:2] == (uid, recurrenceid):
4.17 + if request[:2] == (uid, recurrenceid) and (
4.18 + not strict or
4.19 + not request[2:] and not type or
4.20 + request[2:] and request[2] == type):
4.21 +
4.22 return True
4.23 +
4.24 return False
4.25
4.26 def get_counters(self, user, uid, recurrenceid=None):
4.27
4.28 """
4.29 - For the given 'user', return a mapping of counter-proposals from other
4.30 - users to nodes representing those proposals for the given 'uid' and
4.31 - optional 'recurrenceid'.
4.32 + For the given 'user', return a list of users from whom counter-proposals
4.33 + have been received for the given 'uid' and optional 'recurrenceid'.
4.34 """
4.35
4.36 filename = self.get_event_filename(user, uid, recurrenceid, "counters")
4.37 - if not filename:
4.38 + if not filename or not exists(filename):
4.39 return False
4.40
4.41 - counters = {}
4.42 -
4.43 - for other in listdir(filename):
4.44 - counter_filename = self.get_event_filename(user, uid, recurrenceid, "counters", other)
4.45 - counters[other] = self._get_object(user, counter_filename)
4.46 -
4.47 - return counters
4.48 + return [name for name in listdir(filename) if isfile(join(filename, name))]
4.49
4.50 def get_counter(self, user, other, uid, recurrenceid=None):
4.51
5.1 --- a/imiptools/client.py Thu Sep 24 19:13:39 2015 +0200
5.2 +++ b/imiptools/client.py Tue Sep 29 00:25:31 2015 +0200
5.3 @@ -25,7 +25,7 @@
5.4 is_new_object, make_freebusy, to_part, \
5.5 uri_dict, uri_items, uri_values
5.6 from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \
5.7 - get_timestamp, to_timezone
5.8 + get_duration, get_time, get_timestamp
5.9 from imiptools.period import can_schedule, remove_period, \
5.10 remove_additional_periods, remove_affected_period, \
5.11 update_freebusy
5.12 @@ -139,28 +139,14 @@
5.13
5.14 def get_offer_period(self):
5.15
5.16 - """
5.17 - Decode a specification of one of the following forms...
5.18 -
5.19 - <number of seconds>
5.20 - <number of days>d
5.21 - """
5.22 + "Decode a specification in the iCalendar duration format."
5.23
5.24 prefs = self.get_preferences()
5.25 duration = prefs and prefs.get("freebusy_offers", config.FREEBUSY_OFFER_DEFAULT)
5.26 - if duration:
5.27 - try:
5.28 - if duration.endswith("d"):
5.29 - return timedelta(days=int(duration[:-1]))
5.30 - else:
5.31 - return timedelta(seconds=int(duration))
5.32
5.33 - # NOTE: Should probably report an error somehow.
5.34 + # NOTE: Should probably report an error somehow if None.
5.35
5.36 - except ValueError:
5.37 - return None
5.38 - else:
5.39 - return None
5.40 + return duration and get_duration(duration) or None
5.41
5.42 def get_organiser_replacement(self):
5.43 prefs = self.get_preferences()
5.44 @@ -281,16 +267,16 @@
5.45
5.46 # Store operations.
5.47
5.48 - def get_stored_object(self, uid, recurrenceid, section=None):
5.49 + def get_stored_object(self, uid, recurrenceid, section=None, username=None):
5.50
5.51 """
5.52 Return the stored object for the current user, with the given 'uid' and
5.53 - 'recurrenceid' from the given 'section' (if specified), or from the
5.54 - standard object collection otherwise.
5.55 + 'recurrenceid' from the given 'section' and for the given 'username' (if
5.56 + specified), or from the standard object collection otherwise.
5.57 """
5.58
5.59 if section == "counters":
5.60 - fragment = self.store.get_counter(self.user, uid, recurrenceid)
5.61 + fragment = self.store.get_counter(self.user, username, uid, recurrenceid)
5.62 else:
5.63 fragment = self.store.get_event(self.user, uid, recurrenceid)
5.64 return fragment and Object(fragment)
5.65 @@ -385,7 +371,7 @@
5.66 "Update the DTSTAMP in the current object."
5.67
5.68 dtstamp = self.obj.get_utc_datetime("DTSTAMP")
5.69 - utcnow = to_timezone(datetime.utcnow(), "UTC")
5.70 + utcnow = get_time()
5.71 self.dtstamp = format_datetime(dtstamp and dtstamp > utcnow and dtstamp or utcnow)
5.72 self.obj["DTSTAMP"] = [(self.dtstamp, {})]
5.73
5.74 @@ -683,7 +669,7 @@
5.75 if offer:
5.76 offer_period = self.get_offer_period()
5.77 if offer_period:
5.78 - expires = format_datetime(to_timezone(datetime.utcnow(), "UTC") + offer_period)
5.79 + expires = get_timestamp(offer_period)
5.80 else:
5.81 return
5.82 else:
5.83 @@ -795,4 +781,85 @@
5.84 for attendee in attendees.keys():
5.85 self.update_freebusy_from_participant(attendee, False, self.remove_freebusy_for_participant)
5.86
5.87 + # Convenience methods for updating free/busy details at the event level.
5.88 +
5.89 + def update_event_in_freebusy(self, for_organiser=True):
5.90 +
5.91 + """
5.92 + Update free/busy information when handling an object, doing so for the
5.93 + organiser of an event if 'for_organiser' is set to a true value.
5.94 + """
5.95 +
5.96 + freebusy = self.store.get_freebusy(self.user)
5.97 +
5.98 + # Obtain the attendance attributes for this user, if available.
5.99 +
5.100 + self.update_freebusy_for_participant(freebusy, self.user, for_organiser)
5.101 +
5.102 + # Remove original recurrence details replaced by additional
5.103 + # recurrences, as well as obsolete additional recurrences.
5.104 +
5.105 + self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))
5.106 + self.store.set_freebusy(self.user, freebusy)
5.107 +
5.108 + if self.publisher and self.is_sharing() and self.is_publishing():
5.109 + self.publisher.set_freebusy(self.user, freebusy)
5.110 +
5.111 + # Update free/busy provider information if the event may recur
5.112 + # indefinitely.
5.113 +
5.114 + if self.possibly_recurring_indefinitely():
5.115 + self.store.append_freebusy_provider(self.user, self.obj)
5.116 +
5.117 + return True
5.118 +
5.119 + def remove_event_from_freebusy(self):
5.120 +
5.121 + "Remove free/busy information when handling an object."
5.122 +
5.123 + freebusy = self.store.get_freebusy(self.user)
5.124 +
5.125 + self.remove_from_freebusy(freebusy)
5.126 + self.remove_freebusy_for_recurrences(freebusy)
5.127 + self.store.set_freebusy(self.user, freebusy)
5.128 +
5.129 + if self.publisher and self.is_sharing() and self.is_publishing():
5.130 + self.publisher.set_freebusy(self.user, freebusy)
5.131 +
5.132 + # Update free/busy provider information if the event may recur
5.133 + # indefinitely.
5.134 +
5.135 + if self.possibly_recurring_indefinitely():
5.136 + self.store.remove_freebusy_provider(self.user, self.obj)
5.137 +
5.138 + def update_event_in_freebusy_offers(self):
5.139 +
5.140 + "Update free/busy offers when handling an object."
5.141 +
5.142 + freebusy = self.store.get_freebusy_offers(self.user)
5.143 +
5.144 + # Obtain the attendance attributes for this user, if available.
5.145 +
5.146 + self.update_freebusy_for_participant(freebusy, self.user, offer=True)
5.147 +
5.148 + # Remove original recurrence details replaced by additional
5.149 + # recurrences, as well as obsolete additional recurrences.
5.150 +
5.151 + self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))
5.152 + self.store.set_freebusy_offers(self.user, freebusy)
5.153 +
5.154 + return True
5.155 +
5.156 + def remove_event_from_freebusy_offers(self):
5.157 +
5.158 + "Remove free/busy offers when handling an object."
5.159 +
5.160 + freebusy = self.store.get_freebusy_offers(self.user)
5.161 +
5.162 + self.remove_from_freebusy(freebusy)
5.163 + self.remove_freebusy_for_recurrences(freebusy)
5.164 + self.store.set_freebusy_offers(self.user, freebusy)
5.165 +
5.166 + return True
5.167 +
5.168 # vim: tabstop=4 expandtab shiftwidth=4
6.1 --- a/imiptools/data.py Thu Sep 24 19:13:39 2015 +0200
6.2 +++ b/imiptools/data.py Tue Sep 29 00:25:31 2015 +0200
6.3 @@ -409,6 +409,25 @@
6.4
6.5 return old_values != set(self.get_date_values("RDATE") or [])
6.6
6.7 + def update_exceptions(self, excluded):
6.8 +
6.9 + """
6.10 + Update the exceptions to any rule by applying the list of 'excluded'
6.11 + periods.
6.12 + """
6.13 +
6.14 + to_exclude = set(excluded).difference(self.get_date_values("EXDATE") or [])
6.15 + if not to_exclude:
6.16 + return False
6.17 +
6.18 + if not self.has_key("EXDATE"):
6.19 + self["EXDATE"] = []
6.20 +
6.21 + for p in to_exclude:
6.22 + self["EXDATE"].append(get_period_item(p.get_start(), p.get_end()))
6.23 +
6.24 + return True
6.25 +
6.26 def correct_object(self, tzid, permitted_values):
6.27
6.28 "Correct the object's period details."
7.1 --- a/imiptools/dates.py Thu Sep 24 19:13:39 2015 +0200
7.2 +++ b/imiptools/dates.py Tue Sep 29 00:25:31 2015 +0200
7.3 @@ -132,7 +132,10 @@
7.4
7.5 def get_duration(value):
7.6
7.7 - "Return a duration for the given 'value'."
7.8 + """
7.9 + Return a duration for the given 'value' as a timedelta object.
7.10 + Where no valid duration is specified, None is returned.
7.11 + """
7.12
7.13 if not value:
7.14 return None
7.15 @@ -433,11 +436,19 @@
7.16 else:
7.17 return None, None
7.18
7.19 -def get_timestamp():
7.20 +def get_timestamp(offset=None):
7.21
7.22 "Return the current time as an iCalendar-compatible string."
7.23
7.24 - return format_datetime(to_timezone(datetime.utcnow(), "UTC"))
7.25 + offset = offset or timedelta(0)
7.26 + return format_datetime(to_timezone(datetime.utcnow(), "UTC") + offset)
7.27 +
7.28 +def get_time(offset=None):
7.29 +
7.30 + "Return the current time."
7.31 +
7.32 + offset = offset or timedelta(0)
7.33 + return to_timezone(datetime.utcnow(), "UTC") + offset
7.34
7.35 def get_tzid(dtstart_attr, dtend_attr):
7.36
8.1 --- a/imiptools/handlers/common.py Thu Sep 24 19:13:39 2015 +0200
8.2 +++ b/imiptools/handlers/common.py Tue Sep 29 00:25:31 2015 +0200
8.3 @@ -110,83 +110,4 @@
8.4
8.5 self.add_result("REFRESH", [get_address(organiser)], obj.to_part("REFRESH"))
8.6
8.7 - def update_event_in_freebusy(self, for_organiser=True):
8.8 -
8.9 - """
8.10 - Update free/busy information when handling an object, doing so for the
8.11 - organiser of an event if 'for_organiser' is set to a true value.
8.12 - """
8.13 -
8.14 - freebusy = self.store.get_freebusy(self.user)
8.15 -
8.16 - # Obtain the attendance attributes for this user, if available.
8.17 -
8.18 - self.update_freebusy_for_participant(freebusy, self.user, for_organiser)
8.19 -
8.20 - # Remove original recurrence details replaced by additional
8.21 - # recurrences, as well as obsolete additional recurrences.
8.22 -
8.23 - self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))
8.24 - self.store.set_freebusy(self.user, freebusy)
8.25 -
8.26 - if self.publisher and self.is_sharing() and self.is_publishing():
8.27 - self.publisher.set_freebusy(self.user, freebusy)
8.28 -
8.29 - # Update free/busy provider information if the event may recur
8.30 - # indefinitely.
8.31 -
8.32 - if self.possibly_recurring_indefinitely():
8.33 - self.store.append_freebusy_provider(self.user, self.obj)
8.34 -
8.35 - return True
8.36 -
8.37 - def remove_event_from_freebusy(self):
8.38 -
8.39 - "Remove free/busy information when handling an object."
8.40 -
8.41 - freebusy = self.store.get_freebusy(self.user)
8.42 -
8.43 - self.remove_from_freebusy(freebusy)
8.44 - self.remove_freebusy_for_recurrences(freebusy)
8.45 - self.store.set_freebusy(self.user, freebusy)
8.46 -
8.47 - if self.publisher and self.is_sharing() and self.is_publishing():
8.48 - self.publisher.set_freebusy(self.user, freebusy)
8.49 -
8.50 - # Update free/busy provider information if the event may recur
8.51 - # indefinitely.
8.52 -
8.53 - if self.possibly_recurring_indefinitely():
8.54 - self.store.remove_freebusy_provider(self.user, self.obj)
8.55 -
8.56 - def update_event_in_freebusy_offers(self):
8.57 -
8.58 - "Update free/busy offers when handling an object."
8.59 -
8.60 - freebusy = self.store.get_freebusy_offers(self.user)
8.61 -
8.62 - # Obtain the attendance attributes for this user, if available.
8.63 -
8.64 - self.update_freebusy_for_participant(freebusy, self.user, offer=True)
8.65 -
8.66 - # Remove original recurrence details replaced by additional
8.67 - # recurrences, as well as obsolete additional recurrences.
8.68 -
8.69 - self.remove_freebusy_for_recurrences(freebusy, self.store.get_recurrences(self.user, self.uid))
8.70 - self.store.set_freebusy_offers(self.user, freebusy)
8.71 -
8.72 - return True
8.73 -
8.74 - def remove_event_from_freebusy_offers(self):
8.75 -
8.76 - "Remove free/busy offers when handling an object."
8.77 -
8.78 - freebusy = self.store.get_freebusy_offers(self.user)
8.79 -
8.80 - self.remove_from_freebusy(freebusy)
8.81 - self.remove_freebusy_for_recurrences(freebusy)
8.82 - self.store.set_freebusy_offers(self.user, freebusy)
8.83 -
8.84 - return True
8.85 -
8.86 # vim: tabstop=4 expandtab shiftwidth=4
9.1 --- a/imipweb/calendar.py Thu Sep 24 19:13:39 2015 +0200
9.2 +++ b/imipweb/calendar.py Tue Sep 29 00:25:31 2015 +0200
9.3 @@ -27,9 +27,9 @@
9.4 to_timezone
9.5 from imiptools.period import add_day_start_points, add_empty_days, add_slots, \
9.6 get_scale, get_slots, get_spans, partition_by_day, Point
9.7 -from imipweb.resource import Resource
9.8 +from imipweb.resource import ResourceClient
9.9
9.10 -class CalendarPage(Resource):
9.11 +class CalendarPage(ResourceClient):
9.12
9.13 "A request handler for the calendar page."
9.14
9.15 @@ -46,13 +46,18 @@
9.16
9.17 args = self.env.get_args()
9.18
9.19 - if not args.has_key("newevent"):
9.20 + for key in args.keys():
9.21 + if key.startswith("newevent-"):
9.22 + i = key[len("newevent-"):]
9.23 + break
9.24 + else:
9.25 return
9.26
9.27 # Create a new event using the available information.
9.28
9.29 slots = args.get("slot", [])
9.30 participants = args.get("participants", [])
9.31 + summary = args.get("summary-%s" % i, [None])[0]
9.32
9.33 if not slots:
9.34 return
9.35 @@ -120,7 +125,7 @@
9.36 end_value, end_attr = get_datetime_item(end, tzid)
9.37
9.38 rwrite(("UID", {}, uid))
9.39 - rwrite(("SUMMARY", {}, "New event at %s" % utcnow))
9.40 + rwrite(("SUMMARY", {}, summary or ("New event at %s" % utcnow)))
9.41 rwrite(("DTSTAMP", {}, utcnow))
9.42 rwrite(("DTSTART", start_attr, start_value))
9.43 rwrite(("DTEND", end_attr, end_value))
9.44 @@ -379,18 +384,7 @@
9.45
9.46 add_empty_days(days, tzid)
9.47
9.48 - # Show the controls permitting day selection as well as the controls
9.49 - # configuring the new event display.
9.50 -
9.51 - self.show_calendar_day_controls(days)
9.52 - self.show_calendar_interval_controls(days)
9.53 -
9.54 - # Show a button for scheduling a new event.
9.55 -
9.56 - page.p(class_="controls")
9.57 - page.input(name="newevent", type="submit", value="New event", id="newevent", class_="newevent-with-periods", accesskey="N")
9.58 - page.span("Select days or periods for a new event.", class_="newevent-no-periods")
9.59 - page.p.close()
9.60 + page.p("Select days or periods for a new event.")
9.61
9.62 # Show controls for hiding empty days and busy slots.
9.63 # The positioning of the control, paragraph and table are important here.
9.64 @@ -409,10 +403,7 @@
9.65
9.66 # Show the calendar itself.
9.67
9.68 - page.table(cellspacing=5, cellpadding=5, class_="calendar")
9.69 - self.show_calendar_participant_headings(partitioned_group_types, partitioned_group_sources, group_columns)
9.70 - self.show_calendar_days(days, partitioned_groups, partitioned_group_types, group_columns)
9.71 - page.table.close()
9.72 + self.show_calendar_days(days, partitioned_groups, partitioned_group_types, partitioned_group_sources, group_columns)
9.73
9.74 # End the form region.
9.75
9.76 @@ -420,16 +411,12 @@
9.77
9.78 # More page fragment methods.
9.79
9.80 - def show_calendar_day_controls(self, days):
9.81 + def show_calendar_day_controls(self, day):
9.82
9.83 - "Show controls for the given 'days' in the calendar."
9.84 + "Show controls for the given 'day' in the calendar."
9.85
9.86 page = self.page
9.87 - slots = self.env.get_args().get("slot", [])
9.88 -
9.89 - for day in days:
9.90 - value, identifier = self._day_value_and_identifier(day)
9.91 - self._slot_selector(value, identifier, slots)
9.92 + daystr, dayid = self._day_value_and_identifier(day)
9.93
9.94 # Generate a dynamic stylesheet to allow day selections to colour
9.95 # specific days.
9.96 @@ -438,13 +425,42 @@
9.97
9.98 page.style(type="text/css")
9.99
9.100 + page.add("""\
9.101 +input.newevent.selector#%s:checked ~ table#region-%s label.day,
9.102 +input.newevent.selector#%s:checked ~ table#region-%s label.timepoint {
9.103 + background-color: #5f4;
9.104 + text-decoration: underline;
9.105 +}
9.106 +""" % (dayid, dayid, dayid, dayid))
9.107 +
9.108 + page.style.close()
9.109 +
9.110 + # Generate controls to select days.
9.111 +
9.112 + slots = self.env.get_args().get("slot", [])
9.113 + value, identifier = self._day_value_and_identifier(day)
9.114 + self._slot_selector(value, identifier, slots)
9.115 +
9.116 + def show_calendar_interval_controls(self, day, intervals):
9.117 +
9.118 + "Show controls for the intervals provided by 'day' and 'intervals'."
9.119 +
9.120 + page = self.page
9.121 + daystr, dayid = self._day_value_and_identifier(day)
9.122 +
9.123 + # Generate a dynamic stylesheet to allow day selections to colour
9.124 + # specific days.
9.125 + # NOTE: The style details need to be coordinated with the static
9.126 + # NOTE: stylesheet.
9.127 +
9.128 l = []
9.129
9.130 - for day in days:
9.131 - daystr, dayid = self._day_value_and_identifier(day)
9.132 + for point, endpoint in intervals:
9.133 + timestr, timeid = self._slot_value_and_identifier(point, endpoint)
9.134 l.append("""\
9.135 -input.newevent.selector#%s:checked ~ table label.day.day-%s,
9.136 -input.newevent.selector#%s:checked ~ table label.timepoint.day-%s""" % (dayid, daystr, dayid, daystr))
9.137 +input.newevent.selector#%s:checked ~ table#region-%s th#region-%s""" % (timeid, dayid, timeid))
9.138 +
9.139 + page.style(type="text/css")
9.140
9.141 page.add(",\n".join(l))
9.142 page.add(""" {
9.143 @@ -455,59 +471,47 @@
9.144
9.145 page.style.close()
9.146
9.147 - def show_calendar_interval_controls(self, days):
9.148 + # Generate controls to select time periods.
9.149 +
9.150 + slots = self.env.get_args().get("slot", [])
9.151 + last = None
9.152
9.153 - "Show controls for the intervals provided by 'days'."
9.154 + # Produce controls for the intervals/slots. Where instants in time are
9.155 + # encountered, they are merged with the following slots, permitting the
9.156 + # selection of contiguous time periods. However, the identifiers
9.157 + # employed by controls corresponding to merged periods will encode the
9.158 + # instant so that labels may reference them conveniently.
9.159
9.160 - page = self.page
9.161 - slots = self.env.get_args().get("slot", [])
9.162 + intervals = list(intervals)
9.163 + intervals.sort()
9.164 +
9.165 + for point, endpoint in intervals:
9.166
9.167 - for day, intervals in days.items():
9.168 - for point, endpoint in intervals:
9.169 - value, identifier = self._slot_value_and_identifier(point, endpoint)
9.170 + # Merge any previous slot with this one, producing a control.
9.171 +
9.172 + if last:
9.173 + _value, identifier = self._slot_value_and_identifier(last, last)
9.174 + value, _identifier = self._slot_value_and_identifier(last, endpoint)
9.175 self._slot_selector(value, identifier, slots)
9.176
9.177 - # Generate a dynamic stylesheet to allow day selections to colour
9.178 - # specific days.
9.179 - # NOTE: The style details need to be coordinated with the static
9.180 - # NOTE: stylesheet.
9.181 + # If representing an instant, hold the slot for merging.
9.182
9.183 - page.style(type="text/css")
9.184 + if endpoint and point.point == endpoint.point:
9.185 + last = point
9.186
9.187 - l = []; l2 = []; l3 = []
9.188 + # If not representing an instant, produce a control.
9.189
9.190 - for day, intervals in days.items():
9.191 - for point, endpoint in intervals:
9.192 - daystr, dayid = self._day_value_and_identifier(day)
9.193 - timestr, timeid = self._slot_value_and_identifier(point, endpoint)
9.194 - l.append("""\
9.195 -input.newevent.selector#%s:checked ~ p .newevent-no-periods,
9.196 -input.newevent.selector#%s:checked ~ p .newevent-no-periods""" % (dayid, timeid))
9.197 - l2.append("""\
9.198 -input.newevent.selector#%s:checked ~ p .newevent-with-periods,
9.199 -input.newevent.selector#%s:checked ~ p .newevent-with-periods""" % (dayid, timeid))
9.200 - l3.append("""\
9.201 -input.newevent.selector#%s:checked ~ table label.timepoint[for=%s]""" % (timeid, timeid))
9.202 + else:
9.203 + value, identifier = self._slot_value_and_identifier(point, endpoint)
9.204 + self._slot_selector(value, identifier, slots)
9.205 + last = None
9.206
9.207 - page.add(",\n".join(l))
9.208 - page.add(""" {
9.209 - display: none;
9.210 -}""")
9.211 + # Produce a control for any unmerged slot.
9.212
9.213 - page.add(",\n".join(l2))
9.214 - page.add(""" {
9.215 - display: inline;
9.216 -}
9.217 -""")
9.218 -
9.219 - page.add(",\n".join(l3))
9.220 - page.add(""" {
9.221 - background-color: #5f4;
9.222 - text-decoration: underline;
9.223 -}
9.224 -""")
9.225 -
9.226 - page.style.close()
9.227 + if last:
9.228 + _value, identifier = self._slot_value_and_identifier(last, last)
9.229 + value, _identifier = self._slot_value_and_identifier(last, endpoint)
9.230 + self._slot_selector(value, identifier, slots)
9.231
9.232 def show_calendar_participant_headings(self, group_types, group_sources, group_columns):
9.233
9.234 @@ -535,13 +539,15 @@
9.235 page.tr.close()
9.236 page.thead.close()
9.237
9.238 - def show_calendar_days(self, days, partitioned_groups, partitioned_group_types, group_columns):
9.239 + def show_calendar_days(self, days, partitioned_groups, partitioned_group_types,
9.240 + partitioned_group_sources, group_columns):
9.241
9.242 """
9.243 Show calendar days, defined by a collection of 'days', the contributing
9.244 period information as 'partitioned_groups' (partitioned by day), the
9.245 'partitioned_group_types' indicating the kind of contribution involved,
9.246 - and the 'group_columns' defining the number of columns in each group.
9.247 + the 'partitioned_group_sources' indicating the origin of each group, and
9.248 + the 'group_columns' defining the number of columns in each group.
9.249 """
9.250
9.251 page = self.page
9.252 @@ -559,6 +565,8 @@
9.253
9.254 # Produce a heading and time points for each day.
9.255
9.256 + i = 0
9.257 +
9.258 for day, intervals in all_days:
9.259 groups_for_day = [partitioned.get(day) for partitioned in partitioned_groups]
9.260 is_empty = True
9.261 @@ -572,18 +580,46 @@
9.262 is_empty = False
9.263 break
9.264
9.265 - page.thead(class_="separator%s" % (is_empty and " empty" or ""))
9.266 - page.tr()
9.267 - page.th(class_="dayheading container", colspan=all_columns+1)
9.268 + daystr, dayid = self._day_value_and_identifier(day)
9.269 +
9.270 + # Put calendar tables within elements for quicker CSS selection.
9.271 +
9.272 + page.div(class_="calendar")
9.273 +
9.274 + # Show the controls permitting day selection as well as the controls
9.275 + # configuring the new event display.
9.276 +
9.277 + self.show_calendar_day_controls(day)
9.278 + self.show_calendar_interval_controls(day, intervals)
9.279 +
9.280 + # Show an actual table containing the day information.
9.281 +
9.282 + page.table(cellspacing=5, cellpadding=5, class_="calendar %s" % (is_empty and " empty" or ""), id="region-%s" % dayid)
9.283 +
9.284 + page.caption(class_="dayheading container separator")
9.285 self._day_heading(day)
9.286 - page.th.close()
9.287 - page.tr.close()
9.288 - page.thead.close()
9.289 + page.caption.close()
9.290
9.291 - page.tbody(class_="points%s" % (is_empty and " empty" or ""))
9.292 + self.show_calendar_participant_headings(partitioned_group_types, partitioned_group_sources, group_columns)
9.293 +
9.294 + page.tbody(class_="points")
9.295 self.show_calendar_points(intervals, groups_for_day, partitioned_group_types, group_columns)
9.296 page.tbody.close()
9.297
9.298 + page.table.close()
9.299 +
9.300 + # Show a button for scheduling a new event.
9.301 +
9.302 + page.p(class_="newevent-with-periods")
9.303 + page.label("Summary:")
9.304 + page.input(name="summary-%d" % i, type="text")
9.305 + page.input(name="newevent-%d" % i, type="submit", value="New event", accesskey="N")
9.306 + page.p.close()
9.307 +
9.308 + page.div.close()
9.309 +
9.310 + i += 1
9.311 +
9.312 def show_calendar_points(self, intervals, groups, group_types, group_columns):
9.313
9.314 """
9.315 @@ -629,12 +665,16 @@
9.316 ])
9.317
9.318 page.tr(class_=css)
9.319 +
9.320 + # Produce a time interval heading, spanning two rows if this point
9.321 + # represents an instant.
9.322 +
9.323 if point.indicator == Point.PRINCIPAL:
9.324 - page.th(class_="timeslot")
9.325 + timestr, timeid = self._slot_value_and_identifier(point, endpoint)
9.326 + page.th(class_="timeslot", id="region-%s" % timeid,
9.327 + rowspan=(endpoint and point.point == endpoint.point and 2 or 1))
9.328 self._time_point(point, endpoint)
9.329 - else:
9.330 - page.th()
9.331 - page.th.close()
9.332 + page.th.close()
9.333
9.334 # Obtain slots for the time point from each group.
9.335
9.336 @@ -687,7 +727,8 @@
9.337 "event",
9.338 has_continued and "continued" or "",
9.339 will_continue and "continues" or "",
9.340 - p.transp == "ORG" and "only-organising" or is_organiser and "organising" or "attending"
9.341 + p.transp == "ORG" and "only-organising" or is_organiser and "organising" or "attending",
9.342 + self._have_request(p.uid, p.recurrenceid, "COUNTER", True) and "counter" or "",
9.343 ])
9.344
9.345 # Only anchor the first cell of events.
9.346 @@ -710,13 +751,12 @@
9.347
9.348 page.span(p.summary or "(Participant is busy)")
9.349
9.350 - # Link to counter-proposals.
9.351 + # Link to requests and events (including ones for
9.352 + # which counter-proposals exist).
9.353
9.354 elif group_type == "request" and self._have_request(p.uid, p.recurrenceid, "COUNTER", True):
9.355 - page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid, "counter"))
9.356 -
9.357 - # Link to requests and events (including ones for
9.358 - # which counter-proposals exist).
9.359 + page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid,
9.360 + {"counter" : self._period_identifier(p)}))
9.361
9.362 else:
9.363 page.a(p.summary, href=self.link_to(p.uid, p.recurrenceid))
9.364 @@ -739,13 +779,12 @@
9.365 """
9.366 Generate a heading for 'day' of the following form:
9.367
9.368 - <label class="day day-20150203" for="day-20150203">Tuesday, 3 February 2015</label>
9.369 + <label class="day" for="day-20150203">Tuesday, 3 February 2015</label>
9.370 """
9.371
9.372 page = self.page
9.373 - daystr = format_datetime(day)
9.374 value, identifier = self._day_value_and_identifier(day)
9.375 - page.label(self.format_date(day, "full"), class_="day day-%s" % daystr, for_=identifier)
9.376 + page.label(self.format_date(day, "full"), class_="day", for_=identifier)
9.377
9.378 def _time_point(self, point, endpoint):
9.379
9.380 @@ -753,15 +792,14 @@
9.381 Generate headings for the 'point' to 'endpoint' period of the following
9.382 form:
9.383
9.384 - <label class="timepoint day-20150203" for="slot-20150203T090000-20150203T100000">09:00:00 CET</label>
9.385 + <label class="timepoint" for="slot-20150203T090000-20150203T100000">09:00:00 CET</label>
9.386 <span class="endpoint">10:00:00 CET</span>
9.387 """
9.388
9.389 page = self.page
9.390 tzid = self.get_tzid()
9.391 - daystr = format_datetime(point.point.date())
9.392 value, identifier = self._slot_value_and_identifier(point, endpoint)
9.393 - page.label(self.format_time(point.point, "long"), class_="timepoint day-%s" % daystr, for_=identifier)
9.394 + page.label(self.format_time(point.point, "long"), class_="timepoint", for_=identifier)
9.395 page.span(self.format_time(endpoint and endpoint.point or get_end_of_day(point.point, tzid), "long"), class_="endpoint")
9.396
9.397 def _slot_selector(self, value, identifier, slots):
9.398 @@ -813,4 +851,7 @@
9.399 identifier = "slot-%s" % value
9.400 return value, identifier
9.401
9.402 + def _period_identifier(self, period):
9.403 + return "%s-%s" % (format_datetime(period.get_start()), format_datetime(period.get_end()))
9.404 +
9.405 # vim: tabstop=4 expandtab shiftwidth=4
10.1 --- a/imipweb/client.py Thu Sep 24 19:13:39 2015 +0200
10.2 +++ b/imipweb/client.py Tue Sep 29 00:25:31 2015 +0200
10.3 @@ -130,7 +130,7 @@
10.4 for p in to_unschedule:
10.5 if not p.origin:
10.6 continue
10.7 - obj["RECURRENCE-ID"] = [p.get_start_item()]
10.8 + obj["RECURRENCE-ID"] = [(format_datetime(p.get_start()), p.get_start_attr())]
10.9 parts.append(obj.to_part("CANCEL"))
10.10
10.11 # Send the updated event, along with a cancellation for each of the
11.1 --- a/imipweb/env.py Thu Sep 24 19:13:39 2015 +0200
11.2 +++ b/imipweb/env.py Tue Sep 29 00:25:31 2015 +0200
11.3 @@ -19,7 +19,7 @@
11.4 this program. If not, see <http://www.gnu.org/licenses/>.
11.5 """
11.6
11.7 -import cgi, os, sys
11.8 +import cgi, os, sys, urlparse
11.9
11.10 getenv = os.environ.get
11.11 setenv = os.environ.__setitem__
11.12 @@ -35,10 +35,13 @@
11.13 self.path = None
11.14 self.path_info = None
11.15 self.user = None
11.16 + self.query_string = None
11.17
11.18 def get_args(self):
11.19 if self.args is None:
11.20 if self.get_method() != "POST":
11.21 + if not self.query_string:
11.22 + self.query_string = getenv("QUERY_STRING")
11.23 setenv("QUERY_STRING", "")
11.24 args = cgi.parse(keep_blank_values=True)
11.25
11.26 @@ -51,6 +54,11 @@
11.27
11.28 return self.args
11.29
11.30 + def get_query(self):
11.31 + if not self.query_string:
11.32 + self.query_string = getenv("QUERY_STRING")
11.33 + return urlparse.parse_qs(self.query_string or "", keep_blank_values=True)
11.34 +
11.35 def get_method(self):
11.36 if self.method is None:
11.37 self.method = getenv("REQUEST_METHOD") or "GET"
12.1 --- a/imipweb/event.py Thu Sep 24 19:13:39 2015 +0200
12.2 +++ b/imipweb/event.py Tue Sep 29 00:25:31 2015 +0200
12.3 @@ -19,26 +19,20 @@
12.4 this program. If not, see <http://www.gnu.org/licenses/>.
12.5 """
12.6
12.7 -from datetime import date, timedelta
12.8 -from imiptools.data import get_uri, uri_dict, uri_values
12.9 -from imiptools.dates import format_datetime, get_datetime_item, \
12.10 - to_date, to_timezone
12.11 +from imiptools.data import get_uri, uri_dict, uri_items, uri_values
12.12 +from imiptools.dates import format_datetime, to_timezone
12.13 from imiptools.mail import Messenger
12.14 from imiptools.period import have_conflict
12.15 -from imipweb.data import EventPeriod, \
12.16 - event_period_from_period, form_period_from_period, \
12.17 - FormDate, FormPeriod, PeriodError
12.18 +from imipweb.data import EventPeriod, event_period_from_period, FormPeriod, PeriodError
12.19 from imipweb.client import ManagerClient
12.20 -from imipweb.resource import Resource
12.21 -import pytz
12.22 +from imipweb.resource import DateTimeFormUtilities, FormUtilities, ResourceClientForObject
12.23
12.24 -class EventPage(Resource):
12.25 -
12.26 - "A request handler for the event page."
12.27 +class EventPageFragment(ResourceClientForObject, DateTimeFormUtilities, FormUtilities):
12.28
12.29 - def __init__(self, resource=None, messenger=None):
12.30 - Resource.__init__(self, resource)
12.31 - self.messenger = messenger or Messenger()
12.32 + "A resource presenting the details of an event."
12.33 +
12.34 + def __init__(self, resource=None):
12.35 + ResourceClientForObject.__init__(self, resource)
12.36
12.37 # Various property values and labels.
12.38
12.39 @@ -59,414 +53,131 @@
12.40 (None, "Not indicated"),
12.41 ]
12.42
12.43 - # Access to stored object information.
12.44 -
12.45 - def is_organiser(self, obj):
12.46 - return get_uri(obj.get_value("ORGANIZER")) == self.user
12.47 -
12.48 - def get_stored_attendees(self, obj):
12.49 - return uri_values(obj.get_values("ATTENDEE") or [])
12.50 -
12.51 - def get_stored_main_period(self, obj):
12.52 + def can_remove_recurrence(self, recurrence):
12.53
12.54 """
12.55 - Return the main event period for the given 'obj'.
12.56 + Return whether the 'recurrence' can be removed from the current object
12.57 + without notification.
12.58 + """
12.59 +
12.60 + return self.can_edit_recurrence(recurrence) and recurrence.origin != "RRULE"
12.61 +
12.62 + def can_edit_recurrence(self, recurrence):
12.63 +
12.64 + "Return whether 'recurrence' can be edited."
12.65 +
12.66 + return self.recurrence_is_new(recurrence) or not self.obj.is_shared()
12.67 +
12.68 + def recurrence_is_new(self, recurrence):
12.69 +
12.70 + "Return whether 'recurrence' is new to the current object."
12.71 +
12.72 + return recurrence not in self.get_stored_recurrences()
12.73 +
12.74 + def can_remove_attendee(self, attendee):
12.75 +
12.76 + """
12.77 + Return whether 'attendee' can be removed from the current object without
12.78 + notification.
12.79 """
12.80
12.81 - dtstart, dtstart_attr = obj.get_datetime_item("DTSTART")
12.82 + return self.can_edit_attendee(attendee) or attendee == self.user
12.83 +
12.84 + def can_edit_attendee(self, attendee):
12.85 +
12.86 + "Return whether 'attendee' can be edited by an organiser."
12.87 +
12.88 + return self.attendee_is_new(attendee) or not self.obj.is_shared()
12.89 +
12.90 + def attendee_is_new(self, attendee):
12.91 +
12.92 + "Return whether 'attendee' is new to the current object."
12.93 +
12.94 + return attendee not in self.get_stored_attendees()
12.95
12.96 - if obj.has_key("DTEND"):
12.97 - dtend, dtend_attr = obj.get_datetime_item("DTEND")
12.98 - elif obj.has_key("DURATION"):
12.99 - duration = obj.get_duration("DURATION")
12.100 - dtend = dtstart + duration
12.101 - dtend_attr = dtstart_attr
12.102 - else:
12.103 - dtend, dtend_attr = dtstart, dtstart_attr
12.104 + # Access to stored object information.
12.105 +
12.106 + def is_organiser(self):
12.107 + return get_uri(self.obj.get_value("ORGANIZER")) == self.user
12.108
12.109 + def get_stored_attendees(self):
12.110 + return uri_values(self.obj.get_values("ATTENDEE") or [])
12.111 +
12.112 + def get_stored_main_period(self):
12.113 +
12.114 + "Return the main event period for the current object."
12.115 +
12.116 + (dtstart, dtstart_attr), (dtend, dtend_attr) = self.obj.get_main_period_items(self.get_tzid())
12.117 return EventPeriod(dtstart, dtend, self.get_tzid(), None, dtstart_attr, dtend_attr)
12.118
12.119 - def get_stored_recurrences(self, obj):
12.120 + def get_stored_recurrences(self):
12.121
12.122 - "Return recurrences computed using the given 'obj'."
12.123 + "Return recurrences computed using the current object."
12.124
12.125 recurrences = []
12.126 - for period in self.get_periods(obj):
12.127 + for period in self.get_periods(self.obj):
12.128 if period.origin != "DTSTART":
12.129 recurrences.append(period)
12.130 return recurrences
12.131
12.132 - # Request logic methods.
12.133 -
12.134 - def is_initial_load(self):
12.135 -
12.136 - "Return whether the event is being loaded and shown for the first time."
12.137 -
12.138 - return not self.env.get_args().has_key("editing")
12.139 -
12.140 - def handle_request(self, obj):
12.141 -
12.142 - """
12.143 - Handle actions involving the given 'obj' as an object's representation,
12.144 - returning an error if one occurred, or None if the request was
12.145 - successfully handled.
12.146 - """
12.147 -
12.148 - # Handle a submitted form.
12.149 -
12.150 - args = self.env.get_args()
12.151 - uid = obj.get_uid()
12.152 - recurrenceid = obj.get_recurrenceid()
12.153 -
12.154 - # Get the possible actions.
12.155 -
12.156 - reply = args.has_key("reply")
12.157 - discard = args.has_key("discard")
12.158 - create = args.has_key("create")
12.159 - cancel = args.has_key("cancel")
12.160 - ignore = args.has_key("ignore")
12.161 - save = args.has_key("save")
12.162 -
12.163 - have_action = reply or discard or create or cancel or ignore or save
12.164 -
12.165 - if not have_action:
12.166 - return ["action"]
12.167 -
12.168 - # If ignoring the object, return to the calendar.
12.169 -
12.170 - if ignore:
12.171 - self.redirect(self.env.get_path())
12.172 - return None
12.173 -
12.174 - # Update the object.
12.175 -
12.176 - single_user = False
12.177 -
12.178 - if reply or create or cancel or save:
12.179 -
12.180 - # Update principal event details if organiser.
12.181 -
12.182 - if self.is_organiser(obj):
12.183 -
12.184 - # Update time periods (main and recurring).
12.185 -
12.186 - try:
12.187 - period = self.handle_main_period()
12.188 - except PeriodError, exc:
12.189 - return exc.args
12.190 -
12.191 - try:
12.192 - periods = self.handle_recurrence_periods()
12.193 - except PeriodError, exc:
12.194 - return exc.args
12.195 -
12.196 - # Set the periods in the object, first obtaining removed and
12.197 - # modified period information.
12.198 -
12.199 - to_unschedule = self.get_removed_periods()
12.200 -
12.201 - obj.set_period(period)
12.202 - obj.set_periods(periods)
12.203 -
12.204 - # Update summary.
12.205 -
12.206 - if args.has_key("summary"):
12.207 - obj["SUMMARY"] = [(args["summary"][0], {})]
12.208 -
12.209 - # Obtain any participants and those to be removed.
12.210 + # Access to current object information.
12.211
12.212 - attendees = self.get_attendees_from_page()
12.213 - removed = [attendees[int(i)] for i in args.get("remove", [])]
12.214 - to_cancel = self.update_attendees(obj, attendees, removed)
12.215 - single_user = not attendees or attendees == [self.user]
12.216 -
12.217 - # Update attendee participation for the current user.
12.218 -
12.219 - if args.has_key("partstat"):
12.220 - self.update_participation(obj, args["partstat"][0])
12.221 -
12.222 - # Process any action.
12.223 -
12.224 - invite = not save and create and not single_user
12.225 - save = save or create and single_user
12.226 -
12.227 - handled = True
12.228 -
12.229 - if reply or invite or cancel:
12.230 -
12.231 - client = ManagerClient(obj, self.user, self.messenger)
12.232 -
12.233 - # Process the object and remove it from the list of requests.
12.234 -
12.235 - if reply and client.process_received_request():
12.236 - self.remove_request(uid, recurrenceid)
12.237 -
12.238 - elif self.is_organiser(obj) and (invite or cancel):
12.239 -
12.240 - # Invitation, uninvitation and unscheduling...
12.241 -
12.242 - if client.process_created_request(
12.243 - invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule):
12.244 -
12.245 - self.remove_request(uid, recurrenceid)
12.246 -
12.247 - # Save single user events.
12.248 -
12.249 - elif save:
12.250 - self.store.set_event(self.user, uid, recurrenceid, node=obj.to_node())
12.251 - self.update_freebusy(uid, recurrenceid, obj)
12.252 - self.remove_request(uid, recurrenceid)
12.253 -
12.254 - # Remove the request and the object.
12.255 -
12.256 - elif discard:
12.257 - self.remove_from_freebusy(uid, recurrenceid)
12.258 - self.remove_event(uid, recurrenceid)
12.259 - self.remove_request(uid, recurrenceid)
12.260 -
12.261 - else:
12.262 - handled = False
12.263 -
12.264 - # Upon handling an action, redirect to the main page.
12.265 -
12.266 - if handled:
12.267 - self.redirect(self.env.get_path())
12.268 -
12.269 - return None
12.270 -
12.271 - def handle_main_period(self):
12.272 -
12.273 - "Return period details for the main start/end period in an event."
12.274 -
12.275 - return self.get_main_period().as_event_period()
12.276 -
12.277 - def handle_recurrence_periods(self):
12.278 -
12.279 - "Return period details for the recurrences specified for an event."
12.280 -
12.281 - return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences())]
12.282 -
12.283 - def get_date_control_values(self, name, multiple=False, tzid_name=None):
12.284 -
12.285 - """
12.286 - Return a dictionary containing date, time and tzid entries for fields
12.287 - starting with 'name'. If 'multiple' is set to a true value, many
12.288 - dictionaries will be returned corresponding to a collection of
12.289 - datetimes. If 'tzid_name' is specified, the time zone information will
12.290 - be acquired from a field starting with 'tzid_name' instead of 'name'.
12.291 - """
12.292 + def get_current_main_period(self):
12.293 + return self.get_stored_main_period()
12.294
12.295 - args = self.env.get_args()
12.296 -
12.297 - dates = args.get("%s-date" % name, [])
12.298 - hours = args.get("%s-hour" % name, [])
12.299 - minutes = args.get("%s-minute" % name, [])
12.300 - seconds = args.get("%s-second" % name, [])
12.301 - tzids = args.get("%s-tzid" % (tzid_name or name), [])
12.302 -
12.303 - # Handle absent values by employing None values.
12.304 -
12.305 - field_values = map(None, dates, hours, minutes, seconds, tzids)
12.306 -
12.307 - if not field_values and not multiple:
12.308 - all_values = FormDate()
12.309 - else:
12.310 - all_values = []
12.311 - for date, hour, minute, second, tzid in field_values:
12.312 - value = FormDate(date, hour, minute, second, tzid or self.get_tzid())
12.313 -
12.314 - # Return a single value or append to a collection of all values.
12.315 -
12.316 - if not multiple:
12.317 - return value
12.318 - else:
12.319 - all_values.append(value)
12.320 -
12.321 - return all_values
12.322 -
12.323 - def get_current_main_period(self, obj):
12.324 -
12.325 - """
12.326 - Return the currently active main period for 'obj' depending on whether
12.327 - editing has begun or whether the object has just been loaded.
12.328 - """
12.329 -
12.330 - if self.is_initial_load() or not self.is_organiser(obj):
12.331 - return self.get_stored_main_period(obj)
12.332 - else:
12.333 - return self.get_main_period()
12.334 -
12.335 - def get_main_period(self):
12.336 -
12.337 - "Return the main period defined in the event form."
12.338 -
12.339 - args = self.env.get_args()
12.340 -
12.341 - dtend_enabled = args.get("dtend-control", [None])[0]
12.342 - dttimes_enabled = args.get("dttimes-control", [None])[0]
12.343 - start = self.get_date_control_values("dtstart")
12.344 - end = self.get_date_control_values("dtend")
12.345 -
12.346 - return FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid())
12.347 -
12.348 - def get_current_recurrences(self, obj):
12.349 -
12.350 - """
12.351 - Return recurrences for 'obj' using the original object where no editing
12.352 - is in progress, using form data otherwise.
12.353 - """
12.354 -
12.355 - if self.is_initial_load() or not self.is_organiser(obj):
12.356 - return self.get_stored_recurrences(obj)
12.357 - else:
12.358 - return self.get_recurrences()
12.359 -
12.360 - def get_recurrences(self):
12.361 -
12.362 - "Return the recurrences defined in the event form."
12.363 -
12.364 - args = self.env.get_args()
12.365 -
12.366 - all_dtend_enabled = args.get("dtend-control-recur", [])
12.367 - all_dttimes_enabled = args.get("dttimes-control-recur", [])
12.368 - all_starts = self.get_date_control_values("dtstart-recur", multiple=True)
12.369 - all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur")
12.370 - all_origins = args.get("recur-origin", [])
12.371 -
12.372 - periods = []
12.373 + def get_current_recurrences(self):
12.374 + return self.get_stored_recurrences()
12.375
12.376 - for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \
12.377 - enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)):
12.378 -
12.379 - dtend_enabled = str(index) in all_dtend_enabled
12.380 - dttimes_enabled = str(index) in all_dttimes_enabled
12.381 - period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin)
12.382 - periods.append(period)
12.383 -
12.384 - return periods
12.385 -
12.386 - def get_removed_periods(self):
12.387 -
12.388 - "Return a list of recurrence periods to remove upon updating an event."
12.389 -
12.390 - to_unschedule = []
12.391 - args = self.env.get_args()
12.392 - for i in args.get("recur-remove", []):
12.393 - to_unschedule.append(periods[int(i)])
12.394 - return to_unschedule
12.395 -
12.396 - def get_current_attendees(self, obj):
12.397 -
12.398 - """
12.399 - Return attendees for 'obj' depending on whether the object is being
12.400 - edited.
12.401 - """
12.402 -
12.403 - if self.is_initial_load() or not self.is_organiser(obj):
12.404 - return self.get_stored_attendees(obj)
12.405 - else:
12.406 - return self.get_attendees_from_page()
12.407 -
12.408 - def get_attendees_from_page(self):
12.409 -
12.410 - """
12.411 - Return attendees from the request, normalised for iCalendar purposes,
12.412 - and without duplicates.
12.413 - """
12.414 -
12.415 - args = self.env.get_args()
12.416 -
12.417 - attendees = args.get("attendee", [])
12.418 - unique_attendees = set()
12.419 - ordered_attendees = []
12.420 -
12.421 - for attendee in attendees:
12.422 - if not attendee.strip():
12.423 - continue
12.424 - attendee = get_uri(attendee)
12.425 - if attendee not in unique_attendees:
12.426 - unique_attendees.add(attendee)
12.427 - ordered_attendees.append(attendee)
12.428 -
12.429 - return ordered_attendees
12.430 -
12.431 - def update_attendees_from_page(self, obj):
12.432 -
12.433 - "Add or remove attendees. This does not affect the stored object."
12.434 -
12.435 - args = self.env.get_args()
12.436 -
12.437 - attendees = self.get_attendees_from_page()
12.438 - existing_attendees = self.get_stored_attendees(obj)
12.439 -
12.440 - if args.has_key("add"):
12.441 - attendees.append("")
12.442 -
12.443 - # Only actually remove attendees if the event is unsent, if the attendee
12.444 - # is new, or if it is the current user being removed.
12.445 -
12.446 - if args.has_key("remove"):
12.447 - for i in args["remove"]:
12.448 - try:
12.449 - attendee = attendees[int(i)]
12.450 - except IndexError:
12.451 - continue
12.452 -
12.453 - existing = attendee in existing_attendees
12.454 -
12.455 - if not existing or not obj.is_shared() or attendee == self.user:
12.456 - attendees.remove(attendee)
12.457 -
12.458 - return attendees
12.459 + def get_current_attendees(self):
12.460 + return self.get_stored_attendees()
12.461
12.462 # Page fragment methods.
12.463
12.464 - def show_request_controls(self, obj):
12.465 + def show_request_controls(self):
12.466
12.467 - "Show form controls for a request concerning 'obj'."
12.468 + "Show form controls for a request."
12.469
12.470 page = self.page
12.471 args = self.env.get_args()
12.472
12.473 - attendees = self.get_current_attendees(obj)
12.474 + attendees = self.get_current_attendees()
12.475 is_attendee = self.user in attendees
12.476 - is_request = self._have_request(obj.get_uid(), obj.get_recurrenceid())
12.477 + is_request = self._have_request(self.uid, self.recurrenceid)
12.478
12.479 # Show appropriate options depending on the role of the user.
12.480
12.481 - if is_attendee and not self.is_organiser(obj):
12.482 + if is_attendee and not self.is_organiser():
12.483 page.p("An action is required for this request:")
12.484
12.485 page.p()
12.486 - self._control("reply", "submit", "Send reply")
12.487 + self.control("reply", "submit", "Send reply")
12.488 page.add(" ")
12.489 - self._control("discard", "submit", "Discard event")
12.490 + self.control("discard", "submit", "Discard event")
12.491 page.add(" ")
12.492 - self._control("ignore", "submit", "Do nothing for now")
12.493 + self.control("ignore", "submit", "Do nothing for now")
12.494 page.p.close()
12.495
12.496 - if self.is_organiser(obj):
12.497 + if self.is_organiser():
12.498 page.p("As organiser, you can perform the following:")
12.499
12.500 page.p()
12.501 - self._control("create", "submit", not obj.is_shared() and "Create event" or "Update event")
12.502 + self.control("create", "submit", not self.obj.is_shared() and "Create event" or "Update event")
12.503 page.add(" ")
12.504
12.505 - if obj.is_shared() and not is_request:
12.506 - self._control("cancel", "submit", "Cancel event")
12.507 + if self.obj.is_shared() and not is_request:
12.508 + self.control("cancel", "submit", "Cancel event")
12.509 else:
12.510 - self._control("discard", "submit", "Discard event")
12.511 + self.control("discard", "submit", "Discard event")
12.512
12.513 page.add(" ")
12.514 - self._control("save", "submit", "Save without sending")
12.515 + self.control("save", "submit", "Save without sending")
12.516 page.p.close()
12.517
12.518 - def show_object_on_page(self, obj, errors=None):
12.519 + def show_object_on_page(self, errors=None):
12.520
12.521 """
12.522 - Show the calendar object with the representation 'obj' on the current
12.523 - page. If 'errors' is given, show a suitable message for the different
12.524 - errors provided.
12.525 + Show the calendar object on the current page. If 'errors' is given, show
12.526 + a suitable message for the different errors provided.
12.527 """
12.528
12.529 page = self.page
12.530 @@ -474,26 +185,21 @@
12.531
12.532 # Add a hidden control to help determine whether editing has already begun.
12.533
12.534 - self._control("editing", "hidden", "true")
12.535 + self.control("editing", "hidden", "true")
12.536
12.537 - uid = obj.get_uid()
12.538 args = self.env.get_args()
12.539
12.540 # Obtain basic event information, generating any necessary editing controls.
12.541
12.542 - if self.is_initial_load() or not self.is_organiser(obj):
12.543 - attendees = self.get_stored_attendees(obj)
12.544 - else:
12.545 - attendees = self.update_attendees_from_page(obj)
12.546 -
12.547 - p = self.get_current_main_period(obj)
12.548 - self.show_object_datetime_controls(p)
12.549 + attendees = self.get_current_attendees()
12.550 + period = self.get_current_main_period()
12.551 + self.show_object_datetime_controls(period)
12.552
12.553 # Obtain any separate recurrences for this event.
12.554
12.555 - recurrenceid = obj.get_recurrenceid()
12.556 - recurrenceids = self._get_active_recurrences(uid)
12.557 - replaced = not recurrenceid and p.is_replaced(recurrenceids)
12.558 + recurrenceids = self._get_active_recurrences(self.uid)
12.559 + replaced = not self.recurrenceid and period.is_replaced(recurrenceids)
12.560 + excluded = period not in self.get_periods(self.obj)
12.561
12.562 # Provide a summary of the object.
12.563
12.564 @@ -508,7 +214,7 @@
12.565 for name, label in self.property_items:
12.566 field = name.lower()
12.567
12.568 - items = obj.get_items(name) or []
12.569 + items = uri_items(self.obj.get_items(name) or [])
12.570 rowspan = len(items)
12.571
12.572 if name == "ATTENDEE":
12.573 @@ -522,7 +228,7 @@
12.574 # Handle datetimes specially.
12.575
12.576 if name in ["DTSTART", "DTEND"]:
12.577 - if not replaced:
12.578 + if not replaced and not excluded:
12.579
12.580 # Obtain the datetime.
12.581
12.582 @@ -532,23 +238,36 @@
12.583 # basis of any potential datetime specified if dt-control is
12.584 # set.
12.585
12.586 - self.show_datetime_controls(obj, is_start and p.get_form_start() or p.get_form_end(), is_start)
12.587 + self.show_datetime_controls(is_start and period.get_form_start() or period.get_form_end(), is_start)
12.588
12.589 elif name == "DTSTART":
12.590 - page.td(class_="objectvalue %s replaced" % field, rowspan=2)
12.591 - page.a("First occurrence replaced by a separate event", href=self.link_to(uid, replaced))
12.592 - page.td.close()
12.593 +
12.594 + # Replaced occurrences link to their replacements.
12.595 +
12.596 + if replaced:
12.597 + page.td(class_="objectvalue %s replaced" % field, rowspan=2)
12.598 + page.a("First occurrence replaced by a separate event", href=self.link_to(self.uid, replaced))
12.599 + page.td.close()
12.600 +
12.601 + # NOTE: Should provide a way of editing recurrences when the
12.602 + # NOTE: first occurrence is excluded, plus a way of
12.603 + # NOTE: reinstating the occurrence.
12.604 +
12.605 + elif excluded:
12.606 + page.td(class_="objectvalue %s excluded" % field, rowspan=2)
12.607 + page.add("First occurrence excluded")
12.608 + page.td.close()
12.609
12.610 page.tr.close()
12.611
12.612 # Handle the summary specially.
12.613
12.614 elif name == "SUMMARY":
12.615 - value = args.get("summary", [obj.get_value(name)])[0]
12.616 + value = args.get("summary", [self.obj.get_value(name)])[0]
12.617
12.618 page.td(class_="objectvalue summary")
12.619 - if self.is_organiser(obj):
12.620 - self._control("summary", "text", value, size=80)
12.621 + if self.is_organiser():
12.622 + self.control("summary", "text", value, size=80)
12.623 else:
12.624 page.add(value)
12.625 page.td.close()
12.626 @@ -568,17 +287,17 @@
12.627
12.628 # Obtain details of attendees to supply attributes.
12.629
12.630 - self.show_attendee(obj, i, value, attendee_map.get(value))
12.631 + self.show_attendee(i, value, attendee_map.get(value))
12.632 page.tr.close()
12.633
12.634 # Allow more attendees to be specified.
12.635
12.636 - if self.is_organiser(obj):
12.637 + if self.is_organiser():
12.638 if not first:
12.639 page.tr()
12.640
12.641 page.td()
12.642 - self._control("add", "submit", "add", id="add", class_="add")
12.643 + self.control("add", "submit", "add", id="add", class_="add")
12.644 page.label("Add attendee", for_="add", class_="add")
12.645 page.td.close()
12.646 page.tr.close()
12.647 @@ -602,59 +321,62 @@
12.648 page.tbody.close()
12.649 page.table.close()
12.650
12.651 - self.show_recurrences(obj, errors)
12.652 - self.show_conflicting_events(obj)
12.653 - self.show_request_controls(obj)
12.654 + self.show_recurrences(errors)
12.655 + self.show_counters()
12.656 + self.show_conflicting_events()
12.657 + self.show_request_controls()
12.658
12.659 page.form.close()
12.660
12.661 - def show_attendee(self, obj, i, attendee, attendee_attr):
12.662 + def show_attendee(self, i, attendee, attendee_attr):
12.663
12.664 """
12.665 - For the given object 'obj', show the attendee in position 'i' with the
12.666 - given 'attendee' value, having 'attendee_attr' as any stored attributes.
12.667 + For the current object, show the attendee in position 'i' with the given
12.668 + 'attendee' value, having 'attendee_attr' as any stored attributes.
12.669 """
12.670
12.671 page = self.page
12.672 args = self.env.get_args()
12.673
12.674 - existing = attendee_attr is not None
12.675 partstat = attendee_attr and attendee_attr.get("PARTSTAT")
12.676
12.677 page.td(class_="objectvalue")
12.678
12.679 # Show a form control as organiser for new attendees.
12.680
12.681 - if self.is_organiser(obj) and (not existing or not obj.is_shared()):
12.682 - self._control("attendee", "value", attendee, size="40")
12.683 + if self.is_organiser() and self.can_edit_attendee(attendee):
12.684 + self.control("attendee", "value", attendee, size="40")
12.685 else:
12.686 - self._control("attendee", "hidden", attendee)
12.687 + self.control("attendee", "hidden", attendee)
12.688 page.add(attendee)
12.689 page.add(" ")
12.690
12.691 # Show participation status, editable for the current user.
12.692
12.693 if attendee == self.user:
12.694 - self._show_menu("partstat", partstat, self.partstat_items, "partstat")
12.695 + self.menu("partstat", partstat, self.partstat_items, "partstat")
12.696
12.697 # Allow the participation indicator to act as a submit
12.698 # button in order to refresh the page and show a control for
12.699 # the current user, if indicated.
12.700
12.701 - elif self.is_organiser(obj) and not existing:
12.702 - self._control("partstat-refresh", "submit", "refresh", id="partstat-%d" % i, class_="refresh")
12.703 + elif self.is_organiser() and self.attendee_is_new(attendee):
12.704 + self.control("partstat-refresh", "submit", "refresh", id="partstat-%d" % i, class_="refresh")
12.705 page.label(dict(self.partstat_items).get(partstat, ""), for_="partstat-%s" % i, class_="partstat")
12.706 +
12.707 + # Otherwise, just show a label with the participation status.
12.708 +
12.709 else:
12.710 page.span(dict(self.partstat_items).get(partstat, ""), class_="partstat")
12.711
12.712 # Permit organisers to remove attendees.
12.713
12.714 - if self.is_organiser(obj):
12.715 + if self.is_organiser():
12.716
12.717 # Permit the removal of newly-added attendees.
12.718
12.719 - remove_type = (not existing or not obj.is_shared() or attendee == self.user) and "submit" or "checkbox"
12.720 - self._control("remove", remove_type, str(i), str(i) in args.get("remove", []), id="remove-%d" % i, class_="remove")
12.721 + remove_type = self.can_remove_attendee(attendee) and "submit" or "checkbox"
12.722 + self.control("remove", remove_type, str(i), str(i) in args.get("remove", []), id="remove-%d" % i, class_="remove")
12.723
12.724 page.label("Remove", for_="remove-%d" % i, class_="remove")
12.725 page.label(for_="remove-%d" % i, class_="removed")
12.726 @@ -664,48 +386,44 @@
12.727
12.728 page.td.close()
12.729
12.730 - def show_recurrences(self, obj, errors=None):
12.731 + def show_recurrences(self, errors=None):
12.732
12.733 """
12.734 - Show recurrences for the object having the given representation 'obj'.
12.735 - If 'errors' is given, show a suitable message for the different errors
12.736 - provided.
12.737 + Show recurrences for the current object. If 'errors' is given, show a
12.738 + suitable message for the different errors provided.
12.739 """
12.740
12.741 page = self.page
12.742
12.743 # Obtain any parent object if this object is a specific recurrence.
12.744
12.745 - uid = obj.get_uid()
12.746 - recurrenceid = obj.get_recurrenceid()
12.747 -
12.748 - if recurrenceid:
12.749 - parent = self.get_stored_object(uid, None)
12.750 + if self.recurrenceid:
12.751 + parent = self.get_stored_object(self.uid, None)
12.752 if not parent:
12.753 return
12.754
12.755 page.p()
12.756 - page.a("This event modifies a recurring event.", href=self.link_to(uid))
12.757 + page.a("This event modifies a recurring event.", href=self.link_to(self.uid))
12.758 page.p.close()
12.759
12.760 # Obtain the periods associated with the event.
12.761 # NOTE: Add a control to add recurrences here.
12.762
12.763 - recurrences = self.get_current_recurrences(obj)
12.764 + recurrences = self.get_current_recurrences()
12.765
12.766 if len(recurrences) < 1:
12.767 return
12.768
12.769 - recurrenceids = self._get_recurrences(uid)
12.770 + recurrenceids = self._get_recurrences(self.uid)
12.771
12.772 page.p("This event occurs on the following occasions within the next %d days:" % self.get_window_size())
12.773
12.774 # Show each recurrence in a separate table if editable.
12.775
12.776 - if self.is_organiser(obj) and recurrences:
12.777 + if self.is_organiser() and recurrences:
12.778
12.779 for index, period in enumerate(recurrences):
12.780 - self.show_recurrence(obj, index, period, recurrenceid, recurrenceids, errors)
12.781 + self.show_recurrence(index, period, self.recurrenceid, recurrenceids, errors)
12.782
12.783 # Otherwise, use a compact single table.
12.784
12.785 @@ -722,21 +440,21 @@
12.786
12.787 for index, period in enumerate(recurrences):
12.788 page.tr()
12.789 - self.show_recurrence_label(period, recurrenceid, recurrenceids, True)
12.790 - self.show_recurrence_label(period, recurrenceid, recurrenceids, False)
12.791 + self.show_recurrence_label(period, self.recurrenceid, recurrenceids, True)
12.792 + self.show_recurrence_label(period, self.recurrenceid, recurrenceids, False)
12.793 page.tr.close()
12.794
12.795 page.tbody.close()
12.796 page.table.close()
12.797
12.798 - def show_recurrence(self, obj, index, period, recurrenceid, recurrenceids, errors=None):
12.799 + def show_recurrence(self, index, period, recurrenceid, recurrenceids, errors=None):
12.800
12.801 """
12.802 - Show recurrence controls for a recurrence provided by 'obj' with the
12.803 - given 'index' position in the list of periods, the given 'period'
12.804 - details, where a 'recurrenceid' indicates any specific recurrence, and
12.805 - where 'recurrenceids' indicates all known additional recurrences for the
12.806 - object.
12.807 + Show recurrence controls for a recurrence provided by the current object
12.808 + with the given 'index' position in the list of periods, the given
12.809 + 'period' details, where a 'recurrenceid' indicates any specific
12.810 + recurrence, and where 'recurrenceids' indicates all known additional
12.811 + recurrences for the object.
12.812
12.813 If 'errors' is given, show a suitable message for the different errors
12.814 provided.
12.815 @@ -761,12 +479,12 @@
12.816 page.tr()
12.817 error = errors and ("dtstart", index) in errors and " error" or ""
12.818 page.th("Start", class_="objectheading start%s" % error)
12.819 - self.show_recurrence_controls(obj, index, period, recurrenceid, recurrenceids, True)
12.820 + self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, True)
12.821 page.tr.close()
12.822 page.tr()
12.823 error = errors and ("dtend", index) in errors and " error" or ""
12.824 page.th("End", class_="objectheading end%s" % error)
12.825 - self.show_recurrence_controls(obj, index, period, recurrenceid, recurrenceids, False)
12.826 + self.show_recurrence_controls(index, period, recurrenceid, recurrenceids, False)
12.827 page.tr.close()
12.828
12.829 # Permit the removal of recurrences.
12.830 @@ -776,8 +494,9 @@
12.831 page.th("")
12.832 page.td()
12.833
12.834 - remove_type = not obj.is_shared() or not period.origin and "submit" or "checkbox"
12.835 - self._control("recur-remove", remove_type, str(index),
12.836 + remove_type = self.can_remove_recurrence(period) and "submit" or "checkbox"
12.837 +
12.838 + self.control("recur-remove", remove_type, str(index),
12.839 str(index) in args.get("recur-remove", []),
12.840 id="recur-remove-%d" % index, class_="remove")
12.841
12.842 @@ -795,53 +514,107 @@
12.843
12.844 page.div.close()
12.845
12.846 - def show_conflicting_events(self, obj):
12.847 + def show_counters(self):
12.848
12.849 - """
12.850 - Show conflicting events for the object having the representation 'obj'.
12.851 - """
12.852 + "Show any counter-proposals for the current object."
12.853
12.854 page = self.page
12.855 - uid = obj.get_uid()
12.856 - recurrenceid = obj.get_recurrenceid()
12.857 - recurrenceids = self._get_active_recurrences(uid)
12.858 + query = self.env.get_query()
12.859 + counter = query.get("counter", [None])[0]
12.860 +
12.861 + attendees = self._get_counters(self.uid, self.recurrenceid)
12.862 + tzid = self.get_tzid()
12.863 +
12.864 + if not attendees:
12.865 + return
12.866 +
12.867 + page.p("The following counter-proposals have been received for this event:")
12.868 +
12.869 + page.table(cellspacing=5, cellpadding=5, class_="counters")
12.870 + page.thead()
12.871 + page.tr()
12.872 + page.th("Attendee", rowspan=2)
12.873 + page.th("Periods", colspan=2)
12.874 + page.tr.close()
12.875 + page.tr()
12.876 + page.th("Start")
12.877 + page.th("End")
12.878 + page.tr.close()
12.879 + page.thead.close()
12.880 + page.tbody()
12.881 +
12.882 + for attendee in attendees:
12.883 + obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee)
12.884 + periods = self.get_periods(obj)
12.885 +
12.886 + first = True
12.887 + for p in periods:
12.888 + identifier = "%s-%s" % (format_datetime(p.get_start_point()), format_datetime(p.get_end_point()))
12.889 + css = identifier == counter and "selected" or ""
12.890 +
12.891 + if first:
12.892 + page.tr(rowspan=len(periods), class_=css)
12.893 + page.td(attendee)
12.894 + first = False
12.895 + else:
12.896 + page.tr(class_=css)
12.897 +
12.898 + start = self.format_datetime(to_timezone(p.get_start(), tzid), "long")
12.899 + end = self.format_datetime(to_timezone(p.get_end(), tzid), "long")
12.900 +
12.901 + page.td(start)
12.902 + page.td(end)
12.903 +
12.904 + page.tr.close()
12.905 +
12.906 + page.tbody.close()
12.907 + page.table.close()
12.908 +
12.909 + def show_conflicting_events(self):
12.910 +
12.911 + "Show conflicting events for the current object."
12.912 +
12.913 + page = self.page
12.914 + recurrenceids = self._get_active_recurrences(self.uid)
12.915
12.916 # Obtain the user's timezone.
12.917
12.918 tzid = self.get_tzid()
12.919 - periods = self.get_periods(obj)
12.920 + periods = self.get_periods(self.obj)
12.921
12.922 # Indicate whether there are conflicting events.
12.923
12.924 conflicts = []
12.925 - attendee_map = uri_dict(obj.get_value_map("ATTENDEE"))
12.926 + attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE"))
12.927
12.928 - for participant in self.get_current_attendees(obj):
12.929 + for participant in self.get_current_attendees():
12.930 if participant == self.user:
12.931 freebusy = self.store.get_freebusy(participant)
12.932 + elif participant:
12.933 + freebusy = self.store.get_freebusy_for_other(self.user, participant)
12.934 else:
12.935 - freebusy = self.store.get_freebusy_for_other(self.user, participant)
12.936 + continue
12.937
12.938 if not freebusy:
12.939 continue
12.940
12.941 # Obtain any time zone details from the suggested event.
12.942
12.943 - _dtstart, attr = obj.get_item("DTSTART")
12.944 + _dtstart, attr = self.obj.get_item("DTSTART")
12.945 tzid = attr.get("TZID", tzid)
12.946
12.947 # Show any conflicts with periods of actual attendance.
12.948
12.949 participant_attr = attendee_map.get(participant)
12.950 partstat = participant_attr and participant_attr.get("PARTSTAT")
12.951 - recurrences = obj.get_recurrence_start_points(recurrenceids, tzid)
12.952 + recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid)
12.953
12.954 for p in have_conflict(freebusy, periods, True):
12.955 - if not recurrenceid and p.is_replaced(recurrences):
12.956 + if not self.recurrenceid and p.is_replaced(recurrences):
12.957 continue
12.958
12.959 if ( # Unidentified or different event
12.960 - (p.uid != uid or recurrenceid and p.recurrenceid and p.recurrenceid != recurrenceid) and
12.961 + (p.uid != self.uid or self.recurrenceid and p.recurrenceid and p.recurrenceid != self.recurrenceid) and
12.962 # Different period or unclear participation with the same period
12.963 (p not in periods or not partstat in ("ACCEPTED", "TENTATIVE")) and
12.964 # Participant not limited to organising
12.965 @@ -893,188 +666,396 @@
12.966 page.tbody.close()
12.967 page.table.close()
12.968
12.969 - # Generation of controls within page fragments.
12.970 +class EventPage(EventPageFragment):
12.971 +
12.972 + "A request handler for the event page."
12.973 +
12.974 + def __init__(self, resource=None, messenger=None):
12.975 + ResourceClientForObject.__init__(self, resource)
12.976 + self.messenger = messenger or Messenger()
12.977
12.978 - def show_object_datetime_controls(self, period, index=None):
12.979 + # Request logic methods.
12.980 +
12.981 + def is_initial_load(self):
12.982 +
12.983 + "Return whether the event is being loaded and shown for the first time."
12.984 +
12.985 + return not self.env.get_args().has_key("editing")
12.986 +
12.987 + def handle_request(self):
12.988
12.989 """
12.990 - Show datetime-related controls if already active or if an object needs
12.991 - them for the given 'period'. The given 'index' is used to parameterise
12.992 - individual controls for dynamic manipulation.
12.993 + Handle actions involving the current object, returning an error if one
12.994 + occurred, or None if the request was successfully handled.
12.995 """
12.996
12.997 - p = form_period_from_period(period)
12.998 + # Handle a submitted form.
12.999
12.1000 - page = self.page
12.1001 args = self.env.get_args()
12.1002 - _id = self.element_identifier
12.1003 - _name = self.element_name
12.1004 - _enable = self.element_enable
12.1005 +
12.1006 + # Get the possible actions.
12.1007 +
12.1008 + reply = args.has_key("reply")
12.1009 + discard = args.has_key("discard")
12.1010 + create = args.has_key("create")
12.1011 + cancel = args.has_key("cancel")
12.1012 + ignore = args.has_key("ignore")
12.1013 + save = args.has_key("save")
12.1014
12.1015 - # Add a dynamic stylesheet to permit the controls to modify the display.
12.1016 - # NOTE: The style details need to be coordinated with the static
12.1017 - # NOTE: stylesheet.
12.1018 + have_action = reply or discard or create or cancel or ignore or save
12.1019 +
12.1020 + if not have_action:
12.1021 + return ["action"]
12.1022
12.1023 - if index is not None:
12.1024 - page.style(type="text/css")
12.1025 + # If ignoring the object, return to the calendar.
12.1026
12.1027 - # Unlike the rules for object properties, these affect recurrence
12.1028 - # properties.
12.1029 + if ignore:
12.1030 + self.redirect(self.env.get_path())
12.1031 + return None
12.1032 +
12.1033 + # Update the object.
12.1034
12.1035 - page.add("""\
12.1036 -input#dttimes-enable-%(index)d,
12.1037 -input#dtend-enable-%(index)d,
12.1038 -input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled,
12.1039 -input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled,
12.1040 -input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled,
12.1041 -input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled {
12.1042 - display: none;
12.1043 -}""" % {"index" : index})
12.1044 + single_user = False
12.1045 +
12.1046 + if reply or create or cancel or save:
12.1047 +
12.1048 + # Update principal event details if organiser.
12.1049 +
12.1050 + if self.is_organiser():
12.1051 +
12.1052 + # Update time periods (main and recurring).
12.1053
12.1054 - page.style.close()
12.1055 + try:
12.1056 + period = self.handle_main_period()
12.1057 + except PeriodError, exc:
12.1058 + return exc.args
12.1059
12.1060 - self._control(
12.1061 - _name("dtend-control", "recur", index), "checkbox",
12.1062 - _enable(index), p.end_enabled,
12.1063 - id=_id("dtend-enable", index)
12.1064 - )
12.1065 + try:
12.1066 + periods = self.handle_recurrence_periods()
12.1067 + except PeriodError, exc:
12.1068 + return exc.args
12.1069 +
12.1070 + # Set the periods in the object, first obtaining removed and
12.1071 + # modified period information.
12.1072 +
12.1073 + to_unschedule, to_exclude = self.get_removed_periods(periods)
12.1074 +
12.1075 + self.obj.set_period(period)
12.1076 + self.obj.set_periods(periods)
12.1077 + self.obj.update_exceptions(to_exclude)
12.1078
12.1079 - self._control(
12.1080 - _name("dttimes-control", "recur", index), "checkbox",
12.1081 - _enable(index), p.times_enabled,
12.1082 - id=_id("dttimes-enable", index)
12.1083 - )
12.1084 + # Update summary.
12.1085 +
12.1086 + if args.has_key("summary"):
12.1087 + self.obj["SUMMARY"] = [(args["summary"][0], {})]
12.1088 +
12.1089 + # Obtain any participants and those to be removed.
12.1090
12.1091 - def show_datetime_controls(self, obj, formdate, show_start):
12.1092 + attendees = self.get_attendees_from_page()
12.1093 + removed = [attendees[int(i)] for i in args.get("remove", [])]
12.1094 + to_cancel = self.update_attendees(self.obj, attendees, removed)
12.1095 + single_user = not attendees or attendees == [self.user]
12.1096 +
12.1097 + # Update attendee participation for the current user.
12.1098
12.1099 - """
12.1100 - Show datetime details from the given 'obj' for the 'formdate', showing
12.1101 - start details if 'show_start' is set to a true value. Details will
12.1102 - appear as controls for organisers and labels for attendees.
12.1103 - """
12.1104 + if args.has_key("partstat"):
12.1105 + self.update_participation(self.obj, args["partstat"][0])
12.1106 +
12.1107 + # Process any action.
12.1108
12.1109 - page = self.page
12.1110 + invite = not save and create and not single_user
12.1111 + save = save or create and single_user
12.1112
12.1113 - # Show controls for editing as organiser.
12.1114 + handled = True
12.1115
12.1116 - if self.is_organiser(obj):
12.1117 - page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
12.1118 + if reply or invite or cancel:
12.1119 +
12.1120 + client = ManagerClient(self.obj, self.user, self.messenger)
12.1121
12.1122 - if show_start:
12.1123 - page.div(class_="dt enabled")
12.1124 - self._show_date_controls("dtstart", formdate)
12.1125 - page.br()
12.1126 - page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")
12.1127 - page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable")
12.1128 - page.div.close()
12.1129 + # Process the object and remove it from the list of requests.
12.1130 +
12.1131 + if reply and client.process_received_request():
12.1132 + self.remove_request(self.uid, self.recurrenceid)
12.1133 +
12.1134 + elif self.is_organiser() and (invite or cancel):
12.1135 +
12.1136 + # Invitation, uninvitation and unscheduling...
12.1137 +
12.1138 + if client.process_created_request(
12.1139 + invite and "REQUEST" or "CANCEL", to_cancel, to_unschedule):
12.1140 +
12.1141 + self.remove_request(self.uid, self.recurrenceid)
12.1142
12.1143 - else:
12.1144 - page.div(class_="dt disabled")
12.1145 - page.label("Specify end date", for_="dtend-enable", class_="enable")
12.1146 - page.div.close()
12.1147 - page.div(class_="dt enabled")
12.1148 - self._show_date_controls("dtend", formdate)
12.1149 - page.br()
12.1150 - page.label("End on same day", for_="dtend-enable", class_="disable")
12.1151 - page.div.close()
12.1152 + # Save single user events.
12.1153 +
12.1154 + elif save:
12.1155 + self.store.set_event(self.user, self.uid, self.recurrenceid, node=self.obj.to_node())
12.1156 + self.update_event_in_freebusy()
12.1157 + self.remove_request(self.uid, self.recurrenceid)
12.1158
12.1159 - page.td.close()
12.1160 + # Remove the request and the object.
12.1161
12.1162 - # Show a label as attendee.
12.1163 + elif discard:
12.1164 + self.remove_event_from_freebusy()
12.1165 + self.remove_event(self.uid, self.recurrenceid)
12.1166 + self.remove_request(self.uid, self.recurrenceid)
12.1167
12.1168 else:
12.1169 - dt = formdate.as_datetime()
12.1170 - if dt:
12.1171 - page.td(self.format_datetime(dt, "full"))
12.1172 - else:
12.1173 - page.td("(Unrecognised date)")
12.1174 + handled = False
12.1175 +
12.1176 + # Upon handling an action, redirect to the main page.
12.1177 +
12.1178 + if handled:
12.1179 + self.redirect(self.env.get_path())
12.1180 +
12.1181 + return None
12.1182 +
12.1183 + def handle_main_period(self):
12.1184 +
12.1185 + "Return period details for the main start/end period in an event."
12.1186 +
12.1187 + return self.get_main_period_from_page().as_event_period()
12.1188 +
12.1189 + def handle_recurrence_periods(self):
12.1190 +
12.1191 + "Return period details for the recurrences specified for an event."
12.1192 +
12.1193 + return [p.as_event_period(i) for i, p in enumerate(self.get_recurrences_from_page())]
12.1194 +
12.1195 + # Access to form-originating object information.
12.1196 +
12.1197 + def get_main_period_from_page(self):
12.1198 +
12.1199 + "Return the main period defined in the event form."
12.1200 +
12.1201 + args = self.env.get_args()
12.1202 +
12.1203 + dtend_enabled = args.get("dtend-control", [None])[0]
12.1204 + dttimes_enabled = args.get("dttimes-control", [None])[0]
12.1205 + start = self.get_date_control_values("dtstart")
12.1206 + end = self.get_date_control_values("dtend")
12.1207 +
12.1208 + period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid())
12.1209 +
12.1210 + # Handle absent main period details.
12.1211 +
12.1212 + if not period.get_start():
12.1213 + return self.get_stored_main_period()
12.1214 + else:
12.1215 + return period
12.1216 +
12.1217 + def get_recurrences_from_page(self):
12.1218 +
12.1219 + "Return the recurrences defined in the event form."
12.1220
12.1221 - def show_recurrence_controls(self, obj, index, period, recurrenceid, recurrenceids, show_start):
12.1222 + args = self.env.get_args()
12.1223 +
12.1224 + all_dtend_enabled = args.get("dtend-control-recur", [])
12.1225 + all_dttimes_enabled = args.get("dttimes-control-recur", [])
12.1226 + all_starts = self.get_date_control_values("dtstart-recur", multiple=True)
12.1227 + all_ends = self.get_date_control_values("dtend-recur", multiple=True, tzid_name="dtstart-recur")
12.1228 + all_origins = args.get("recur-origin", [])
12.1229 +
12.1230 + periods = []
12.1231 +
12.1232 + for index, (start, end, dtend_enabled, dttimes_enabled, origin) in \
12.1233 + enumerate(map(None, all_starts, all_ends, all_dtend_enabled, all_dttimes_enabled, all_origins)):
12.1234 +
12.1235 + dtend_enabled = str(index) in all_dtend_enabled
12.1236 + dttimes_enabled = str(index) in all_dttimes_enabled
12.1237 + period = FormPeriod(start, end, dtend_enabled, dttimes_enabled, self.get_tzid(), origin)
12.1238 + periods.append(period)
12.1239 +
12.1240 + return periods
12.1241 +
12.1242 + def set_recurrences_in_page(self, recurrences):
12.1243 +
12.1244 + "Set the recurrences defined in the event form."
12.1245 +
12.1246 + args = self.env.get_args()
12.1247 +
12.1248 + args["dtend-control-recur"] = []
12.1249 + args["dttimes-control-recur"] = []
12.1250 + args["recur-origin"] = []
12.1251 +
12.1252 + all_starts = []
12.1253 + all_ends = []
12.1254 +
12.1255 + for index, period in enumerate(recurrences):
12.1256 + if period.end_enabled:
12.1257 + args["dtend-control-recur"].append(str(index))
12.1258 + if period.times_enabled:
12.1259 + args["dttimes-control-recur"].append(str(index))
12.1260 + args["recur-origin"].append(period.origin or "")
12.1261 +
12.1262 + all_starts.append(period.get_form_start())
12.1263 + all_ends.append(period.get_form_end())
12.1264 +
12.1265 + self.set_date_control_values("dtstart-recur", all_starts)
12.1266 + self.set_date_control_values("dtend-recur", all_ends, tzid_name="dtstart-recur")
12.1267 +
12.1268 + def get_removed_periods(self, periods):
12.1269
12.1270 """
12.1271 - Show datetime details from the given 'obj' for the recurrence having the
12.1272 - given 'index', with the recurrence period described by 'period',
12.1273 - indicating a start, end and origin of the period from the event details,
12.1274 - employing any 'recurrenceid' and 'recurrenceids' for the object to
12.1275 - configure the displayed information.
12.1276 + Return those from the recurrence 'periods' to remove upon updating an
12.1277 + event along with those to exclude in a tuple of the form (unscheduled,
12.1278 + excluded).
12.1279 + """
12.1280 +
12.1281 + args = self.env.get_args()
12.1282 + to_unschedule = []
12.1283 + to_exclude = []
12.1284
12.1285 - If 'show_start' is set to a true value, the start details will be shown;
12.1286 - otherwise, the end details will be shown.
12.1287 + for i in args.get("recur-remove", []):
12.1288 + try:
12.1289 + period = periods[int(i)]
12.1290 + except (IndexError, ValueError):
12.1291 + continue
12.1292 +
12.1293 + if not self.can_edit_recurrence(period):
12.1294 + to_unschedule.append(period)
12.1295 + else:
12.1296 + to_exclude.append(period)
12.1297 +
12.1298 + return to_unschedule, to_exclude
12.1299 +
12.1300 + def get_attendees_from_page(self):
12.1301 +
12.1302 + """
12.1303 + Return attendees from the request, normalised for iCalendar purposes.
12.1304 """
12.1305
12.1306 - page = self.page
12.1307 - _id = self.element_identifier
12.1308 - _name = self.element_name
12.1309 + args = self.env.get_args()
12.1310 + attendees = []
12.1311 +
12.1312 + for attendee in args.get("attendee", []):
12.1313 + if attendee.strip():
12.1314 + attendee = get_uri(attendee)
12.1315 + attendees.append(attendee)
12.1316
12.1317 - p = event_period_from_period(period)
12.1318 - replaced = not recurrenceid and p.is_replaced(recurrenceids)
12.1319 + return attendees
12.1320 +
12.1321 + def update_attendees_from_page(self):
12.1322
12.1323 - # Show controls for editing as organiser.
12.1324 + "Add or remove attendees. This does not affect the stored object."
12.1325 +
12.1326 + args = self.env.get_args()
12.1327 +
12.1328 + attendees = self.get_attendees_from_page()
12.1329
12.1330 - if self.is_organiser(obj) and not replaced:
12.1331 - page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
12.1332 + if args.has_key("add"):
12.1333 + attendees.append("")
12.1334
12.1335 - read_only = period.origin == "RRULE"
12.1336 + # Only actually remove attendees if the event is unsent, if the attendee
12.1337 + # is new, or if it is the current user being removed.
12.1338 +
12.1339 + if args.has_key("remove"):
12.1340 + still_to_remove = []
12.1341
12.1342 - if show_start:
12.1343 - page.div(class_="dt enabled")
12.1344 - self._show_date_controls(_name("dtstart", "recur", index), p.get_form_start(), index=index, read_only=read_only)
12.1345 - if not read_only:
12.1346 - page.br()
12.1347 - page.label("Specify times", for_=_id("dttimes-enable", index), class_="time disabled enable")
12.1348 - page.label("Specify dates only", for_=_id("dttimes-enable", index), class_="time enabled disable")
12.1349 - page.div.close()
12.1350 + for i in args["remove"]:
12.1351 + try:
12.1352 + attendee = attendees[int(i)]
12.1353 + except IndexError:
12.1354 + continue
12.1355 +
12.1356 + if self.can_remove_attendee(attendee):
12.1357 + attendees.remove(attendee)
12.1358 + else:
12.1359 + still_to_remove.append(i)
12.1360
12.1361 - # Put the origin somewhere.
12.1362 + args["remove"] = still_to_remove
12.1363 +
12.1364 + args["attendee"] = attendees
12.1365 + return attendees
12.1366 +
12.1367 + def update_recurrences_from_page(self):
12.1368 +
12.1369 + "Add or remove recurrences. This does not affect the stored object."
12.1370
12.1371 - self._control("recur-origin", "hidden", p.origin or "")
12.1372 + args = self.env.get_args()
12.1373 +
12.1374 + recurrences = self.get_recurrences_from_page()
12.1375 +
12.1376 + # NOTE: Addition of recurrences to be supported.
12.1377 +
12.1378 + # Only actually remove recurrences if the event is unsent, or if the
12.1379 + # recurrence is new, but only for explicit recurrences.
12.1380
12.1381 - else:
12.1382 - page.div(class_="dt disabled")
12.1383 - if not read_only:
12.1384 - page.label("Specify end date", for_=_id("dtend-enable", index), class_="enable")
12.1385 - page.div.close()
12.1386 - page.div(class_="dt enabled")
12.1387 - self._show_date_controls(_name("dtend", "recur", index), p.get_form_end(), index=index, show_tzid=False, read_only=read_only)
12.1388 - if not read_only:
12.1389 - page.br()
12.1390 - page.label("End on same day", for_=_id("dtend-enable", index), class_="disable")
12.1391 - page.div.close()
12.1392 + if args.has_key("recur-remove"):
12.1393 + still_to_remove = []
12.1394 +
12.1395 + for i in args["recur-remove"]:
12.1396 + try:
12.1397 + recurrence = recurrences[int(i)]
12.1398 + except IndexError:
12.1399 + continue
12.1400
12.1401 - page.td.close()
12.1402 + if self.can_remove_recurrence(recurrence):
12.1403 + recurrences.remove(recurrence)
12.1404 + else:
12.1405 + still_to_remove.append(i)
12.1406
12.1407 - # Show label as attendee.
12.1408 + args["recur-remove"] = still_to_remove
12.1409
12.1410 - else:
12.1411 - self.show_recurrence_label(p, recurrenceid, recurrenceids, show_start)
12.1412 + self.set_recurrences_in_page(recurrences)
12.1413 + return recurrences
12.1414
12.1415 - def show_recurrence_label(self, period, recurrenceid, recurrenceids, show_start):
12.1416 + # Access to current object information.
12.1417 +
12.1418 + def get_current_main_period(self):
12.1419
12.1420 """
12.1421 - Show datetime details for the given 'period', employing any
12.1422 - 'recurrenceid' and 'recurrenceids' for the object to configure the
12.1423 - displayed information.
12.1424 + Return the currently active main period for the current object depending
12.1425 + on whether editing has begun or whether the object has just been loaded.
12.1426 + """
12.1427
12.1428 - If 'show_start' is set to a true value, the start details will be shown;
12.1429 - otherwise, the end details will be shown.
12.1430 + if self.is_initial_load() or not self.is_organiser():
12.1431 + return self.get_stored_main_period()
12.1432 + else:
12.1433 + return self.get_main_period_from_page()
12.1434 +
12.1435 + def get_current_recurrences(self):
12.1436 +
12.1437 + """
12.1438 + Return recurrences for the current object using the original object
12.1439 + details where no editing is in progress, using form data otherwise.
12.1440 """
12.1441
12.1442 - page = self.page
12.1443 + if self.is_initial_load() or not self.is_organiser():
12.1444 + return self.get_stored_recurrences()
12.1445 + else:
12.1446 + return self.get_recurrences_from_page()
12.1447 +
12.1448 + def update_current_recurrences(self):
12.1449
12.1450 - p = event_period_from_period(period)
12.1451 - replaced = not recurrenceid and p.is_replaced(recurrenceids)
12.1452 + "Return an updated collection of recurrences for the current object."
12.1453 +
12.1454 + if self.is_initial_load() or not self.is_organiser():
12.1455 + return self.get_stored_recurrences()
12.1456 + else:
12.1457 + return self.update_recurrences_from_page()
12.1458 +
12.1459 + def get_current_attendees(self):
12.1460
12.1461 - css = " ".join([
12.1462 - replaced and "replaced" or "",
12.1463 - p.is_affected(recurrenceid) and "affected" or ""
12.1464 - ])
12.1465 + """
12.1466 + Return attendees for the current object depending on whether the object
12.1467 + has been edited or instead provides such information from its stored
12.1468 + form.
12.1469 + """
12.1470
12.1471 - formdate = show_start and p.get_form_start() or p.get_form_end()
12.1472 - dt = formdate.as_datetime()
12.1473 - if dt:
12.1474 - page.td(self.format_datetime(dt, "long"), class_=css)
12.1475 + if self.is_initial_load() or not self.is_organiser():
12.1476 + return self.get_stored_attendees()
12.1477 else:
12.1478 - page.td("(Unrecognised date)")
12.1479 + return self.get_attendees_from_page()
12.1480 +
12.1481 + def update_current_attendees(self):
12.1482 +
12.1483 + "Return an updated collection of attendees for the current object."
12.1484 +
12.1485 + if self.is_initial_load() or not self.is_organiser():
12.1486 + return self.get_stored_attendees()
12.1487 + else:
12.1488 + return self.update_attendees_from_page()
12.1489
12.1490 # Full page output methods.
12.1491
12.1492 @@ -1082,146 +1063,24 @@
12.1493
12.1494 "Show an object request using the given 'path_info' for the current user."
12.1495
12.1496 - uid, recurrenceid, section = self.get_identifiers(path_info)
12.1497 - obj = self.get_stored_object(uid, recurrenceid, section)
12.1498 + uid, recurrenceid = self.get_identifiers(path_info)
12.1499 + obj = self.get_stored_object(uid, recurrenceid)
12.1500 + self.set_object(obj)
12.1501
12.1502 if not obj:
12.1503 return False
12.1504
12.1505 - errors = self.handle_request(obj)
12.1506 + errors = self.handle_request()
12.1507
12.1508 if not errors:
12.1509 return True
12.1510
12.1511 + self.update_current_attendees()
12.1512 + self.update_current_recurrences()
12.1513 +
12.1514 self.new_page(title="Event")
12.1515 - self.show_object_on_page(obj, errors)
12.1516 + self.show_object_on_page(errors)
12.1517
12.1518 return True
12.1519
12.1520 - # Utility methods.
12.1521 -
12.1522 - def _control(self, name, type, value, selected=False, **kw):
12.1523 -
12.1524 - """
12.1525 - Show a control with the given 'name', 'type' and 'value', with
12.1526 - 'selected' indicating whether it should be selected (checked or
12.1527 - equivalent), and with keyword arguments setting other properties.
12.1528 - """
12.1529 -
12.1530 - page = self.page
12.1531 - if selected:
12.1532 - page.input(name=name, type=type, value=value, checked=selected, **kw)
12.1533 - else:
12.1534 - page.input(name=name, type=type, value=value, **kw)
12.1535 -
12.1536 - def _show_menu(self, name, default, items, class_="", index=None):
12.1537 -
12.1538 - """
12.1539 - Show a select menu having the given 'name', set to the given 'default',
12.1540 - providing the given (value, label) 'items', and employing the given CSS
12.1541 - 'class_' if specified.
12.1542 - """
12.1543 -
12.1544 - page = self.page
12.1545 - values = self.env.get_args().get(name, [default])
12.1546 - if index is not None:
12.1547 - values = values[index:]
12.1548 - values = values and values[0:1] or [default]
12.1549 -
12.1550 - page.select(name=name, class_=class_)
12.1551 - for v, label in items:
12.1552 - if v is None:
12.1553 - continue
12.1554 - if v in values:
12.1555 - page.option(label, value=v, selected="selected")
12.1556 - else:
12.1557 - page.option(label, value=v)
12.1558 - page.select.close()
12.1559 -
12.1560 - def _show_date_controls(self, name, default, index=None, show_tzid=True, read_only=False):
12.1561 -
12.1562 - """
12.1563 - Show date controls for a field with the given 'name' and 'default' form
12.1564 - date value.
12.1565 -
12.1566 - If 'index' is specified, default field values will be overridden by the
12.1567 - element from a collection of existing form values with the specified
12.1568 - index; otherwise, field values will be overridden by a single form
12.1569 - value.
12.1570 -
12.1571 - If 'show_tzid' is set to a false value, the time zone menu will not be
12.1572 - provided.
12.1573 -
12.1574 - If 'read_only' is set to a true value, the controls will be hidden and
12.1575 - labels will be employed instead.
12.1576 - """
12.1577 -
12.1578 - page = self.page
12.1579 -
12.1580 - # Show dates for up to one week around the current date.
12.1581 -
12.1582 - dt = default.as_datetime()
12.1583 - if not dt:
12.1584 - dt = date.today()
12.1585 -
12.1586 - base = to_date(dt)
12.1587 -
12.1588 - # Show a date label with a hidden field if read-only.
12.1589 -
12.1590 - if read_only:
12.1591 - self._control("%s-date" % name, "hidden", format_datetime(base))
12.1592 - page.span(self.format_date(base, "long"))
12.1593 -
12.1594 - # Show dates for up to one week around the current date.
12.1595 - # NOTE: Support paging to other dates.
12.1596 -
12.1597 - else:
12.1598 - items = []
12.1599 - for i in range(-7, 8):
12.1600 - d = base + timedelta(i)
12.1601 - items.append((format_datetime(d), self.format_date(d, "full")))
12.1602 - self._show_menu("%s-date" % name, format_datetime(base), items, index=index)
12.1603 -
12.1604 - # Show time details.
12.1605 -
12.1606 - page.span(class_="time enabled")
12.1607 -
12.1608 - if read_only:
12.1609 - page.span("%s:%s:%s" % (default.get_hour(), default.get_minute(), default.get_second()))
12.1610 - self._control("%s-hour" % name, "hidden", default.get_hour())
12.1611 - self._control("%s-minute" % name, "hidden", default.get_minute())
12.1612 - self._control("%s-second" % name, "hidden", default.get_second())
12.1613 - else:
12.1614 - self._control("%s-hour" % name, "text", default.get_hour(), maxlength=2, size=2)
12.1615 - page.add(":")
12.1616 - self._control("%s-minute" % name, "text", default.get_minute(), maxlength=2, size=2)
12.1617 - page.add(":")
12.1618 - self._control("%s-second" % name, "text", default.get_second(), maxlength=2, size=2)
12.1619 -
12.1620 - # Show time zone details.
12.1621 -
12.1622 - if show_tzid:
12.1623 - page.add(" ")
12.1624 - tzid = default.get_tzid() or self.get_tzid()
12.1625 -
12.1626 - # Show a label if read-only or a menu otherwise.
12.1627 -
12.1628 - if read_only:
12.1629 - self._control("%s-tzid" % name, "hidden", tzid)
12.1630 - page.span(tzid)
12.1631 - else:
12.1632 - self._show_timezone_menu("%s-tzid" % name, tzid, index)
12.1633 -
12.1634 - page.span.close()
12.1635 -
12.1636 - def _show_timezone_menu(self, name, default, index=None):
12.1637 -
12.1638 - """
12.1639 - Show timezone controls using a menu with the given 'name', set to the
12.1640 - given 'default' unless a field of the given 'name' provides a value.
12.1641 - """
12.1642 -
12.1643 - entries = [(tzid, tzid) for tzid in pytz.all_timezones]
12.1644 - self._show_menu(name, default, entries, index=index)
12.1645 -
12.1646 # vim: tabstop=4 expandtab shiftwidth=4
13.1 --- a/imipweb/resource.py Thu Sep 24 19:13:39 2015 +0200
13.2 +++ b/imipweb/resource.py Tue Sep 29 00:25:31 2015 +0200
13.3 @@ -19,27 +19,34 @@
13.4 this program. If not, see <http://www.gnu.org/licenses/>.
13.5 """
13.6
13.7 -from datetime import datetime
13.8 -from imiptools.client import Client
13.9 +from datetime import datetime, timedelta
13.10 +from imiptools.client import Client, ClientForObject
13.11 from imiptools.data import get_uri, uri_values
13.12 -from imiptools.dates import get_recurrence_start_point
13.13 +from imiptools.dates import format_datetime, get_recurrence_start_point, to_date
13.14 from imiptools.period import remove_period, remove_affected_period
13.15 +from imipweb.data import event_period_from_period, form_period_from_period, FormDate
13.16 from imipweb.env import CGIEnvironment
13.17 +from urllib import urlencode
13.18 import babel.dates
13.19 import imip_store
13.20 import markup
13.21 +import pytz
13.22
13.23 -class Resource(Client):
13.24 +class Resource:
13.25
13.26 - "A Web application resource and calendar client."
13.27 + "A Web application resource."
13.28
13.29 def __init__(self, resource=None):
13.30 +
13.31 + """
13.32 + Initialise a resource, allowing it to share the environment of any given
13.33 + existing 'resource'.
13.34 + """
13.35 +
13.36 self.encoding = "utf-8"
13.37 self.env = CGIEnvironment(self.encoding)
13.38
13.39 - user = self.env.get_user()
13.40 - Client.__init__(self, user and get_uri(user) or None)
13.41 -
13.42 + self.objects = {}
13.43 self.locale = None
13.44 self.requests = None
13.45
13.46 @@ -47,14 +54,6 @@
13.47 self.page = resource and resource.page or markup.page()
13.48 self.html_ids = None
13.49
13.50 - self.store = imip_store.FileStore()
13.51 - self.objects = {}
13.52 -
13.53 - try:
13.54 - self.publisher = imip_store.FilePublisher()
13.55 - except OSError:
13.56 - self.publisher = None
13.57 -
13.58 # Presentation methods.
13.59
13.60 def new_page(self, title):
13.61 @@ -83,30 +82,19 @@
13.62 self.new_page(title="Redirect")
13.63 self.page.p("Redirecting to: %s" % url)
13.64
13.65 - def link_to(self, uid, recurrenceid=None, section=None):
13.66 + def link_to(self, uid, recurrenceid=None, args=None):
13.67
13.68 """
13.69 - Return a link to an object with the given 'uid', 'recurrenceid' and
13.70 - 'section'. See get_identifiers for the decoding of such links.
13.71 + Return a link to an object with the given 'uid' and 'recurrenceid'.
13.72 + See get_identifiers for the decoding of such links.
13.73 +
13.74 + If 'args' is specified, the given dictionary is encoded and included.
13.75 """
13.76
13.77 path = [uid]
13.78 if recurrenceid:
13.79 path.append(recurrenceid)
13.80 - if section:
13.81 - path.append(section)
13.82 - return self.env.new_url("/".join(path))
13.83 -
13.84 - # Control naming helpers.
13.85 -
13.86 - def element_identifier(self, name, index=None):
13.87 - return index is not None and "%s-%d" % (name, index) or name
13.88 -
13.89 - def element_name(self, name, suffix, index=None):
13.90 - return index is not None and "%s-%s" % (name, suffix) or name
13.91 -
13.92 - def element_enable(self, index=None):
13.93 - return index is not None and str(index) or "enable"
13.94 + return "%s%s" % (self.env.new_url("/".join(path)), args and ("?%s" % urlencode(args)) or "")
13.95
13.96 # Access to objects.
13.97
13.98 @@ -122,26 +110,18 @@
13.99 # UID only.
13.100
13.101 if len(parts) == 1:
13.102 - return parts[0], None, None
13.103 -
13.104 - # UID and RECURRENCE-ID or UID and section.
13.105 + return parts[0], None
13.106
13.107 - elif len(parts) == 2:
13.108 - if parts[1] == "counter":
13.109 - return parts[0], None, "counters"
13.110 - else:
13.111 - return parts[0], parts[1], parts[2] == "counter" and "counters" or None
13.112 -
13.113 - # UID, RECURRENCE-ID and section.
13.114 + # UID and RECURRENCE-ID.
13.115
13.116 else:
13.117 - return parts[:3]
13.118 + return parts[:2]
13.119
13.120 - def _get_object(self, uid, recurrenceid=None, section=None):
13.121 - if self.objects.has_key((uid, recurrenceid, section)):
13.122 - return self.objects[(uid, recurrenceid, section)]
13.123 + def _get_object(self, uid, recurrenceid=None, section=None, username=None):
13.124 + if self.objects.has_key((uid, recurrenceid, section, username)):
13.125 + return self.objects[(uid, recurrenceid, section, username)]
13.126
13.127 - obj = self.objects[(uid, recurrenceid, section)] = self.get_stored_object(uid, recurrenceid, section)
13.128 + obj = self.objects[(uid, recurrenceid, section, username)] = self.get_stored_object(uid, recurrenceid, section, username)
13.129 return obj
13.130
13.131 def _get_recurrences(self, uid):
13.132 @@ -158,6 +138,9 @@
13.133 def _have_request(self, uid, recurrenceid=None, type=None, strict=False):
13.134 return self.store.have_request(self._get_requests(), uid, recurrenceid, type, strict)
13.135
13.136 + def _get_counters(self, uid, recurrenceid=None):
13.137 + return self.store.get_counters(self.user, uid, recurrenceid)
13.138 +
13.139 def _get_request_summary(self):
13.140
13.141 "Return a list of periods comprising the request summary."
13.142 @@ -165,15 +148,27 @@
13.143 summary = []
13.144
13.145 for uid, recurrenceid, request_type in self._get_requests():
13.146 - obj = self.get_stored_object(uid, recurrenceid)
13.147 - if obj:
13.148 - recurrenceids = self._get_active_recurrences(uid)
13.149 +
13.150 + # Obtain either normal objects or counter-proposals.
13.151 +
13.152 + if not request_type:
13.153 + objs = [self._get_object(uid, recurrenceid)]
13.154 + elif request_type == "COUNTER":
13.155 + objs = []
13.156 + for attendee in self.store.get_counters(self.user, uid, recurrenceid):
13.157 + objs.append(self._get_object(uid, recurrenceid, "counters", attendee))
13.158
13.159 - # Obtain only active periods, not those replaced by redefined
13.160 - # recurrences, converting to free/busy periods.
13.161 + # For each object, obtain the periods involved.
13.162 +
13.163 + for obj in objs:
13.164 + if obj:
13.165 + recurrenceids = self._get_active_recurrences(uid)
13.166
13.167 - for p in obj.get_active_periods(recurrenceids, self.get_tzid(), self.get_window_end()):
13.168 - summary.append(obj.get_freebusy_period(p))
13.169 + # Obtain only active periods, not those replaced by redefined
13.170 + # recurrences, converting to free/busy periods.
13.171 +
13.172 + for p in obj.get_active_periods(recurrenceids, self.get_tzid(), self.get_window_end()):
13.173 + summary.append(obj.get_freebusy_period(p))
13.174
13.175 return summary
13.176
13.177 @@ -208,55 +203,403 @@
13.178 def remove_event(self, uid, recurrenceid=None):
13.179 return self.store.remove_event(self.user, uid, recurrenceid)
13.180
13.181 - def update_freebusy(self, uid, recurrenceid, obj):
13.182 +class ResourceClient(Resource, Client):
13.183 +
13.184 + "A Web application resource and calendar client."
13.185 +
13.186 + def __init__(self, resource=None):
13.187 + Resource.__init__(self, resource)
13.188 + user = self.env.get_user()
13.189 + Client.__init__(self, user and get_uri(user) or None)
13.190 +
13.191 +class ResourceClientForObject(Resource, ClientForObject):
13.192 +
13.193 + "A Web application resource and calendar client for a specific object."
13.194 +
13.195 + def __init__(self, resource=None):
13.196 + Resource.__init__(self, resource)
13.197 + user = self.env.get_user()
13.198 + ClientForObject.__init__(self, None, user and get_uri(user) or None)
13.199 +
13.200 +class FormUtilities:
13.201 +
13.202 + "Utility methods resource mix-in."
13.203 +
13.204 + def control(self, name, type, value, selected=False, **kw):
13.205 +
13.206 + """
13.207 + Show a control with the given 'name', 'type' and 'value', with
13.208 + 'selected' indicating whether it should be selected (checked or
13.209 + equivalent), and with keyword arguments setting other properties.
13.210 + """
13.211 +
13.212 + page = self.page
13.213 + if type in ("checkbox", "radio") and selected:
13.214 + page.input(name=name, type=type, value=value, checked=selected, **kw)
13.215 + else:
13.216 + page.input(name=name, type=type, value=value, **kw)
13.217 +
13.218 + def menu(self, name, default, items, class_="", index=None):
13.219 +
13.220 + """
13.221 + Show a select menu having the given 'name', set to the given 'default',
13.222 + providing the given (value, label) 'items', and employing the given CSS
13.223 + 'class_' if specified.
13.224 + """
13.225 +
13.226 + page = self.page
13.227 + values = self.env.get_args().get(name, [default])
13.228 + if index is not None:
13.229 + values = values[index:]
13.230 + values = values and values[0:1] or [default]
13.231 +
13.232 + page.select(name=name, class_=class_)
13.233 + for v, label in items:
13.234 + if v is None:
13.235 + continue
13.236 + if v in values:
13.237 + page.option(label, value=v, selected="selected")
13.238 + else:
13.239 + page.option(label, value=v)
13.240 + page.select.close()
13.241 +
13.242 + def date_controls(self, name, default, index=None, show_tzid=True, read_only=False):
13.243
13.244 """
13.245 - Update stored free/busy details for the event with the given 'uid' and
13.246 - 'recurrenceid' having a representation of 'obj'.
13.247 + Show date controls for a field with the given 'name' and 'default' form
13.248 + date value.
13.249 +
13.250 + If 'index' is specified, default field values will be overridden by the
13.251 + element from a collection of existing form values with the specified
13.252 + index; otherwise, field values will be overridden by a single form
13.253 + value.
13.254 +
13.255 + If 'show_tzid' is set to a false value, the time zone menu will not be
13.256 + provided.
13.257 +
13.258 + If 'read_only' is set to a true value, the controls will be hidden and
13.259 + labels will be employed instead.
13.260 + """
13.261 +
13.262 + page = self.page
13.263 +
13.264 + # Show dates for up to one week around the current date.
13.265 +
13.266 + dt = default.as_datetime()
13.267 + if not dt:
13.268 + dt = date.today()
13.269 +
13.270 + base = to_date(dt)
13.271 +
13.272 + # Show a date label with a hidden field if read-only.
13.273 +
13.274 + if read_only:
13.275 + self.control("%s-date" % name, "hidden", format_datetime(base))
13.276 + page.span(self.format_date(base, "long"))
13.277 +
13.278 + # Show dates for up to one week around the current date.
13.279 + # NOTE: Support paging to other dates.
13.280 +
13.281 + else:
13.282 + items = []
13.283 + for i in range(-7, 8):
13.284 + d = base + timedelta(i)
13.285 + items.append((format_datetime(d), self.format_date(d, "full")))
13.286 + self.menu("%s-date" % name, format_datetime(base), items, index=index)
13.287 +
13.288 + # Show time details.
13.289 +
13.290 + page.span(class_="time enabled")
13.291 +
13.292 + if read_only:
13.293 + page.span("%s:%s:%s" % (default.get_hour(), default.get_minute(), default.get_second()))
13.294 + self.control("%s-hour" % name, "hidden", default.get_hour())
13.295 + self.control("%s-minute" % name, "hidden", default.get_minute())
13.296 + self.control("%s-second" % name, "hidden", default.get_second())
13.297 + else:
13.298 + self.control("%s-hour" % name, "text", default.get_hour(), maxlength=2, size=2)
13.299 + page.add(":")
13.300 + self.control("%s-minute" % name, "text", default.get_minute(), maxlength=2, size=2)
13.301 + page.add(":")
13.302 + self.control("%s-second" % name, "text", default.get_second(), maxlength=2, size=2)
13.303 +
13.304 + # Show time zone details.
13.305 +
13.306 + if show_tzid:
13.307 + page.add(" ")
13.308 + tzid = default.get_tzid() or self.get_tzid()
13.309 +
13.310 + # Show a label if read-only or a menu otherwise.
13.311 +
13.312 + if read_only:
13.313 + self.control("%s-tzid" % name, "hidden", tzid)
13.314 + page.span(tzid)
13.315 + else:
13.316 + self.timezone_menu("%s-tzid" % name, tzid, index)
13.317 +
13.318 + page.span.close()
13.319 +
13.320 + def timezone_menu(self, name, default, index=None):
13.321 +
13.322 + """
13.323 + Show timezone controls using a menu with the given 'name', set to the
13.324 + given 'default' unless a field of the given 'name' provides a value.
13.325 + """
13.326 +
13.327 + entries = [(tzid, tzid) for tzid in pytz.all_timezones]
13.328 + self.menu(name, default, entries, index=index)
13.329 +
13.330 +class DateTimeFormUtilities:
13.331 +
13.332 + "Date/time control methods resource mix-in."
13.333 +
13.334 + # Control naming helpers.
13.335 +
13.336 + def element_identifier(self, name, index=None):
13.337 + return index is not None and "%s-%d" % (name, index) or name
13.338 +
13.339 + def element_name(self, name, suffix, index=None):
13.340 + return index is not None and "%s-%s" % (name, suffix) or name
13.341 +
13.342 + def element_enable(self, index=None):
13.343 + return index is not None and str(index) or "enable"
13.344 +
13.345 + def show_object_datetime_controls(self, period, index=None):
13.346 +
13.347 + """
13.348 + Show datetime-related controls if already active or if an object needs
13.349 + them for the given 'period'. The given 'index' is used to parameterise
13.350 + individual controls for dynamic manipulation.
13.351 """
13.352
13.353 - is_only_organiser = self.user not in uri_values(obj.get_values("ATTENDEE"))
13.354 + p = form_period_from_period(period)
13.355 +
13.356 + page = self.page
13.357 + args = self.env.get_args()
13.358 + _id = self.element_identifier
13.359 + _name = self.element_name
13.360 + _enable = self.element_enable
13.361
13.362 - freebusy = self.store.get_freebusy(self.user)
13.363 + # Add a dynamic stylesheet to permit the controls to modify the display.
13.364 + # NOTE: The style details need to be coordinated with the static
13.365 + # NOTE: stylesheet.
13.366 +
13.367 + if index is not None:
13.368 + page.style(type="text/css")
13.369 +
13.370 + # Unlike the rules for object properties, these affect recurrence
13.371 + # properties.
13.372
13.373 - Client.update_freebusy(self, freebusy, self.get_periods(obj),
13.374 - is_only_organiser and "ORG" or obj.get_value("TRANSP"),
13.375 - uid, recurrenceid,
13.376 - obj.get_value("SUMMARY"),
13.377 - obj.get_value("ORGANIZER"))
13.378 + page.add("""\
13.379 +input#dttimes-enable-%(index)d,
13.380 +input#dtend-enable-%(index)d,
13.381 +input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled,
13.382 +input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled,
13.383 +input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled,
13.384 +input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled {
13.385 + display: none;
13.386 +}""" % {"index" : index})
13.387 +
13.388 + page.style.close()
13.389 +
13.390 + self.control(
13.391 + _name("dtend-control", "recur", index), "checkbox",
13.392 + _enable(index), p.end_enabled,
13.393 + id=_id("dtend-enable", index)
13.394 + )
13.395 +
13.396 + self.control(
13.397 + _name("dttimes-control", "recur", index), "checkbox",
13.398 + _enable(index), p.times_enabled,
13.399 + id=_id("dttimes-enable", index)
13.400 + )
13.401 +
13.402 + def show_datetime_controls(self, formdate, show_start):
13.403 +
13.404 + """
13.405 + Show datetime details from the current object for the 'formdate',
13.406 + showing start details if 'show_start' is set to a true value. Details
13.407 + will appear as controls for organisers and labels for attendees.
13.408 + """
13.409 +
13.410 + page = self.page
13.411 +
13.412 + # Show controls for editing as organiser.
13.413
13.414 - # Subtract any recurrences from the free/busy details of a parent
13.415 - # object.
13.416 + if self.is_organiser():
13.417 + page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
13.418 +
13.419 + if show_start:
13.420 + page.div(class_="dt enabled")
13.421 + self.date_controls("dtstart", formdate)
13.422 + page.br()
13.423 + page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")
13.424 + page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable")
13.425 + page.div.close()
13.426
13.427 - for recurrenceid in self._get_recurrences(uid):
13.428 - remove_affected_period(freebusy, uid, obj.get_recurrence_start_point(recurrenceid, self.get_tzid()))
13.429 + else:
13.430 + page.div(class_="dt disabled")
13.431 + page.label("Specify end date", for_="dtend-enable", class_="enable")
13.432 + page.div.close()
13.433 + page.div(class_="dt enabled")
13.434 + self.date_controls("dtend", formdate)
13.435 + page.br()
13.436 + page.label("End on same day", for_="dtend-enable", class_="disable")
13.437 + page.div.close()
13.438 +
13.439 + page.td.close()
13.440 +
13.441 + # Show a label as attendee.
13.442
13.443 - self.store.set_freebusy(self.user, freebusy)
13.444 - self.publish_freebusy(freebusy)
13.445 + else:
13.446 + dt = formdate.as_datetime()
13.447 + if dt:
13.448 + page.td(self.format_datetime(dt, "full"))
13.449 + else:
13.450 + page.td("(Unrecognised date)")
13.451 +
13.452 + def show_recurrence_controls(self, index, period, recurrenceid, recurrenceids, show_start):
13.453 +
13.454 + """
13.455 + Show datetime details from the current object for the recurrence having
13.456 + the given 'index', with the recurrence period described by 'period',
13.457 + indicating a start, end and origin of the period from the event details,
13.458 + employing any 'recurrenceid' and 'recurrenceids' for the object to
13.459 + configure the displayed information.
13.460
13.461 - # Update free/busy provider information if the event may recur
13.462 - # indefinitely.
13.463 + If 'show_start' is set to a true value, the start details will be shown;
13.464 + otherwise, the end details will be shown.
13.465 + """
13.466 +
13.467 + page = self.page
13.468 + _id = self.element_identifier
13.469 + _name = self.element_name
13.470 +
13.471 + p = event_period_from_period(period)
13.472 + replaced = not recurrenceid and p.is_replaced(recurrenceids)
13.473 +
13.474 + # Show controls for editing as organiser.
13.475 +
13.476 + if self.is_organiser() and not replaced:
13.477 + page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
13.478 +
13.479 + read_only = period.origin == "RRULE"
13.480
13.481 - if obj.possibly_recurring_indefinitely():
13.482 - self.store.append_freebusy_provider(self.user, obj)
13.483 + if show_start:
13.484 + page.div(class_="dt enabled")
13.485 + self.date_controls(_name("dtstart", "recur", index), p.get_form_start(), index=index, read_only=read_only)
13.486 + if not read_only:
13.487 + page.br()
13.488 + page.label("Specify times", for_=_id("dttimes-enable", index), class_="time disabled enable")
13.489 + page.label("Specify dates only", for_=_id("dttimes-enable", index), class_="time enabled disable")
13.490 + page.div.close()
13.491 +
13.492 + # Put the origin somewhere.
13.493 +
13.494 + self.control("recur-origin", "hidden", p.origin or "")
13.495 +
13.496 + else:
13.497 + page.div(class_="dt disabled")
13.498 + if not read_only:
13.499 + page.label("Specify end date", for_=_id("dtend-enable", index), class_="enable")
13.500 + page.div.close()
13.501 + page.div(class_="dt enabled")
13.502 + self.date_controls(_name("dtend", "recur", index), p.get_form_end(), index=index, show_tzid=False, read_only=read_only)
13.503 + if not read_only:
13.504 + page.br()
13.505 + page.label("End on same day", for_=_id("dtend-enable", index), class_="disable")
13.506 + page.div.close()
13.507
13.508 - def remove_from_freebusy(self, uid, recurrenceid=None):
13.509 - freebusy = self.store.get_freebusy(self.user)
13.510 - remove_period(freebusy, uid, recurrenceid)
13.511 - self.store.set_freebusy(self.user, freebusy)
13.512 - self.publish_freebusy(freebusy)
13.513 + page.td.close()
13.514 +
13.515 + # Show label as attendee.
13.516 +
13.517 + else:
13.518 + self.show_recurrence_label(p, recurrenceid, recurrenceids, show_start)
13.519 +
13.520 + def show_recurrence_label(self, period, recurrenceid, recurrenceids, show_start):
13.521 +
13.522 + """
13.523 + Show datetime details for the given 'period', employing any
13.524 + 'recurrenceid' and 'recurrenceids' for the object to configure the
13.525 + displayed information.
13.526 +
13.527 + If 'show_start' is set to a true value, the start details will be shown;
13.528 + otherwise, the end details will be shown.
13.529 + """
13.530 +
13.531 + page = self.page
13.532 +
13.533 + p = event_period_from_period(period)
13.534 + replaced = not recurrenceid and p.is_replaced(recurrenceids)
13.535 +
13.536 + css = " ".join([
13.537 + replaced and "replaced" or "",
13.538 + p.is_affected(recurrenceid) and "affected" or ""
13.539 + ])
13.540
13.541 - # Update free/busy provider information if the event may recur
13.542 - # indefinitely.
13.543 + formdate = show_start and p.get_form_start() or p.get_form_end()
13.544 + dt = formdate.as_datetime()
13.545 + if dt:
13.546 + page.td(self.format_datetime(dt, "long"), class_=css)
13.547 + else:
13.548 + page.td("(Unrecognised date)")
13.549 +
13.550 + def get_date_control_values(self, name, multiple=False, tzid_name=None):
13.551 +
13.552 + """
13.553 + Return a form date object representing fields starting with 'name'. If
13.554 + 'multiple' is set to a true value, many date objects will be returned
13.555 + corresponding to a collection of datetimes.
13.556
13.557 - if obj.possibly_recurring_indefinitely():
13.558 - self.store.remove_freebusy_provider(self.user, obj)
13.559 + If 'tzid_name' is specified, the time zone information will be acquired
13.560 + from fields starting with 'tzid_name' instead of 'name'.
13.561 + """
13.562 +
13.563 + args = self.env.get_args()
13.564 +
13.565 + dates = args.get("%s-date" % name, [])
13.566 + hours = args.get("%s-hour" % name, [])
13.567 + minutes = args.get("%s-minute" % name, [])
13.568 + seconds = args.get("%s-second" % name, [])
13.569 + tzids = args.get("%s-tzid" % (tzid_name or name), [])
13.570 +
13.571 + # Handle absent values by employing None values.
13.572 +
13.573 + field_values = map(None, dates, hours, minutes, seconds, tzids)
13.574
13.575 - def publish_freebusy(self, freebusy):
13.576 + if not field_values and not multiple:
13.577 + all_values = FormDate()
13.578 + else:
13.579 + all_values = []
13.580 + for date, hour, minute, second, tzid in field_values:
13.581 + value = FormDate(date, hour, minute, second, tzid or self.get_tzid())
13.582 +
13.583 + # Return a single value or append to a collection of all values.
13.584 +
13.585 + if not multiple:
13.586 + return value
13.587 + else:
13.588 + all_values.append(value)
13.589 +
13.590 + return all_values
13.591
13.592 - "Publish the details if configured to share them."
13.593 + def set_date_control_values(self, name, formdates, tzid_name=None):
13.594 +
13.595 + """
13.596 + Replace form fields starting with 'name' using the values of the given
13.597 + 'formdates'.
13.598
13.599 - if self.publisher and self.is_sharing() and self.is_publishing():
13.600 - self.publisher.set_freebusy(self.user, freebusy)
13.601 + If 'tzid_name' is specified, the time zone information will be stored in
13.602 + fields starting with 'tzid_name' instead of 'name'.
13.603 + """
13.604 +
13.605 + args = self.env.get_args()
13.606 +
13.607 + args["%s-date" % name] = [d.date for d in formdates]
13.608 + args["%s-hour" % name] = [d.hour for d in formdates]
13.609 + args["%s-minute" % name] = [d.minute for d in formdates]
13.610 + args["%s-second" % name] = [d.second for d in formdates]
13.611 + args["%s-tzid" % (tzid_name or name)] = [d.tzid for d in formdates]
13.612
13.613 # vim: tabstop=4 expandtab shiftwidth=4
14.1 --- a/tests/test_resource_invitation_constraints.sh Thu Sep 24 19:13:39 2015 +0200
14.2 +++ b/tests/test_resource_invitation_constraints.sh Tue Sep 29 00:25:31 2015 +0200
14.3 @@ -35,7 +35,7 @@
14.4 echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
14.5 echo 'share' > "$PREFS/$USER/freebusy_sharing"
14.6 echo '10,12,14,16,18:0,15,30,45' > "$PREFS/$USER/permitted_times"
14.7 -echo '60' > "$PREFS/$USER/freebusy_offers"
14.8 +echo 'PT60S' > "$PREFS/$USER/freebusy_offers"
14.9
14.10 "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \
14.11 | "$SHOWMAIL" \
15.1 --- a/tests/test_resource_invitation_constraints_alternative.sh Thu Sep 24 19:13:39 2015 +0200
15.2 +++ b/tests/test_resource_invitation_constraints_alternative.sh Tue Sep 29 00:25:31 2015 +0200
15.3 @@ -35,7 +35,7 @@
15.4 echo 'Europe/Oslo' > "$PREFS/$USER/TZID"
15.5 echo 'share' > "$PREFS/$USER/freebusy_sharing"
15.6 echo '10,12,14,16,18:0,15,30,45' > "$PREFS/$USER/permitted_times"
15.7 -echo '60' > "$PREFS/$USER/freebusy_offers"
15.8 +echo 'PT60S' > "$PREFS/$USER/freebusy_offers"
15.9
15.10 "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/fb-request-sauna-all.txt" 2>> $ERROR \
15.11 | "$SHOWMAIL" \
16.1 --- a/vContent.py Thu Sep 24 19:13:39 2015 +0200
16.2 +++ b/vContent.py Tue Sep 29 00:25:31 2015 +0200
16.3 @@ -62,6 +62,12 @@
16.4
16.5 pass
16.6
16.7 +class WriteError(Exception):
16.8 +
16.9 + "General writing errors."
16.10 +
16.11 + pass
16.12 +
16.13 # Reader and parser classes.
16.14
16.15 class Reader:
16.16 @@ -570,12 +576,15 @@
16.17 encoding = parameters.get("ENCODING")
16.18 charset = parameters.get("CHARSET")
16.19
16.20 - if encoding == "QUOTED-PRINTABLE":
16.21 - value = quopri.encodestring(value.encode(charset or "iso-8859-1"))
16.22 - elif encoding == "BASE64":
16.23 - value = base64.encodestring(value)
16.24 + try:
16.25 + if encoding == "QUOTED-PRINTABLE":
16.26 + value = quopri.encodestring(value.encode(charset or "iso-8859-1"))
16.27 + elif encoding == "BASE64":
16.28 + value = base64.encodestring(value)
16.29
16.30 - return self.encode_content(value)
16.31 + return self.encode_content(value)
16.32 + except TypeError:
16.33 + raise WriteError, "Property %r value with parameters %r cannot be encoded: %r" % (name, parameters, value)
16.34
16.35 # Overrideable methods.
16.36