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