1 #!/usr/bin/env python 2 3 import cgi, os, sys 4 5 sys.path.append("/var/lib/imip-agent") 6 7 from imiptools.content import Handler, \ 8 format_datetime, get_address, get_datetime, \ 9 get_item, get_uri, get_utc_datetime, get_value, \ 10 get_values, parse_object, to_part, to_timezone 11 from imiptools.mail import Messenger 12 from imiptools.period import have_conflict, get_slots, get_spans 13 from vCalendar import to_node 14 import markup 15 import imip_store 16 17 getenv = os.environ.get 18 setenv = os.environ.__setitem__ 19 20 class CGIEnvironment: 21 22 "A CGI-compatible environment." 23 24 def __init__(self): 25 self.args = None 26 self.method = None 27 self.path = None 28 self.path_info = None 29 self.user = None 30 31 def get_args(self): 32 if self.args is None: 33 if self.get_method() != "POST": 34 setenv("QUERY_STRING", "") 35 self.args = cgi.parse(keep_blank_values=True) 36 return self.args 37 38 def get_method(self): 39 if self.method is None: 40 self.method = getenv("REQUEST_METHOD") or "GET" 41 return self.method 42 43 def get_path(self): 44 if self.path is None: 45 self.path = getenv("SCRIPT_NAME") or "" 46 return self.path 47 48 def get_path_info(self): 49 if self.path_info is None: 50 self.path_info = getenv("PATH_INFO") or "" 51 return self.path_info 52 53 def get_user(self): 54 if self.user is None: 55 self.user = getenv("REMOTE_USER") or "" 56 return self.user 57 58 def get_output(self): 59 return sys.stdout 60 61 def get_url(self): 62 path = self.get_path() 63 path_info = self.get_path_info() 64 return "%s%s" % (path.rstrip("/"), path_info) 65 66 class ManagerHandler(Handler): 67 68 "A content handler for use by the manager." 69 70 def __init__(self, details, objtype, user, messenger): 71 Handler.__init__(self, details) 72 self.objtype = objtype 73 self.user = user 74 self.messenger = messenger 75 76 self.organisers = map(get_address, self.get_values("ORGANIZER")) 77 78 # Communication methods. 79 80 def send_message(self, sender): 81 82 """ 83 Create a full calendar object and send it to the organisers from the 84 given 'sender'. 85 """ 86 87 node = to_node({self.objtype : [(self.details, {})]}) 88 part = to_part("REPLY", [node]) 89 message = self.messenger.make_message([part], self.organisers, sender=sender) 90 self.messenger.sendmail(self.organisers, message.as_string(), sender=sender) 91 92 # Action methods. 93 94 def process_request(self, accept): 95 96 """ 97 Process the current request for the given 'user', accepting any request 98 when 'accept' is true, declining requests otherwise. Return whether any 99 action was taken. 100 """ 101 102 # When accepting or declining, do so only on behalf of this user, 103 # preserving any other attributes set as an attendee. 104 105 for attendee, attendee_attr in self.get_items("ATTENDEE"): 106 107 if attendee == self.user: 108 freebusy = self.store.get_freebusy(attendee) 109 110 attendee_attr["PARTSTAT"] = accept and "ACCEPTED" or "DECLINED" 111 self.details["ATTENDEE"] = [(attendee, attendee_attr)] 112 self.send_message(get_address(attendee)) 113 114 return True 115 116 return False 117 118 class Manager: 119 120 "A simple manager application." 121 122 def __init__(self, messenger=None): 123 self.messenger = messenger or Messenger() 124 125 self.env = CGIEnvironment() 126 user = self.env.get_user() 127 self.user = user and get_uri(user) or None 128 self.out = self.env.get_output() 129 self.page = markup.page() 130 self.encoding = "utf-8" 131 132 self.store = imip_store.FileStore() 133 134 try: 135 self.publisher = imip_store.FilePublisher() 136 except OSError: 137 self.publisher = None 138 139 def _get_object(self, uid): 140 f = uid and self.store.get_event(self.user, uid) or None 141 142 if not f: 143 return None 144 145 obj = parse_object(f, "utf-8") 146 147 if not obj: 148 return None 149 150 objtype = obj.keys()[0] 151 return obj[objtype][0] 152 153 # Data management methods. 154 155 def remove_request(self, uid): 156 return self.store.dequeue_request(self.user, uid) 157 158 # Presentation methods. 159 160 def new_page(self, title): 161 self.page.init(title=title, charset=self.encoding) 162 163 def status(self, code, message): 164 print >>self.out, "Status:", code, message 165 166 def no_user(self): 167 self.status(403, "Forbidden") 168 self.new_page(title="Forbidden") 169 self.page.p("You are not logged in and thus cannot access scheduling requests.") 170 171 def no_page(self): 172 self.status(404, "Not Found") 173 self.new_page(title="Not Found") 174 self.page.p("No page is provided at the given address.") 175 176 def show_requests(self): 177 178 "Show requests for the current user." 179 180 # NOTE: This list could be more informative, but it is envisaged that 181 # NOTE: the requests would be visited directly anyway. 182 183 self.new_page(title="Pending Requests") 184 185 requests = self.store.get_requests(self.user) 186 187 if requests: 188 self.page.p("Pending requests:") 189 190 self.page.ul() 191 192 for request in requests: 193 self.page.li() 194 self.page.a(request, href="%s/%s" % (self.env.get_url().rstrip("/"), request)) 195 self.page.li.close() 196 197 self.page.ul.close() 198 199 else: 200 self.page.p("There are no pending requests.") 201 202 def show_request(self, path_info): 203 204 "Show a request using the given 'path_info' for the current user." 205 206 uid = path_info.lstrip("/").split("/", 1)[0] 207 request = self._get_object(uid) 208 209 if not request: 210 return False 211 212 # Handle a submitted form. 213 214 args = self.env.get_args() 215 show_form = False 216 217 accept = args.has_key("accept") 218 decline = args.has_key("decline") 219 220 if accept or decline: 221 222 handler = ManagerHandler(request, objtype, self.user, self.messenger) 223 224 if handler.process_request(accept): 225 226 # Remove the request from the list. 227 228 self.remove_request(uid) 229 230 elif args.has_key("ignore"): 231 232 # Remove the request from the list. 233 234 self.remove_request(uid) 235 236 else: 237 show_form = True 238 239 self.new_page(title="Request") 240 241 # Provide a summary of the request. 242 243 self.page.p("The following request was received:") 244 self.page.dl() 245 246 for name in ["SUMMARY", "DTSTART", "DTEND", "ORGANIZER", "ATTENDEE"]: 247 for value in get_values(request, name): 248 self.page.dt(name) 249 self.page.dd(value) 250 251 self.page.dl.close() 252 253 dtstart = format_datetime(get_utc_datetime(request, "DTSTART")) 254 dtend = format_datetime(get_utc_datetime(request, "DTEND")) 255 256 # Indicate whether there are conflicting events. 257 258 freebusy = self.store.get_freebusy(self.user) 259 260 if freebusy: 261 262 # Obtain any time zone details from the suggested event. 263 264 _dtstart, attr = get_item(request, "DTSTART") 265 tzid = attr.get("TZID") 266 267 # Show any conflicts. 268 269 for t in have_conflict(freebusy, [(dtstart, dtend)], True): 270 start, end, found_uid = t[:3] 271 if uid != found_uid: 272 start = format_datetime(to_timezone(get_datetime(start), tzid)) 273 end = format_datetime(to_timezone(get_datetime(end), tzid)) 274 self.page.p("Event conflicts with another from %s to %s." % (start, end)) 275 276 # Show a form if no action has just been taken. 277 278 if show_form: 279 self.page.p("Action to take for this request:") 280 self.page.form(method="POST") 281 self.page.p() 282 self.page.input(name="accept", type="submit", value="Accept") 283 self.page.add(" ") 284 self.page.input(name="decline", type="submit", value="Decline") 285 self.page.add(" ") 286 self.page.input(name="ignore", type="submit", value="Ignore") 287 self.page.p.close() 288 self.page.form.close() 289 290 return True 291 292 def show_calendar(self): 293 294 "Show the calendar for the current user." 295 296 self.new_page(title="Calendar") 297 298 freebusy = self.store.get_freebusy(self.user) 299 page = self.page 300 301 if not freebusy: 302 page.p("No events scheduled.") 303 return 304 305 # Day view: start at the earliest known day and produce days until the 306 # latest known day, perhaps with expandable sections of empty days. 307 308 # Month view: start at the earliest known month and produce months until 309 # the latest known month, perhaps with expandable sections of empty 310 # months. 311 312 # Details of users to invite to new events could be superimposed on the 313 # calendar. 314 315 # Requests could be listed and linked to their tentative positions in 316 # the calendar. 317 318 slots = get_slots(freebusy) 319 spans = get_spans(slots) 320 321 page.table(border=1, cellspacing=0, cellpadding=5) 322 323 for point, active in slots: 324 page.tr() 325 page.th(class_="timeslot") 326 page.add(point) 327 page.th.close() 328 329 for t in active: 330 if t: 331 start, end, uid = t[:3] 332 span = spans[uid] 333 if point == start: 334 335 page.td(class_="event", rowspan=span) 336 obj = self._get_object(uid) 337 if obj: 338 page.add(get_value(obj, "SUMMARY")) 339 page.td.close() 340 else: 341 page.td(class_="empty") 342 page.td.close() 343 344 page.tr.close() 345 346 page.table.close() 347 348 def select_action(self): 349 350 "Select the desired action and show the result." 351 352 path_info = self.env.get_path_info().rstrip("/") 353 if not path_info: 354 self.show_requests() 355 elif path_info.rsplit("/", 1)[-1] == "calendar": 356 self.show_calendar() 357 elif self.show_request(path_info): 358 pass 359 else: 360 self.no_page() 361 362 def __call__(self): 363 364 "Interpret a request and show an appropriate response." 365 366 if not self.user: 367 self.no_user() 368 else: 369 self.select_action() 370 371 # Write the headers and actual content. 372 373 print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding 374 print >>self.out 375 self.out.write(unicode(self.page).encode(self.encoding)) 376 377 if __name__ == "__main__": 378 Manager( 379 Messenger( 380 "imip-agent@example.com", 381 "Calendar system message", 382 "This is a message from the calendar system." 383 ) 384 )() 385 386 # vim: tabstop=4 expandtab shiftwidth=4