1 #!/usr/bin/env python 2 3 """ 4 Common resource functionality for Web calendar clients. 5 6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from datetime import datetime, timedelta 23 from imiptools.client import Client, ClientForObject 24 from imiptools.data import get_address, get_uri, uri_item, uri_values 25 from imiptools.dates import format_datetime, get_recurrence_start_point, to_date 26 from imiptools.period import remove_period, remove_affected_period 27 from imipweb.data import event_period_from_period, form_period_from_period, \ 28 FormDate, PeriodError 29 from imipweb.env import CGIEnvironment 30 from urllib import urlencode 31 import babel.dates 32 import imip_store 33 import markup 34 import pytz 35 36 class Resource: 37 38 "A Web application resource." 39 40 def __init__(self, resource=None): 41 42 """ 43 Initialise a resource, allowing it to share the environment of any given 44 existing 'resource'. 45 """ 46 47 self.encoding = "utf-8" 48 self.env = CGIEnvironment(self.encoding) 49 50 self.objects = {} 51 self.locale = None 52 self.requests = None 53 54 self.out = resource and resource.out or self.env.get_output() 55 self.page = resource and resource.page or markup.page() 56 self.html_ids = None 57 58 # Presentation methods. 59 60 def new_page(self, title): 61 self.page.init(title=title, charset=self.encoding, css=self.env.new_url("styles.css")) 62 self.html_ids = set() 63 64 def status(self, code, message): 65 self.header("Status", "%s %s" % (code, message)) 66 67 def header(self, header, value): 68 print >>self.out, "%s: %s" % (header, value) 69 70 def no_user(self): 71 self.status(403, "Forbidden") 72 self.new_page(title="Forbidden") 73 self.page.p("You are not logged in and thus cannot access scheduling requests.") 74 75 def no_page(self): 76 self.status(404, "Not Found") 77 self.new_page(title="Not Found") 78 self.page.p("No page is provided at the given address.") 79 80 def redirect(self, url): 81 self.status(302, "Redirect") 82 self.header("Location", url) 83 self.new_page(title="Redirect") 84 self.page.p("Redirecting to: %s" % url) 85 86 def link_to(self, uid=None, recurrenceid=None, args=None): 87 88 """ 89 Return a link to a resource, being an object with any given 'uid' and 90 'recurrenceid', or the main resource otherwise. 91 92 See get_identifiers for the decoding of such links. 93 94 If 'args' is specified, the given dictionary is encoded and included. 95 """ 96 97 path = [] 98 if uid: 99 path.append(uid) 100 if recurrenceid: 101 path.append(recurrenceid) 102 return "%s%s" % (self.env.new_url("/".join(path)), args and ("?%s" % urlencode(args)) or "") 103 104 # Access to objects. 105 106 def get_identifiers(self, path_info): 107 108 """ 109 Return identifiers provided by 'path_info', potentially encoded by 110 'link_to'. 111 """ 112 113 parts = path_info.lstrip("/").split("/") 114 115 # UID only. 116 117 if len(parts) == 1: 118 return parts[0], None 119 120 # UID and RECURRENCE-ID. 121 122 else: 123 return parts[:2] 124 125 def _get_object(self, uid, recurrenceid=None, section=None, username=None): 126 if self.objects.has_key((uid, recurrenceid, section, username)): 127 return self.objects[(uid, recurrenceid, section, username)] 128 129 obj = self.objects[(uid, recurrenceid, section, username)] = self.get_stored_object(uid, recurrenceid, section, username) 130 return obj 131 132 def _get_recurrences(self, uid): 133 return self.store.get_recurrences(self.user, uid) 134 135 def _get_active_recurrences(self, uid): 136 return self.store.get_active_recurrences(self.user, uid) 137 138 def _get_requests(self): 139 if self.requests is None: 140 self.requests = self.store.get_requests(self.user) 141 return self.requests 142 143 def _have_request(self, uid, recurrenceid=None, type=None, strict=False): 144 return self.store.have_request(self._get_requests(), uid, recurrenceid, type, strict) 145 146 def _is_request(self): 147 return self._have_request(self.uid, self.recurrenceid) 148 149 def _get_counters(self, uid, recurrenceid=None): 150 return self.store.get_counters(self.user, uid, recurrenceid) 151 152 def _get_request_summary(self): 153 154 "Return a list of periods comprising the request summary." 155 156 summary = [] 157 158 for uid, recurrenceid, request_type in self._get_requests(): 159 160 # Obtain either normal objects or counter-proposals. 161 162 if not request_type: 163 objs = [self._get_object(uid, recurrenceid)] 164 elif request_type == "COUNTER": 165 objs = [] 166 for attendee in self.store.get_counters(self.user, uid, recurrenceid): 167 objs.append(self._get_object(uid, recurrenceid, "counters", attendee)) 168 169 # For each object, obtain the periods involved. 170 171 for obj in objs: 172 if obj: 173 recurrenceids = self._get_recurrences(uid) 174 175 # Obtain only active periods, not those replaced by redefined 176 # recurrences, converting to free/busy periods. 177 178 for p in obj.get_active_periods(recurrenceids, self.get_tzid(), self.get_window_end()): 179 summary.append(obj.get_freebusy_period(p)) 180 181 return summary 182 183 # Preference methods. 184 185 def get_user_locale(self): 186 if not self.locale: 187 self.locale = self.get_preferences().get("LANG", "en") 188 return self.locale 189 190 # Prettyprinting of dates and times. 191 192 def format_date(self, dt, format): 193 return self._format_datetime(babel.dates.format_date, dt, format) 194 195 def format_time(self, dt, format): 196 return self._format_datetime(babel.dates.format_time, dt, format) 197 198 def format_datetime(self, dt, format): 199 return self._format_datetime( 200 isinstance(dt, datetime) and babel.dates.format_datetime or babel.dates.format_date, 201 dt, format) 202 203 def _format_datetime(self, fn, dt, format): 204 return fn(dt, format=format, locale=self.get_user_locale()) 205 206 class ResourceClient(Resource, Client): 207 208 "A Web application resource and calendar client." 209 210 def __init__(self, resource=None): 211 Resource.__init__(self, resource) 212 user = self.env.get_user() 213 Client.__init__(self, user and get_uri(user) or None) 214 215 class ResourceClientForObject(Resource, ClientForObject): 216 217 "A Web application resource and calendar client for a specific object." 218 219 def __init__(self, resource=None, messenger=None): 220 Resource.__init__(self, resource) 221 user = self.env.get_user() 222 ClientForObject.__init__(self, None, user and get_uri(user) or None, messenger) 223 224 # Communication methods. 225 226 def send_message(self, parts, sender, from_organiser, bcc_sender): 227 228 """ 229 Send the given 'parts' to the appropriate recipients, also sending a 230 copy to the 'sender'. The 'from_organiser' value indicates whether the 231 organiser is sending this message (and is thus equivalent to "as 232 organiser"). 233 """ 234 235 # As organiser, send an invitation to attendees, excluding oneself if 236 # also attending. The updated event will be saved by the outgoing 237 # handler. 238 239 organiser = get_uri(self.obj.get_value("ORGANIZER")) 240 attendees = uri_values(self.obj.get_values("ATTENDEE")) 241 242 if from_organiser: 243 recipients = [get_address(attendee) for attendee in attendees if attendee != self.user] 244 else: 245 recipients = [get_address(organiser)] 246 247 # Since the outgoing handler updates this user's free/busy details, 248 # the stored details will probably not have the updated details at 249 # this point, so we update our copy for serialisation as the bundled 250 # free/busy object. 251 252 freebusy = self.store.get_freebusy(self.user) 253 self.update_freebusy(freebusy, self.user, from_organiser) 254 255 # Bundle free/busy information if appropriate. 256 257 part = self.get_freebusy_part(freebusy) 258 if part: 259 parts.append(part) 260 261 self._send_message(sender, recipients, parts, bcc_sender) 262 263 def _send_message(self, sender, recipients, parts, bcc_sender): 264 265 """ 266 Send a message, explicitly specifying the 'sender' as an outgoing BCC 267 recipient since the generic calendar user will be the actual sender. 268 """ 269 270 if not bcc_sender: 271 message = self.messenger.make_outgoing_message(parts, recipients) 272 self.messenger.sendmail(recipients, message.as_string()) 273 else: 274 message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender) 275 self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender) 276 277 def send_message_to_self(self, parts): 278 279 "Send a message composed of the given 'parts' to the given user." 280 281 sender = get_address(self.user) 282 message = self.messenger.make_outgoing_message(parts, [sender]) 283 self.messenger.sendmail([sender], message.as_string()) 284 285 # Action methods. 286 287 def process_declined_counter(self, attendee): 288 289 "Process a declined counter-proposal." 290 291 # Obtain the counter-proposal for the attendee. 292 293 obj = self.get_stored_object(self.uid, self.recurrenceid, "counters", attendee) 294 if not obj: 295 return False 296 297 method = "DECLINECOUNTER" 298 self.update_senders(obj=obj) 299 obj.update_dtstamp() 300 obj.update_sequence(False) 301 self._send_message(get_address(self.user), [get_address(attendee)], [obj.to_part(method)], True) 302 return True 303 304 def process_received_request(self, changed=False): 305 306 """ 307 Process the current request for the current user. Return whether any 308 action was taken. If 'changed' is set to a true value, or if 'attendees' 309 is specified and differs from the stored attendees, a counter-proposal 310 will be sent instead of a reply. 311 """ 312 313 # Reply only on behalf of this user. 314 315 attendee_attr = self.update_participation() 316 317 if not attendee_attr: 318 return False 319 320 if not changed: 321 self.obj["ATTENDEE"] = [(self.user, attendee_attr)] 322 else: 323 self.update_senders() 324 325 self.update_dtstamp() 326 self.update_sequence(False) 327 self.send_message([self.obj.to_part(changed and "COUNTER" or "REPLY")], get_address(self.user), False, True) 328 return True 329 330 def process_created_request(self, method, to_cancel=None, to_unschedule=None): 331 332 """ 333 Process the current request, sending a created request of the given 334 'method' to attendees. Return whether any action was taken. 335 336 If 'to_cancel' is specified, a list of participants to be sent cancel 337 messages is provided. 338 339 If 'to_unschedule' is specified, a list of periods to be unscheduled is 340 provided. 341 """ 342 343 # Here, the organiser should be the current user. 344 345 organiser, organiser_attr = uri_item(self.obj.get_item("ORGANIZER")) 346 347 self.update_sender(organiser_attr) 348 self.update_senders() 349 self.update_dtstamp() 350 self.update_sequence(True) 351 352 if method == "REQUEST": 353 methods, parts = self.get_message_parts(self.obj, "REQUEST") 354 355 # Add message parts with cancelled occurrence information. 356 357 unscheduled_parts = self.get_unscheduled_parts(to_unschedule) 358 359 # Send the updated event, along with a cancellation for each of the 360 # unscheduled occurrences. 361 362 self.send_message(parts + unscheduled_parts, get_address(organiser), True, False) 363 364 # Since the organiser can update the SEQUENCE but this can leave any 365 # mail/calendar client lagging, issue a PUBLISH message to the 366 # user's address. 367 368 methods, parts = self.get_message_parts(self.obj, "PUBLISH") 369 self.send_message_to_self(parts + unscheduled_parts) 370 371 # When cancelling, replace the attendees with those for whom the event 372 # is now cancelled. 373 374 if method == "CANCEL" or to_cancel: 375 if to_cancel: 376 obj = self.obj.copy() 377 obj["ATTENDEE"] = to_cancel 378 else: 379 obj = self.obj 380 381 # Send a cancellation to all uninvited attendees. 382 383 parts = [obj.to_part("CANCEL")] 384 self.send_message(parts, get_address(organiser), True, False) 385 386 # Issue a CANCEL message to the user's address. 387 388 self.send_message_to_self(parts) 389 390 return True 391 392 class FormUtilities: 393 394 "Utility methods resource mix-in." 395 396 def prefixed_args(self, prefix, convert=None): 397 398 """ 399 Return values for all arguments having the given 'prefix' in their 400 names, removing the prefix to obtain each value from the argument name 401 itself. The 'convert' callable can be specified to perform a conversion 402 (to int, for example). 403 """ 404 405 args = self.env.get_args() 406 407 values = [] 408 for name in args.keys(): 409 if name.startswith(prefix): 410 value = name[len(prefix):] 411 if convert: 412 try: 413 value = convert(value) 414 except ValueError: 415 pass 416 values.append(value) 417 return values 418 419 def control(self, name, type, value, selected=False, **kw): 420 421 """ 422 Show a control with the given 'name', 'type' and 'value', with 423 'selected' indicating whether it should be selected (checked or 424 equivalent), and with keyword arguments setting other properties. 425 """ 426 427 page = self.page 428 if type in ("checkbox", "radio") and selected: 429 page.input(name=name, type=type, value=value, checked="checked", **kw) 430 else: 431 page.input(name=name, type=type, value=value, **kw) 432 433 def menu(self, name, default, items, class_="", index=None): 434 435 """ 436 Show a select menu having the given 'name', set to the given 'default', 437 providing the given (value, label) 'items', and employing the given CSS 438 'class_' if specified. 439 """ 440 441 page = self.page 442 values = self.env.get_args().get(name, [default]) 443 if index is not None: 444 values = values[index:] 445 values = values and values[0:1] or [default] 446 447 page.select(name=name, class_=class_) 448 for v, label in items: 449 if v is None: 450 continue 451 if v in values: 452 page.option(label, value=v, selected="selected") 453 else: 454 page.option(label, value=v) 455 page.select.close() 456 457 def date_controls(self, name, default, index=None, show_tzid=True, read_only=False): 458 459 """ 460 Show date controls for a field with the given 'name' and 'default' form 461 date value. 462 463 If 'index' is specified, default field values will be overridden by the 464 element from a collection of existing form values with the specified 465 index; otherwise, field values will be overridden by a single form 466 value. 467 468 If 'show_tzid' is set to a false value, the time zone menu will not be 469 provided. 470 471 If 'read_only' is set to a true value, the controls will be hidden and 472 labels will be employed instead. 473 """ 474 475 page = self.page 476 477 # Show dates for up to one week around the current date. 478 479 page.span(class_="date enabled") 480 481 dt = default.as_datetime() 482 if not dt: 483 dt = date.today() 484 485 base = to_date(dt) 486 487 # Show a date label with a hidden field if read-only. 488 489 if read_only: 490 self.control("%s-date" % name, "hidden", format_datetime(base)) 491 page.span(self.format_date(base, "long")) 492 493 # Show dates for up to one week around the current date. 494 # NOTE: Support paging to other dates. 495 496 else: 497 items = [] 498 for i in range(-7, 8): 499 d = base + timedelta(i) 500 items.append((format_datetime(d), self.format_date(d, "full"))) 501 self.menu("%s-date" % name, format_datetime(base), items, index=index) 502 503 page.span.close() 504 505 # Show time details. 506 507 page.span(class_="time enabled") 508 509 if read_only: 510 page.span("%s:%s:%s" % (default.get_hour(), default.get_minute(), default.get_second())) 511 self.control("%s-hour" % name, "hidden", default.get_hour()) 512 self.control("%s-minute" % name, "hidden", default.get_minute()) 513 self.control("%s-second" % name, "hidden", default.get_second()) 514 else: 515 self.control("%s-hour" % name, "text", default.get_hour(), maxlength=2, size=2) 516 page.add(":") 517 self.control("%s-minute" % name, "text", default.get_minute(), maxlength=2, size=2) 518 page.add(":") 519 self.control("%s-second" % name, "text", default.get_second(), maxlength=2, size=2) 520 521 # Show time zone details. 522 523 if show_tzid: 524 page.add(" ") 525 tzid = default.get_tzid() or self.get_tzid() 526 527 # Show a label if read-only or a menu otherwise. 528 529 if read_only: 530 self.control("%s-tzid" % name, "hidden", tzid) 531 page.span(tzid) 532 else: 533 self.timezone_menu("%s-tzid" % name, tzid, index) 534 535 page.span.close() 536 537 def timezone_menu(self, name, default, index=None): 538 539 """ 540 Show timezone controls using a menu with the given 'name', set to the 541 given 'default' unless a field of the given 'name' provides a value. 542 """ 543 544 entries = [(tzid, tzid) for tzid in pytz.all_timezones] 545 self.menu(name, default, entries, index=index) 546 547 class DateTimeFormUtilities: 548 549 "Date/time control methods resource mix-in." 550 551 # Control naming helpers. 552 553 def element_identifier(self, name, index=None): 554 return index is not None and "%s-%d" % (name, index) or name 555 556 def element_name(self, name, suffix, index=None): 557 return index is not None and "%s-%s" % (name, suffix) or name 558 559 def element_enable(self, index=None): 560 return index is not None and str(index) or "enable" 561 562 def show_object_datetime_controls(self, period, index=None): 563 564 """ 565 Show datetime-related controls if already active or if an object needs 566 them for the given 'period'. The given 'index' is used to parameterise 567 individual controls for dynamic manipulation. 568 """ 569 570 p = form_period_from_period(period) 571 572 page = self.page 573 args = self.env.get_args() 574 _id = self.element_identifier 575 _name = self.element_name 576 _enable = self.element_enable 577 578 # Add a dynamic stylesheet to permit the controls to modify the display. 579 # NOTE: The style details need to be coordinated with the static 580 # NOTE: stylesheet. 581 582 if index is not None: 583 page.style(type="text/css") 584 585 # Unlike the rules for object properties, these affect recurrence 586 # properties. 587 588 page.add("""\ 589 input#dttimes-enable-%(index)d, 590 input#dtend-enable-%(index)d, 591 input#dttimes-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue .time.enabled, 592 input#dttimes-enable-%(index)d:checked ~ .recurrence td.objectvalue .time.disabled, 593 input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .dt.enabled, 594 input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .dt.disabled { 595 display: none; 596 } 597 598 input#dtend-enable-%(index)d:not(:checked) ~ .recurrence td.objectvalue.dtend .date.enabled, 599 input#dtend-enable-%(index)d:checked ~ .recurrence td.objectvalue.dtend .date.disabled { 600 visibility: hidden; 601 }""" % {"index" : index}) 602 603 page.style.close() 604 605 self.control( 606 _name("dtend-control", "recur", index), "checkbox", 607 _enable(index), p.end_enabled, 608 id=_id("dtend-enable", index) 609 ) 610 611 self.control( 612 _name("dttimes-control", "recur", index), "checkbox", 613 _enable(index), p.times_enabled, 614 id=_id("dttimes-enable", index) 615 ) 616 617 def show_datetime_controls(self, formdate, show_start): 618 619 """ 620 Show datetime details from the current object for the 'formdate', 621 showing start details if 'show_start' is set to a true value. Details 622 will appear as controls for organisers and labels for attendees. 623 """ 624 625 page = self.page 626 627 # Show controls for editing. 628 629 page.td(class_="objectvalue dt%s" % (show_start and "start" or "end")) 630 631 if show_start: 632 page.div(class_="dt enabled") 633 self.date_controls("dtstart", formdate) 634 page.br() 635 page.label("Specify times", for_="dttimes-enable", class_="time disabled enable") 636 page.label("Specify dates only", for_="dttimes-enable", class_="time enabled disable") 637 page.div.close() 638 639 else: 640 self.date_controls("dtend", formdate) 641 page.div(class_="dt disabled") 642 page.label("Specify end date", for_="dtend-enable", class_="enable") 643 page.div.close() 644 page.div(class_="dt enabled") 645 page.label("End on same day", for_="dtend-enable", class_="disable") 646 page.div.close() 647 648 page.td.close() 649 650 def show_recurrence_controls(self, index, period, recurrenceid, show_start): 651 652 """ 653 Show datetime details from the current object for the recurrence having 654 the given 'index', with the recurrence period described by 'period', 655 indicating a start, end and origin of the period from the event details, 656 employing any 'recurrenceid' for the object to configure the displayed 657 information. 658 659 If 'show_start' is set to a true value, the start details will be shown; 660 otherwise, the end details will be shown. 661 """ 662 663 page = self.page 664 _id = self.element_identifier 665 _name = self.element_name 666 667 period = form_period_from_period(period) 668 669 # Show controls for editing. 670 671 if not period.replaced: 672 page.td(class_="objectvalue dt%s" % (show_start and "start" or "end")) 673 674 read_only = period.origin == "RRULE" 675 676 if show_start: 677 page.div(class_="dt enabled") 678 self.date_controls(_name("dtstart", "recur", index), period.get_form_start(), index=index, read_only=read_only) 679 if not read_only: 680 page.br() 681 page.label("Specify times", for_=_id("dttimes-enable", index), class_="time disabled enable") 682 page.label("Specify dates only", for_=_id("dttimes-enable", index), class_="time enabled disable") 683 page.div.close() 684 685 # Put the origin somewhere. 686 687 self.control("recur-origin", "hidden", period.origin or "") 688 self.control("recur-replaced", "hidden", period.replaced and str(index) or "") 689 690 else: 691 self.date_controls(_name("dtend", "recur", index), period.get_form_end(), index=index, show_tzid=False, read_only=read_only) 692 if not read_only: 693 page.div(class_="dt disabled") 694 page.label("Specify end date", for_=_id("dtend-enable", index), class_="enable") 695 page.div.close() 696 page.div(class_="dt enabled") 697 page.label("End on same day", for_=_id("dtend-enable", index), class_="disable") 698 page.div.close() 699 700 page.td.close() 701 702 # Show label as attendee. 703 704 else: 705 self.show_recurrence_label(index, period, recurrenceid, show_start) 706 707 def show_recurrence_label(self, index, period, recurrenceid, show_start): 708 709 """ 710 Show datetime details from the current object for the recurrence having 711 the given 'index', for the given recurrence 'period', employing any 712 'recurrenceid' for the object to configure the displayed information. 713 714 If 'show_start' is set to a true value, the start details will be shown; 715 otherwise, the end details will be shown. 716 """ 717 718 page = self.page 719 _name = self.element_name 720 721 try: 722 p = event_period_from_period(period) 723 except PeriodError, exc: 724 affected = False 725 else: 726 affected = p.is_affected(recurrenceid) 727 728 period = form_period_from_period(period) 729 730 css = " ".join([ 731 period.replaced and "replaced" or "", 732 affected and "affected" or "" 733 ]) 734 735 formdate = show_start and period.get_form_start() or period.get_form_end() 736 dt = formdate.as_datetime() 737 if dt: 738 page.td(class_=css) 739 if show_start: 740 self.date_controls(_name("dtstart", "recur", index), period.get_form_start(), index=index, read_only=True) 741 self.control("recur-origin", "hidden", period.origin or "") 742 self.control("recur-replaced", "hidden", period.replaced and str(index) or "") 743 else: 744 self.date_controls(_name("dtend", "recur", index), period.get_form_end(), index=index, show_tzid=False, read_only=True) 745 page.td.close() 746 else: 747 page.td("(Unrecognised date)") 748 749 def get_date_control_values(self, name, multiple=False, tzid_name=None): 750 751 """ 752 Return a form date object representing fields starting with 'name'. If 753 'multiple' is set to a true value, many date objects will be returned 754 corresponding to a collection of datetimes. 755 756 If 'tzid_name' is specified, the time zone information will be acquired 757 from fields starting with 'tzid_name' instead of 'name'. 758 """ 759 760 args = self.env.get_args() 761 762 dates = args.get("%s-date" % name, []) 763 hours = args.get("%s-hour" % name, []) 764 minutes = args.get("%s-minute" % name, []) 765 seconds = args.get("%s-second" % name, []) 766 tzids = args.get("%s-tzid" % (tzid_name or name), []) 767 768 # Handle absent values by employing None values. 769 770 field_values = map(None, dates, hours, minutes, seconds, tzids) 771 772 if not field_values and not multiple: 773 all_values = FormDate() 774 else: 775 all_values = [] 776 for date, hour, minute, second, tzid in field_values: 777 value = FormDate(date, hour, minute, second, tzid or self.get_tzid()) 778 779 # Return a single value or append to a collection of all values. 780 781 if not multiple: 782 return value 783 else: 784 all_values.append(value) 785 786 return all_values 787 788 def set_date_control_values(self, name, formdates, tzid_name=None): 789 790 """ 791 Replace form fields starting with 'name' using the values of the given 792 'formdates'. 793 794 If 'tzid_name' is specified, the time zone information will be stored in 795 fields starting with 'tzid_name' instead of 'name'. 796 """ 797 798 args = self.env.get_args() 799 800 args["%s-date" % name] = [d.date for d in formdates] 801 args["%s-hour" % name] = [d.hour for d in formdates] 802 args["%s-minute" % name] = [d.minute for d in formdates] 803 args["%s-second" % name] = [d.second for d in formdates] 804 args["%s-tzid" % (tzid_name or name)] = [d.tzid for d in formdates] 805 806 # vim: tabstop=4 expandtab shiftwidth=4