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 print >>self.out, "Status:", code, message 182 183 def no_user(self): 184 self.status(403, "Forbidden") 185 self.new_page(title="Forbidden") 186 self.page.p("You are not logged in and thus cannot access scheduling requests.") 187 188 def no_page(self): 189 self.status(404, "Not Found") 190 self.new_page(title="Not Found") 191 self.page.p("No page is provided at the given address.") 192 193 # Request logic and page fragment methods. 194 195 def handle_request(self, uid, request): 196 197 "Handle actions involving the given 'uid' and 'request' object." 198 199 # Handle a submitted form. 200 201 args = self.env.get_args() 202 show_form = False 203 204 accept = args.has_key("accept") 205 decline = args.has_key("decline") 206 207 if accept or decline: 208 209 handler = ManagerHandler(request, self.user, self.messenger) 210 211 if handler.process_request(accept): 212 213 # Remove the request from the list. 214 215 self.remove_request(uid) 216 217 elif args.has_key("ignore"): 218 219 # Remove the request from the list. 220 221 self.remove_request(uid) 222 223 else: 224 show_form = True 225 226 return show_form 227 228 def show_request_form(self): 229 230 "Show a form for a request." 231 232 self.page.p("Action to take for this request:") 233 self.page.form(method="POST") 234 self.page.p() 235 self.page.input(name="accept", type="submit", value="Accept") 236 self.page.add(" ") 237 self.page.input(name="decline", type="submit", value="Decline") 238 self.page.add(" ") 239 self.page.input(name="ignore", type="submit", value="Ignore") 240 self.page.p.close() 241 self.page.form.close() 242 243 def show_object_on_page(self, uid, obj): 244 245 """ 246 Show the calendar object with the given 'uid' and representation 'obj' 247 on the current page. 248 """ 249 250 details = self._get_details(obj) 251 252 # Provide a summary of the object. 253 254 self.page.dl() 255 256 for name in ["SUMMARY", "DTSTART", "DTEND", "ORGANIZER", "ATTENDEE"]: 257 for value in get_values(details, name): 258 self.page.dt(name) 259 self.page.dd(value) 260 261 self.page.dl.close() 262 263 dtstart = format_datetime(get_utc_datetime(details, "DTSTART")) 264 dtend = format_datetime(get_utc_datetime(details, "DTEND")) 265 266 # Indicate whether there are conflicting events. 267 268 freebusy = self.store.get_freebusy(self.user) 269 270 if freebusy: 271 272 # Obtain any time zone details from the suggested event. 273 274 _dtstart, attr = get_item(details, "DTSTART") 275 tzid = attr.get("TZID") 276 277 # Show any conflicts. 278 279 for t in have_conflict(freebusy, [(dtstart, dtend)], True): 280 start, end, found_uid = t[:3] 281 if uid != found_uid: 282 start = format_datetime(to_timezone(get_datetime(start), tzid)) 283 end = format_datetime(to_timezone(get_datetime(end), tzid)) 284 self.page.p("Event conflicts with another from %s to %s." % (start, end)) 285 286 def show_requests_on_page(self): 287 288 "Show requests for the current user." 289 290 # NOTE: This list could be more informative, but it is envisaged that 291 # NOTE: the requests would be visited directly anyway. 292 293 requests = self._get_requests() 294 295 if requests: 296 self.page.p("Pending requests:") 297 298 self.page.ul() 299 300 for request in requests: 301 self.page.li() 302 self.page.a(request, href="%s/%s" % (self.env.get_url().rstrip("/"), request)) 303 self.page.li.close() 304 305 self.page.ul.close() 306 307 else: 308 self.page.p("There are no pending requests.") 309 310 # Full page output methods. 311 312 def show_object(self, path_info): 313 314 "Show an object request using the given 'path_info' for the current user." 315 316 uid = self._get_uid(path_info) 317 obj = self._get_object(uid) 318 319 if not obj: 320 return False 321 322 self.new_page(title="Event") 323 324 is_request = uid in self._get_requests() 325 326 show_form = is_request and self.handle_request(uid, obj) 327 328 self.show_object_on_page(uid, obj) 329 330 if show_form: 331 self.show_request_form() 332 333 return True 334 335 def show_calendar(self): 336 337 "Show the calendar for the current user." 338 339 self.new_page(title="Calendar") 340 self.show_requests_on_page() 341 342 freebusy = self.store.get_freebusy(self.user) 343 page = self.page 344 345 if not freebusy: 346 page.p("No events scheduled.") 347 return 348 349 # Day view: start at the earliest known day and produce days until the 350 # latest known day, perhaps with expandable sections of empty days. 351 352 # Month view: start at the earliest known month and produce months until 353 # the latest known month, perhaps with expandable sections of empty 354 # months. 355 356 # Details of users to invite to new events could be superimposed on the 357 # calendar. 358 359 # Requests could be listed and linked to their tentative positions in 360 # the calendar. 361 362 slots = get_slots(freebusy) 363 spans = get_spans(slots) 364 365 page.table(border=1, cellspacing=0, cellpadding=5) 366 367 for point, active in slots: 368 page.tr() 369 page.th(class_="timeslot") 370 page.add(point) 371 page.th.close() 372 373 for t in active: 374 if t: 375 start, end, uid = t[:3] 376 span = spans[uid] 377 if point == start: 378 379 page.td(class_="event", rowspan=span) 380 obj = self._get_object(uid) 381 if obj: 382 details = self._get_details(obj) 383 page.a(get_value(details, "SUMMARY"), href="%s/%s" % (self.env.get_url().rstrip("/"), uid)) 384 page.td.close() 385 else: 386 page.td(class_="empty") 387 page.td.close() 388 389 page.tr.close() 390 391 page.table.close() 392 393 def select_action(self): 394 395 "Select the desired action and show the result." 396 397 path_info = self.env.get_path_info().strip("/") 398 399 if not path_info: 400 self.show_calendar() 401 elif self.show_object(path_info): 402 pass 403 else: 404 self.no_page() 405 406 def __call__(self): 407 408 "Interpret a request and show an appropriate response." 409 410 if not self.user: 411 self.no_user() 412 else: 413 self.select_action() 414 415 # Write the headers and actual content. 416 417 print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding 418 print >>self.out 419 self.out.write(unicode(self.page).encode(self.encoding)) 420 421 if __name__ == "__main__": 422 Manager( 423 Messenger( 424 "imip-agent@example.com", 425 "Calendar system message", 426 "This is a message from the calendar system." 427 ) 428 )() 429 430 # vim: tabstop=4 expandtab shiftwidth=4