imip-agent

Annotated imipweb/resource.py

557:1006d9525386
2015-05-17 Paul Boddie Removed the special get_periods_for_freebusy function, introducing start and end point methods for period objects. Moved the is_replaced and is_affected methods into the Web resource class.
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@557 25
from imiptools.dates import format_datetime, format_time, get_recurrence_start, \
paul@557 26
                            to_recurrence_start
paul@458 27
from imiptools.period import FreeBusyPeriod, \
paul@458 28
                             remove_period, remove_affected_period, update_freebusy
paul@446 29
from imipweb.env import CGIEnvironment
paul@446 30
import babel.dates
paul@446 31
import imip_store
paul@446 32
import markup
paul@446 33
paul@446 34
class Resource(Client):
paul@446 35
paul@446 36
    "A Web application resource and calendar client."
paul@446 37
paul@446 38
    def __init__(self, resource=None):
paul@446 39
        self.encoding = "utf-8"
paul@446 40
        self.env = CGIEnvironment(self.encoding)
paul@446 41
paul@446 42
        user = self.env.get_user()
paul@446 43
        Client.__init__(self, user and get_uri(user) or None)
paul@446 44
paul@446 45
        self.locale = None
paul@446 46
        self.requests = None
paul@446 47
paul@446 48
        self.out = resource and resource.out or self.env.get_output()
paul@446 49
        self.page = resource and resource.page or markup.page()
paul@446 50
        self.html_ids = None
paul@446 51
paul@446 52
        self.store = imip_store.FileStore()
paul@446 53
        self.objects = {}
paul@446 54
paul@446 55
        try:
paul@446 56
            self.publisher = imip_store.FilePublisher()
paul@446 57
        except OSError:
paul@446 58
            self.publisher = None
paul@446 59
paul@446 60
    # Presentation methods.
paul@446 61
paul@446 62
    def new_page(self, title):
paul@446 63
        self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css"))
paul@446 64
        self.html_ids = set()
paul@446 65
paul@446 66
    def status(self, code, message):
paul@446 67
        self.header("Status", "%s %s" % (code, message))
paul@446 68
paul@446 69
    def header(self, header, value):
paul@446 70
        print >>self.out, "%s: %s" % (header, value)
paul@446 71
paul@446 72
    def no_user(self):
paul@446 73
        self.status(403, "Forbidden")
paul@446 74
        self.new_page(title="Forbidden")
paul@446 75
        self.page.p("You are not logged in and thus cannot access scheduling requests.")
paul@446 76
paul@446 77
    def no_page(self):
paul@446 78
        self.status(404, "Not Found")
paul@446 79
        self.new_page(title="Not Found")
paul@446 80
        self.page.p("No page is provided at the given address.")
paul@446 81
paul@446 82
    def redirect(self, url):
paul@446 83
        self.status(302, "Redirect")
paul@446 84
        self.header("Location", url)
paul@446 85
        self.new_page(title="Redirect")
paul@446 86
        self.page.p("Redirecting to: %s" % url)
paul@446 87
paul@446 88
    def link_to(self, uid, recurrenceid=None):
paul@446 89
        if recurrenceid:
paul@446 90
            return self.env.new_url("/".join([uid, recurrenceid]))
paul@446 91
        else:
paul@446 92
            return self.env.new_url(uid)
paul@446 93
paul@446 94
    # Access to objects.
paul@446 95
paul@446 96
    def _suffixed_name(self, name, index=None):
paul@446 97
        return index is not None and "%s-%d" % (name, index) or name
paul@446 98
paul@446 99
    def _simple_suffixed_name(self, name, suffix, index=None):
paul@446 100
        return index is not None and "%s-%s" % (name, suffix) or name
paul@446 101
paul@446 102
    def _get_identifiers(self, path_info):
paul@446 103
        parts = path_info.lstrip("/").split("/")
paul@446 104
        if len(parts) == 1:
paul@446 105
            return parts[0], None
paul@446 106
        else:
paul@446 107
            return parts[:2]
paul@446 108
paul@446 109
    def _get_object(self, uid, recurrenceid=None):
paul@446 110
        if self.objects.has_key((uid, recurrenceid)):
paul@446 111
            return self.objects[(uid, recurrenceid)]
paul@446 112
paul@446 113
        fragment = uid and self.store.get_event(self.user, uid, recurrenceid) or None
paul@446 114
        obj = self.objects[(uid, recurrenceid)] = fragment and Object(fragment)
paul@446 115
        return obj
paul@446 116
paul@446 117
    def _get_recurrences(self, uid):
paul@446 118
        return self.store.get_recurrences(self.user, uid)
paul@446 119
paul@446 120
    def _get_requests(self):
paul@446 121
        if self.requests is None:
paul@446 122
            cancellations = self.store.get_cancellations(self.user)
paul@446 123
            requests = set(self.store.get_requests(self.user))
paul@446 124
            self.requests = requests.difference(cancellations)
paul@446 125
        return self.requests
paul@446 126
paul@446 127
    def _get_request_summary(self):
paul@446 128
        summary = []
paul@446 129
        for uid, recurrenceid in self._get_requests():
paul@446 130
            obj = self._get_object(uid, recurrenceid)
paul@446 131
            if obj:
paul@557 132
                periods = obj.get_periods(self.get_tzid(), self.get_window_end())
