imip-agent

Annotated imipweb/data.py

787:9cf10fe21c3a
2015-09-28 Paul Boddie Separated attendee/recurrence manipulation from presentation, introducing form field dictionary updates from form period/date objects, also simplifying the processing of attendees, removing filtering operations during editing. imipweb-client-simplification
paul@497 1
#!/usr/bin/env python
paul@497 2
paul@497 3
"""
paul@497 4
Web interface data abstractions.
paul@497 5
paul@497 6
Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
paul@497 7
paul@497 8
This program is free software; you can redistribute it and/or modify it under
paul@497 9
the terms of the GNU General Public License as published by the Free Software
paul@497 10
Foundation; either version 3 of the License, or (at your option) any later
paul@497 11
version.
paul@497 12
paul@497 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@497 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@497 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@497 16
details.
paul@497 17
paul@497 18
You should have received a copy of the GNU General Public License along with
paul@497 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@497 20
"""
paul@497 21
paul@556 22
from datetime import datetime, timedelta
paul@539 23
from imiptools.dates import end_date_from_calendar, end_date_to_calendar, \
paul@538 24
                            format_datetime, get_datetime, get_end_of_day, \
paul@532 25
                            to_date
paul@621 26
from imiptools.period import RecurringPeriod
paul@497 27
paul@498 28
class PeriodError(Exception):
paul@498 29
    pass
paul@498 30
paul@539 31
class EventPeriod(RecurringPeriod):
paul@497 32
paul@498 33
    """
paul@498 34
    A simple period plus attribute details, compatible with RecurringPeriod, and
paul@498 35
    intended to represent information obtained from an iCalendar resource.
paul@498 36
    """
paul@497 37
paul@541 38
    def __init__(self, start, end, tzid=None, origin=None, start_attr=None, end_attr=None, form_start=None, form_end=None):
paul@528 39
paul@528 40
        """
paul@528 41
        Initialise a period with the given 'start' and 'end' datetimes, together
paul@528 42
        with optional 'start_attr' and 'end_attr' metadata, 'form_start' and
paul@528 43
        'form_end' values provided as textual input, and with an optional
paul@528 44
        'origin' indicating the kind of period this object describes.
paul@528 45
        """
paul@528 46
paul@541 47
        RecurringPeriod.__init__(self, start, end, tzid, origin, start_attr, end_attr)
paul@498 48
        self.form_start = form_start
paul@498 49
        self.form_end = form_end
paul@498 50
paul@498 51
    def as_tuple(self):
paul@541 52
        return self.start, self.end, self.tzid, self.origin, self.start_attr, self.end_attr, self.form_start, self.form_end
paul@498 53
paul@498 54
    def __repr__(self):
paul@541 55
        return "EventPeriod(%r)" % (self.as_tuple(),)
paul@499 56
paul@499 57
    def as_event_period(self):
paul@499 58
        return self
paul@499 59
paul@700 60
    def get_start_item(self):
paul@700 61
        return self.get_start(), self.get_start_attr()
paul@700 62
paul@700 63
    def get_end_item(self):
paul@700 64
        return self.get_end(), self.get_end_attr()
paul@700 65
paul@499 66
    # Form data compatibility methods.
paul@498 67
paul@498 68
    def get_form_start(self):
paul@498 69
        if not self.form_start:
paul@499 70
            self.form_start = self.get_form_date(self.get_start(), self.start_attr)
paul@498 71
        return self.form_start
paul@498 72
paul@498 73
    def get_form_end(self):
paul@498 74
        if not self.form_end:
paul@539 75
            self.form_end = self.get_form_date(end_date_from_calendar(self.get_end()), self.end_attr)
paul@498 76
        return self.form_end
paul@498 77
paul@498 78
    def as_form_period(self):
paul@498 79
        return FormPeriod(
paul@499 80
            self.get_form_start(),
paul@499 81
            self.get_form_end(),
paul@556 82
            isinstance(self.end, datetime) or self.get_start() != self.get_end() - timedelta(1),
paul@532 83
            isinstance(self.start, datetime) or isinstance(self.end, datetime),
paul@541 84
            self.tzid,
paul@532 85
            self.origin
paul@498 86
            )
paul@498 87
paul@498 88
    def get_form_date(self, dt, attr=None):
paul@498 89
        return FormDate(
paul@498 90
            format_datetime(to_date(dt)),
paul@498 91
            isinstance(dt, datetime) and str(dt.hour) or None,
paul@498 92
            isinstance(dt, datetime) and str(dt.minute) or None,
paul@498 93
            isinstance(dt, datetime) and str(dt.second) or None,
paul@498 94
            attr and attr.get("TZID") or None,
paul@498 95
            dt, attr
paul@498 96
            )
