imip-agent

Annotated imipweb/resource.py

1238:eb97b1194e2e
2017-06-04 Paul Boddie Obtain only future event periods when scheduling. Added support for specifying the start of the scheduling window, instead of employing the current moment in time as the start, in order to support testing of events defined in the past.
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@764 22
from datetime import 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@845 28
                         FormDate, 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@923 307
    def menu(self, name, default, items, values=None, class_="", index=None):
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@923 317
        values = values or self.env.get_args().get(name, [default])
paul@764 318
        if index is not None:
paul@764 319
            values = values[index:]
paul@764 320
            values = values and values[0:1] or [default]
paul@764 321
paul@764 322
        page.select(name=name, class_=class_)
paul@764 323
        for v, label in items:
paul@764 324
            if v is None:
paul@764 325
                continue
paul@764 326
            if v in values:
paul@764 327
                page.option(label, value=v, selected="selected")
paul@764 328
            else:
paul@764 329
                page.option(label, value=v)
paul@764 330
        page.select.close()
paul@764 331
paul@764 332
    def date_controls(self, name, default, index=None, show_tzid=True, read_only=False):
paul@764 333
paul@764 334
        """
paul@764 335
        Show date controls for a field with the given 'name' and 'default' form
paul@764 336
        date value.
paul@764 337
paul@764 338
        If 'index' is specified, default field values will be overridden by the
paul@764 339
        element from a collection of existing form values with the specified
paul@764 340
        index; otherwise, field values will be overridden by a single form
paul@764 341
        value.
paul@764 342
paul@764 343
        If 'show_tzid' is set to a false value, the time zone menu will not be
paul@764 344
        provided.
paul@764 345
paul@764 346
        If 'read_only' is set to a true value, the controls will be hidden and
paul@764 347
        labels will be employed instead.
paul@764 348
        """
paul@764 349
paul@764 350
        page = self.page
paul@764 351
paul@764 352
        # Show dates for up to one week around the current date.
paul@764 353
paul@886 354
        page.span(class_="date enabled")
paul@886 355
paul@764 356
        dt = default.as_datetime()
paul@764 357
        if not dt:
paul@764 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@764 376
            self.menu("%s-date" % name, format_datetime(base), items, index=index)
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@764 408
                self.timezone_menu("%s-tzid" % name, tzid, index)
paul@764 409
paul@764 410
        page.span.close()
paul@764 411
paul@764 412
    def timezone_menu(self, name, default, index=None):
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@764 420
        self.menu(name, default, entries, index=index)
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@776 435
        return index is not None and str(index) or "enable"
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
        args = self.env.get_args()
paul@765 449
        _id = self.element_identifier
paul@765 450
        _name = self.element_name
paul@765 451
        _enable = self.element_enable
paul@765 452
paul@765 453
        # Add a dynamic stylesheet to permit the controls to modify the display.
paul@765 454
        # NOTE: The style details need to be coordinated with the static
paul@765 455
        # NOTE: stylesheet.
paul@765 456
paul@765 457
        if index is not None:
paul@765 458
            page.style(type="text/css")
paul@765 459
paul@765 460
            # Unlike the rules for object properties, these affect recurrence
paul@765 461
            # properties.
paul@765 462
paul@765 463
            page.add("""\
paul@765 464
input#dttimes-enable-%(index)d,
paul@765 465
input#dtend-enable-%(index)d,
paul@765 466
input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled,
paul@765 467
input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled,
paul@765 468
input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled,
paul@765 469
input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled {
paul@765 470
    display: none;
paul@886 471
}
paul@886 472
paul@886 473
input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .date.enabled,
paul@886 474
input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .date.disabled {
paul@886 475
    visibility: hidden;
paul@765 476
}""" % {"index" : index})
paul@765 477
paul@765 478
            page.style.close()
paul@765 479
paul@765 480
        self.control(
paul@765 481
            _name("dtend-control", "recur", index), "checkbox",
paul@765 482
            _enable(index), p.end_enabled,
paul@765 483
            id=_id("dtend-enable", index)
paul@765 484
            )
paul@765 485
paul@765 486
        self.control(
paul@765 487
            _name("dttimes-control", "recur", index), "checkbox",
paul@765 488
            _enable(index), p.times_enabled,
paul@765 489
            id=_id("dttimes-enable", index)
paul@765 490
            )
paul@765 491
paul@765 492
    def show_datetime_controls(self, formdate, show_start):
