imip-agent

Annotated imipweb/resource.py

468:801ac79598b1
2015-03-31 Paul Boddie Introduced Client as a base class of the handlers, so that self.user can be used to refer to the current user (typically the recipient) in the different handlers, and so that various convenience methods can be more widely used. Introduced testing for free/busy sharing before publishing details on the Web.
paul@446 1
#!/usr/bin/env python
paul@446 2
paul@446 3
"""
paul@446 4
Common resource functionality for Web calendar clients.
paul@446 5
paul@446 6
Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
paul@446 7
paul@446 8
This program is free software; you can redistribute it and/or modify it under
paul@446 9
the terms of the GNU General Public License as published by the Free Software
paul@446 10
Foundation; either version 3 of the License, or (at your option) any later
paul@446 11
version.
paul@446 12
paul@446 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@446 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@446 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@446 16
details.
paul@446 17
paul@446 18
You should have received a copy of the GNU General Public License along with
paul@446 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@446 20
"""
paul@446 21
paul@446 22
from datetime import datetime
paul@446 23
from imiptools.client import Client
paul@446 24
from imiptools.data import get_uri, get_window_end, Object, uri_values
paul@446 25
from imiptools.dates import format_datetime, format_time
paul@458 26
from imiptools.period import FreeBusyPeriod, \
paul@458 27
                             remove_period, remove_affected_period, update_freebusy
paul@446 28
from imipweb.env import CGIEnvironment
paul@446 29
import babel.dates
paul@446 30
import imip_store
paul@446 31
import markup
paul@446 32
paul@446 33
class Resource(Client):
paul@446 34
paul@446 35
    "A Web application resource and calendar client."
paul@446 36
paul@446 37
    def __init__(self, resource=None):
paul@446 38
        self.encoding = "utf-8"
paul@446 39
        self.env = CGIEnvironment(self.encoding)
paul@446 40
paul@446 41
        user = self.env.get_user()
paul@446 42
        Client.__init__(self, user and get_uri(user) or None)
paul@446 43
paul@446 44
        self.locale = None
paul@446 45
        self.requests = None
paul@446 46
paul@446 47
        self.out = resource and resource.out or self.env.get_output()
paul@446 48
        self.page = resource and resource.page or markup.page()
paul@446 49
        self.html_ids = None
paul@446 50
paul@446 51
        self.store = imip_store.FileStore()
paul@446 52
        self.objects = {}
paul@446 53
paul@446 54
        try:
paul@446 55
            self.publisher = imip_store.FilePublisher()
paul@446 56
        except OSError:
paul@446 57
            self.publisher = None
paul@446 58
paul@446 59
    # Presentation methods.
paul@446 60
paul@446 61
    def new_page(self, title):
paul@446 62
        self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css"))
paul@446 63
        self.html_ids = set()
paul@446 64
paul@446 65
    def status(self, code, message):
paul@446 66
        self.header("Status", "%s %s" % (code, message))
paul@446 67
paul@446 68
    def header(self, header, value):
paul@446 69
        print >>self.out, "%s: %s" % (header, value)
paul@446 70
paul@446 71
    def no_user(self):
paul@446 72
        self.status(403, "Forbidden")
paul@446 73
        self.new_page(title="Forbidden")
paul@446 74
        self.page.p("You are not logged in and thus cannot access scheduling requests.")
paul@446 75
paul@446 76
    def no_page(self):
paul@446 77
        self.status(404, "Not Found")
paul@446 78
        self.new_page(title="Not Found")
paul@446 79
        self.page.p("No page is provided at the given address.")
paul@446 80
paul@446 81
    def redirect(self, url):
paul@446 82
        self.status(302, "Redirect")
paul@446 83
        self.header("Location", url)
paul@446 84
        self.new_page(title="Redirect")
paul@446 85
        self.page.p("Redirecting to: %s" % url)
paul@446 86
paul@446 87
    def link_to(self, uid, recurrenceid=None):
paul@446 88
        if recurrenceid:
paul@446 89
            return self.env.new_url("/".join([uid, recurrenceid]))
paul@446 90
        else:
paul@446 91
            return self.env.new_url(uid)
paul@446 92
paul@446 93
    # Access to objects.
paul@446 94
paul@446 95
    def _suffixed_name(self, name, index=None):
paul@446 96
        return index is not None and "%s-%d" % (name, index) or name
paul@446 97
paul@446 98
    def _simple_suffixed_name(self, name, suffix, index=None):
paul@446 99
        return index is not None and "%s-%s" % (name, suffix) or name
paul@446 100
paul@446 101
    def _get_identifiers(self, path_info):
paul@446 102
        parts = path_info.lstrip("/").split("/")
paul@446 103
        if len(parts) == 1:
paul@446 104
            return parts[0], None
paul@446 105
        else:
paul@446 106
            return parts[:2]
paul@446 107
paul@446 108
    def _get_object(self, uid, recurrenceid=None):
paul@446 109
        if self.objects.has_key((uid, recurrenceid)):
paul@446 110
            return self.objects[(uid, recurrenceid)]
paul@446 111
paul@446 112
        fragment = uid and self.store.get_event(self.user, uid, recurrenceid) or None
paul@446 113
        obj = self.objects[(uid, recurrenceid)] = fragment and Object(fragment)
paul@446 114
        return obj
