imip-agent

Annotated imipweb/data.py

1069:37921ab84c01
2016-03-06 Paul Boddie Moved imip_store into a new imiptools.stores package as the file module.
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@868 38
    def __init__(self, start, end, tzid=None, origin=None, start_attr=None, end_attr=None, form_start=None, form_end=None, replaced=False):
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@868 50
        self.replaced = replaced
paul@498 51
paul@498 52
    def as_tuple(self):
paul@868 53
        return self.start, self.end, self.tzid, self.origin, self.start_attr, self.end_attr, self.form_start, self.form_end, self.replaced
paul@498 54
paul@498 55
    def __repr__(self):
paul@833 56
        return "EventPeriod%r" % (self.as_tuple(),)
paul@499 57
paul@499 58
    def as_event_period(self):
paul@499 59
        return self
paul@499 60
paul@700 61
    def get_start_item(self):
paul@700 62
        return self.get_start(), self.get_start_attr()
paul@700 63
paul@700 64
    def get_end_item(self):
paul@700 65
        return self.get_end(), self.get_end_attr()
paul@700 66
paul@499 67
    # Form data compatibility methods.
paul@498 68
paul@498 69
    def get_form_start(self):
paul@498 70
        if not self.form_start:
paul@499 71
            self.form_start = self.get_form_date(self.get_start(), self.start_attr)
paul@498 72
        return self.form_start
paul@498 73
paul@498 74
    def get_form_end(self):
paul@498 75
        if not self.form_end:
paul@539 76
            self.form_end = self.get_form_date(end_date_from_calendar(self.get_end()), self.end_attr)
paul@498 77
        return self.form_end
paul@498 78
paul@498 79
    def as_form_period(self):
paul@498 80
        return FormPeriod(
paul@499 81
            self.get_form_start(),
paul@499 82
            self.get_form_end(),
paul@556 83
            isinstance(self.end, datetime) or self.get_start() != self.get_end() - timedelta(1),
paul@532 84
            isinstance(self.start, datetime) or isinstance(self.end, datetime),
paul@541 85
            self.tzid,
paul@868 86
            self.origin,
paul@868 87
            self.replaced
paul@498 88
            )
paul@498 89
paul@498 90
    def get_form_date(self, dt, attr=None):
paul@498 91
        return FormDate(
paul@498 92
            format_datetime(to_date(dt)),
paul@498 93
            isinstance(dt, datetime) and str(dt.hour) or None,
paul@498 94
            isinstance(dt, datetime) and str(dt.minute) or None,
paul@498 95
            isinstance(dt, datetime) and str(dt.second) or None,
paul@498 96
            attr and attr.get("TZID") or None,
paul@498 97
            dt, attr
paul@498 98
            )
paul@498 99
paul@620 100
class FormPeriod(RecurringPeriod):
paul@498 101
paul@498 102
    "A period whose information originates from a form."
paul@498 103
paul@868 104
    def __init__(self, start, end, end_enabled=True, times_enabled=True, tzid=None, origin=None, replaced=False):
paul@498 105
        self.start = start
paul@498 106
        self.end = end
paul@498 107
        self.end_enabled = end_enabled
paul@498 108
        self.times_enabled = times_enabled
paul@541 109
        self.tzid = tzid
paul@499 110
        self.origin = origin
paul@868 111
        self.replaced = replaced
paul@497 112
paul@497 113
    def as_tuple(self):
paul@868 114
        return self.start, self.end, self.end_enabled, self.times_enabled, self.tzid, self.origin, self.replaced
paul@497 115
paul@497 116
    def __repr__(self):
paul@833 117
        return "FormPeriod%r" % (self.as_tuple(),)
paul@498 118
paul@499 119
    def as_event_period(self, index=None):
paul@528 120
paul@528 121
        """
paul@528 122
        Return a converted version of this object as an event period suitable
paul@528 123
        for iCalendar usage. If 'index' is indicated, include it in any error
paul@528 124
        raised in the conversion process.
paul@528 125
        """
paul@528 126
paul@700 127
        dtstart, dtstart_attr = self.get_start_item()
paul@528 128
        if not dtstart:
paul@499 129
            raise PeriodError(*[index is not None and ("dtstart", index) or "dtstart"])