paul@765 493
paul@765 494
        """
paul@765 495
        Show datetime details from the current object for the 'formdate',
paul@765 496
        showing start details if 'show_start' is set to a true value. Details
paul@765 497
        will appear as controls for organisers and labels for attendees.
paul@765 498
        """
paul@765 499
paul@765 500
        page = self.page
paul@765 501
paul@867 502
        # Show controls for editing.
paul@765 503
paul@867 504
        page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
paul@765 505
paul@867 506
        if show_start:
paul@867 507
            page.div(class_="dt enabled")
paul@867 508
            self.date_controls("dtstart", formdate)
paul@867 509
            page.br()
paul@867 510
            page.label("Specify times", for_="dttimes-enable", class_="time disabled enable")
paul@867 511
            page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable")
paul@867 512
            page.div.close()
paul@765 513
paul@765 514
        else:
paul@886 515
            self.date_controls("dtend", formdate)
paul@867 516
            page.div(class_="dt disabled")
paul@867 517
            page.label("Specify end date", for_="dtend-enable", class_="enable")
paul@867 518
            page.div.close()
paul@867 519
            page.div(class_="dt enabled")
paul@867 520
            page.label("End on same day", for_="dtend-enable", class_="disable")
paul@867 521
            page.div.close()
paul@867 522
paul@867 523
        page.td.close()
paul@765 524
paul@868 525
    def show_recurrence_controls(self, index, period, recurrenceid, show_start):
paul@765 526
paul@765 527
        """
paul@765 528
        Show datetime details from the current object for the recurrence having
paul@765 529
        the given 'index', with the recurrence period described by 'period',
paul@765 530
        indicating a start, end and origin of the period from the event details,
paul@868 531
        employing any 'recurrenceid' for the object to configure the displayed
paul@868 532
        information.
paul@765 533
paul@765 534
        If 'show_start' is set to a true value, the start details will be shown;
paul@765 535
        otherwise, the end details will be shown.
paul@765 536
        """
paul@765 537
paul@765 538
        page = self.page
paul@765 539
        _id = self.element_identifier
paul@765 540
        _name = self.element_name
paul@765 541
paul@845 542
        period = form_period_from_period(period)
paul@765 543
paul@867 544
        # Show controls for editing.
paul@765 545
paul@868 546
        if not period.replaced:
paul@868 547
            page.td(class_="objectvalue dt%s" % (show_start and "start" or "end"))
paul@765 548
paul@765 549
            read_only = period.origin == "RRULE"
paul@765 550
paul@765 551
            if show_start:
paul@765 552
                page.div(class_="dt enabled")
paul@845 553
                self.date_controls(_name("dtstart", "recur", index), period.get_form_start(), index=index, read_only=read_only)
paul@765 554
                if not read_only:
paul@765 555
                    page.br()
paul@765 556
                    page.label("Specify times", for_=_id("dttimes-enable", index), class_="time disabled enable")
paul@765 557
                    page.label("Specify dates only", for_=_id("dttimes-enable", index), class_="time enabled disable")
paul@765 558
                page.div.close()
paul@765 559
paul@765 560
                # Put the origin somewhere.
paul@765 561
paul@845 562
                self.control("recur-origin", "hidden", period.origin or "")
paul@868 563
                self.control("recur-replaced", "hidden", period.replaced and str(index) or "")
paul@765 564
paul@765 565
            else:
paul@845 566
                self.date_controls(_name("dtend", "recur", index), period.get_form_end(), index=index, show_tzid=False, read_only=read_only)
paul@765 567
                if not read_only:
paul@886 568
                    page.div(class_="dt disabled")
paul@886 569
                    page.label("Specify end date", for_=_id("dtend-enable", index), class_="enable")
paul@886 570
                    page.div.close()
paul@886 571
                    page.div(class_="dt enabled")
paul@765 572
                    page.label("End on same day", for_=_id("dtend-enable", index), class_="disable")
paul@886 573
                    page.div.close()
paul@765 574
paul@765 575
            page.td.close()
paul@765 576
paul@765 577
        # Show label as attendee.
paul@765 578
paul@765 579
        else:
paul@868 580
            self.show_recurrence_label(index, period, recurrenceid, show_start)
paul@765 581
paul@868 582
    def show_recurrence_label(self, index, period, recurrenceid, show_start):
paul@765 583
paul@765 584
        """
paul@852 585
        Show datetime details from the current object for the recurrence having
paul@852 586
        the given 'index', for the given recurrence 'period', employing any
paul@868 587
        'recurrenceid' for the object to configure the displayed information.
paul@765 588
paul@765 589
        If 'show_start' is set to a true value, the start details will be shown;
paul@765 590
        otherwise, the end details will be shown.
paul@765 591
        """
