1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - EventAggregator Macro 4 5 @copyright: 2008, 2009 by Paul Boddie <paul@boddie.org.uk> 6 @copyright: 2000-2004 Juergen Hermann <jh@web.de>, 7 2005-2008 MoinMoin:ThomasWaldmann. 8 @license: GNU GPL (v2 or later), see COPYING.txt for details. 9 """ 10 11 from MoinMoin import wikiutil 12 import EventAggregatorSupport 13 import calendar 14 15 linkToPage = EventAggregatorSupport.linkToPage 16 17 try: 18 set 19 except NameError: 20 from sets import Set as set 21 22 Dependencies = ['pages'] 23 24 # Abstractions. 25 26 class View: 27 28 "A view of the event calendar." 29 30 def __init__(self, page, calendar_name, first, last): 31 32 """ 33 Initialise the view with the current 'page', a 'calendar_name' (which 34 may be None), and the 'first' and 'last' months. 35 """ 36 37 self.page = page 38 self.calendar_name = calendar_name 39 40 if self.calendar_name is not None: 41 42 # Store the view parameters. 43 44 span = EventAggregatorSupport.span(first, last) 45 self.number_of_months = span[0] * 12 + span[1] + 1 46 47 self.previous_month_start = EventAggregatorSupport.prevmonth(first) 48 self.next_month_start = EventAggregatorSupport.nextmonth(first) 49 self.previous_month_end = EventAggregatorSupport.prevmonth(last) 50 self.next_month_end = EventAggregatorSupport.nextmonth(last) 51 52 self.previous_set_start = EventAggregatorSupport.monthupdate(first, -self.number_of_months) 53 self.next_set_start = EventAggregatorSupport.monthupdate(first, self.number_of_months) 54 self.previous_set_end = EventAggregatorSupport.monthupdate(last, -self.number_of_months) 55 self.next_set_end = EventAggregatorSupport.monthupdate(last, self.number_of_months) 56 57 def getMonthQueryString(self, argname, month): 58 if month is not None: 59 return "%s-%s=%04d-%02d" % ((self.calendar_name, argname) + month) 60 else: 61 return "" 62 63 def writeMonthHeading(self, year, month): 64 page = self.page 65 request = page.request 66 fmt = page.formatter 67 _ = request.getText 68 69 output = [] 70 71 month_label = _(EventAggregatorSupport.getMonthLabel(month)) 72 73 # Prepare navigation links. 74 75 if self.calendar_name is not None: 76 calendar_name = self.calendar_name 77 78 # Links to the previous set of months and to a calendar shifted 79 # back one month. 80 81 previous_set_link = "%s&%s" % ( 82 self.getMonthQueryString("start", self.previous_set_start), 83 self.getMonthQueryString("end", self.previous_set_end) 84 ) 85 previous_month_link = "%s&%s" % ( 86 self.getMonthQueryString("start", self.previous_month_start), 87 self.getMonthQueryString("end", self.previous_month_end) 88 ) 89 90 # Links to the next set of months and to a calendar shifted 91 # forward one month. 92 93 next_set_link = "%s&%s" % ( 94 self.getMonthQueryString("start", self.next_set_start), 95 self.getMonthQueryString("end", self.next_set_end) 96 ) 97 next_month_link = "%s&%s" % ( 98 self.getMonthQueryString("start", self.next_month_start), 99 self.getMonthQueryString("end", self.next_month_end) 100 ) 101 102 # A link leading to this month being at the top of the calendar. 103 104 full_month_label = "%s %s" % (month_label, year) 105 end_month = EventAggregatorSupport.monthupdate((year, month), self.number_of_months - 1) 106 107 month_link = "%s&%s" % ( 108 self.getMonthQueryString("start", (year, month)), 109 self.getMonthQueryString("end", end_month) 110 ) 111 112 output.append(fmt.span(on=1, css_class="previous-month")) 113 output.append(linkToPage(request, page, "<<", previous_set_link)) 114 output.append(fmt.text(" ")) 115 output.append(linkToPage(request, page, "<", previous_month_link)) 116 output.append(fmt.span(on=0)) 117 118 output.append(fmt.span(on=1, css_class="next-month")) 119 output.append(linkToPage(request, page, ">", next_month_link)) 120 output.append(fmt.text(" ")) 121 output.append(linkToPage(request, page, ">>", next_set_link)) 122 output.append(fmt.span(on=0)) 123 124 output.append(linkToPage(request, page, full_month_label, month_link)) 125 126 else: 127 output.append(fmt.span(on=1)) 128 output.append(fmt.text(month_label)) 129 output.append(fmt.span(on=0)) 130 output.append(fmt.text(" ")) 131 output.append(fmt.span(on=1)) 132 output.append(fmt.text(unicode(year))) 133 output.append(fmt.span(on=0)) 134 135 return "".join(output) 136 137 # HTML-related functions. 138 139 def getColour(s): 140 colour = [0, 0, 0] 141 digit = 0 142 for c in s: 143 colour[digit] += ord(c) 144 colour[digit] = colour[digit] % 256 145 digit += 1 146 digit = digit % 3 147 return tuple(colour) 148 149 def getBlackOrWhite(colour): 150 if sum(colour) / 3.0 > 127: 151 return (0, 0, 0) 152 else: 153 return (255, 255, 255) 154 155 def getMonthActionQueryString(argname, month): 156 if month is not None: 157 return "%s=%04d-%02d" % ((argname,) + month) 158 else: 159 return "" 160 161 # Macro functions. 162 163 def execute(macro, args): 164 165 """ 166 Execute the 'macro' with the given 'args': an optional list of selected 167 category names (categories whose pages are to be shown), together with 168 optional named arguments of the following forms: 169 170 start=YYYY-MM shows event details starting from the specified month 171 start=current-N shows event details relative to the current month 172 end=YYYY-MM shows event details ending at the specified month 173 end=current+N shows event details relative to the current month 174 175 mode=calendar shows a calendar view of events 176 mode=list shows a list of events by month 177 mode=ics provides iCalendar data for the events 178 179 names=daily shows the name of an event on every day of that event 180 names=weekly shows the name of an event once per week 181 182 calendar=NAME uses the given NAME to provide request parameters which 183 can be used to control the calendar view 184 """ 185 186 request = macro.request 187 fmt = macro.formatter 188 page = fmt.page 189 _ = request.getText 190 191 # Interpret the arguments. 192 193 try: 194 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 195 except AttributeError: 196 parsed_args = args.split(",") 197 198 parsed_args = [arg for arg in parsed_args if arg] 199 200 # Get special arguments. 201 202 category_names = [] 203 calendar_start = None 204 calendar_end = None 205 mode = "calendar" 206 name_usage = "weekly" 207 calendar_name = None 208 209 for arg in parsed_args: 210 if arg.startswith("start="): 211 calendar_start = EventAggregatorSupport.getParameterMonth(arg[6:]) 212 213 elif arg.startswith("end="): 214 calendar_end = EventAggregatorSupport.getParameterMonth(arg[4:]) 215 216 elif arg.startswith("mode="): 217 mode = arg[5:] 218 219 elif arg.startswith("names="): 220 name_usage = arg[6:] 221 222 elif arg.startswith("calendar="): 223 calendar_name = arg[9:] 224 225 else: 226 category_names.append(arg) 227 228 # Find request parameters to override settings. 229 230 if calendar_name is not None: 231 calendar_start = EventAggregatorSupport.getFormMonth(request, calendar_name, "start") or calendar_start 232 calendar_end = EventAggregatorSupport.getFormMonth(request, calendar_name, "end") or calendar_end 233 234 # Get the events. 235 236 events, shown_events, all_shown_events, earliest, latest = \ 237 EventAggregatorSupport.getEvents(request, category_names, calendar_start, calendar_end) 238 239 # Get a concrete period of time. 240 241 first, last = EventAggregatorSupport.getConcretePeriod(calendar_start, calendar_end, earliest, latest) 242 243 # Define a view of the calendar, retaining useful navigational information. 244 245 view = View(page, calendar_name, first, last) 246 247 # Make a calendar. 248 249 output = [] 250 251 # Output download controls. 252 253 download_all_link = "action=EventAggregatorSummary&doit=1&%s" % ( 254 "&".join([("category=%s" % name) for name in category_names]) 255 ) 256 download_link = download_all_link + ("&%s&%s" % ( 257 getMonthActionQueryString("start", calendar_start), 258 getMonthActionQueryString("end", calendar_end) 259 )) 260 subscribe_all_link = download_all_link + "&format=RSS" 261 subscribe_link = download_link + "&format=RSS" 262 263 output.append(fmt.div(on=1, css_class="event-controls")) 264 output.append(fmt.span(on=1, css_class="event-download")) 265 output.append(linkToPage(request, page, _("Download this view"), download_link)) 266 output.append(fmt.span(on=0)) 267 output.append(fmt.span(on=1, css_class="event-download")) 268 output.append(linkToPage(request, page, _("Download this calendar"), download_all_link)) 269 output.append(fmt.span(on=0)) 270 output.append(fmt.span(on=1, css_class="event-download")) 271 output.append(linkToPage(request, page, _("Subscribe to this view"), subscribe_link)) 272 output.append(fmt.span(on=0)) 273 output.append(fmt.span(on=1, css_class="event-download")) 274 output.append(linkToPage(request, page, _("Subscribe to this calendar"), subscribe_all_link)) 275 output.append(fmt.span(on=0)) 276 output.append(fmt.div(on=0)) 277 278 # Output top-level information. 279 280 # Start of list view output. 281 282 if mode == "list": 283 output.append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 284 285 # Start of table view output. 286 287 elif mode == "table": 288 289 # Output a table. 290 291 output.append(fmt.table(on=1, attrs={"tableclass" : "event-table"})) 292 293 output.append(fmt.table_row(on=1)) 294 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 295 output.append(fmt.text(_("Event dates"))) 296 output.append(fmt.table_cell(on=0)) 297 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 298 output.append(fmt.text(_("Event location"))) 299 output.append(fmt.table_cell(on=0)) 300 output.append(fmt.table_cell(on=1, attrs={"class" : "event-table-heading"})) 301 output.append(fmt.text(_("Event details"))) 302 output.append(fmt.table_cell(on=0)) 303 output.append(fmt.table_row(on=0)) 304 305 # Visit all months in the requested range, or across known events. 306 307 for year, month in EventAggregatorSupport.daterange(first, last): 308 309 # Either output a calendar view... 310 311 if mode == "calendar": 312 313 # Output a month. 314 315 output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"})) 316 317 output.append(fmt.table_row(on=1)) 318 output.append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "21"})) 319 320 # Either write a month heading or produce links for navigable 321 # calendars. 322 323 output.append(view.writeMonthHeading(year, month)) 324 325 output.append(fmt.table_cell(on=0)) 326 output.append(fmt.table_row(on=0)) 327 328 # Weekday headings. 329 330 output.append(fmt.table_row(on=1)) 331 332 for weekday in range(0, 7): 333 output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"})) 334 output.append(fmt.text(_(EventAggregatorSupport.getDayLabel(weekday)))) 335 output.append(fmt.table_cell(on=0)) 336 337 output.append(fmt.table_row(on=0)) 338 339 # Process the days of the month. 340 341 start_weekday, number_of_days = calendar.monthrange(year, month) 342 343 # The start weekday is the weekday of day number 1. 344 # Find the first day of the week, counting from below zero, if 345 # necessary, in order to land on the first day of the month as 346 # day number 1. 347 348 first_day = 1 - start_weekday 349 350 while first_day <= number_of_days: 351 352 # Find events in this week and determine how to mark them on the 353 # calendar. 354 355 week_start = (year, month, max(first_day, 1)) 356 week_end = (year, month, min(first_day + 6, number_of_days)) 357 358 week_coverage, week_events = EventAggregatorSupport.getCoverage( 359 week_start, week_end, shown_events.get((year, month), [])) 360 361 # Output a week, starting with the day numbers. 362 363 output.append(fmt.table_row(on=1)) 364 365 for weekday in range(0, 7): 366 day = first_day + weekday 367 date = (year, month, day) 368 369 # Output out-of-month days. 370 371 if day < 1 or day > number_of_days: 372 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-excluded", "colspan" : "3"})) 373 output.append(fmt.table_cell(on=0)) 374 375 # Output normal days. 376 377 else: 378 if date in week_coverage: 379 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-busy", "colspan" : "3"})) 380 else: 381 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-empty", "colspan" : "3"})) 382 383 # Make a link to a new event action. 384 385 new_event_link = "action=EventAggregatorNewEvent&start-day=%d&start-month=%d&start-year=%d" % ( 386 day, month, year) 387 388 # Output the day number. 389 390 output.append(fmt.div(on=1)) 391 output.append(fmt.span(on=1, css_class="event-day-number")) 392 output.append(linkToPage(request, page, unicode(day), new_event_link)) 393 output.append(fmt.span(on=0)) 394 output.append(fmt.div(on=0)) 395 396 # End of day. 397 398 output.append(fmt.table_cell(on=0)) 399 400 # End of day numbers. 401 402 output.append(fmt.table_row(on=0)) 403 404 # Either generate empty days... 405 406 if not week_events: 407 output.append(fmt.table_row(on=1)) 408 409 for weekday in range(0, 7): 410 day = first_day + weekday 411 date = (year, month, day) 412 413 # Output out-of-month days. 414 415 if day < 1 or day > number_of_days: 416 output.append(fmt.table_cell(on=1, 417 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 418 output.append(fmt.table_cell(on=0)) 419 420 # Output empty days. 421 422 else: 423 output.append(fmt.table_cell(on=1, 424 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"})) 425 426 output.append(fmt.table_row(on=0)) 427 428 # Or visit each set of scheduled events... 429 430 else: 431 for coverage, events in week_events: 432 433 # Output each set. 434 435 output.append(fmt.table_row(on=1)) 436 437 # Then, output day details. 438 439 for weekday in range(0, 7): 440 day = first_day + weekday 441 date = (year, month, day) 442 443 # Skip out-of-month days. 444 445 if day < 1 or day > number_of_days: 446 output.append(fmt.table_cell(on=1, 447 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 448 output.append(fmt.table_cell(on=0)) 449 continue 450 451 # Output the day. 452 453 if date not in coverage: 454 output.append(fmt.table_cell(on=1, 455 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"})) 456 457 # Get event details for the current day. 458 459 for event_page, event_details in events: 460 if not (event_details["start"] <= date <= event_details["end"]): 461 continue 462 463 # Get basic properties of the event. 464 465 starts_today = event_details["start"] == date 466 ends_today = event_details["end"] == date 467 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details) 468 469 # Generate a colour for the event. 470 471 bg = getColour(event_page.page_name) 472 fg = getBlackOrWhite(bg) 473 style = ("background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg)) 474 475 # Determine if the event name should be shown. 476 477 start_of_period = starts_today or weekday == 0 or day == 1 478 479 if name_usage == "daily" or start_of_period: 480 hide_text = 0 481 else: 482 hide_text = 1 483 484 # Output start of day gap and determine whether 485 # any event content should be explicitly output 486 # for this day. 487 488 if starts_today: 489 490 # Single day events... 491 492 if ends_today: 493 colspan = 3 494 event_day_type = "event-day-single" 495 496 # Events starting today... 497 498 else: 499 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-start-gap"})) 500 output.append(fmt.table_cell(on=0)) 501 502 # Calculate the span of this cell. 503 # Events whose names appear on every day... 504 505 if name_usage == "daily": 506 colspan = 2 507 event_day_type = "event-day-starting" 508 509 # Events whose names appear once per week... 510 511 else: 512 if event_details["end"] <= week_end: 513 event_length = event_details["end"][2] - day + 1 514 colspan = (event_length - 2) * 3 + 4 515 else: 516 event_length = week_end[2] - day + 1 517 colspan = (event_length - 1) * 3 + 2 518 519 event_day_type = "event-day-multiple" 520 521 # Events continuing from a previous week... 522 523 elif start_of_period: 524 525 # End of continuing event... 526 527 if ends_today: 528 colspan = 2 529 event_day_type = "event-day-ending" 530 531 # Events continuing for at least one more day... 532 533 else: 534 535 # Calculate the span of this cell. 536 # Events whose names appear on every day... 537 538 if name_usage == "daily": 539 colspan = 3 540 event_day_type = "event-day-full" 541 542 # Events whose names appear once per week... 543 544 else: 545 if event_details["end"] <= week_end: 546 event_length = event_details["end"][2] - day + 1 547 colspan = (event_length - 1) * 3 + 2 548 else: 549 event_length = week_end[2] - day + 1 550 colspan = event_length * 3 551 552 event_day_type = "event-day-multiple" 553 554 # Continuing events whose names appear on every day... 555 556 elif name_usage == "daily": 557 if ends_today: 558 colspan = 2 559 event_day_type = "event-day-ending" 560 else: 561 colspan = 3 562 event_day_type = "event-day-full" 563 564 # Continuing events whose names appear once per week... 565 566 else: 567 colspan = None 568 569 # Output the main content only if it is not 570 # continuing from a previous day. 571 572 if colspan is not None: 573 574 # Colour the cell for continuing events. 575 576 attrs={ 577 "class" : "event-day-content event-day-busy %s" % event_day_type, 578 "colspan" : str(colspan) 579 } 580 581 if not (starts_today and ends_today): 582 attrs["style"] = style 583 584 output.append(fmt.table_cell(on=1, attrs=attrs)) 585 586 # Output the event. 587 588 if starts_today and ends_today or not hide_text: 589 590 output.append(fmt.div(on=1, css_class="event-summary-box")) 591 output.append(fmt.div(on=1, css_class="event-summary", style=style)) 592 output.append(linkToPage(request, event_page, event_summary)) 593 output.append(fmt.div(on=0)) 594 595 # Add a pop-up element for long summaries. 596 597 output.append(fmt.div(on=1, css_class="event-summary-popup", style=style)) 598 output.append(linkToPage(request, event_page, event_summary)) 599 output.append(fmt.div(on=0)) 600 601 output.append(fmt.div(on=0)) 602 603 # Output end of day content. 604 605 output.append(fmt.div(on=0)) 606 607 # Output end of day gap. 608 609 if ends_today and not starts_today: 610 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-end-gap"})) 611 output.append(fmt.table_cell(on=0)) 612 613 # End of day. 614 615 output.append(fmt.table_cell(on=0)) 616 617 # End of set. 618 619 output.append(fmt.table_row(on=0)) 620 621 # Add a spacer. 622 623 output.append(fmt.table_row(on=1)) 624 625 for weekday in range(0, 7): 626 day = first_day + weekday 627 css_classes = "event-day-spacer" 628 629 # Skip out-of-month days. 630 631 if day < 1 or day > number_of_days: 632 css_classes += " event-day-excluded" 633 634 output.append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"})) 635 output.append(fmt.table_cell(on=0)) 636 637 output.append(fmt.table_row(on=0)) 638 639 # Process the next week... 640 641 first_day += 7 642 643 # End of month. 644 645 output.append(fmt.table(on=0)) 646 647 # Or output a summary view... 648 649 elif mode == "list": 650 651 # Output a list. 652 653 output.append(fmt.listitem(on=1, attr={"class" : "event-listings-month"})) 654 output.append(fmt.div(on=1, attr={"class" : "event-listings-month-heading"})) 655 656 # Either write a month heading or produce links for navigable 657 # calendars. 658 659 output.append(view.writeMonthHeading(year, month)) 660 661 output.append(fmt.div(on=0)) 662 663 output.append(fmt.bullet_list(on=1, attr={"class" : "event-month-listings"})) 664 665 # Get the events in order. 666 667 ordered_events = EventAggregatorSupport.getOrderedEvents(shown_events.get((year, month), [])) 668 669 # Show the events in order. 670 671 for event_page, event_details in ordered_events: 672 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details) 673 674 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 675 676 # Link to the page using the summary. 677 678 output.append(fmt.paragraph(on=1)) 679 output.append(linkToPage(request, event_page, event_summary)) 680 output.append(fmt.paragraph(on=0)) 681 682 # Start and end dates. 683 684 output.append(fmt.paragraph(on=1)) 685 output.append(fmt.span(on=1)) 686 output.append(fmt.text("%04d-%02d-%02d" % event_details["start"])) 687 output.append(fmt.span(on=0)) 688 output.append(fmt.text(" - ")) 689 output.append(fmt.span(on=1)) 690 output.append(fmt.text("%04d-%02d-%02d" % event_details["end"])) 691 output.append(fmt.span(on=0)) 692 output.append(fmt.paragraph(on=0)) 693 694 # Location. 695 696 if event_details.has_key("location"): 697 output.append(fmt.paragraph(on=1)) 698 output.append(fmt.text(event_details["location"])) 699 output.append(fmt.paragraph(on=1)) 700 701 # Topics. 702 703 if event_details.has_key("topics") or event_details.has_key("categories"): 704 output.append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 705 706 for topic in event_details.get("topics") or event_details.get("categories"): 707 output.append(fmt.listitem(on=1)) 708 output.append(fmt.text(topic)) 709 output.append(fmt.listitem(on=0)) 710 711 output.append(fmt.bullet_list(on=0)) 712 713 output.append(fmt.listitem(on=0)) 714 715 output.append(fmt.bullet_list(on=0)) 716 717 # Or output a table of events... 718 719 elif mode == "table": 720 721 # Get the events in order. 722 723 ordered_events = EventAggregatorSupport.getOrderedEvents(shown_events.get((year, month), [])) 724 725 # Show the events in order. 726 727 for event_page, event_details in ordered_events: 728 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details) 729 730 # Prepare CSS classes with category-related styling. 731 732 css_classes = ["event-table-details"] 733 734 for topic in event_details.get("topics") or event_details.get("categories"): 735 736 # Filter the category text to avoid illegal characters. 737 738 css_classes.append("event-table-category-%s" % "".join(filter(lambda c: c.isalnum(), topic))) 739 740 attrs = {"class" : " ".join(css_classes)} 741 742 output.append(fmt.table_row(on=1)) 743 744 # Start and end dates. 745 746 output.append(fmt.table_cell(on=1, attrs=attrs)) 747 output.append(fmt.span(on=1)) 748 output.append(fmt.text("%04d-%02d-%02d" % event_details["start"])) 749 output.append(fmt.span(on=0)) 750 output.append(fmt.text(" - ")) 751 output.append(fmt.span(on=1)) 752 output.append(fmt.text("%04d-%02d-%02d" % event_details["end"])) 753 output.append(fmt.span(on=0)) 754 output.append(fmt.table_cell(on=0)) 755 756 # Location. 757 758 output.append(fmt.table_cell(on=1, attrs=attrs)) 759 760 if event_details.has_key("location"): 761 output.append(fmt.text(event_details["location"])) 762 763 output.append(fmt.table_cell(on=0)) 764 765 # Link to the page using the summary. 766 767 output.append(fmt.table_cell(on=1, attrs=attrs)) 768 output.append(linkToPage(request, event_page, event_summary)) 769 output.append(fmt.table_cell(on=0)) 770 771 output.append(fmt.table_row(on=0)) 772 773 # Output top-level information. 774 775 # End of list view output. 776 777 if mode == "list": 778 output.append(fmt.bullet_list(on=0)) 779 780 # End of table view output. 781 782 elif mode == "table": 783 output.append(fmt.table(on=0)) 784 785 return ''.join(output) 786 787 # vim: tabstop=4 expandtab shiftwidth=4