imip-agent

Annotated imipweb/resource.py

1266:e3cda8fa8ee7
2017-09-14 Paul Boddie Introduced recurrence identifiers for periods originating from stored data, thus permitting the tracking of these periods and the operations they experience during editing.
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@1230 6
Copyright (C) 2014, 2015, 2016, 2017 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@1248 22
from datetime import date, datetime, timedelta
paul@756 23
from imiptools.client import Client, ClientForObject
paul@909 24
from imiptools.data import get_uri
paul@909 25
from imiptools.dates import format_datetime, to_date
paul@1230 26
from imiptools.freebusy import FreeBusyCollection
paul@845 27
from imipweb.data import event_period_from_period, form_period_from_period, \
paul@1244 28
                         PeriodError
paul@446 29
from imipweb.env import CGIEnvironment
paul@777 30
from urllib import urlencode
paul@446 31
import babel.dates
paul@1162 32
import hashlib, hmac
paul@446 33
import markup
paul@764 34
import pytz
paul@1162 35
import time
paul@446 36
paul@756 37
class Resource:
paul@446 38
paul@756 39
    "A Web application resource."
paul@446 40
paul@446 41
    def __init__(self, resource=None):
paul@756 42
paul@756 43
        """
paul@756 44
        Initialise a resource, allowing it to share the environment of any given
paul@756 45
        existing 'resource'.
paul@756 46
        """
paul@756 47
paul@446 48
        self.encoding = "utf-8"
paul@446 49
        self.env = CGIEnvironment(self.encoding)
paul@446 50
paul@756 51
        self.objects = {}
paul@446 52
        self.locale = None
paul@446 53
        self.requests = None
paul@446 54
paul@446 55
        self.out = resource and resource.out or self.env.get_output()
paul@446 56
        self.page = resource and resource.page or markup.page()
paul@446 57
        self.html_ids = 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@875 87
    def link_to(self, uid=None, recurrenceid=None, args=None):
paul@755 88
paul@755 89
        """
paul@875 90
        Return a link to a resource, being an object with any given 'uid' and
paul@875 91
        'recurrenceid', or the main resource otherwise.
paul@875 92
paul@763 93
        See get_identifiers for the decoding of such links.
paul@777 94
paul@777 95
        If 'args' is specified, the given dictionary is encoded and included.
paul@755 96
        """
paul@755 97
paul@875 98
        path = []
paul@875 99
        if uid:
paul@875 100
            path.append(uid)
paul@446 101
        if recurrenceid:
paul@755 102
            path.append(recurrenceid)
paul@777 103
        return "%s%s" % (self.env.new_url("/".join(path)), args and ("?%s" % urlencode(args)) or "")
paul@446 104
paul@623 105
    # Access to objects.
paul@623 106
paul@755 107
    def get_identifiers(self, path_info):
paul@755 108
paul@755 109
        """
paul@755 110
        Return identifiers provided by 'path_info', potentially encoded by
paul@755 111
        'link_to'.
paul@755 112
        """
paul@755 113
paul@446 114
        parts = path_info.lstrip("/").split("/")
paul@755 115
paul@755 116
        # UID only.
paul@755 117
paul@446 118
        if len(parts) == 1:
paul@763 119
            return parts[0], None
paul@755 120
paul@763 121
        # UID and RECURRENCE-ID.
paul@755 122
paul@446 123
        else:
paul@763 124
            return parts[:2]
paul@446 125
paul@769 126
    def _get_object(self, uid, recurrenceid=None, section=None, username=None):
paul@769 127
        if self.objects.has_key((uid, recurrenceid, section, username)):
paul@769 128
            return self.objects[(uid, recurrenceid, section, username)]
paul@446 129
paul@769 130
        obj = self.objects[(uid, recurrenceid, section, username)] = self.get_stored_object(uid, recurrenceid, section, username)
paul@446 131
        return obj
paul@446 132
paul@446 133
    def _get_recurrences(self, uid):
paul@446 134
        return self.store.get_recurrences(self.user, uid)
paul@446 135
paul@694 136
    def _get_active_recurrences(self, uid):
paul@694 137
        return self.store.get_active_recurrences(self.user, uid)
paul@694 138
paul@446 139
    def _get_requests(self):
paul@446 140
        if self.requests is None:
paul@699 141
            self.requests = self.store.get_requests(self.user)
paul@446 142
        return self.requests
