1 #!/usr/bin/env python 2 3 import cgi, os, sys 4 5 sys.path.append("/var/lib/imip-agent") 6 7 from imiptools import make_message, sendmail 8 from imiptools.content import format_datetime, get_address, get_datetime, \ 9 get_item, get_items, get_periods, get_uri, \ 10 get_utc_datetime, get_value, get_values, \ 11 parse_object, to_part, to_timezone, \ 12 update_freebusy 13 from imiptools.period import have_conflict 14 from vCalendar import to_node 15 import markup 16 import imip_store 17 18 getenv = os.environ.get 19 setenv = os.environ.__setitem__ 20 21 class CGIEnvironment: 22 23 "A CGI-compatible environment." 24 25 def __init__(self): 26 self.args = None 27 self.method = None 28 self.path = None 29 self.path_info = None 30 self.user = None 31 32 def get_args(self): 33 if self.args is None: 34 if self.get_method() != "POST": 35 setenv("QUERY_STRING", "") 36 self.args = cgi.parse(keep_blank_values=True) 37 return self.args 38 39 def get_method(self): 40 if self.method is None: 41 self.method = getenv("REQUEST_METHOD") or "GET" 42 return self.method 43 44 def get_path(self): 45 if self.path is None: 46 self.path = getenv("SCRIPT_NAME") or "" 47 return self.path 48 49 def get_path_info(self): 50 if self.path_info is None: 51 self.path_info = getenv("PATH_INFO") or "" 52 return self.path_info 53 54 def get_user(self): 55 if self.user is None: 56 self.user = getenv("REMOTE_USER") or "" 57 return self.user 58 59 def get_output(self): 60 return sys.stdout 61 62 def get_url(self): 63 path = self.get_path() 64 path_info = self.get_path_info() 65 return "%s%s" % (path.rstrip("/"), path_info) 66 67 class Manager: 68 69 "A simple manager application." 70 71 def __init__(self): 72 self.env = CGIEnvironment() 73 user = self.env.get_user() 74 self.user = user and get_uri(user) or None 75 self.out = self.env.get_output() 76 self.page = markup.page() 77 self.encoding = "utf-8" 78 79 self.store = imip_store.FileStore() 80 81 try: 82 self.publisher = imip_store.FilePublisher() 83 except OSError: 84 self.publisher = None 85 86 # Communication methods. 87 88 def send_message(self, objtype, obj, sender, recipients): 89 90 # Create a full calendar object and send it. 91 # NOTE: Should parameterise the subject and body text. 92 93 node = to_node({objtype : [(obj, {})]}) 94 part = to_part("REPLY", [node]) 95 message = make_message([part], recipients, sender, "Response to request", "Response to a calendar request") 96 sendmail(sender, recipients, message.as_string()) 97 98 # Data management methods. 99 100 def remove_request(self, uid): 101 requests = self.store.get_requests(self.user) 102 if uid in requests: 103 requests.remove(uid) 104 self.store.set_requests(self.user, requests) 105 106 # Presentation methods. 107 108 def new_page(self, title): 109 self.page.init(title=title, charset=self.encoding) 110 111 def status(self, code, message): 112 print >>self.out, "Status:", code, message 113 114 def no_user(self): 115 self.status(403, "Forbidden") 116 self.new_page(title="Forbidden") 117 self.page.p("You are not logged in and thus cannot access scheduling requests.") 118 119 def no_page(self): 120 self.status(404, "Not Found") 121 self.new_page(title="Not Found") 122 self.page.p("No page is provided at the given address.") 123 124 def show_requests(self): 125 126 "Show requests for the current user." 127 128 # NOTE: This list could be more informative, but it is envisaged that 129 # NOTE: the requests would be visited directly anyway. 130 131 self.new_page(title="Pending Requests") 132 self.page.ul() 133 134 requests = self.store.get_requests(self.user) 135 136 for request in requests: 137 self.page.li() 138 self.page.a(request, href="%s/%s" % (self.env.get_url().rstrip("/"), request)) 139 self.page.li.close() 140 141 self.page.ul.close() 142 143 def show_request(self, path_info): 144 145 "Show a request using the given 'path_info' for the current user." 146 147 uid = path_info.lstrip("/").split("/", 1)[0] 148 f = uid and self.store.get_event(self.user, uid) or None 149 150 if not f: 151 return False 152 153 request = parse_object(f, "utf-8") 154 155 if not request: 156 return False 157 158 objtype = request.keys()[0] 159 request = request[objtype][0] 160 161 # Handle a submitted form. 162 163 args = self.env.get_args() 164 show_form = False 165 166 organisers = map(get_address, get_values(request, "ORGANIZER")) 167 freebusy = self.store.get_freebusy(self.user) 168 169 accept = args.has_key("accept") 170 decline = args.has_key("decline") 171 172 if accept or decline: 173 174 # When accepting or declining, do so only on behalf of this user, 175 # preserving any other attributes set as an attendee. 176 177 for attendee, attendee_attr in get_items(request, "ATTENDEE"): 178 if attendee == self.user: 179 attendee_attr["PARTSTAT"] = accept and "ACCEPTED" or "DECLINED" 180 request["ATTENDEE"] = [(attendee, attendee_attr)] 181 self.send_message(objtype, request, get_address(attendee), organisers) 182 183 # Remove the request from the list. 184 185 self.remove_request(uid) 186 187 # Update the free/busy information. 188 189 if accept: 190 periods = get_periods(request) 191 update_freebusy(freebusy, attendee, periods, get_value(request, "TRANSP"), uid, self.store) 192 193 if self.publisher: 194 self.publisher.set_freebusy(attendee, freebusy) 195 196 break 197 198 elif args.has_key("ignore"): 199 200 # Remove the request from the list. 201 202 self.remove_request(uid) 203 204 else: 205 show_form = True 206 207 self.new_page(title="Request") 208 209 # Provide a summary of the request. 210 211 self.page.p("The following request was received:") 212 self.page.dl() 213 214 for name in ["SUMMARY", "DTSTART", "DTEND", "ORGANIZER", "ATTENDEE"]: 215 for value in get_values(request, name): 216 self.page.dt(name) 217 self.page.dd(value) 218 219 self.page.dl.close() 220 221 dtstart = format_datetime(get_utc_datetime(request, "DTSTART")) 222 dtend = format_datetime(get_utc_datetime(request, "DTEND")) 223 224 # Indicate whether there are conflicting events. 225 226 if freebusy: 227 228 # Obtain any time zone details from the suggested event. 229 230 _dtstart, attr = get_item(request, "DTSTART") 231 tzid = attr.get("TZID") 232 233 # Show any conflicts. 234 235 for start, end, found_uid in have_conflict(freebusy, [(dtstart, dtend)], True): 236 if uid != found_uid: 237 start = format_datetime(to_timezone(get_datetime(start), tzid)) 238 end = format_datetime(to_timezone(get_datetime(end), tzid)) 239 self.page.p("Event conflicts with another from %s to %s." % (start, end)) 240 241 # Show a form if no action has just been taken. 242 243 if show_form: 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 return True 256 257 def select_action(self): 258 259 "Select the desired action and show the result." 260 261 path_info = self.env.get_path_info().rstrip("/") 262 if not path_info: 263 self.show_requests() 264 elif self.show_request(path_info): 265 pass 266 else: 267 self.no_page() 268 269 def show(self): 270 271 "Interpret a request and show an appropriate response." 272 273 if not self.user: 274 self.no_user() 275 else: 276 self.select_action() 277 278 # Write the headers and actual content. 279 280 print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding 281 print >>self.out 282 self.out.write(unicode(self.page).encode(self.encoding)) 283 284 if __name__ == "__main__": 285 Manager().show() 286 287 # vim: tabstop=4 expandtab shiftwidth=4