imip-agent

Annotated imip_manager.py

78:2d5d4b71ab4e
2014-10-27 Paul Boddie Added support for declining and ignoring requests. Made parse_object return the entire object if objtype is not specified.
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@78 86
    # Communication methods.
paul@78 87
paul@78 88
    def send_message(self, objtype, obj, sender, recipients):
paul@78 89
paul@78 90
        # Create a full calendar object and send it.
paul@78 91
        # NOTE: Should parameterise the subject and body text.
paul@78 92
paul@78 93
        node = to_node({objtype : [(obj, {})]})
paul@78 94
        part = to_part("REPLY", [node])
paul@78 95
        message = make_message([part], recipients, sender, "Response to request", "Response to a calendar request")
paul@78 96
        sendmail(sender, recipients, message.as_string())
paul@78 97
paul@78 98
    # Data management methods.
paul@78 99
paul@78 100
    def remove_request(self, uid):
paul@78 101
        requests = self.store.get_requests(self.user)
paul@78 102
        if uid in requests:
paul@78 103
            requests.remove(uid)
paul@78 104
            self.store.set_requests(self.user, requests)
paul@78 105
paul@78 106
    # Presentation methods.
paul@78 107
paul@69 108
    def new_page(self, title):
paul@69 109
        self.page.init(title=title, charset=self.encoding)
paul@69 110
paul@69 111
    def status(self, code, message):
paul@69 112
        print >>self.out, "Status:", code, message
paul@69 113
paul@69 114
    def no_user(self):
paul@69 115
        self.status(403, "Forbidden")
paul@69 116
        self.new_page(title="Forbidden")
paul@69 117
        self.page.p("You are not logged in and thus cannot access scheduling requests.")
paul@69 118
paul@70 119
    def no_page(self):
paul@70 120
        self.status(404, "Not Found")
paul@70 121
        self.new_page(title="Not Found")
paul@70 122
        self.page.p("No page is provided at the given address.")
paul@70 123
paul@69 124
    def show_requests(self):
paul@69 125
paul@69 126
        "Show requests for the current user."
paul@69 127
paul@69 128
        # NOTE: This list could be more informative, but it is envisaged that
paul@69 129
        # NOTE: the requests would be visited directly anyway.
paul@69 130
paul@69 131
        self.new_page(title="Pending Requests")
paul@69 132
        self.page.ul()
paul@69 133
paul@69 134
        requests = self.store.get_requests(self.user)
paul@70 135
paul@69 136
        for request in requests:
paul@69 137
            self.page.li()
paul@77 138
            self.page.a(request, href="%s/%s" % (self.env.get_url().rstrip("/"), request))
paul@69 139
            self.page.li.close()
paul@69 140
paul@69 141
        self.page.ul.close()
paul@69 142
paul@70 143
    def show_request(self, path_info):
paul@70 144
paul@70 145
        "Show a request using the given 'path_info' for the current user."
paul@70 146
paul@70 147
        uid = path_info.lstrip("/").split("/", 1)[0]
paul@70 148
        f = uid and self.store.get_event(self.user, uid) or None
paul@70 149
paul@70 150
        if not f:
paul@70 151
            return False
paul@70 152
paul@70 153
        request = parse_object(f, "utf-8")
paul@70 154
paul@70 155
        if not request:
paul@70 156
            return False
paul@70 157
paul@78 158
        objtype = request.keys()[0]
paul@78 159
        request = request[objtype][0]
paul@78 160
paul@73 161
        # Handle a submitted form.
paul@73 162
paul@73 163
        args = self.env.get_args()
paul@73 164
        show_form = False
paul@73 165
paul@78 166
        organisers = map(get_address, get_values(request, "ORGANIZER"))
paul@77 167
        freebusy = self.store.get_freebusy(self.user)
paul@77 168
paul@78 169
        accept = args.has_key("accept")
paul@78 170
        decline = args.has_key("decline")
paul@77 171
paul@78 172
        if accept or decline:
paul@78 173
paul@78 174
            # When accepting or declining, do so only on behalf of this user,
paul@78 175
            # preserving any other attributes set as an attendee.
paul@77 176
paul@77 177
            for attendee, attendee_attr in get_items(request, "ATTENDEE"):
paul@77 178
                if attendee == self.user:
paul@78 179
                    attendee_attr["PARTSTAT"] = accept and "ACCEPTED" or "DECLINED"
paul@77 180
                    request["ATTENDEE"] = [(attendee, attendee_attr)]
paul@78 181
                    self.send_message(objtype, request, get_address(attendee), organisers)