paul@499 130
paul@700 131
        dtend, dtend_attr = self.get_end_item()
paul@528 132
        if not dtend:
paul@499 133
            raise PeriodError(*[index is not None and ("dtend", index) or "dtend"])
paul@499 134
paul@499 135
        if dtstart > dtend:
paul@499 136
            raise PeriodError(*[
paul@499 137
                index is not None and ("dtstart", index) or "dtstart",
paul@499 138
                index is not None and ("dtend", index) or "dtend"
paul@499 139
                ])
paul@499 140
paul@868 141
        return EventPeriod(dtstart, end_date_to_calendar(dtend), self.tzid, self.origin, dtstart_attr, dtend_attr, self.start, self.end, self.replaced)
paul@499 142
paul@499 143
    # Period data methods.
paul@499 144
paul@498 145
    def get_start(self):
paul@620 146
        return self.start.as_datetime(self.times_enabled)
paul@498 147
paul@498 148
    def get_end(self):
paul@620 149
paul@620 150
        # Handle specified end datetimes.
paul@620 151
paul@620 152
        if self.end_enabled:
paul@620 153
            dtend = self.end.as_datetime(self.times_enabled)
paul@620 154
            if not dtend:
paul@620 155
                return None
paul@620 156
paul@886 157
        # Handle same day times.
paul@886 158
paul@886 159
        elif self.times_enabled:
paul@886 160
            formdate = FormDate(self.start.date, self.end.hour, self.end.minute, self.end.second, self.end.tzid)
paul@886 161
            dtend = formdate.as_datetime(self.times_enabled)
paul@886 162
            if not dtend:
paul@886 163
                return None
paul@886 164
paul@620 165
        # Otherwise, treat the end date as the start date. Datetimes are
paul@620 166
        # handled by making the event occupy the rest of the day.
paul@620 167
paul@620 168
        else:
paul@620 169
            dtstart, dtstart_attr = self.get_start_item()
paul@620 170
            if dtstart:
paul@620 171
                if isinstance(dtstart, datetime):
paul@620 172
                    dtend = get_end_of_day(dtstart, dtstart_attr["TZID"])
paul@620 173
                else:
paul@620 174
                    dtend = dtstart
paul@620 175
            else:
paul@620 176
                return None
paul@620 177
paul@528 178
        return dtend
paul@528 179
paul@620 180
    def get_start_attr(self):
paul@620 181
        return self.start.get_attributes(self.times_enabled)
paul@528 182
paul@620 183
    def get_end_attr(self):
paul@620 184
        return self.end.get_attributes(self.times_enabled)
paul@498 185
paul@499 186
    # Form data methods.
paul@498 187
paul@498 188
    def get_form_start(self):
paul@498 189
        return self.start
paul@498 190
paul@498 191
    def get_form_end(self):
paul@498 192
        return self.end
paul@498 193
paul@498 194
    def as_form_period(self):
paul@498 195
        return self
paul@497 196
paul@498 197
class FormDate:
paul@498 198
paul@498 199
    "Date information originating from form information."
paul@498 200
paul@498 201
    def __init__(self, date=None, hour=None, minute=None, second=None, tzid=None, dt=None, attr=None):
paul@498 202
        self.date = date
paul@498 203
        self.hour = hour
paul@498 204
        self.minute = minute
paul@498 205
        self.second = second
paul@498 206
        self.tzid = tzid
paul@498 207
        self.dt = dt
paul@498 208
        self.attr = attr
paul@498 209
paul@498 210
    def as_tuple(self):
paul@498 211
        return self.date, self.hour, self.minute, self.second, self.tzid, self.dt, self.attr
paul@498 212
paul@498 213
    def __repr__(self):
paul@833 214
        return "FormDate%r" % (self.as_tuple(),)
paul@498 215
paul@498 216
    def get_component(self, value):
paul@498 217
        return (value or "").rjust(2, "0")[:2]
paul@498 218
paul@498 219
    def get_hour(self):
paul@498 220
        return self.get_component(self.hour)
paul@498 221
paul@498 222
    def get_minute(self):
paul@498 223
        return self.get_component(self.minute)
paul@498 224
paul@498 225
    def get_second(self):
paul@498 226
        return self.get_component(self.second)
paul@498 227
paul@498 228
    def get_date_string(self):