paul@446 115
paul@446 116
    def _get_recurrences(self, uid):
paul@446 117
        return self.store.get_recurrences(self.user, uid)
paul@446 118
paul@446 119
    def _get_requests(self):
paul@446 120
        if self.requests is None:
paul@446 121
            cancellations = self.store.get_cancellations(self.user)
paul@446 122
            requests = set(self.store.get_requests(self.user))
paul@446 123
            self.requests = requests.difference(cancellations)
paul@446 124
        return self.requests
paul@446 125
paul@446 126
    def _get_request_summary(self):
paul@446 127
        summary = []
paul@446 128
        for uid, recurrenceid in self._get_requests():
paul@446 129
            obj = self._get_object(uid, recurrenceid)
paul@446 130
            if obj:
paul@446 131
                periods = obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end())
paul@446 132
                recurrenceids = self._get_recurrences(uid)
paul@446 133
paul@446 134
                # Convert the periods to more substantial free/busy items.
paul@446 135
paul@458 136
                for p in periods:
paul@446 137
paul@446 138
                    # Subtract any recurrences from the free/busy details of a
paul@446 139
                    # parent object.
paul@446 140
paul@458 141
                    if recurrenceid or p.start not in recurrenceids:
paul@458 142
                        summary.append(
paul@458 143
                            FreeBusyPeriod(
paul@458 144
                                p.start, p.end, uid,
paul@458 145
                                obj.get_value("TRANSP"),
paul@458 146
                                recurrenceid,
paul@458 147
                                obj.get_value("SUMMARY"),
paul@458 148
                                obj.get_value("ORGANIZER")
paul@458 149
                                ))
paul@446 150
        return summary
paul@446 151
paul@446 152
    # Preference methods.
paul@446 153
paul@446 154
    def get_user_locale(self):
paul@446 155
        if not self.locale:
paul@446 156
            self.locale = self.get_preferences().get("LANG", "en")
paul@446 157
        return self.locale
paul@446 158
paul@446 159
    # Prettyprinting of dates and times.
paul@446 160
paul@446 161
    def format_date(self, dt, format):
paul@446 162
        return self._format_datetime(babel.dates.format_date, dt, format)
paul@446 163
paul@446 164
    def format_time(self, dt, format):
paul@446 165
        return self._format_datetime(babel.dates.format_time, dt, format)
paul@446 166
paul@446 167
    def format_datetime(self, dt, format):
paul@446 168
        return self._format_datetime(
paul@446 169
            isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date,
paul@446 170
            dt, format)
paul@446 171
paul@446 172
    def _format_datetime(self, fn, dt, format):
paul@446 173
        return fn(dt, format=format, locale=self.get_user_locale())
paul@446 174
paul@446 175
    # Data management methods.
paul@446 176
paul@446 177
    def remove_request(self, uid, recurrenceid=None):
paul@446 178
        return self.store.dequeue_request(self.user, uid, recurrenceid)
paul@446 179
paul@446 180
    def remove_event(self, uid, recurrenceid=None):
paul@446 181
        return self.store.remove_event(self.user, uid, recurrenceid)
paul@446 182
paul@446 183
    def update_freebusy(self, uid, recurrenceid, obj):
paul@446 184
paul@446 185
        """
paul@446 186
        Update stored free/busy details for the event with the given 'uid' and
paul@446 187
        'recurrenceid' having a representation of 'obj'.
paul@446 188
        """
paul@446 189
paul@446 190
        is_only_organiser = self.user not in uri_values(obj.get_values("ATTENDEE"))
paul@446 191
paul@446 192
        freebusy = self.store.get_freebusy(self.user)
paul@446 193
paul@446 194
        update_freebusy(freebusy,
paul@446 195
            obj.get_periods_for_freebusy(self.get_tzid(), self.get_window_end()),
paul@446 196
            is_only_organiser and "ORG" or obj.get_value("TRANSP"),
paul@446 197
            uid, recurrenceid,
paul@446 198
            obj.get_value("SUMMARY"),
paul@446 199
            obj.get_value("ORGANIZER"))
paul@446 200
paul@446 201
        # Subtract any recurrences from the free/busy details of a parent
paul@446 202
        # object.
paul@446 203
paul@446 204
        for recurrenceid in self._get_recurrences(uid):
paul@446 205
            remove_affected_period(freebusy, uid, recurrenceid)
paul@446 206
paul@446 207
        self.store.set_freebusy(self.user, freebusy)
paul@468 208
        self.publish_freebusy(freebusy)
paul@447 209
paul@446 210
    def remove_from_freebusy(self, uid, recurrenceid=None):
paul@446 211
        freebusy = self.store.get_freebusy(self.user)
paul@446 212
        remove_period(freebusy, uid, recurrenceid)
paul@446 213
        self.store.set_freebusy(self.user, freebusy)
paul@468 214
        self.publish_freebusy(freebusy)
paul@468 215
paul@468 216
    def publish_freebusy(self, freebusy):
paul@468 217
paul@468 218
        "Publish the details if configured to share them."
paul@468 219
paul@468 220
        if self.publisher and self.is_sharing():
paul@468 221
            self.publisher.set_freebusy(self.user, freebusy)
paul@446 222
paul@446 223
# vim: tabstop=4 expandtab shiftwidth=4