imip-agent

Annotated imipweb/resource.py

523:b9c05d30449f
2015-05-15 Paul Boddie Support the cancellation of previously unseparated recurrences.
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@511 25
from imiptools.dates import format_datetime, format_time, to_recurrence_start
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@511 205
            remove_affected_period(freebusy, uid, to_recurrence_start(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