imip-agent

Annotated imip_manager.py

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