paul@77 182
paul@77 183
                    # Remove the request from the list.
paul@77 184
paul@78 185
                    self.remove_request(uid)
paul@77 186
paul@77 187
                    # Update the free/busy information.
paul@77 188
paul@78 189
                    if accept:
paul@78 190
                        periods = get_periods(request)
paul@78 191
                        update_freebusy(freebusy, attendee, periods, get_value(request, "TRANSP"), uid, self.store)
paul@77 192
paul@78 193
                        if self.publisher:
paul@78 194
                            self.publisher.set_freebusy(attendee, freebusy)
paul@77 195
paul@77 196
                    break
paul@77 197
paul@73 198
        elif args.has_key("ignore"):
paul@78 199
paul@78 200
            # Remove the request from the list.
paul@78 201
paul@78 202
            self.remove_request(uid)
paul@78 203
paul@73 204
        else:
paul@73 205
            show_form = True
paul@73 206
paul@70 207
        self.new_page(title="Request")
paul@73 208
paul@73 209
        # Provide a summary of the request.
paul@73 210
paul@70 211
        self.page.p("The following request was received:")
paul@70 212
        self.page.dl()
paul@70 213
paul@73 214
        for name in ["SUMMARY", "DTSTART", "DTEND", "ORGANIZER", "ATTENDEE"]:
paul@70 215
            for value in get_values(request, name):
paul@70 216
                self.page.dt(name)
paul@70 217
                self.page.dd(value)
paul@70 218
paul@70 219
        self.page.dl.close()
paul@73 220
paul@73 221
        dtstart = format_datetime(get_utc_datetime(request, "DTSTART"))
paul@73 222
        dtend = format_datetime(get_utc_datetime(request, "DTEND"))
paul@73 223
paul@73 224
        # Indicate whether there are conflicting events.
paul@73 225
paul@73 226
        if freebusy:
paul@73 227
paul@73 228
            # Obtain any time zone details from the suggested event.
paul@73 229
paul@73 230
            _dtstart, attr = get_item(request, "DTSTART")
paul@73 231
            tzid = attr.get("TZID")
paul@73 232
paul@73 233
            # Show any conflicts.
paul@73 234
paul@77 235
            for start, end, found_uid in have_conflict(freebusy, [(dtstart, dtend)], True):
paul@77 236
                if uid != found_uid:
paul@77 237
                    start = format_datetime(to_timezone(get_datetime(start), tzid))
paul@77 238
                    end = format_datetime(to_timezone(get_datetime(end), tzid))
paul@77 239
                    self.page.p("Event conflicts with another from %s to %s." % (start, end))
paul@73 240
paul@73 241
        # Show a form if no action has just been taken.
paul@73 242
paul@73 243
        if show_form:
paul@73 244
            self.page.p("Action to take for this request:")
paul@73 245
            self.page.form(method="POST")
paul@73 246
            self.page.p()
paul@73 247
            self.page.input(name="accept", type="submit", value="Accept")
paul@73 248
            self.page.add(" ")
paul@73 249
            self.page.input(name="decline", type="submit", value="Decline")
paul@73 250
            self.page.add(" ")
paul@73 251
            self.page.input(name="ignore", type="submit", value="Ignore")
paul@73 252
            self.page.p.close()
paul@73 253
            self.page.form.close()
paul@73 254
paul@70 255
        return True
paul@70 256
paul@69 257
    def select_action(self):
paul@69 258
paul@69 259
        "Select the desired action and show the result."
paul@69 260
paul@69 261
        path_info = self.env.get_path_info().rstrip("/")
paul@69 262
        if not path_info:
paul@69 263
            self.show_requests()
paul@70 264
        elif self.show_request(path_info):
paul@70 265
            pass
paul@70 266
        else:
paul@70 267
            self.no_page()
paul@69 268
paul@69 269
    def show(self):
paul@69 270
paul@69 271
        "Interpret a request and show an appropriate response."
paul@69 272
paul@69 273
        if not self.user:
paul@69 274
            self.no_user()
paul@69 275
        else:
paul@69 276
            self.select_action()
paul@69 277
paul@70 278
        # Write the headers and actual content.
paul@70 279
paul@69 280
        print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding
paul@69 281
        print >>self.out
paul@69 282
        self.out.write(unicode(self.page).encode(self.encoding))
paul@69 283
paul@69 284
if __name__ == "__main__":
paul@69 285
    Manager().show()
paul@69 286
paul@69 287
# vim: tabstop=4 expandtab shiftwidth=4