paul@498 229
        return self.date or ""
paul@498 230
paul@498 231
    def get_datetime_string(self):
paul@498 232
        if not self.date:
paul@498 233
            return ""
paul@498 234
paul@498 235
        hour = self.hour; minute = self.minute; second = self.second
paul@498 236
paul@498 237
        if hour or minute or second:
paul@498 238
            time = "T%s%s%s" % tuple(map(self.get_component, (hour, minute, second)))
paul@498 239
        else:
paul@498 240
            time = ""
paul@498 241
            
paul@498 242
        return "%s%s" % (self.date, time)
paul@498 243
paul@498 244
    def get_tzid(self):
paul@498 245
        return self.tzid
paul@498 246
paul@528 247
    def as_datetime(self, with_time=True):
paul@498 248
paul@528 249
        "Return a datetime for this object."
paul@498 250
paul@498 251
        # Return any original datetime details.
paul@498 252
paul@498 253
        if self.dt:
paul@528 254
            return self.dt
paul@498 255
paul@528 256
        # Otherwise, construct a datetime.
paul@498 257
paul@528 258
        s, attr = self.as_datetime_item(with_time)
paul@528 259
        if s:
paul@528 260
            return get_datetime(s, attr)
paul@498 261
        else:
paul@528 262
            return None
paul@528 263
paul@528 264
    def as_datetime_item(self, with_time=True):
paul@498 265
paul@528 266
        """
paul@528 267
        Return a (datetime string, attr) tuple for the datetime information
paul@528 268
        provided by this object, where both tuple elements will be None if no
paul@528 269
        suitable date or datetime information exists.
paul@528 270
        """
paul@498 271
paul@528 272
        s = None
paul@528 273
        if with_time:
paul@528 274
            s = self.get_datetime_string()
paul@528 275
            attr = self.get_attributes(True)
paul@528 276
        if not s:
paul@528 277
            s = self.get_date_string()
paul@528 278
            attr = self.get_attributes(False)
paul@528 279
        if not s:
paul@528 280
            return None, None
paul@528 281
        return s, attr
paul@498 282
paul@528 283
    def get_attributes(self, with_time=True):
paul@528 284
paul@528 285
        "Return attributes for the date or datetime represented by this object."
paul@498 286
paul@528 287
        if with_time:
paul@528 288
            return {"TZID" : self.get_tzid(), "VALUE" : "DATE-TIME"}
paul@528 289
        else:
paul@528 290
            return {"VALUE" : "DATE"}
paul@498 291
paul@499 292
def event_period_from_period(period):
paul@624 293
paul@624 294
    """
paul@624 295
    Convert a 'period' to one suitable for use in an iCalendar representation.
paul@624 296
    In an "event period" representation, the end day of any date-level event is
paul@624 297
    encoded as the "day after" the last day actually involved in the event.
paul@624 298
    """
paul@624 299
paul@499 300
    if isinstance(period, EventPeriod):
paul@499 301
        return period
paul@499 302
    elif isinstance(period, FormPeriod):
paul@499 303
        return period.as_event_period()
paul@499 304
    else:
paul@528 305
        dtstart, dtstart_attr = period.get_start_item()
paul@528 306
        dtend, dtend_attr = period.get_end_item()
paul@539 307
        if not isinstance(period, RecurringPeriod):
paul@539 308
            dtend = end_date_to_calendar(dtend)
paul@541 309
        return EventPeriod(dtstart, dtend, period.tzid, period.origin, dtstart_attr, dtend_attr)
paul@499 310
paul@499 311
def form_period_from_period(period):
paul@624 312
paul@624 313
    """
paul@624 314
    Convert a 'period' into a representation usable in a user-editable form.
paul@624 315
    In a "form period" representation, the end day of any date-level event is
paul@624 316
    presented in a "natural" form, not the iCalendar "day after" form.
paul@624 317
    """
paul@624 318
paul@499 319
    if isinstance(period, EventPeriod):
paul@499 320
        return period.as_form_period()
paul@499 321
    elif isinstance(period, FormPeriod):
paul@499 322
        return period
paul@499 323
    else:
paul@499 324
        return event_period_from_period(period).as_form_period()
paul@499 325
paul@497 326
# vim: tabstop=4 expandtab shiftwidth=4