paul@446 143
paul@755 144
    def _have_request(self, uid, recurrenceid=None, type=None, strict=False):
paul@755 145
        return self.store.have_request(self._get_requests(), uid, recurrenceid, type, strict)
paul@751 146
paul@818 147
    def _is_request(self):
paul@818 148
        return self._have_request(self.uid, self.recurrenceid)
paul@818 149
paul@766 150
    def _get_counters(self, uid, recurrenceid=None):
paul@766 151
        return self.store.get_counters(self.user, uid, recurrenceid)
paul@766 152
paul@1238 153
    def _get_request_summary(self, view_period):
paul@630 154
paul@1238 155
        """
paul@1238 156
        Return a list of periods comprising the request summary within the given
paul@1238 157
        'view_period'.
paul@1238 158
        """
paul@630 159
paul@1099 160
        summary = FreeBusyCollection()
paul@630 161
paul@751 162
        for uid, recurrenceid, request_type in self._get_requests():
paul@769 163
paul@769 164
            # Obtain either normal objects or counter-proposals.
paul@769 165
paul@769 166
            if not request_type:
paul@769 167
                objs = [self._get_object(uid, recurrenceid)]
paul@769 168
            elif request_type == "COUNTER":
paul@769 169
                objs = []
paul@769 170
                for attendee in self.store.get_counters(self.user, uid, recurrenceid):
paul@769 171
                    objs.append(self._get_object(uid, recurrenceid, "counters", attendee))
paul@446 172
paul@769 173
            # For each object, obtain the periods involved.
paul@769 174
paul@769 175
            for obj in objs:
paul@769 176
                if obj:
paul@855 177
                    recurrenceids = self._get_recurrences(uid)
paul@446 178
paul@769 179
                    # Obtain only active periods, not those replaced by redefined
paul@769 180
                    # recurrences, converting to free/busy periods.
paul@769 181
paul@1238 182
                    for p in obj.get_active_periods(recurrenceids, self.get_tzid(),
paul@1238 183
                        start=view_period.get_start(), end=view_period.get_end()):
paul@1238 184
paul@769 185
                        summary.append(obj.get_freebusy_period(p))
paul@446 186
paul@446 187
        return summary
paul@446 188
paul@446 189
    # Preference methods.
paul@446 190
paul@446 191
    def get_user_locale(self):
paul@446 192
        if not self.locale:
paul@1029 193
            self.locale = self.get_preferences().get("LANG", "en", True) or "en"
paul@446 194
        return self.locale
paul@446 195
paul@446 196
    # Prettyprinting of dates and times.
paul@446 197
paul@446 198
    def format_date(self, dt, format):
paul@446 199
        return self._format_datetime(babel.dates.format_date, dt, format)
paul@446 200
paul@446 201
    def format_time(self, dt, format):
paul@446 202
        return self._format_datetime(babel.dates.format_time, dt, format)
paul@446 203
paul@446 204
    def format_datetime(self, dt, format):
paul@446 205
        return self._format_datetime(
paul@446 206
            isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date,
paul@446 207
            dt, format)
paul@446 208
paul@446 209
    def _format_datetime(self, fn, dt, format):
paul@446 210
        return fn(dt, format=format, locale=self.get_user_locale())
paul@446 211
paul@756 212
class ResourceClient(Resource, Client):
paul@446 213
paul@756 214
    "A Web application resource and calendar client."
paul@446 215
paul@756 216
    def __init__(self, resource=None):
paul@756 217
        Resource.__init__(self, resource)
paul@756 218
        user = self.env.get_user()
paul@756 219
        Client.__init__(self, user and get_uri(user) or None)
paul@446 220
paul@756 221
class ResourceClientForObject(Resource, ClientForObject):
paul@447 222
paul@756 223
    "A Web application resource and calendar client for a specific object."
paul@672 224
paul@807 225
    def __init__(self, resource=None, messenger=None):
paul@756 226
        Resource.__init__(self, resource)
paul@756 227
        user = self.env.get_user()
paul@807 228
        ClientForObject.__init__(self, None, user and get_uri(user) or None, messenger)
paul@807 229
paul@764 230
class FormUtilities:
paul@764 231
paul@765 232
    "Utility methods resource mix-in."
paul@764 233
paul@1162 234
    def get_validation_token(self, details=None):
paul@1162 235
paul@1162 236
        "Return a token suitable for validating a form submission."