paul@498 97
paul@620 98
class FormPeriod(RecurringPeriod):
paul@498 99
paul@498 100
    "A period whose information originates from a form."
paul@498 101
paul@541 102
    def __init__(self, start, end, end_enabled=True, times_enabled=True, tzid=None, origin=None):
paul@498 103
        self.start = start
paul@498 104
        self.end = end
paul@498 105
        self.end_enabled = end_enabled
paul@498 106
        self.times_enabled = times_enabled
paul@541 107
        self.tzid = tzid
paul@499 108
        self.origin = origin
paul@497 109
paul@497 110
    def as_tuple(self):
paul@541 111
        return self.start, self.end, self.end_enabled, self.times_enabled, self.tzid, self.origin
paul@497 112
paul@497 113
    def __repr__(self):
paul@541 114
        return "FormPeriod(%r)" % (self.as_tuple(),)
paul@498 115
paul@499 116
    def as_event_period(self, index=None):
paul@528 117
paul@528 118
        """
paul@528 119
        Return a converted version of this object as an event period suitable
paul@528 120
        for iCalendar usage. If 'index' is indicated, include it in any error
paul@528 121
        raised in the conversion process.
paul@528 122
        """
paul@528 123
paul@700 124
        dtstart, dtstart_attr = self.get_start_item()
paul@528 125
        if not dtstart:
paul@499 126
            raise PeriodError(*[index is not None and ("dtstart", index) or "dtstart"])
paul@499 127
paul@700 128
        dtend, dtend_attr = self.get_end_item()
paul@528 129
        if not dtend:
paul@499 130
            raise PeriodError(*[index is not None and ("dtend", index) or "dtend"])
paul@499 131
paul@499 132
        if dtstart > dtend:
paul@499 133
            raise PeriodError(*[
paul@499 134
                index is not None and ("dtstart", index) or "dtstart",
paul@499 135
                index is not None and ("dtend", index) or "dtend"
paul@499 136
                ])
paul@499 137
paul@544 138
        return EventPeriod(dtstart, end_date_to_calendar(dtend), self.tzid, self.origin, dtstart_attr, dtend_attr, self.start, self.end)
paul@499 139
paul@499 140
    # Period data methods.
paul@499 141
paul@498 142
    def get_start(self):
paul@620 143
        return self.start.as_datetime(self.times_enabled)
paul@498 144
paul@498 145
    def get_end(self):
paul@620 146
paul@620 147
        # Handle specified end datetimes.
paul@620 148
paul@620 149
        if self.end_enabled:
paul@620 150
            dtend = self.end.as_datetime(self.times_enabled)
paul@620 151
            if not dtend:
paul@620 152
                return None
paul@620 153
paul@620 154
        # Otherwise, treat the end date as the start date. Datetimes are
paul@620 155
        # handled by making the event occupy the rest of the day.
paul@620 156
paul@620 157
        else:
paul@620 158
            dtstart, dtstart_attr = self.get_start_item()
paul@620 159
            if dtstart:
paul@620 160
                if isinstance(dtstart, datetime):
paul@620 161
                    dtend = get_end_of_day(dtstart, dtstart_attr["TZID"])
paul@620 162
                else:
paul@620 163
                    dtend = dtstart
paul@620 164
            else:
paul@620 165
                return None
paul@620 166
paul@528 167
        return dtend
paul@528 168
paul@620 169
    def get_start_attr(self):
paul@620 170
        return self.start.get_attributes(self.times_enabled)
paul@528 171
paul@620 172
    def get_end_attr(self):
paul@620 173
        return self.end.get_attributes(self.times_enabled)
paul@498 174
paul@499 175
    # Form data methods.
paul@498 176
paul@498 177
    def get_form_start(self):
paul@498 178
        return self.start
paul@498 179
paul@498 180
    def get_form_end(self):
paul@498 181
        return self.end
paul@498 182
paul@498 183
    def as_form_period(self):
paul@498 184
        return self
paul@497 185
paul@498 186
class FormDate:
paul@498 187
paul@498 188
    "Date information originating from form information."
paul@498 189
paul@498 190
    def __init__(self, date=None, hour=None, minute=None, second=None, tzid=None, dt=None, attr=None):
paul@498 191
        self.date = date
paul@498 192
        self.hour = hour
paul@498 193
        self.minute = minute
paul@498 194
        self.second = second
paul@498 195
        self.tzid = tzid
paul@498 196
        self.dt = dt
paul@498 197
        self.attr = attr
paul@498 198
paul@498 199
    def as_tuple(self):
paul@498 200
        return self.date, self.hour, self.minute, self.second, self.tzid, self.dt, self.attr
paul@498 201
paul@498 202
    def __repr__(self):
paul@541 203
        return "FormDate(%r)" % (self.as_tuple(),)
