imip-agent

imip_manager.py

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