paul@1162 237
paul@1162 238
        # Use a secret held in the user's preferences.
paul@1162 239
paul@1162 240
        prefs = self.get_preferences()
paul@1162 241
        if not prefs.has_key("secret"):
paul@1162 242
            prefs["secret"] = str(time.time())
paul@1162 243
paul@1162 244
        # Combine it with the user identity and any supplied details.
paul@1162 245
paul@1162 246
        secret = prefs["secret"].encode("utf-8")
paul@1162 247
        details = u"".join([self.env.get_user()] + (details or [])).encode("utf-8")
paul@1162 248
paul@1162 249
        return hmac.new(secret, details, hashlib.sha256).hexdigest()
paul@1162 250
paul@1162 251
    def check_validation_token(self, name="token", details=None):
paul@1162 252
paul@1162 253
        """
paul@1162 254
        Check the field having the given 'name', returning if its value matches
paul@1162 255
        the validation token generated using any given 'details'.
paul@1162 256
        """
paul@1162 257
paul@1162 258
        return self.env.get_args().get(name, [None])[0] == self.get_validation_token(details)
paul@1162 259
paul@1162 260
    def validator(self, name="token", details=None):
paul@1162 261
paul@1162 262
        """
paul@1162 263
        Show a control having the given 'name' that is used to validate form
paul@1162 264
        submissions, employing any additional 'details' in the construction of
paul@1162 265
        the validation token.
paul@1162 266
        """
paul@1162 267
paul@1162 268
        self.page.input(name=name, type="hidden", value=self.get_validation_token(details))
paul@1162 269
paul@813 270
    def prefixed_args(self, prefix, convert=None):
paul@813 271
paul@813 272
        """
paul@813 273
        Return values for all arguments having the given 'prefix' in their
paul@813 274
        names, removing the prefix to obtain each value from the argument name
paul@813 275
        itself. The 'convert' callable can be specified to perform a conversion
paul@813 276
        (to int, for example).
paul@813 277
        """
paul@813 278
paul@813 279
        args = self.env.get_args()
paul@813 280
paul@813 281
        values = []
paul@813 282
        for name in args.keys():
paul@813 283
            if name.startswith(prefix):
paul@813 284
                value = name[len(prefix):]
paul@813 285
                if convert:
paul@813 286
                    try:
paul@813 287
                        value = convert(value)
paul@813 288
                    except ValueError:
paul@813 289
                        pass
paul@813 290
                values.append(value)
paul@813 291
        return values
paul@813 292
paul@764 293
    def control(self, name, type, value, selected=False, **kw):
paul@764 294
paul@764 295
        """
paul@764 296
        Show a control with the given 'name', 'type' and 'value', with
paul@764 297
        'selected' indicating whether it should be selected (checked or
paul@764 298
        equivalent), and with keyword arguments setting other properties.
paul@764 299
        """
paul@764 300
paul@764 301
        page = self.page
paul@779 302
        if type in ("checkbox", "radio") and selected:
paul@852 303
            page.input(name=name, type=type, value=value, checked="checked", **kw)
paul@764 304
        else:
paul@764 305
            page.input(name=name, type=type, value=value, **kw)
paul@764 306
paul@1250 307
    def menu(self, name, default, items, values=None, class_=""):
paul@764 308
paul@764 309
        """
paul@764 310
        Show a select menu having the given 'name', set to the given 'default',
paul@923 311
        providing the given (value, label) 'items', selecting the given 'values'
paul@923 312
        (or using the request parameters if not specified), and employing the
paul@923 313
        given CSS 'class_' if specified.
paul@764 314
        """
paul@764 315
paul@764 316
        page = self.page
paul@1250 317
        values = values or [default]
paul@764 318
paul@764 319
        page.select(name=name, class_=class_)
paul@764 320
        for v, label in items:
paul@764 321
            if v is None:
paul@764 322
                continue
paul@764 323
            if v in values:
paul@764 324
                page.option(label, value=v, selected="selected")
paul@764 325
            else:
paul@764 326
                page.option(label, value=v)
paul@764 327
        page.select.close()
paul@764 328
paul@1250 329
    def date_controls(self, name, default, show_tzid=True, read_only=False):
