imip-agent

imip_manager.py

151:b776588ba8a9
2015-01-13 Paul Boddie Experiment with day headings using Babel formatting.
     1 #!/usr/bin/env python     2      3 """     4 A Web interface to a user's calendar.     5      6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>     7      8 This program is free software; you can redistribute it and/or modify it under     9 the terms of the GNU General Public License as published by the Free Software    10 Foundation; either version 3 of the License, or (at your option) any later    11 version.    12     13 This program is distributed in the hope that it will be useful, but WITHOUT    14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    15 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    16 details.    17     18 You should have received a copy of the GNU General Public License along with    19 this program.  If not, see <http://www.gnu.org/licenses/>.    20 """    21     22 # Edit this path to refer to the location of the imiptools libraries, if    23 # necessary.    24     25 LIBRARY_PATH = "/var/lib/imip-agent"    26     27 import babel.dates    28 import cgi, os, sys    29     30 sys.path.append(LIBRARY_PATH)    31     32 from imiptools.content import Handler, \    33                               format_datetime, get_address, get_datetime, \    34                               get_item, get_uri, get_utc_datetime, get_value, \    35                               get_values, parse_object, to_part, to_timezone    36 from imiptools.mail import Messenger    37 from imiptools.period import have_conflict, get_slots, get_spans    38 from imiptools.profile import Preferences    39 from vCalendar import to_node    40 import markup    41 import imip_store    42     43 getenv = os.environ.get    44 setenv = os.environ.__setitem__    45     46 class CGIEnvironment:    47     48     "A CGI-compatible environment."    49     50     def __init__(self):    51         self.args = None    52         self.method = None    53         self.path = None    54         self.path_info = None    55         self.user = None    56     57     def get_args(self):    58         if self.args is None:    59             if self.get_method() != "POST":    60                 setenv("QUERY_STRING", "")    61             self.args = cgi.parse(keep_blank_values=True)    62         return self.args    63     64     def get_method(self):    65         if self.method is None:    66             self.method = getenv("REQUEST_METHOD") or "GET"    67         return self.method    68     69     def get_path(self):    70         if self.path is None:    71             self.path = getenv("SCRIPT_NAME") or ""    72         return self.path    73     74     def get_path_info(self):    75         if self.path_info is None:    76             self.path_info = getenv("PATH_INFO") or ""    77         return self.path_info    78     79     def get_user(self):    80         if self.user is None:    81             self.user = getenv("REMOTE_USER") or ""    82         return self.user    83     84     def get_output(self):    85         return sys.stdout    86     87     def get_url(self):    88         path = self.get_path()    89         path_info = self.get_path_info()    90         return "%s%s" % (path.rstrip("/"), path_info)    91     92 class ManagerHandler(Handler):    93     94     """    95     A content handler for use by the manager, as opposed to operating within the    96     mail processing pipeline.    97     """    98     99     def __init__(self, obj, user, messenger):   100         details, details_attr = obj.values()[0]   101         Handler.__init__(self, details)   102         self.obj = obj   103         self.user = user   104         self.messenger = messenger   105    106         self.organisers = map(get_address, self.get_values("ORGANIZER"))   107    108     # Communication methods.   109    110     def send_message(self, sender):   111    112         """   113         Create a full calendar object and send it to the organisers, sending a   114         copy to the 'sender'.   115         """   116    117         node = to_node(self.obj)   118         part = to_part("REPLY", [node])   119         message = self.messenger.make_message([part], self.organisers, outgoing_bcc=sender)   120         self.messenger.sendmail(self.organisers, message.as_string(), outgoing_bcc=sender)   121    122     # Action methods.   123    124     def process_request(self, accept):   125    126         """   127         Process the current request for the given 'user', accepting any request   128         when 'accept' is true, declining requests otherwise. Return whether any   129         action was taken.   130         """   131    132         # When accepting or declining, do so only on behalf of this user,   133         # preserving any other attributes set as an attendee.   134    135         for attendee, attendee_attr in self.get_items("ATTENDEE"):   136    137             if attendee == self.user:   138                 freebusy = self.store.get_freebusy(attendee)   139    140                 attendee_attr["PARTSTAT"] = accept and "ACCEPTED" or "DECLINED"   141                 if self.messenger and self.messenger.sender != get_address(attendee):   142                     attendee_attr["SENT-BY"] = get_uri(self.messenger.sender)   143                 self.details["ATTENDEE"] = [(attendee, attendee_attr)]   144                 self.send_message(get_address(attendee))   145    146                 return True   147    148         return False   149    150 class Manager:   151    152     "A simple manager application."   153    154     def __init__(self, messenger=None):   155         self.messenger = messenger or Messenger()   156    157         self.env = CGIEnvironment()   158         user = self.env.get_user()   159         self.user = user and get_uri(user) or None   160         self.preferences = None   161         self.locale = None   162         self.requests = None   163    164         self.out = self.env.get_output()   165         self.page = markup.page()   166         self.encoding = "utf-8"   167    168         self.store = imip_store.FileStore()   169    170         try:   171             self.publisher = imip_store.FilePublisher()   172         except OSError:   173             self.publisher = None   174    175     def _get_uid(self, path_info):   176         return path_info.lstrip("/").split("/", 1)[0]   177    178     def _get_object(self, uid):   179         f = uid and self.store.get_event(self.user, uid) or None   180    181         if not f:   182             return None   183    184         obj = parse_object(f, "utf-8")   185    186         if not obj:   187             return None   188    189         return obj   190    191     def _get_details(self, obj):   192         details, details_attr = obj.values()[0]   193         return details   194    195     def _get_requests(self):   196         if self.requests is None:   197             self.requests = self.store.get_requests(self.user)   198         return self.requests   199    200     # Preference methods.   201    202     def get_user_locale(self):   203         if not self.locale:   204             self.locale = self.get_preferences().get("LANG", "C")   205         return self.locale   206    207     def get_preferences(self):   208         if not self.preferences:   209             self.preferences = Preferences(self.user)   210         return self.preferences   211    212     def format_date(self, dt, format):   213         return self._format_datetime(babel.dates.format_date, dt, format)   214    215     def format_time(self, dt, format):   216         return self._format_datetime(babel.dates.format_time, dt, format)   217    218     def format_datetime(self, dt, format):   219         return self._format_datetime(babel.dates.format_datetime, dt, format)   220    221     def _format_datetime(self, fn, dt, format):   222         return fn(dt, format=format, locale=self.get_user_locale())   223    224     # Data management methods.   225    226     def remove_request(self, uid):   227         return self.store.dequeue_request(self.user, uid)   228    229     # Presentation methods.   230    231     def new_page(self, title):   232         self.page.init(title=title, charset=self.encoding)   233    234     def status(self, code, message):   235         self.header("Status", "%s %s" % (code, message))   236    237     def header(self, header, value):   238         print >>self.out, "%s: %s" % (header, value)   239    240     def no_user(self):   241         self.status(403, "Forbidden")   242         self.new_page(title="Forbidden")   243         self.page.p("You are not logged in and thus cannot access scheduling requests.")   244    245     def no_page(self):   246         self.status(404, "Not Found")   247         self.new_page(title="Not Found")   248         self.page.p("No page is provided at the given address.")   249    250     def redirect(self, url):   251         self.status(302, "Redirect")   252         self.header("Location", url)   253         self.new_page(title="Redirect")   254         self.page.p("Redirecting to: %s" % url)   255    256     # Request logic and page fragment methods.   257    258     def handle_request(self, uid, request):   259    260         "Handle actions involving the given 'uid' and 'request' object."   261    262         # Handle a submitted form.   263    264         args = self.env.get_args()   265         handled = True   266    267         accept = args.has_key("accept")   268         decline = args.has_key("decline")   269    270         if accept or decline:   271    272             handler = ManagerHandler(request, self.user, self.messenger)   273    274             if handler.process_request(accept):   275    276                 # Remove the request from the list.   277    278                 self.remove_request(uid)   279    280         elif args.has_key("ignore"):   281    282             # Remove the request from the list.   283    284             self.remove_request(uid)   285    286         else:   287             handled = False   288    289         if handled:   290             self.redirect(self.env.get_path())   291    292         return handled   293    294     def show_request_form(self):   295    296         "Show a form for a request."   297    298         self.page.p("Action to take for this request:")   299         self.page.form(method="POST")   300         self.page.p()   301         self.page.input(name="accept", type="submit", value="Accept")   302         self.page.add(" ")   303         self.page.input(name="decline", type="submit", value="Decline")   304         self.page.add(" ")   305         self.page.input(name="ignore", type="submit", value="Ignore")   306         self.page.p.close()   307         self.page.form.close()   308    309     def show_object_on_page(self, uid, obj):   310    311         """   312         Show the calendar object with the given 'uid' and representation 'obj'   313         on the current page.   314         """   315    316         details = self._get_details(obj)   317    318         # Provide a summary of the object.   319    320         self.page.dl()   321    322         for name in ["SUMMARY", "DTSTART", "DTEND", "ORGANIZER", "ATTENDEE"]:   323             if name in ["DTSTART", "DTEND"]:   324                 value, attr = get_item(details, name)   325                 tzid = attr.get("TZID")   326                 value = self.format_datetime(to_timezone(get_datetime(value), tzid), "full")   327                 self.page.dt(name)   328                 self.page.dd(value)   329             else:   330                 for value in get_values(details, name):   331                     self.page.dt(name)   332                     self.page.dd(value)   333    334         self.page.dl.close()   335    336         dtstart = format_datetime(get_utc_datetime(details, "DTSTART"))   337         dtend = format_datetime(get_utc_datetime(details, "DTEND"))   338    339         # Indicate whether there are conflicting events.   340    341         freebusy = self.store.get_freebusy(self.user)   342    343         if freebusy:   344    345             # Obtain any time zone details from the suggested event.   346    347             _dtstart, attr = get_item(details, "DTSTART")   348             tzid = attr.get("TZID")   349    350             # Show any conflicts.   351    352             for t in have_conflict(freebusy, [(dtstart, dtend)], True):   353                 start, end, found_uid = t[:3]   354                 if uid != found_uid:   355                     start = self.format_datetime(to_timezone(get_datetime(start), tzid), "full")   356                     end = self.format_datetime(to_timezone(get_datetime(end), tzid), "full")   357                     self.page.p("Event conflicts with another from %s to %s." % (start, end))   358    359     def show_requests_on_page(self):   360    361         "Show requests for the current user."   362    363         # NOTE: This list could be more informative, but it is envisaged that   364         # NOTE: the requests would be visited directly anyway.   365    366         requests = self._get_requests()   367    368         if requests:   369             self.page.p("Pending requests:")   370    371             self.page.ul()   372    373             for request in requests:   374                 self.page.li()   375                 self.page.a(request, href="%s/%s" % (self.env.get_url().rstrip("/"), request))   376                 self.page.li.close()   377    378             self.page.ul.close()   379    380         else:   381             self.page.p("There are no pending requests.")   382    383     # Full page output methods.   384    385     def show_object(self, path_info):   386    387         "Show an object request using the given 'path_info' for the current user."   388    389         uid = self._get_uid(path_info)   390         obj = self._get_object(uid)   391    392         if not obj:   393             return False   394    395         is_request = uid in self._get_requests()   396         handled = is_request and self.handle_request(uid, obj)   397    398         if handled:   399             return True   400    401         self.new_page(title="Event")   402    403         self.show_object_on_page(uid, obj)   404    405         if is_request and not handled:   406             self.show_request_form()   407    408         return True   409    410     def show_calendar(self):   411    412         "Show the calendar for the current user."   413    414         self.new_page(title="Calendar")   415         self.show_requests_on_page()   416    417         freebusy = self.store.get_freebusy(self.user)   418         page = self.page   419    420         if not freebusy:   421             page.p("No events scheduled.")   422             return   423    424         # Set the locale and obtain the user's timezone.   425    426         prefs = self.get_preferences()   427         tzid = prefs.get("TZID")   428    429         # Day view: start at the earliest known day and produce days until the   430         # latest known day, perhaps with expandable sections of empty days.   431    432         # Month view: start at the earliest known month and produce months until   433         # the latest known month, perhaps with expandable sections of empty   434         # months.   435    436         # Details of users to invite to new events could be superimposed on the   437         # calendar.   438    439         # Requests could be listed and linked to their tentative positions in   440         # the calendar.   441    442         slots = get_slots(freebusy)   443         spans = get_spans(slots)   444    445         page.table(border=1, cellspacing=0, cellpadding=5)   446    447         last_day = None   448         columns = max(map(lambda i: len(i[1]), slots)) + 1   449    450         for point, active in slots:   451             dt = to_timezone(get_datetime(point), tzid or "UTC")   452             day = dt.date()   453    454             if not last_day or last_day < day:   455                 page.tr()   456                 page.th(class_="timeslot", colspan=columns)   457                 page.add(self.format_date(day, "full"))   458                 page.th.close()   459                 page.tr.close()   460    461             page.tr()   462             page.th(class_="timeslot")   463             page.add(self.format_date(dt, "full"))   464             page.br()   465             page.add(self.format_time(dt, "long"))   466             page.th.close()   467    468             for t in active:   469                 if t:   470                     start, end, uid = t[:3]   471                     span = spans[uid]   472                     if point == start:   473    474                         page.td(class_="event", rowspan=span)   475                         obj = self._get_object(uid)   476                         if obj:   477                             details = self._get_details(obj)   478                             page.a(get_value(details, "SUMMARY"), href="%s/%s" % (self.env.get_url().rstrip("/"), uid))   479                         page.td.close()   480                 else:   481                     page.td(class_="empty")   482                     page.td.close()   483    484             page.tr.close()   485             last_day = day   486    487         page.table.close()   488    489     def select_action(self):   490    491         "Select the desired action and show the result."   492    493         path_info = self.env.get_path_info().strip("/")   494    495         if not path_info:   496             self.show_calendar()   497         elif self.show_object(path_info):   498             pass   499         else:   500             self.no_page()   501    502     def __call__(self):   503    504         "Interpret a request and show an appropriate response."   505    506         if not self.user:   507             self.no_user()   508         else:   509             self.select_action()   510    511         # Write the headers and actual content.   512    513         print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding   514         print >>self.out   515         self.out.write(unicode(self.page).encode(self.encoding))   516    517 if __name__ == "__main__":   518     Manager()()   519    520 # vim: tabstop=4 expandtab shiftwidth=4