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