imip-agent

Annotated imip_manager.py

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