paul@764 330
paul@764 331
        """
paul@764 332
        Show date controls for a field with the given 'name' and 'default' form
paul@764 333
        date value.
paul@764 334
paul@764 335
        If 'show_tzid' is set to a false value, the time zone menu will not be
paul@764 336
        provided.
paul@764 337
paul@764 338
        If 'read_only' is set to a true value, the controls will be hidden and
paul@764 339
        labels will be employed instead.
paul@764 340
        """
paul@764 341
paul@764 342
        page = self.page
paul@764 343
paul@764 344
        # Show dates for up to one week around the current date.
paul@764 345
paul@886 346
        page.span(class_="date enabled")
paul@886 347
paul@764 348
        dt = default.as_datetime()
paul@1248 349
paul@1248 350
        # For invalid datetimes, try to get a date instead.
paul@1248 351
paul@764 352
        if not dt:
paul@1248 353
            dt = default.as_datetime(with_time=False)
paul@1248 354
paul@1248 355
            # For invalid dates, just use today's date.
paul@1248 356
paul@1248 357
            if not dt:
paul@1248 358
                dt = date.today()
paul@764 359
paul@764 360
        base = to_date(dt)
paul@764 361
paul@764 362
        # Show a date label with a hidden field if read-only.
paul@764 363
paul@764 364
        if read_only:
paul@764 365
            self.control("%s-date" % name, "hidden", format_datetime(base))
paul@764 366
            page.span(self.format_date(base, "long"))
paul@764 367
paul@764 368
        # Show dates for up to one week around the current date.
paul@764 369
        # NOTE: Support paging to other dates.
paul@764 370
paul@764 371
        else:
paul@764 372
            items = []
paul@764 373
            for i in range(-7, 8):
paul@764 374
                d = base + timedelta(i)
paul@764 375
                items.append((format_datetime(d), self.format_date(d, "full")))
paul@1250 376
            self.menu("%s-date" % name, format_datetime(base), items)
paul@764 377
paul@886 378
        page.span.close()
paul@886 379
paul@764 380
        # Show time details.
paul@764 381
paul@764 382
        page.span(class_="time enabled")
paul@764 383
paul@764 384
        if read_only:
paul@764 385
            page.span("%s:%s:%s" % (default.get_hour(), default.get_minute(), default.get_second()))
paul@764 386
            self.control("%s-hour" % name, "hidden", default.get_hour())
paul@764 387
            self.control("%s-minute" % name, "hidden", default.get_minute())
paul@764 388
            self.control("%s-second" % name, "hidden", default.get_second())
paul@764 389
        else:
paul@764 390
            self.control("%s-hour" % name, "text", default.get_hour(), maxlength=2, size=2)
paul@764 391
            page.add(":")
paul@764 392
            self.control("%s-minute" % name, "text", default.get_minute(), maxlength=2, size=2)
paul@764 393
            page.add(":")
paul@764 394
            self.control("%s-second" % name, "text", default.get_second(), maxlength=2, size=2)
paul@764 395
paul@764 396
        # Show time zone details.
paul@764 397
paul@764 398
        if show_tzid:
paul@764 399
            page.add(" ")
paul@764 400
            tzid = default.get_tzid() or self.get_tzid()
paul@764 401
paul@764 402
            # Show a label if read-only or a menu otherwise.
paul@764 403
paul@764 404
            if read_only:
paul@764 405
                self.control("%s-tzid" % name, "hidden", tzid)
paul@764 406
                page.span(tzid)
paul@764 407
            else:
paul@1250 408
                self.timezone_menu("%s-tzid" % name, tzid)
paul@764 409
paul@764 410
        page.span.close()
paul@764 411
paul@1250 412
    def timezone_menu(self, name, default):
paul@764 413
paul@764 414
        """
paul@764 415
        Show timezone controls using a menu with the given 'name', set to the
paul@764 416
        given 'default' unless a field of the given 'name' provides a value.
paul@764 417
        """
paul@764 418
paul@764 419
        entries = [(tzid, tzid) for tzid in pytz.all_timezones]
paul@1250 420
        self.menu(name, default, entries)
paul@764 421
paul@765 422
class DateTimeFormUtilities:
paul@765 423
paul@765 424
    "Date/time control methods resource mix-in."
paul@765 425
paul@776 426
    # Control naming helpers.
paul@776 427
paul@776 428
    def element_identifier(self, name, index=None):
paul@776 429
        return index is not None and "%s-%d" % (name, index) or name
paul@776 430
paul@776 431
    def element_name(self, name, suffix, index=None):
paul@776 432
        return index is not None and "%s-%s" % (name, suffix) or name