paul@446 133
                recurrenceids = self._get_recurrences(uid)
paul@446 134
paul@446 135
                # Convert the periods to more substantial free/busy items.
paul@446 136
paul@458 137
                for p in periods:
paul@446 138
paul@446 139
                    # Subtract any recurrences from the free/busy details of a
paul@446 140
                    # parent object.
paul@446 141
paul@557 142
                    if recurrenceid or not self.is_replaced(p, recurrenceids):
paul@458 143
                        summary.append(
paul@458 144
                            FreeBusyPeriod(
paul@529 145
                                p.get_start(),
paul@529 146
                                p.get_end(),
paul@529 147
                                uid,
paul@458 148
                                obj.get_value("TRANSP"),
paul@458 149
                                recurrenceid,
paul@458 150
                                obj.get_value("SUMMARY"),
paul@541 151
                                obj.get_value("ORGANIZER"),
paul@541 152
                                self.get_tzid()
paul@458 153
                                ))
paul@446 154
        return summary
paul@446 155
paul@557 156
    # Period and recurrence testing.
paul@557 157
paul@557 158
    def is_replaced(self, period, recurrenceids):
paul@557 159
        for s in recurrenceids:
paul@557 160
            dt = get_recurrence_start(s, self.get_tzid())
paul@557 161
            if period.get_start() == dt:
paul@557 162
                return s
paul@557 163
        return None
paul@557 164
paul@557 165
    def is_affected(self, period, recurrenceid):
paul@557 166
        if not recurrenceid:
paul@557 167
            return None
paul@557 168
        dt = get_recurrence_start(recurrenceid, self.get_tzid())
paul@557 169
        if period.get_start() == dt:
paul@557 170
            return recurrenceid
paul@557 171
        return None
paul@557 172
paul@446 173
    # Preference methods.
paul@446 174
paul@446 175
    def get_user_locale(self):
paul@446 176
        if not self.locale:
paul@446 177
            self.locale = self.get_preferences().get("LANG", "en")
paul@446 178
        return self.locale
paul@446 179
paul@446 180
    # Prettyprinting of dates and times.
paul@446 181
paul@446 182
    def format_date(self, dt, format):
paul@446 183
        return self._format_datetime(babel.dates.format_date, dt, format)
paul@446 184
paul@446 185
    def format_time(self, dt, format):
paul@446 186
        return self._format_datetime(babel.dates.format_time, dt, format)
paul@446 187
paul@446 188
    def format_datetime(self, dt, format):
paul@446 189
        return self._format_datetime(
paul@446 190
            isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date,
paul@446 191
            dt, format)
paul@446 192
paul@446 193
    def _format_datetime(self, fn, dt, format):
paul@446 194
        return fn(dt, format=format, locale=self.get_user_locale())
paul@446 195
paul@446 196
    # Data management methods.
paul@446 197
paul@446 198
    def remove_request(self, uid, recurrenceid=None):
paul@446 199
        return self.store.dequeue_request(self.user, uid, recurrenceid)
paul@446 200
paul@446 201
    def remove_event(self, uid, recurrenceid=None):
paul@446 202
        return self.store.remove_event(self.user, uid, recurrenceid)
paul@446 203
paul@446 204
    def update_freebusy(self, uid, recurrenceid, obj):
paul@446 205
paul@446 206
        """
paul@446 207
        Update stored free/busy details for the event with the given 'uid' and
paul@446 208
        'recurrenceid' having a representation of 'obj'.
paul@446 209
        """
paul@446 210
paul@446 211
        is_only_organiser = self.user not in uri_values(obj.get_values("ATTENDEE"))
paul@446 212
paul@446 213
        freebusy = self.store.get_freebusy(self.user)
paul@446 214
paul@446 215
        update_freebusy(freebusy,
paul@557 216
            obj.get_periods(self.get_tzid(), self.get_window_end()),
paul@446 217
            is_only_organiser and "ORG" or obj.get_value("TRANSP"),
paul@446 218
            uid, recurrenceid,
paul@446 219
            obj.get_value("SUMMARY"),
paul@446 220
            obj.get_value("ORGANIZER"))
paul@446 221
paul@446 222
        # Subtract any recurrences from the free/busy details of a parent
paul@446 223
        # object.
paul@446 224
paul@446 225
        for recurrenceid in self._get_recurrences(uid):
paul@511 226
            remove_affected_period(freebusy, uid, to_recurrence_start(recurrenceid))
paul@446 227
paul@446 228
        self.store.set_freebusy(self.user, freebusy)
paul@468 229
        self.publish_freebusy(freebusy)
paul@447 230
paul@446 231
    def remove_from_freebusy(self, uid, recurrenceid=None):
paul@446 232
        freebusy = self.store.get_freebusy(self.user)
paul@446 233
        remove_period(freebusy, uid, recurrenceid)
paul@446 234
        self.store.set_freebusy(self.user, freebusy)
paul@468 235
        self.publish_freebusy(freebusy)
paul@468 236
paul@468 237
    def publish_freebusy(self, freebusy):
paul@468 238
paul@468 239
        "Publish the details if configured to share them."
paul@468 240
paul@468 241
        if self.publisher and self.is_sharing():
paul@468 242
            self.publisher.set_freebusy(self.user, freebusy)
paul@446 243
paul@446 244
# vim: tabstop=4 expandtab shiftwidth=4