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 """ 69 A content handler for use by the manager, as opposed to operating within the 70 mail processing pipeline. 71 """ 72 73 def __init__(self, obj, user, messenger): 74 details, details_attr = obj.values()[0] 75 Handler.__init__(self, details) 76 self.obj = obj 77 self.user = user 78 self.messenger = messenger 79 80 self.organisers = map(get_address, self.get_values("ORGANIZER")) 81 82 # Communication methods. 83 84 def send_message(self, sender): 85 86 """ 87 Create a full calendar object and send it to the organisers from the 88 given 'sender'. 89 """ 90 91 node = to_node(self.obj) 92 part = to_part("REPLY", [node]) 93 message = self.messenger.make_message([part], self.organisers, sender=sender) 94 self.messenger.sendmail(self.organisers, message.as_string(), sender=sender) 95 96 # Action methods. 97 98 def process_request(self, accept): 99 100 """ 101 Process the current request for the given 'user', accepting any request 102 when 'accept' is true, declining requests otherwise. Return whether any 103 action was taken. 104 """ 105 106 # When accepting or declining, do so only on behalf of this user, 107 # preserving any other attributes set as an attendee. 108 109 for attendee, attendee_attr in self.get_items("ATTENDEE"): 110 111 if attendee == self.user: 112 freebusy = self.store.get_freebusy(attendee) 113 114 attendee_attr["PARTSTAT"] = accept and "ACCEPTED" or "DECLINED" 115 self.details["ATTENDEE"] = [(attendee, attendee_attr)] 116 self.send_message(get_address(attendee)) 117 118 return True 119 120 return False 121 122 class Manager: 123 124 "A simple manager application." 125 126 def __init__(self, messenger=None): 127 self.messenger = messenger or Messenger() 128 129 self.env = CGIEnvironment() 130 user = self.env.get_user() 131 self.user = user and get_uri(user) or None 132 self.requests = None 133 134 self.out = self.env.get_output() 135 self.page = markup.page() 136 self.encoding = "utf-8" 137 138 self.store = imip_store.FileStore() 139 140 try: 141 self.publisher = imip_store.FilePublisher() 142 except OSError: 143 self.publisher = None 144 145 def _get_uid(self, path_info): 146 return path_info.lstrip("/").split("/", 1)[0] 147 148 def _get_object(self, uid): 149 f = uid and self.store.get_event(self.user, uid) or None 150 151 if not f: 152 return None 153 154 obj = parse_object(f, "utf-8") 155 156 if not obj: 157 return None 158 159 return obj 160 161 def _get_details(self, obj): 162 details, details_attr = obj.values()[0] 163 return details 164 165 def _get_requests(self): 166 if self.requests is None: 167 self.requests = self.store.get_requests(self.user) 168 return self.requests 169 170 # Data management methods. 171 172 def remove_request(self, uid): 173 return self.store.dequeue_request(self.user, uid) 174 175 # Presentation methods. 176 177 def new_page(self, title): 178 self.page.init(title=title, charset=self.encoding) 179 180 def status(self, code, message): 181 self.header("Status", "%s %s" % (code, message)) 182 183 def header(self, header, value): 184 print >>self.out, "%s: %s" % (header, value) 185 186 def no_user(self): 187 self.status(403, "Forbidden") 188 self.new_page(title="Forbidden") 189 self.page.p("You are not logged in and thus cannot access scheduling requests.") 190 191 def no_page(self): 192 self.status(404, "Not Found") 193 self.new_page(title="Not Found") 194 self.page.p("No page is provided at the given address.") 195 196 def redirect(self, url): 197 self.status(302, "Redirect") 198 self.header("Location", url) 199 self.new_page(title="Redirect") 200 self.page.p("Redirecting to: %s" % url) 201 202 # Request logic and page fragment methods. 203 204 def handle_request(self, uid, request): 205 206 "Handle actions involving the given 'uid' and 'request' object." 207 208 # Handle a submitted form. 209 210 args = self.env.get_args() 211 handled = True 212 213 accept = args.has_key("accept") 214 decline = args.has_key("decline") 215 216 if accept or decline: 217 218 handler = ManagerHandler(request, self.user, self.messenger) 219 220 if handler.process_request(accept): 221 222 # Remove the request from the list. 223 224 self.remove_request(uid) 225 226 elif args.has_key("ignore"): 227 228 # Remove the request from the list. 229 230 self.remove_request(uid) 231 232 else: 233 handled = False 234 235 if handled: 236 self.redirect(self.env.get_path()) 237 238 return handled 239 240 def show_request_form(self): 241 242 "Show a form for a request." 243 244 self.page.p("Action to take for this request:") 245 self.page.form(method="POST") 246 self.page.p() 247 self.page.input(name="accept", type="submit", value="Accept") 248 self.page.add(" ") 249 self.page.input(name="decline", type="submit", value="Decline") 250 self.page.add(" ") 251 self.page.input(name="ignore", type="submit", value="Ignore") 252 self.page.p.close() 253 self.page.form.close() 254 255 def show_object_on_page(self, uid, obj): 256 257 """ 258 Show the calendar object with the given 'uid' and representation 'obj' 259 on the current page. 260 """ 261 262 details = self._get_details(obj) 263 264 # Provide a summary of the object. 265 266 self.page.dl() 267 268 for name in ["SUMMARY", "DTSTART", "DTEND", "ORGANIZER", "ATTENDEE"]: 269 for value in get_values(details, name): 270 self.page.dt(name) 271 self.page.dd(value) 272 273 self.page.dl.close() 274 275 dtstart = format_datetime(get_utc_datetime(details, "DTSTART")) 276 dtend = format_datetime(get_utc_datetime(details, "DTEND")) 277 278 # Indicate whether there are conflicting events. 279 280 freebusy = self.store.get_freebusy(self.user) 281 282 if freebusy: 283 284 # Obtain any time zone details from the suggested event. 285 286 _dtstart, attr = get_item(details, "DTSTART") 287 tzid = attr.get("TZID") 288 289 # Show any conflicts. 290 291 for t in have_conflict(freebusy, [(dtstart, dtend)], True): 292 start, end, found_uid = t[:3] 293 if uid != found_uid: 294 start = format_datetime(to_timezone(get_datetime(start), tzid)) 295 end = format_datetime(to_timezone(get_datetime(end), tzid)) 296 self.page.p("Event conflicts with another from %s to %s." % (start, end)) 297 298 def show_requests_on_page(self): 299 300 "Show requests for the current user." 301 302 # NOTE: This list could be more informative, but it is envisaged that 303 # NOTE: the requests would be visited directly anyway. 304 305 requests = self._get_requests() 306 307 if requests: 308 self.page.p("Pending requests:") 309 310 self.page.ul() 311 312 for request in requests: 313 self.page.li() 314 self.page.a(request, href="%s/%s" % (self.env.get_url().rstrip("/"), request)) 315 self.page.li.close() 316 317 self.page.ul.close() 318 319 else: 320 self.page.p("There are no pending requests.") 321 322 # Full page output methods. 323 324 def show_object(self, path_info): 325 326 "Show an object request using the given 'path_info' for the current user." 327 328 uid = self._get_uid(path_info) 329 obj = self._get_object(uid) 330 331 if not obj: 332 return False 333 334 is_request = uid in self._get_requests() 335 handled = is_request and self.handle_request(uid, obj) 336 337 if handled: 338 return True 339 340 self.new_page(title="Event") 341 342 self.show_object_on_page(uid, obj) 343 344 if is_request and not handled: 345 self.show_request_form() 346 347 return True 348 349 def show_calendar(self): 350 351 "Show the calendar for the current user." 352 353 self.new_page(title="Calendar") 354 self.show_requests_on_page() 355 356 freebusy = self.store.get_freebusy(self.user) 357 page = self.page 358 359 if not freebusy: 360 page.p("No events scheduled.") 361 return 362 363 # Day view: start at the earliest known day and produce days until the 364 # latest known day, perhaps with expandable sections of empty days. 365 366 # Month view: start at the earliest known month and produce months until 367 # the latest known month, perhaps with expandable sections of empty 368 # months. 369 370 # Details of users to invite to new events could be superimposed on the 371 # calendar. 372 373 # Requests could be listed and linked to their tentative positions in 374 # the calendar. 375 376 slots = get_slots(freebusy) 377 spans = get_spans(slots) 378 379 page.table(border=1, cellspacing=0, cellpadding=5) 380 381 for point, active in slots: 382 page.tr() 383 page.th(class_="timeslot") 384 page.add(point) 385 page.th.close() 386 387 for t in active: 388 if t: 389 start, end, uid = t[:3] 390 span = spans[uid] 391 if point == start: 392 393 page.td(class_="event", rowspan=span) 394 obj = self._get_object(uid) 395 if obj: 396 details = self._get_details(obj) 397 page.a(get_value(details, "SUMMARY"), href="%s/%s" % (self.env.get_url().rstrip("/"), uid)) 398 page.td.close() 399 else: 400 page.td(class_="empty") 401 page.td.close() 402 403 page.tr.close() 404 405 page.table.close() 406 407 def select_action(self): 408 409 "Select the desired action and show the result." 410 411 path_info = self.env.get_path_info().strip("/") 412 413 if not path_info: 414 self.show_calendar() 415 elif self.show_object(path_info): 416 pass 417 else: 418 self.no_page() 419 420 def __call__(self): 421 422 "Interpret a request and show an appropriate response." 423 424 if not self.user: 425 self.no_user() 426 else: 427 self.select_action() 428 429 # Write the headers and actual content. 430 431 print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding 432 print >>self.out 433 self.out.write(unicode(self.page).encode(self.encoding)) 434 435 if __name__ == "__main__": 436 Manager( 437 Messenger( 438 "imip-agent@example.com", 439 "Calendar system message", 440 "This is a message from the calendar system." 441 ) 442 )() 443 444 # vim: tabstop=4 expandtab shiftwidth=4