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