1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/imip_resource.py Tue Oct 21 19:58:20 2014 +0200
1.3 @@ -0,0 +1,290 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +Handlers for a resource.
1.8 +"""
1.9 +
1.10 +from datetime import date, datetime, timedelta
1.11 +from imiptools.content import Handler, format_datetime, get_value, parse_object
1.12 +from imiptools.period import insert_period, period_overlaps, remove_period
1.13 +from vCalendar import to_node
1.14 +from vRecurrence import get_parameters, get_rule
1.15 +
1.16 +class Event(Handler):
1.17 +
1.18 + "An event handler."
1.19 +
1.20 + def add(self):
1.21 + pass
1.22 +
1.23 + def cancel(self):
1.24 + pass
1.25 +
1.26 + def counter(self):
1.27 +
1.28 + "Since this handler does not send requests, it will not handle replies."
1.29 +
1.30 + pass
1.31 +
1.32 + def declinecounter(self):
1.33 +
1.34 + """
1.35 + Since this handler does not send counter proposals, it will not handle
1.36 + replies to such proposals.
1.37 + """
1.38 +
1.39 + pass
1.40 +
1.41 + def publish(self):
1.42 + pass
1.43 +
1.44 + def refresh(self):
1.45 + pass
1.46 +
1.47 + def reply(self):
1.48 +
1.49 + "Since this handler does not send requests, it will not handle replies."
1.50 +
1.51 + pass
1.52 +
1.53 + def request(self):
1.54 +
1.55 + """
1.56 + Respond to a request by preparing a reply containing accept/decline
1.57 + information for each indicated attendee.
1.58 +
1.59 + No support for countering requests is implemented.
1.60 + """
1.61 +
1.62 + oa = self.require_organiser_and_attendees()
1.63 + if not oa:
1.64 + return None
1.65 +
1.66 + (organiser, organiser_attr), attendees = oa
1.67 +
1.68 + # Process each attendee separately.
1.69 +
1.70 + calendar = []
1.71 +
1.72 + for attendee, attendee_attr in attendees.items():
1.73 +
1.74 + # Check for event using UID.
1.75 +
1.76 + f = self.store.get_event(attendee, self.uid)
1.77 + event = f and parse_object(f, "utf-8", "VEVENT")
1.78 +
1.79 + # If found, compare SEQUENCE and potentially DTSTAMP.
1.80 +
1.81 + if event:
1.82 + sequence = get_value(event, "SEQUENCE")
1.83 + dtstamp = get_value(event, "DTSTAMP")
1.84 +
1.85 + # If the request refers to an older version of the event, ignore
1.86 + # it.
1.87 +
1.88 + old_dtstamp = self.dtstamp < dtstamp
1.89 +
1.90 + if sequence is not None and (
1.91 + int(self.sequence) < int(sequence) or
1.92 + int(self.sequence) == int(sequence) and old_dtstamp
1.93 + ) or old_dtstamp:
1.94 +
1.95 + continue
1.96 +
1.97 + # If newer than any old version, discard old details from the
1.98 + # free/busy record and check for suitability.
1.99 +
1.100 + dtstart = self.get_utc_datetime("DTSTART")
1.101 + dtend = self.get_utc_datetime("DTEND")
1.102 +
1.103 + # NOTE: Need also DURATION support.
1.104 +
1.105 + duration = dtend - dtstart
1.106 +
1.107 + # Recurrence rules create multiple instances to be checked.
1.108 + # Conflicts may only be assessed within a period defined by policy
1.109 + # for the agent, with instances outside that period being considered
1.110 + # unchecked.
1.111 +
1.112 + # NOTE: Need to expose the 100 day window in the configuration.
1.113 +
1.114 + window_end = datetime.now() + timedelta(100)
1.115 +
1.116 + # NOTE: Need also RDATE and EXDATE support.
1.117 +
1.118 + rrule = self.get_value("RRULE")
1.119 +
1.120 + if rrule:
1.121 + selector = get_rule(dtstart, rrule)
1.122 + parameters = get_parameters(rrule)
1.123 + periods = []
1.124 + for start in selector.materialise(dtstart, window_end, parameters.get("COUNT"), parameters.get("BYSETPOS")):
1.125 + start = datetime(*start, tzinfo=timezone("UTC"))
1.126 + end = start + duration
1.127 + periods.append((format_datetime(start), format_datetime(end)))
1.128 + else:
1.129 + periods = [(format_datetime(dtstart), format_datetime(dtend))]
1.130 +
1.131 + conflict = False
1.132 + freebusy = self.store.get_freebusy(attendee)
1.133 +
1.134 + if freebusy:
1.135 + remove_period(freebusy, self.uid)
1.136 + conflict = True
1.137 + for start, end in periods:
1.138 + if period_overlaps(freebusy, (start, end)):
1.139 + break
1.140 + else:
1.141 + conflict = False
1.142 + else:
1.143 + freebusy = []
1.144 +
1.145 + # If the event can be scheduled, it is registered and a reply sent
1.146 + # accepting the event. (The attendee has PARTSTAT=ACCEPTED as an
1.147 + # attribute.)
1.148 +
1.149 + if not conflict:
1.150 + for start, end in periods:
1.151 + insert_period(freebusy, (start, end, self.uid))
1.152 +
1.153 + if self.get_value("TRANSP") in (None, "OPAQUE"):
1.154 + self.store.set_freebusy(attendee, freebusy)
1.155 +
1.156 + if self.publisher:
1.157 + self.publisher.set_freebusy(attendee, freebusy)
1.158 +
1.159 + self.store.set_event(attendee, self.uid, to_node(
1.160 + {"VEVENT" : [(self.details, {})]}
1.161 + ))
1.162 + attendee_attr["PARTSTAT"] = "ACCEPTED"
1.163 +
1.164 + # If the event cannot be scheduled, it is not registered and a reply
1.165 + # sent declining the event. (The attendee has PARTSTAT=DECLINED as an
1.166 + # attribute.)
1.167 +
1.168 + else:
1.169 + attendee_attr["PARTSTAT"] = "DECLINED"
1.170 +
1.171 + self.details["ATTENDEE"] = [(attendee, attendee_attr)]
1.172 + calendar.append(to_node(
1.173 + {"VEVENT" : [(self.details, {})]}
1.174 + ))
1.175 +
1.176 + return calendar
1.177 +
1.178 +class Freebusy(Handler):
1.179 +
1.180 + "A free/busy handler."
1.181 +
1.182 + def publish(self):
1.183 + pass
1.184 +
1.185 + def reply(self):
1.186 +
1.187 + "Since this handler does not send requests, it will not handle replies."
1.188 +
1.189 + pass
1.190 +
1.191 + def request(self):
1.192 +
1.193 + """
1.194 + Respond to a request by preparing a reply containing free/busy
1.195 + information for each indicated attendee.
1.196 + """
1.197 +
1.198 + oa = self.require_organiser_and_attendees()
1.199 + if not oa:
1.200 + return None
1.201 +
1.202 + (organiser, organiser_attr), attendees = oa
1.203 +
1.204 + # Construct an appropriate fragment.
1.205 +
1.206 + calendar = []
1.207 + cwrite = calendar.append
1.208 +
1.209 + # Get the details for each attendee.
1.210 +
1.211 + for attendee, attendee_attr in attendees.items():
1.212 + freebusy = self.store.get_freebusy(attendee)
1.213 +
1.214 + if freebusy:
1.215 + record = []
1.216 + rwrite = record.append
1.217 +
1.218 + rwrite(("ORGANIZER", organiser_attr, organiser))
1.219 + rwrite(("ATTENDEE", attendee_attr, attendee))
1.220 + rwrite(("UID", {}, self.uid))
1.221 +
1.222 + for start, end, uid in freebusy:
1.223 + rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, [start, end]))
1.224 +
1.225 + cwrite(("VFREEBUSY", {}, record))
1.226 +
1.227 + # Return the reply.
1.228 +
1.229 + return calendar
1.230 +
1.231 +class Journal(Handler):
1.232 +
1.233 + "A journal entry handler."
1.234 +
1.235 + def add(self):
1.236 + pass
1.237 +
1.238 + def cancel(self):
1.239 + pass
1.240 +
1.241 + def publish(self):
1.242 + pass
1.243 +
1.244 +class Todo(Handler):
1.245 +
1.246 + "A to-do item handler."
1.247 +
1.248 + def add(self):
1.249 + pass
1.250 +
1.251 + def cancel(self):
1.252 + pass
1.253 +
1.254 + def counter(self):
1.255 +
1.256 + "Since this handler does not send requests, it will not handle replies."
1.257 +
1.258 + pass
1.259 +
1.260 + def declinecounter(self):
1.261 +
1.262 + """
1.263 + Since this handler does not send counter proposals, it will not handle
1.264 + replies to such proposals.
1.265 + """
1.266 +
1.267 + pass
1.268 +
1.269 + def publish(self):
1.270 + pass
1.271 +
1.272 + def refresh(self):
1.273 + pass
1.274 +
1.275 + def reply(self):
1.276 +
1.277 + "Since this handler does not send requests, it will not handle replies."
1.278 +
1.279 + pass
1.280 +
1.281 + def request(self):
1.282 + pass
1.283 +
1.284 +# Handler registry.
1.285 +
1.286 +handlers = [
1.287 + ("VFREEBUSY", Freebusy),
1.288 + ("VEVENT", Event),
1.289 + ("VTODO", Todo),
1.290 + ("VJOURNAL", Journal),
1.291 + ]
1.292 +
1.293 +# vim: tabstop=4 expandtab shiftwidth=4