1.1 --- a/imip_store.py Sun Sep 06 01:06:28 2015 +0200
1.2 +++ b/imip_store.py Sun Sep 06 01:15:30 2015 +0200
1.3 @@ -201,17 +201,21 @@
1.4
1.5 return all_events
1.6
1.7 - def get_active_events(self, user):
1.8 + def get_event_filename(self, user, uid, recurrenceid=None, dirname=None):
1.9
1.10 - "Return a set of uncancelled events of the form (uid, recurrenceid)."
1.11 -
1.12 - all_events = self.get_all_events(user)
1.13 + """
1.14 + Get the filename providing the event for the given 'user' with the given
1.15 + 'uid'. If the optional 'recurrenceid' is specified, a specific instance
1.16 + or occurrence of an event is returned.
1.17
1.18 - # Filter out cancelled events.
1.19 + Where 'dirname' is specified, the given directory name is used as the
1.20 + base of the location within which any filename will reside.
1.21 + """
1.22
1.23 - cancelled = self.get_cancellations(user) or []
1.24 - all_events.difference_update(cancelled)
1.25 - return all_events
1.26 + if recurrenceid:
1.27 + return self.get_recurrence_filename(user, uid, recurrenceid, dirname)
1.28 + else:
1.29 + return self.get_complete_event_filename(user, uid, dirname)
1.30
1.31 def get_event(self, user, uid, recurrenceid=None):
1.32
1.33 @@ -221,20 +225,33 @@
1.34 occurrence of an event is returned.
1.35 """
1.36
1.37 - if recurrenceid:
1.38 - return self.get_recurrence(user, uid, recurrenceid)
1.39 - else:
1.40 - return self.get_complete_event(user, uid)
1.41 + filename = self.get_event_filename(user, uid, recurrenceid)
1.42 + if not filename or not exists(filename):
1.43 + return None
1.44 +
1.45 + return filename and self._get_object(user, filename)
1.46 +
1.47 + def get_complete_event_filename(self, user, uid, dirname=None):
1.48 +
1.49 + """
1.50 + Get the filename providing the event for the given 'user' with the given
1.51 + 'uid'.
1.52 +
1.53 + Where 'dirname' is specified, the given directory name is used as the
1.54 + base of the location within which any filename will reside.
1.55 + """
1.56 +
1.57 + return self.get_object_in_store(user, dirname, "objects", uid)
1.58
1.59 def get_complete_event(self, user, uid):
1.60
1.61 "Get the event for the given 'user' with the given 'uid'."
1.62
1.63 - filename = self.get_object_in_store(user, "objects", uid)
1.64 + filename = self.get_complete_event_filename(user, uid)
1.65 if not filename or not exists(filename):
1.66 return None
1.67
1.68 - return self._get_object(user, filename)
1.69 + return filename and self._get_object(user, filename)
1.70
1.71 def set_event(self, user, uid, recurrenceid, node):
1.72
1.73 @@ -290,7 +307,16 @@
1.74
1.75 """
1.76 Get additional event instances for an event of the given 'user' with the
1.77 - indicated 'uid'.
1.78 + indicated 'uid'. Both active and cancelled recurrences are returned.
1.79 + """
1.80 +
1.81 + return self.get_active_recurrences(user, uid) + self.get_cancelled_recurrences(user, uid)
1.82 +
1.83 + def get_active_recurrences(self, user, uid):
1.84 +
1.85 + """
1.86 + Get additional event instances for an event of the given 'user' with the
1.87 + indicated 'uid'. Cancelled recurrences are not returned.
1.88 """
1.89
1.90 filename = self.get_object_in_store(user, "recurrences", uid)
1.91 @@ -299,6 +325,31 @@
1.92
1.93 return [name for name in listdir(filename) if isfile(join(filename, name))]
1.94
1.95 + def get_cancelled_recurrences(self, user, uid):
1.96 +
1.97 + """
1.98 + Get additional event instances for an event of the given 'user' with the
1.99 + indicated 'uid'. Only cancelled recurrences are returned.
1.100 + """
1.101 +
1.102 + filename = self.get_object_in_store(user, "cancelled", "recurrences", uid)
1.103 + if not filename or not exists(filename):
1.104 + return []
1.105 +
1.106 + return [name for name in listdir(filename) if isfile(join(filename, name))]
1.107 +
1.108 + def get_recurrence_filename(self, user, uid, recurrenceid, dirname=None):
1.109 +
1.110 + """
1.111 + For the event of the given 'user' with the given 'uid', return the
1.112 + filename providing the recurrence with the given 'recurrenceid'.
1.113 +
1.114 + Where 'dirname' is specified, the given directory name is used as the
1.115 + base of the location within which any filename will reside.
1.116 + """
1.117 +
1.118 + return self.get_object_in_store(user, dirname, "recurrences", uid, recurrenceid)
1.119 +
1.120 def get_recurrence(self, user, uid, recurrenceid):
1.121
1.122 """
1.123 @@ -306,11 +357,11 @@
1.124 specific recurrence indicated by the 'recurrenceid'.
1.125 """
1.126
1.127 - filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
1.128 + filename = self.get_recurrence_filename(user, uid, recurrenceid)
1.129 if not filename or not exists(filename):
1.130 return None
1.131
1.132 - return self._get_object(user, filename)
1.133 + return filename and self._get_object(user, filename)
1.134
1.135 def set_recurrence(self, user, uid, recurrenceid, node):
1.136
1.137 @@ -526,12 +577,6 @@
1.138
1.139 return self._get_requests(user, "requests")
1.140
1.141 - def get_cancellations(self, user):
1.142 -
1.143 - "Get cancellations for the given 'user'."
1.144 -
1.145 - return self._get_requests(user, "cancellations")
1.146 -
1.147 def _set_requests(self, user, requests, queue):
1.148
1.149 """
1.150 @@ -563,12 +608,6 @@
1.151
1.152 return self._set_requests(user, requests, "requests")
1.153
1.154 - def set_cancellations(self, user, cancellations):
1.155 -
1.156 - "For the given 'user', set the list of queued 'cancellations'."
1.157 -
1.158 - return self._set_requests(user, cancellations, "cancellations")
1.159 -
1.160 def _set_request(self, user, uid, recurrenceid, queue):
1.161
1.162 """
1.163 @@ -599,12 +638,6 @@
1.164
1.165 return self._set_request(user, uid, recurrenceid, "requests")
1.166
1.167 - def set_cancellation(self, user, uid, recurrenceid=None):
1.168 -
1.169 - "For the given 'user', set the queued 'uid' and 'recurrenceid'."
1.170 -
1.171 - return self._set_request(user, uid, recurrenceid, "cancellations")
1.172 -
1.173 def queue_request(self, user, uid, recurrenceid=None):
1.174
1.175 """
1.176 @@ -641,15 +674,16 @@
1.177 def cancel_event(self, user, uid, recurrenceid=None):
1.178
1.179 """
1.180 - Queue an event for cancellation for 'user' having the given 'uid'. If
1.181 - the optional 'recurrenceid' is specified, a specific instance or
1.182 - occurrence of an event is cancelled.
1.183 + Cancel an event for 'user' having the given 'uid'. If the optional
1.184 + 'recurrenceid' is specified, a specific instance or occurrence of an
1.185 + event is cancelled.
1.186 """
1.187
1.188 - cancellations = self.get_cancellations(user) or []
1.189 + filename = self.get_event_filename(user, uid, recurrenceid)
1.190 + cancelled_filename = self.get_event_filename(user, uid, recurrenceid, "cancellations")
1.191
1.192 - if (uid, recurrenceid) not in cancellations:
1.193 - return self.set_cancellation(user, uid, recurrenceid)
1.194 + if filename and cancelled_filename and exists(filename):
1.195 + return self.move_object(filename, cancelled_filename)
1.196
1.197 return False
1.198
2.1 --- a/imiptools/client.py Sun Sep 06 01:06:28 2015 +0200
2.2 +++ b/imiptools/client.py Sun Sep 06 01:15:30 2015 +0200
2.3 @@ -331,17 +331,17 @@
2.4
2.5 # Object-related tests.
2.6
2.7 - def get_attendance(self, user=None):
2.8 + def get_attendance(self, user=None, obj=None):
2.9
2.10 """
2.11 Return the attendance attributes for 'user', or the current user if
2.12 'user' is not specified.
2.13 """
2.14
2.15 - attendees = uri_dict(self.obj.get_value_map("ATTENDEE"))
2.16 + attendees = uri_dict((obj or self.obj).get_value_map("ATTENDEE"))
2.17 return attendees.get(user or self.user) or {}
2.18
2.19 - def is_participating(self, user, as_organiser=False):
2.20 + def is_participating(self, user, as_organiser=False, obj=None):
2.21
2.22 """
2.23 Return whether, subject to the 'user' indicating an identity and the
2.24 @@ -349,7 +349,7 @@
2.25 participating in the current object event.
2.26 """
2.27
2.28 - attr = self.get_attendance(user)
2.29 + attr = self.get_attendance(user, obj=obj)
2.30 return as_organiser or not attr or attr.get("PARTSTAT") != "DECLINED"
2.31
2.32 def get_overriding_transparency(self, user, as_organiser=False):
3.1 --- a/imiptools/filesys.py Sun Sep 06 01:06:28 2015 +0200
3.2 +++ b/imiptools/filesys.py Sun Sep 06 01:15:30 2015 +0200
3.3 @@ -21,20 +21,55 @@
3.4
3.5 import errno
3.6 from imiptools.config import DEFAULT_PERMISSIONS, DEFAULT_DIR_PERMISSIONS
3.7 -from os.path import abspath, commonprefix, exists, join
3.8 -from os import chmod, makedirs, mkdir, rmdir
3.9 +from os.path import abspath, commonprefix, exists, join, split
3.10 +from os import chmod, makedirs, mkdir, rename, rmdir
3.11 from time import sleep, time
3.12
3.13 -def check_dir(base, dir):
3.14 - return commonprefix([base, abspath(dir)]) == base
3.15 +def check_dir(base, filename):
3.16 +
3.17 + "Return whether 'base' contains 'filename'."
3.18 +
3.19 + return commonprefix([base, abspath(filename)]) == base
3.20 +
3.21 +def remaining_parts(base, filename):
3.22 +
3.23 + "Return the remaining parts from 'base' provided by 'filename'."
3.24 +
3.25 + if not check_dir(base, filename):
3.26 + return None
3.27 +
3.28 + filename = abspath(filename)
3.29 +
3.30 + parts = []
3.31 + while True:
3.32 + filename, part = split(filename)
3.33 + if check_dir(base, filename):
3.34 + parts.insert(0, part)
3.35 + else:
3.36 + break
3.37 +
3.38 + return parts
3.39
3.40 def fix_permissions(filename, is_dir=False):
3.41 +
3.42 + """
3.43 + Fix permissions for 'filename', with 'is_dir' indicating whether the object
3.44 + should be a directory or not.
3.45 + """
3.46 +
3.47 try:
3.48 chmod(filename, is_dir and DEFAULT_DIR_PERMISSIONS or DEFAULT_PERMISSIONS)
3.49 except OSError:
3.50 pass
3.51
3.52 def make_path(base, parts):
3.53 +
3.54 + """
3.55 + Make the path within 'base' having components defined by the given 'parts'.
3.56 + Note that this function does not check the parts for suitability. To do so,
3.57 + use the FileBase methods instead.
3.58 + """
3.59 +
3.60 for part in parts:
3.61 pathname = join(base, part)
3.62 if not exists(pathname):
3.63 @@ -55,6 +90,14 @@
3.64 fix_permissions(self.store_dir, True)
3.65
3.66 def get_file_object(self, base, *parts):
3.67 +
3.68 + """
3.69 + Within the given 'base' location, return a path corresponding to the
3.70 + given 'parts'.
3.71 + """
3.72 +
3.73 + # Handle "empty" components.
3.74 +
3.75 pathname = join(base, *parts)
3.76 return check_dir(base, pathname) and pathname or None
3.77
3.78 @@ -67,6 +110,10 @@
3.79
3.80 parent = expected = self.store_dir
3.81
3.82 + # Handle "empty" components.
3.83 +
3.84 + parts = [p for p in parts if p]
3.85 +
3.86 for part in parts:
3.87 filename = self.get_file_object(expected, part)
3.88 if not filename:
3.89 @@ -79,11 +126,33 @@
3.90
3.91 return filename
3.92
3.93 + def move_object(self, source, target):
3.94 +
3.95 + "Move 'source' to 'target'."
3.96 +
3.97 + if not self.ensure_parent(target):
3.98 + return False
3.99 + rename(source, target)
3.100 +
3.101 + def ensure_parent(self, target):
3.102 +
3.103 + "Ensure that the parent of 'target' exists."
3.104 +
3.105 + parts = remaining_parts(self.store_dir, target)
3.106 + if not parts or not self.get_file_object(self.store_dir, *parts[:-1]):
3.107 + return False
3.108 +
3.109 + make_path(self.store_dir, parts[:-1])
3.110 + return True
3.111 +
3.112 # Locking methods.
3.113 # This uses the directory creation method exploited by MoinMoin.util.lock.
3.114 # However, a simple single lock type mechanism is employed here.
3.115
3.116 def get_lock_dir(self, *parts):
3.117 +
3.118 + "Return the lock directory defined by the given 'parts'."
3.119 +
3.120 parts = parts and list(parts) or []
3.121 parts.append(self.lock_name)
3.122 return self.get_object_in_store(*parts)
4.1 --- a/imiptools/handlers/person.py Sun Sep 06 01:06:28 2015 +0200
4.2 +++ b/imiptools/handlers/person.py Sun Sep 06 01:15:30 2015 +0200
4.3 @@ -205,29 +205,33 @@
4.4
4.5 method = "REQUEST"
4.6
4.7 - # Get the parent event, add SENT-BY details to the organiser.
4.8 + for attendee in attendees:
4.9 + responses = []
4.10
4.11 - obj = self.get_stored_object_version()
4.12 - organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
4.13 - self.update_sender(organiser_attr)
4.14 - responses = [obj.to_node()]
4.15 + # Get the parent event, add SENT-BY details to the organiser.
4.16 +
4.17 + obj = self.get_stored_object_version()
4.18
4.19 - # Get recurrences.
4.20 -
4.21 - cancelled = self.store.get_cancellations(self.user)
4.22 + if self.is_participating(attendee, obj=obj):
4.23 + organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
4.24 + self.update_sender(organiser_attr)
4.25 + responses.append(obj.to_node())
4.26
4.27 - if not self.recurrenceid:
4.28 - for recurrenceid in self.store.get_recurrences(self.user, self.uid):
4.29 - if not cancelled or (self.uid, recurrenceid) not in cancelled:
4.30 + # Get recurrences.
4.31 +
4.32 + if not self.recurrenceid:
4.33 + for recurrenceid in self.store.get_active_recurrences(self.user, self.uid):
4.34
4.35 # Get the recurrence, add SENT-BY details to the organiser.
4.36
4.37 obj = self.get_stored_object(self.uid, recurrenceid)
4.38 - organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
4.39 - self.update_sender(organiser_attr)
4.40 - responses.append(obj.to_node())
4.41
4.42 - self.add_result(method, map(get_address, attendees), to_part(method, responses))
4.43 + if self.is_participating(attendee, obj=obj):
4.44 + organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
4.45 + self.update_sender(organiser_attr)
4.46 + responses.append(obj.to_node())
4.47 +
4.48 + self.add_result(method, [get_address(attendee)], to_part(method, responses))
4.49
4.50 return True
4.51
5.1 --- a/imiptools/handlers/person_outgoing.py Sun Sep 06 01:06:28 2015 +0200
5.2 +++ b/imiptools/handlers/person_outgoing.py Sun Sep 06 01:15:30 2015 +0200
5.3 @@ -146,14 +146,9 @@
5.4 given_attendees = set(uri_values(self.obj.get_values("ATTENDEE")))
5.5 cancel_entire_event = not all_attendees.difference(given_attendees)
5.6
5.7 - # Keep the event for the organiser.
5.8 -
5.9 - if cancel_entire_event:
5.10 - self.store.cancel_event(self.user, self.uid, self.recurrenceid)
5.11 -
5.12 # Otherwise, remove the given attendees and update the event.
5.13
5.14 - elif obj:
5.15 + if not cancel_entire_event and obj:
5.16 for attendee in given_attendees:
5.17 if attendees.has_key(attendee):
5.18 del attendees[attendee]
5.19 @@ -175,6 +170,12 @@
5.20
5.21 self.store.set_event(self.user, self.uid, self.recurrenceid, (obj or self.obj).to_node())
5.22
5.23 + # Perform any cancellation after recording the latest state of the
5.24 + # event.
5.25 +
5.26 + if cancel_entire_event:
5.27 + self.store.cancel_event(self.user, self.uid, self.recurrenceid)
5.28 +
5.29 # Remove any associated request.
5.30
5.31 self.store.dequeue_request(self.user, self.uid, self.recurrenceid)
6.1 --- a/imipweb/event.py Sun Sep 06 01:06:28 2015 +0200
6.2 +++ b/imipweb/event.py Sun Sep 06 01:15:30 2015 +0200
6.3 @@ -492,7 +492,7 @@
6.4 # Obtain any separate recurrences for this event.
6.5
6.6 recurrenceid = obj.get_recurrenceid()
6.7 - recurrenceids = self._get_recurrences(uid)
6.8 + recurrenceids = self._get_active_recurrences(uid)
6.9 replaced = not recurrenceid and p.is_replaced(recurrenceids)
6.10
6.11 # Provide a summary of the object.
6.12 @@ -798,7 +798,7 @@
6.13 page = self.page
6.14 uid = obj.get_uid()
6.15 recurrenceid = obj.get_recurrenceid()
6.16 - recurrenceids = self._get_recurrences(uid)
6.17 + recurrenceids = self._get_active_recurrences(uid)
6.18
6.19 # Obtain the user's timezone.
6.20
7.1 --- a/imipweb/resource.py Sun Sep 06 01:06:28 2015 +0200
7.2 +++ b/imipweb/resource.py Sun Sep 06 01:15:30 2015 +0200
7.3 @@ -119,6 +119,9 @@
7.4 def _get_recurrences(self, uid):
7.5 return self.store.get_recurrences(self.user, uid)
7.6
7.7 + def _get_active_recurrences(self, uid):
7.8 + return self.store.get_active_recurrences(self.user, uid)
7.9 +
7.10 def _get_requests(self):
7.11 if self.requests is None:
7.12 cancellations = self.store.get_cancellations(self.user)
7.13 @@ -135,7 +138,7 @@
7.14 for uid, recurrenceid in self._get_requests():
7.15 obj = self.get_stored_object(uid, recurrenceid)
7.16 if obj:
7.17 - recurrenceids = self._get_recurrences(uid)
7.18 + recurrenceids = self._get_active_recurrences(uid)
7.19
7.20 # Obtain only active periods, not those replaced by redefined
7.21 # recurrences, converting to free/busy periods.
8.1 --- a/tests/templates/event-cancel-person-recurring-rescheduled-instance.txt Sun Sep 06 01:06:28 2015 +0200
8.2 +++ b/tests/templates/event-cancel-person-recurring-rescheduled-instance.txt Sun Sep 06 01:15:30 2015 +0200
8.3 @@ -26,8 +26,8 @@
8.4 ATTENDEE;RSVP=TRUE:mailto:vincent.vole@example.com
8.5 ATTENDEE;RSVP=TRUE:mailto:paul.boddie@example.com
8.6 DTSTAMP:20141009T182400Z
8.7 -DTSTART;TZID=Europe/Oslo:20141010T100000
8.8 -DTEND;TZID=Europe/Oslo:20141010T110000
8.9 +DTSTART;TZID=Europe/Oslo:20141011T100000
8.10 +DTEND;TZID=Europe/Oslo:20141011T110000
8.11 SUMMARY:Recurring event
8.12 UID:event8@example.com
8.13 RECURRENCE-ID;TZID=Europe/Oslo:20141010T100000
9.1 --- a/tests/test_person_invitation_refresh.sh Sun Sep 06 01:06:28 2015 +0200
9.2 +++ b/tests/test_person_invitation_refresh.sh Sun Sep 06 01:15:30 2015 +0200
9.3 @@ -120,11 +120,13 @@
9.4 && echo "Success" \
9.5 || echo "Failed"
9.6
9.7 -# Cancel a recurrence.
9.8 +# Cancel a recurrence. Both the original and rescheduled recurrences should be
9.9 +# absent from the free/busy collection.
9.10
9.11 "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-cancel-person-recurring-rescheduled-instance.txt" 2>> $ERROR
9.12
9.13 - ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "$FBSENDERFILE" \
9.14 + ! grep -q "^20141010T080000Z${TAB}20141010T090000Z" "$FBSENDERFILE" \
9.15 +&& ! grep -q "^20141011T080000Z${TAB}20141011T090000Z" "$FBSENDERFILE" \
9.16 && echo "Success" \
9.17 || echo "Failed"
9.18
10.1 --- a/tools/make_freebusy.py Sun Sep 06 01:06:28 2015 +0200
10.2 +++ b/tools/make_freebusy.py Sun Sep 06 01:15:30 2015 +0200
10.3 @@ -70,7 +70,7 @@
10.4 all_events = not reset_updated_list and store.get_freebusy_providers(user, window_end)
10.5
10.6 if not all_events:
10.7 - all_events = store.get_active_events(user)
10.8 + all_events = store.get_all_events(user)
10.9 fb = []
10.10
10.11 # With providers of additional periods, append to the existing collection.