paul@776 433
paul@776 434
    def element_enable(self, index=None):
paul@1244 435
        return str(index or 0)
paul@776 436
paul@765 437
    def show_object_datetime_controls(self, period, index=None):
paul@765 438
paul@765 439
        """
paul@765 440
        Show datetime-related controls if already active or if an object needs
paul@765 441
        them for the given 'period'. The given 'index' is used to parameterise
paul@765 442
        individual controls for dynamic manipulation.
paul@765 443
        """
paul@765 444
paul@765 445
        p = form_period_from_period(period)
paul@765 446
paul@765 447
        page = self.page
paul@765 448
        _id = self.element_identifier
paul@765 449
        _name = self.element_name
paul@765 450
        _enable = self.element_enable
paul@765 451
paul@765 452
        # Add a dynamic stylesheet to permit the controls to modify the display.
paul@765 453
        # NOTE: The style details need to be coordinated with the static
paul@765 454
        # NOTE: stylesheet.
paul@765 455
paul@765 456
        if index is not None:
paul@765 457
            page.style(type="text/css")
paul@765 458
paul@765 459
            # Unlike the rules for object properties, these affect recurrence
paul@765 460
            # properties.
paul@765 461
paul@765 462
            page.add("""\
paul@765 463
input#dttimes-enable-%(index)d,
paul@765 464
input#dtend-enable-%(index)d,
paul@765 465
input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled,
paul@765 466
input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled,
paul@765 467
input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled,
paul@765 468
input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled {
paul@765 469
    display: none;
paul@886 470
}
paul@886 471
paul@886 472
input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .date.enabled,
paul@886 473
input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .date.disabled {
paul@886 474
    visibility: hidden;
paul@765 475
}""" % {"index" : index})
paul@765 476
paul@765 477
            page.style.close()
paul@765 478
paul@765 479
        self.control(
paul@765 480
            _name("dtend-control", "recur", index), "checkbox",
paul@765 481
            _enable(index), p.end_enabled,
paul@765 482
            id=_id("dtend-enable", index)
paul@765 483
            )
paul@765 484
paul@765 485
        self.control(
paul@765 486
            _name("dttimes-control", "recur", index), "checkbox",
paul@765 487
            _enable(index), p.times_enabled,
paul@765 488
            id=_id("dttimes-enable", index)
paul@765 489
            )
paul@765 490
paul@765 491
    def show_datetime_controls(self, formdate, show_start):
paul@765 492
paul@765 493
        """
paul@765 494
        Show datetime details from the current object for the 'formdate',
paul@765 495
        showing start details if 'show_start' is set to a true value. Details
paul@765 496
        will appear as controls for organisers and labels for attendees.
paul@765 497
        """
paul@765 498
paul@765 499
        page = self.page
paul@765 500
paul@867 501
        # Show controls for editing.
paul@765 502
paul@867 503
        page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
paul@765 504
paul@867 505
        if show_start:
paul@867 506
            page.div(class_="dt enabled")
paul@867 507
            self.date_controls("dtstart", formdate)
paul@867 508
            page.br()
paul@867 509
            page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")
paul@867 510
            page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable")
paul@867 511
            page.div.close()
paul@765 512
paul@765 513
        else:
paul@886 514
            self.date_controls("dtend", formdate)
paul@867 515
            page.div(class_="dt disabled")
paul@867 516
            page.label("Specify end date", for_="dtend-enable", class_="enable")
paul@867 517
            page.div.close()
paul@867 518
            page.div(class_="dt enabled")
paul@867 519
            page.label("End on same day", for_="dtend-enable", class_="disable")
paul@867 520
            page.div.close()
paul@867 521
paul@867 522
        page.td.close()
paul@765 523
paul@868 524
    def show_recurrence_controls(self, index, period, recurrenceid, show_start):
paul@765 525
paul@765 526
        """
paul@765 527
        Show datetime details from the current object for the recurrence having
paul@765 528
        the given 'index', with the recurrence period described by 'period',
paul@765 529
        indicating a start, end and origin of the period from the event details,
paul@868 530
        employing any 'recurrenceid' for the object to configure the displayed
paul@868 531
        information.
paul@765 532
paul@765 533
        If 'show_start' is set to a true value, the start details will be shown;
paul@765 534
        otherwise, the end details will be shown.
paul@765 535
        """
paul@765 536
paul@765 537
        page = self.page