paul@498 204
paul@498 205
    def get_component(self, value):
paul@498 206
        return (value or "").rjust(2, "0")[:2]
paul@498 207
paul@498 208
    def get_hour(self):
paul@498 209
        return self.get_component(self.hour)
paul@498 210
paul@498 211
    def get_minute(self):
paul@498 212
        return self.get_component(self.minute)
paul@498 213
paul@498 214
    def get_second(self):
paul@498 215
        return self.get_component(self.second)
paul@498 216
paul@498 217
    def get_date_string(self):
paul@498 218
        return self.date or ""
paul@498 219
paul@498 220
    def get_datetime_string(self):
paul@498 221
        if not self.date:
paul@498 222
            return ""
paul@498 223
paul@498 224
        hour = self.hour; minute = self.minute; second = self.second
paul@498 225
paul@498 226
        if hour or minute or second:
paul@498 227
            time = "T%s%s%s" % tuple(map(self.get_component, (hour, minute, second)))
paul@498 228
        else:
paul@498 229
            time = ""
paul@498 230
            
paul@498 231
        return "%s%s" % (self.date, time)
paul@498 232
paul@498 233
    def get_tzid(self):
paul@498 234
        return self.tzid
paul@498 235
paul@528 236
    def as_datetime(self, with_time=True):
paul@498 237
paul@528 238
        "Return a datetime for this object."
paul@498 239
paul@498 240
        # Return any original datetime details.
paul@498 241
paul@498 242
        if self.dt:
paul@528 243
            return self.dt
paul@498 244
paul@528 245
        # Otherwise, construct a datetime.
paul@498 246
paul@528 247
        s, attr = self.as_datetime_item(with_time)
paul@528 248
        if s:
paul@528 249
            return get_datetime(s, attr)
paul@498 250
        else:
paul@528 251
            return None
paul@528 252
paul@528 253
    def as_datetime_item(self, with_time=True):
paul@498 254
paul@528 255
        """
paul@528 256
        Return a (datetime string, attr) tuple for the datetime information
paul@528 257
        provided by this object, where both tuple elements will be None if no
paul@528 258
        suitable date or datetime information exists.
paul@528 259
        """
paul@498 260
paul@528 261
        s = None
paul@528 262
        if with_time:
paul@528 263
            s = self.get_datetime_string()
paul@528 264
            attr = self.get_attributes(True)
paul@528 265
        if not s:
paul@528 266
            s = self.get_date_string()
paul@528 267
            attr = self.get_attributes(False)
paul@528 268
        if not s:
paul@528 269
            return None, None
paul@528 270
        return s, attr
paul@498 271
paul@528 272
    def get_attributes(self, with_time=True):
paul@528 273
paul@528 274
        "Return attributes for the date or datetime represented by this object."
paul@498 275
paul@528 276
        if with_time:
paul@528 277
            return {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"}
paul@528 278
        else:
paul@528 279
            return {"VALUE" : "DATE"}
paul@498 280
paul@499 281
def event_period_from_period(period):
paul@624 282
paul@624 283
    """
paul@624 284
    Convert a 'period' to one suitable for use in an iCalendar representation.
paul@624 285
    In an "event period" representation, the end day of any date-level event is
paul@624 286
    encoded as the "day after" the last day actually involved in the event.
paul@624 287
    """
paul@624 288
paul@499 289
    if isinstance(period, EventPeriod):
paul@499 290
        return period
paul@499 291
    elif isinstance(period, FormPeriod):
paul@499 292
        return period.as_event_period()
paul@499 293
    else:
paul@528 294
        dtstart, dtstart_attr = period.get_start_item()
paul@528 295
        dtend, dtend_attr = period.get_end_item()
paul@539 296
        if not isinstance(period, RecurringPeriod):
paul@539 297
            dtend = end_date_to_calendar(dtend)
paul@541 298
        return EventPeriod(dtstart, dtend, period.tzid, period.origin, dtstart_attr, dtend_attr)
paul@499 299
paul@499 300
def form_period_from_period(period):
paul@624 301
paul@624 302
    """
paul@624 303
    Convert a 'period' into a representation usable in a user-editable form.
paul@624 304
    In a "form period" representation, the end day of any date-level event is
paul@624 305
    presented in a "natural" form, not the iCalendar "day after" form.
paul@624 306
    """
paul@624 307
paul@499 308
    if isinstance(period, EventPeriod):
paul@499 309
        return period.as_form_period()
paul@499 310
    elif isinstance(period, FormPeriod):
paul@499 311
        return period
paul@499 312
    else:
paul@499 313
        return event_period_from_period(period).as_form_period()
paul@499 314
paul@497 315
# vim: tabstop=4 expandtab shiftwidth=4