paul@765 592
paul@765 593
        page = self.page
paul@852 594
        _name = self.element_name
paul@765 595
paul@845 596
        try:
paul@845 597
            p = event_period_from_period(period)
paul@845 598
        except PeriodError, exc:
paul@845 599
            affected = False
paul@845 600
        else:
paul@845 601
            affected = p.is_affected(recurrenceid)
paul@845 602
paul@845 603
        period = form_period_from_period(period)
paul@765 604
paul@765 605
        css = " ".join([
paul@868 606
            period.replaced and "replaced" or "",
paul@845 607
            affected and "affected" or ""
paul@765 608
            ])
paul@765 609
paul@845 610
        formdate = show_start and period.get_form_start() or period.get_form_end()
paul@765 611
        dt = formdate.as_datetime()
paul@765 612
        if dt:
paul@852 613
            page.td(class_=css)
paul@852 614
            if show_start:
paul@852 615
                self.date_controls(_name("dtstart", "recur", index), period.get_form_start(), index=index, read_only=True)
paul@852 616
                self.control("recur-origin", "hidden", period.origin or "")
paul@868 617
                self.control("recur-replaced", "hidden", period.replaced and str(index) or "")
paul@852 618
            else:
paul@852 619
                self.date_controls(_name("dtend", "recur", index), period.get_form_end(), index=index, show_tzid=False, read_only=True)
paul@852 620
            page.td.close()
paul@765 621
        else:
paul@765 622
            page.td("(Unrecognised date)")
paul@765 623
paul@765 624
    def get_date_control_values(self, name, multiple=False, tzid_name=None):
paul@765 625
paul@765 626
        """
paul@787 627
        Return a form date object representing fields starting with 'name'. If
paul@787 628
        'multiple' is set to a true value, many date objects will be returned
paul@787 629
        corresponding to a collection of datetimes.
paul@787 630
paul@787 631
        If 'tzid_name' is specified, the time zone information will be acquired
paul@787 632
        from fields starting with 'tzid_name' instead of 'name'.
paul@765 633
        """
paul@765 634
paul@765 635
        args = self.env.get_args()
paul@765 636
paul@765 637
        dates = args.get("%s-date" % name, [])
paul@765 638
        hours = args.get("%s-hour" % name, [])
paul@765 639
        minutes = args.get("%s-minute" % name, [])
paul@765 640
        seconds = args.get("%s-second" % name, [])
paul@765 641
        tzids = args.get("%s-tzid" % (tzid_name or name), [])
paul@765 642
paul@765 643
        # Handle absent values by employing None values.
paul@765 644
paul@765 645
        field_values = map(None, dates, hours, minutes, seconds, tzids)
paul@765 646
paul@765 647
        if not field_values and not multiple:
paul@765 648
            all_values = FormDate()
paul@765 649
        else:
paul@765 650
            all_values = []
paul@765 651
            for date, hour, minute, second, tzid in field_values:
paul@765 652
                value = FormDate(date, hour, minute, second, tzid or self.get_tzid())
paul@765 653
paul@765 654
                # Return a single value or append to a collection of all values.
paul@765 655
paul@765 656
                if not multiple:
paul@765 657
                    return value
paul@765 658
                else:
paul@765 659
                    all_values.append(value)
paul@765 660
paul@765 661
        return all_values
paul@765 662
paul@787 663
    def set_date_control_values(self, name, formdates, tzid_name=None):
paul@787 664
paul@787 665
        """
paul@787 666
        Replace form fields starting with 'name' using the values of the given
paul@787 667
        'formdates'.
paul@787 668
paul@787 669
        If 'tzid_name' is specified, the time zone information will be stored in
paul@787 670
        fields starting with 'tzid_name' instead of 'name'.
paul@787 671
        """
paul@787 672
paul@787 673
        args = self.env.get_args()
paul@787 674
paul@787 675
        args["%s-date" % name] = [d.date for d in formdates]
paul@787 676
        args["%s-hour" % name] = [d.hour for d in formdates]
paul@787 677
        args["%s-minute" % name] = [d.minute for d in formdates]
paul@787 678
        args["%s-second" % name] = [d.second for d in formdates]
paul@787 679
        args["%s-tzid" % (tzid_name or name)] = [d.tzid for d in formdates]
paul@787 680
paul@446 681
# vim: tabstop=4 expandtab shiftwidth=4