paul@765 538
        _id = self.element_identifier
paul@765 539
        _name = self.element_name
paul@765 540
paul@845 541
        period = form_period_from_period(period)
paul@765 542
paul@867 543
        # Show controls for editing.
paul@765 544
paul@868 545
        if not period.replaced:
paul@868 546
            page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
paul@765 547
paul@765 548
            read_only = period.origin == "RRULE"
paul@765 549
paul@765 550
            if show_start:
paul@765 551
                page.div(class_="dt enabled")
paul@1250 552
                self.date_controls(_name("dtstart", "recur", index), period.get_form_start(), read_only=read_only)
paul@765 553
                if not read_only:
paul@765 554
                    page.br()
paul@765 555
                    page.label("Specify times", for_=_id("dttimes-enable", index), class_="time disabled enable")
paul@765 556
                    page.label("Specify dates only", for_=_id("dttimes-enable", index), class_="time enabled disable")
paul@765 557
                page.div.close()
paul@765 558
paul@1266 559
                self.show_recurrence_state(index, period)
paul@765 560
            else:
paul@1250 561
                self.date_controls(_name("dtend", "recur", index), period.get_form_end(), show_tzid=False, read_only=read_only)
paul@765 562
                if not read_only:
paul@886 563
                    page.div(class_="dt disabled")
paul@886 564
                    page.label("Specify end date", for_=_id("dtend-enable", index), class_="enable")
paul@886 565
                    page.div.close()
paul@886 566
                    page.div(class_="dt enabled")
paul@765 567
                    page.label("End on same day", for_=_id("dtend-enable", index), class_="disable")
paul@886 568
                    page.div.close()
paul@765 569
paul@765 570
            page.td.close()
paul@765 571
paul@765 572
        # Show label as attendee.
paul@765 573
paul@765 574
        else:
paul@868 575
            self.show_recurrence_label(index, period, recurrenceid, show_start)
paul@765 576
paul@868 577
    def show_recurrence_label(self, index, period, recurrenceid, show_start):
paul@765 578
paul@765 579
        """
paul@852 580
        Show datetime details from the current object for the recurrence having
paul@852 581
        the given 'index', for the given recurrence 'period', employing any
paul@868 582
        'recurrenceid' for the object to configure the displayed information.
paul@765 583
paul@765 584
        If 'show_start' is set to a true value, the start details will be shown;
paul@765 585
        otherwise, the end details will be shown.
paul@765 586
        """
paul@765 587
paul@765 588
        page = self.page
paul@852 589
        _name = self.element_name
paul@765 590
paul@845 591
        try:
paul@845 592
            p = event_period_from_period(period)
paul@845 593
        except PeriodError, exc:
paul@845 594
            affected = False
paul@845 595
        else:
paul@845 596
            affected = p.is_affected(recurrenceid)
paul@845 597
paul@845 598
        period = form_period_from_period(period)
paul@765 599
paul@765 600
        css = " ".join([
paul@868 601
            period.replaced and "replaced" or "",
paul@845 602
            affected and "affected" or ""
paul@765 603
            ])
paul@765 604
paul@845 605
        formdate = show_start and period.get_form_start() or period.get_form_end()
paul@765 606
        dt = formdate.as_datetime()
paul@765 607
        if dt:
paul@852 608
            page.td(class_=css)
paul@852 609
            if show_start:
paul@1250 610
                self.date_controls(_name("dtstart", "recur", index), period.get_form_start(), read_only=True)
paul@1266 611
                self.show_recurrence_state(index, period)
paul@852 612
            else:
paul@1250 613
                self.date_controls(_name("dtend", "recur", index), period.get_form_end(), show_tzid=False, read_only=True)
paul@852 614
            page.td.close()
paul@765 615
        else:
paul@765 616
            page.td("(Unrecognised date)")
paul@765 617
paul@1266 618
    def show_recurrence_state(self, index, period):
paul@1266 619
paul@1266 620
        "Insert at 'index' additional state held by 'period'."
paul@1266 621
paul@1266 622
        self.control("recur-origin", "hidden", period.origin or "")
paul@1266 623
        self.control("recur-replaced", "hidden", period.replaced and str(index) or "")
paul@1266 624
        self.control("recur-id", "hidden", period.recurrenceid or "")
paul@1266 625
paul@446 626
# vim: tabstop=4 expandtab shiftwidth=4