imip-agent

Annotated imip_manager.py

82:56ec48e6cedb
2014-10-28 Paul Boddie Moved message sending functionality into a separate class.
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@82 7
from imiptools import Messenger
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@82 70
    def __init__(self, details, objtype, user, messenger):
paul@79 71
        Handler.__init__(self, details)
paul@79 72
        self.objtype = objtype
paul@79 73
        self.user = user
paul@82 74
        self.messenger = messenger
paul@82 75
paul@79 76
        self.organisers = map(get_address, self.get_values("ORGANIZER"))
paul@79 77
paul@79 78
    # Communication methods.
paul@79 79
paul@79 80
    def send_message(self, sender):
paul@79 81
paul@79 82
        """
paul@79 83
        Create a full calendar object and send it to the organisers from the
paul@79 84
        given 'sender'.
paul@79 85
        """
paul@79 86
paul@79 87
        node = to_node({self.objtype : [(self.details, {})]})
paul@79 88
        part = to_part("REPLY", [node])
paul@82 89
        message = self.messenger.make_message([part], self.organisers)
paul@82 90
        self.messenger.sendmail(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@82 131
    def __init__(self, messenger=None):
paul@82 132
        self.messenger = messenger or Messenger()
paul@82 133
paul@69 134
        self.env = CGIEnvironment()
paul@69 135
        user = self.env.get_user()
paul@77 136
        self.user = user and get_uri(user) or None
paul@69 137
        self.out = self.env.get_output()
paul@69 138
        self.page = markup.page()
paul@69 139
        self.encoding = "utf-8"
paul@69 140
paul@77 141
        self.store = imip_store.FileStore()
paul@77 142
paul@77 143
        try:
paul@77 144
            self.publisher = imip_store.FilePublisher()
paul@77 145
        except OSError:
paul@77 146
            self.publisher = None
paul@77 147
paul@78 148
    # Data management methods.
paul@78 149
paul@78 150
    def remove_request(self, uid):
paul@78 151
        requests = self.store.get_requests(self.user)
paul@78 152
        if uid in requests:
paul@78 153
            requests.remove(uid)
paul@78 154
            self.store.set_requests(self.user, requests)
paul@78 155
paul@78 156
    # Presentation methods.
paul@78 157
paul@69 158
    def new_page(self, title):
paul@69 159
        self.page.init(title=title, charset=self.encoding)
paul@69 160
paul@69 161
    def status(self, code, message):
paul@69 162
        print >>self.out, "Status:", code, message
paul@69 163
paul@69 164
    def no_user(self):
paul@69 165
        self.status(403, "Forbidden")
paul@69 166
        self.new_page(title="Forbidden")
paul@69 167
        self.page.p("You are not logged in and thus cannot access scheduling requests.")
paul@69 168
paul@70 169
    def no_page(self):
paul@70 170
        self.status(404, "Not Found")
paul@70 171
        self.new_page(title="Not Found")
paul@70 172
        self.page.p("No page is provided at the given address.")
paul@70 173
paul@69 174
    def show_requests(self):
paul@69 175
paul@69 176
        "Show requests for the current user."
paul@69 177
paul@69 178
        # NOTE: This list could be more informative, but it is envisaged that
paul@69 179
        # NOTE: the requests would be visited directly anyway.
paul@69 180
paul@69 181
        self.new_page(title="Pending Requests")
paul@69 182
paul@69 183
        requests = self.store.get_requests(self.user)
paul@70 184
paul@80 185
        if requests:
paul@80 186
            self.page.ul()
paul@69 187
paul@80 188
            for request in requests:
paul@80 189
                self.page.li()
paul@80 190
                self.page.a(request, href="%s/%s" % (self.env.get_url().rstrip("/"), request))
paul@80 191
                self.page.li.close()
paul@80 192
paul@80 193
            self.page.ul.close()
paul@80 194
paul@80 195
        else:
paul@80 196
            self.page.p("There are no pending requests.")
paul@69 197
paul@70 198
    def show_request(self, path_info):
paul@70 199
paul@70 200
        "Show a request using the given 'path_info' for the current user."
paul@70 201
paul@70 202
        uid = path_info.lstrip("/").split("/", 1)[0]
paul@70 203
        f = uid and self.store.get_event(self.user, uid) or None
paul@70 204
paul@70 205
        if not f:
paul@70 206
            return False
paul@70 207
paul@70 208
        request = parse_object(f, "utf-8")
paul@70 209
paul@70 210
        if not request:
paul@70 211
            return False
paul@70 212
paul@78 213
        objtype = request.keys()[0]
paul@78 214
        request = request[objtype][0]
paul@78 215
paul@73 216
        # Handle a submitted form.
paul@73 217
paul@73 218
        args = self.env.get_args()
paul@73 219
        show_form = False
paul@73 220
paul@78 221
        accept = args.has_key("accept")
paul@78 222
        decline = args.has_key("decline")
paul@77 223
paul@78 224
        if accept or decline:
paul@78 225
paul@82 226
            handler = ManagerHandler(request, objtype, self.user, self.messenger)
paul@77 227
paul@79 228
            if handler.process_request(accept):
paul@77 229
paul@79 230
                # Remove the request from the list.
paul@77 231
paul@79 232
                self.remove_request(uid)
paul@77 233
paul@73 234
        elif args.has_key("ignore"):
paul@78 235
paul@78 236
            # Remove the request from the list.
paul@78 237
paul@78 238
            self.remove_request(uid)
paul@78 239
paul@73 240
        else:
paul@73 241
            show_form = True
paul@73 242
paul@70 243
        self.new_page(title="Request")
paul@73 244
paul@73 245
        # Provide a summary of the request.
paul@73 246
paul@70 247
        self.page.p("The following request was received:")
paul@70 248
        self.page.dl()
paul@70 249
paul@73 250
        for name in ["SUMMARY", "DTSTART", "DTEND", "ORGANIZER", "ATTENDEE"]:
paul@70 251
            for value in get_values(request, name):
paul@70 252
                self.page.dt(name)
paul@70 253
                self.page.dd(value)
paul@70 254
paul@70 255
        self.page.dl.close()
paul@73 256
paul@73 257
        dtstart = format_datetime(get_utc_datetime(request, "DTSTART"))
paul@73 258
        dtend = format_datetime(get_utc_datetime(request, "DTEND"))
paul@73 259
paul@73 260
        # Indicate whether there are conflicting events.
paul@73 261
paul@79 262
        freebusy = self.store.get_freebusy(self.user)
paul@79 263
paul@73 264
        if freebusy:
paul@73 265
paul@73 266
            # Obtain any time zone details from the suggested event.
paul@73 267
paul@73 268
            _dtstart, attr = get_item(request, "DTSTART")
paul@73 269
            tzid = attr.get("TZID")
paul@73 270
paul@73 271
            # Show any conflicts.
paul@73 272
paul@77 273
            for start, end, found_uid in have_conflict(freebusy, [(dtstart, dtend)], True):
paul@77 274
                if uid != found_uid:
paul@77 275
                    start = format_datetime(to_timezone(get_datetime(start), tzid))
paul@77 276
                    end = format_datetime(to_timezone(get_datetime(end), tzid))
paul@77 277
                    self.page.p("Event conflicts with another from %s to %s." % (start, end))
paul@73 278
paul@73 279
        # Show a form if no action has just been taken.
paul@73 280
paul@73 281
        if show_form:
paul@73 282
            self.page.p("Action to take for this request:")
paul@73 283
            self.page.form(method="POST")
paul@73 284
            self.page.p()
paul@73 285
            self.page.input(name="accept", type="submit", value="Accept")
paul@73 286
            self.page.add(" ")
paul@73 287
            self.page.input(name="decline", type="submit", value="Decline")
paul@73 288
            self.page.add(" ")
paul@73 289
            self.page.input(name="ignore", type="submit", value="Ignore")
paul@73 290
            self.page.p.close()
paul@73 291
            self.page.form.close()
paul@73 292
paul@70 293
        return True
paul@70 294
paul@69 295
    def select_action(self):
paul@69 296
paul@69 297
        "Select the desired action and show the result."
paul@69 298
paul@69 299
        path_info = self.env.get_path_info().rstrip("/")
paul@69 300
        if not path_info:
paul@69 301
            self.show_requests()
paul@70 302
        elif self.show_request(path_info):
paul@70 303
            pass
paul@70 304
        else:
paul@70 305
            self.no_page()
paul@69 306
paul@82 307
    def __call__(self):
paul@69 308
paul@69 309
        "Interpret a request and show an appropriate response."
paul@69 310
paul@69 311
        if not self.user:
paul@69 312
            self.no_user()
paul@69 313
        else:
paul@69 314
            self.select_action()
paul@69 315
paul@70 316
        # Write the headers and actual content.
paul@70 317
paul@69 318
        print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding
paul@69 319
        print >>self.out
paul@69 320
        self.out.write(unicode(self.page).encode(self.encoding))
paul@69 321
paul@69 322
if __name__ == "__main__":
paul@82 323
    Manager()()
paul@69 324
paul@69 325
# vim: tabstop=4 expandtab shiftwidth=4