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