imip-agent

Annotated imip_manager.py

80:8b8e9d689126
2014-10-27 Paul Boddie Indicate when no pending requests exist.
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
paul@69 181
        requests = self.store.get_requests(self.user)
paul@70 182
paul@80 183
        if requests:
paul@80 184
            self.page.ul()
paul@69 185
paul@80 186
            for request in requests:
paul@80 187
                self.page.li()
paul@80 188
                self.page.a(request, href="%s/%s" % (self.env.get_url().rstrip("/"), request))
paul@80 189
                self.page.li.close()
paul@80 190
paul@80 191
            self.page.ul.close()
paul@80 192
paul@80 193
        else:
paul@80 194
            self.page.p("There are no pending requests.")
paul@69 195
paul@70 196
    def show_request(self, path_info):
paul@70 197
paul@70 198
        "Show a request using the given 'path_info' for the current user."
paul@70 199
paul@70 200
        uid = path_info.lstrip("/").split("/", 1)[0]
paul@70 201
        f = uid and self.store.get_event(self.user, uid) or None
paul@70 202
paul@70 203
        if not f:
paul@70 204
            return False
paul@70 205
paul@70 206
        request = parse_object(f, "utf-8")
paul@70 207
paul@70 208
        if not request:
paul@70 209
            return False
paul@70 210
paul@78 211
        objtype = request.keys()[0]
paul@78 212
        request = request[objtype][0]
paul@78 213
paul@73 214
        # Handle a submitted form.
paul@73 215
paul@73 216
        args = self.env.get_args()
paul@73 217
        show_form = False
paul@73 218
paul@78 219
        accept = args.has_key("accept")
paul@78 220
        decline = args.has_key("decline")
paul@77 221
paul@78 222
        if accept or decline:
paul@78 223
paul@79 224
            handler = ManagerHandler(request, objtype, self.user)
paul@77 225
paul@79 226
            if handler.process_request(accept):
paul@77 227
paul@79 228
                # Remove the request from the list.
paul@77 229
paul@79 230
                self.remove_request(uid)
paul@77 231
paul@73 232
        elif args.has_key("ignore"):
paul@78 233
paul@78 234
            # Remove the request from the list.
paul@78 235
paul@78 236
            self.remove_request(uid)
paul@78 237
paul@73 238
        else:
paul@73 239
            show_form = True
paul@73 240
paul@70 241
        self.new_page(title="Request")
paul@73 242
paul@73 243
        # Provide a summary of the request.
paul@73 244
paul@70 245
        self.page.p("The following request was received:")
paul@70 246
        self.page.dl()
paul@70 247
paul@73 248
        for name in ["SUMMARY", "DTSTART", "DTEND", "ORGANIZER", "ATTENDEE"]:
paul@70 249
            for value in get_values(request, name):
paul@70 250
                self.page.dt(name)
paul@70 251
                self.page.dd(value)
paul@70 252
paul@70 253
        self.page.dl.close()
paul@73 254
paul@73 255
        dtstart = format_datetime(get_utc_datetime(request, "DTSTART"))
paul@73 256
        dtend = format_datetime(get_utc_datetime(request, "DTEND"))
paul@73 257
paul@73 258
        # Indicate whether there are conflicting events.
paul@73 259
paul@79 260
        freebusy = self.store.get_freebusy(self.user)
paul@79 261
paul@73 262
        if freebusy:
paul@73 263
paul@73 264
            # Obtain any time zone details from the suggested event.
paul@73 265
paul@73 266
            _dtstart, attr = get_item(request, "DTSTART")
paul@73 267
            tzid = attr.get("TZID")
paul@73 268
paul@73 269
            # Show any conflicts.
paul@73 270
paul@77 271
            for start, end, found_uid in have_conflict(freebusy, [(dtstart, dtend)], True):
paul@77 272
                if uid != found_uid:
paul@77 273
                    start = format_datetime(to_timezone(get_datetime(start), tzid))
paul@77 274
                    end = format_datetime(to_timezone(get_datetime(end), tzid))
paul@77 275
                    self.page.p("Event conflicts with another from %s to %s." % (start, end))
paul@73 276
paul@73 277
        # Show a form if no action has just been taken.
paul@73 278
paul@73 279
        if show_form:
paul@73 280
            self.page.p("Action to take for this request:")
paul@73 281
            self.page.form(method="POST")
paul@73 282
            self.page.p()
paul@73 283
            self.page.input(name="accept", type="submit", value="Accept")
paul@73 284
            self.page.add(" ")
paul@73 285
            self.page.input(name="decline", type="submit", value="Decline")
paul@73 286
            self.page.add(" ")
paul@73 287
            self.page.input(name="ignore", type="submit", value="Ignore")
paul@73 288
            self.page.p.close()
paul@73 289
            self.page.form.close()
paul@73 290
paul@70 291
        return True
paul@70 292
paul@69 293
    def select_action(self):
paul@69 294
paul@69 295
        "Select the desired action and show the result."
paul@69 296
paul@69 297
        path_info = self.env.get_path_info().rstrip("/")
paul@69 298
        if not path_info:
paul@69 299
            self.show_requests()
paul@70 300
        elif self.show_request(path_info):
paul@70 301
            pass
paul@70 302
        else:
paul@70 303
            self.no_page()
paul@69 304
paul@69 305
    def show(self):
paul@69 306
paul@69 307
        "Interpret a request and show an appropriate response."
paul@69 308
paul@69 309
        if not self.user:
paul@69 310
            self.no_user()
paul@69 311
        else:
paul@69 312
            self.select_action()
paul@69 313
paul@70 314
        # Write the headers and actual content.
paul@70 315
paul@69 316
        print >>self.out, "Content-Type: text/html; charset=%s" % self.encoding
paul@69 317
        print >>self.out
paul@69 318
        self.out.write(unicode(self.page).encode(self.encoding))
paul@69 319
paul@69 320
if __name__ == "__main__":
paul@69 321
    Manager().show()
paul@69 322
paul@69 323
# vim: tabstop=4 expandtab shiftwidth=4