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 if mode == "list": 281 output.append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 282 283 # Visit all months in the requested range, or across known events. 284 285 for year, month in EventAggregatorSupport.daterange(first, last): 286 287 # Either output a calendar view... 288 289 if mode == "calendar": 290 291 # Output a month. 292 293 output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"})) 294 295 output.append(fmt.table_row(on=1)) 296 output.append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "21"})) 297 298 # Either write a month heading or produce links for navigable 299 # calendars. 300 301 output.append(view.writeMonthHeading(year, month)) 302 303 output.append(fmt.table_cell(on=0)) 304 output.append(fmt.table_row(on=0)) 305 306 # Weekday headings. 307 308 output.append(fmt.table_row(on=1)) 309 310 for weekday in range(0, 7): 311 output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading", "colspan" : "3"})) 312 output.append(fmt.text(_(EventAggregatorSupport.getDayLabel(weekday)))) 313 output.append(fmt.table_cell(on=0)) 314 315 output.append(fmt.table_row(on=0)) 316 317 # Process the days of the month. 318 319 start_weekday, number_of_days = calendar.monthrange(year, month) 320 321 # The start weekday is the weekday of day number 1. 322 # Find the first day of the week, counting from below zero, if 323 # necessary, in order to land on the first day of the month as 324 # day number 1. 325 326 first_day = 1 - start_weekday 327 328 while first_day <= number_of_days: 329 330 # Find events in this week and determine how to mark them on the 331 # calendar. 332 333 week_start = (year, month, max(first_day, 1)) 334 week_end = (year, month, min(first_day + 6, number_of_days)) 335 336 week_coverage, week_events = EventAggregatorSupport.getCoverage( 337 week_start, week_end, shown_events.get((year, month), [])) 338 339 # Output a week, starting with the day numbers. 340 341 output.append(fmt.table_row(on=1)) 342 343 for weekday in range(0, 7): 344 day = first_day + weekday 345 date = (year, month, day) 346 347 # Output out-of-month days. 348 349 if day < 1 or day > number_of_days: 350 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-excluded", "colspan" : "3"})) 351 output.append(fmt.table_cell(on=0)) 352 353 # Output normal days. 354 355 else: 356 if date in week_coverage: 357 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-busy", "colspan" : "3"})) 358 else: 359 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-empty", "colspan" : "3"})) 360 361 # Make a link to a new event action. 362 363 new_event_link = "action=EventAggregatorNewEvent&start-day=%d&start-month=%d&start-year=%d" % ( 364 day, month, year) 365 366 # Output the day number. 367 368 output.append(fmt.div(on=1)) 369 output.append(fmt.span(on=1, css_class="event-day-number")) 370 output.append(linkToPage(request, page, unicode(day), new_event_link)) 371 output.append(fmt.span(on=0)) 372 output.append(fmt.div(on=0)) 373 374 # End of day. 375 376 output.append(fmt.table_cell(on=0)) 377 378 # End of day numbers. 379 380 output.append(fmt.table_row(on=0)) 381 382 # Either generate empty days... 383 384 if not week_events: 385 output.append(fmt.table_row(on=1)) 386 387 for weekday in range(0, 7): 388 day = first_day + weekday 389 date = (year, month, day) 390 391 # Output out-of-month days. 392 393 if day < 1 or day > number_of_days: 394 output.append(fmt.table_cell(on=1, 395 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 396 output.append(fmt.table_cell(on=0)) 397 398 # Output empty days. 399 400 else: 401 output.append(fmt.table_cell(on=1, 402 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"})) 403 404 output.append(fmt.table_row(on=0)) 405 406 # Or visit each set of scheduled events... 407 408 else: 409 for coverage, events in week_events: 410 411 # Output each set. 412 413 output.append(fmt.table_row(on=1)) 414 415 # Then, output day details. 416 417 for weekday in range(0, 7): 418 day = first_day + weekday 419 date = (year, month, day) 420 421 # Skip out-of-month days. 422 423 if day < 1 or day > number_of_days: 424 output.append(fmt.table_cell(on=1, 425 attrs={"class" : "event-day-content event-day-excluded", "colspan" : "3"})) 426 output.append(fmt.table_cell(on=0)) 427 continue 428 429 # Output the day. 430 431 if date not in coverage: 432 output.append(fmt.table_cell(on=1, 433 attrs={"class" : "event-day-content event-day-empty", "colspan" : "3"})) 434 435 # Get event details for the current day. 436 437 for event_page, event_details in events: 438 if not (event_details["start"] <= date <= event_details["end"]): 439 continue 440 441 # Get basic properties of the event. 442 443 starts_today = event_details["start"] == date 444 ends_today = event_details["end"] == date 445 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details) 446 447 # Generate a colour for the event. 448 449 bg = getColour(event_page.page_name) 450 fg = getBlackOrWhite(bg) 451 style = ("background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg)) 452 453 # Determine if the event name should be shown. 454 455 start_of_period = starts_today or weekday == 0 or day == 1 456 457 if name_usage == "daily" or start_of_period: 458 hide_text = 0 459 else: 460 hide_text = 1 461 462 # Output start of day gap and determine whether 463 # any event content should be explicitly output 464 # for this day. 465 466 if starts_today: 467 468 # Single day events... 469 470 if ends_today: 471 colspan = 3 472 event_day_type = "event-day-single" 473 474 # Events starting today... 475 476 else: 477 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-start-gap"})) 478 output.append(fmt.table_cell(on=0)) 479 480 # Calculate the span of this cell. 481 # Events whose names appear on every day... 482 483 if name_usage == "daily": 484 colspan = 2 485 event_day_type = "event-day-starting" 486 487 # Events whose names appear once per week... 488 489 else: 490 if event_details["end"] <= week_end: 491 event_length = event_details["end"][2] - day + 1 492 colspan = (event_length - 2) * 3 + 4 493 else: 494 event_length = week_end[2] - day + 1 495 colspan = (event_length - 1) * 3 + 2 496 497 event_day_type = "event-day-multiple" 498 499 # Events continuing from a previous week... 500 501 elif start_of_period: 502 503 # End of continuing event... 504 505 if ends_today: 506 colspan = 2 507 event_day_type = "event-day-ending" 508 509 # Events continuing for at least one more day... 510 511 else: 512 513 # Calculate the span of this cell. 514 # Events whose names appear on every day... 515 516 if name_usage == "daily": 517 colspan = 3 518 event_day_type = "event-day-full" 519 520 # Events whose names appear once per week... 521 522 else: 523 if event_details["end"] <= week_end: 524 event_length = event_details["end"][2] - day + 1 525 colspan = (event_length - 1) * 3 + 2 526 else: 527 event_length = week_end[2] - day + 1 528 colspan = event_length * 3 529 530 event_day_type = "event-day-multiple" 531 532 # Continuing events whose names appear on every day... 533 534 elif name_usage == "daily": 535 if ends_today: 536 colspan = 2 537 event_day_type = "event-day-ending" 538 else: 539 colspan = 3 540 event_day_type = "event-day-full" 541 542 # Continuing events whose names appear once per week... 543 544 else: 545 colspan = None 546 547 # Output the main content only if it is not 548 # continuing from a previous day. 549 550 if colspan is not None: 551 552 # Colour the cell for continuing events. 553 554 attrs={ 555 "class" : "event-day-content event-day-busy %s" % event_day_type, 556 "colspan" : str(colspan) 557 } 558 559 if not (starts_today and ends_today): 560 attrs["style"] = style 561 562 output.append(fmt.table_cell(on=1, attrs=attrs)) 563 564 # Output the event. 565 566 if starts_today and ends_today or not hide_text: 567 568 output.append(fmt.div(on=1, css_class="event-summary-box")) 569 output.append(fmt.div(on=1, css_class="event-summary", style=style)) 570 output.append(linkToPage(request, event_page, event_summary)) 571 output.append(fmt.div(on=0)) 572 573 # Add a pop-up element for long summaries. 574 575 output.append(fmt.div(on=1, css_class="event-summary-popup", style=style)) 576 output.append(linkToPage(request, event_page, event_summary)) 577 output.append(fmt.div(on=0)) 578 579 output.append(fmt.div(on=0)) 580 581 # Output end of day content. 582 583 output.append(fmt.div(on=0)) 584 585 # Output end of day gap. 586 587 if ends_today and not starts_today: 588 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-end-gap"})) 589 output.append(fmt.table_cell(on=0)) 590 591 # End of day. 592 593 output.append(fmt.table_cell(on=0)) 594 595 # End of set. 596 597 output.append(fmt.table_row(on=0)) 598 599 # Add a spacer. 600 601 output.append(fmt.table_row(on=1)) 602 603 for weekday in range(0, 7): 604 day = first_day + weekday 605 css_classes = "event-day-spacer" 606 607 # Skip out-of-month days. 608 609 if day < 1 or day > number_of_days: 610 css_classes += " event-day-excluded" 611 612 output.append(fmt.table_cell(on=1, attrs={"class" : css_classes, "colspan" : "3"})) 613 output.append(fmt.table_cell(on=0)) 614 615 output.append(fmt.table_row(on=0)) 616 617 # Process the next week... 618 619 first_day += 7 620 621 # End of month. 622 623 output.append(fmt.table(on=0)) 624 625 # Or output a summary view... 626 627 elif mode == "list": 628 629 # Output a list. 630 631 output.append(fmt.listitem(on=1, attr={"class" : "event-listings-month"})) 632 output.append(fmt.div(on=1, attr={"class" : "event-listings-month-heading"})) 633 634 # Either write a month heading or produce links for navigable 635 # calendars. 636 637 output.append(view.writeMonthHeading(year, month)) 638 639 output.append(fmt.div(on=0)) 640 641 output.append(fmt.bullet_list(on=1, attr={"class" : "event-month-listings"})) 642 643 # Get the events in order. 644 645 ordered_events = EventAggregatorSupport.getOrderedEvents(shown_events.get((year, month), [])) 646 647 # Show the events in order. 648 649 for event_page, event_details in ordered_events: 650 event_summary = EventAggregatorSupport.getEventSummary(event_page, event_details) 651 652 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 653 654 # Link to the page using the summary. 655 656 output.append(fmt.paragraph(on=1)) 657 output.append(linkToPage(request, event_page, event_summary)) 658 output.append(fmt.paragraph(on=0)) 659 660 # Start and end dates. 661 662 output.append(fmt.paragraph(on=1)) 663 output.append(fmt.span(on=1)) 664 output.append(fmt.text("%04d-%02d-%02d" % event_details["start"])) 665 output.append(fmt.span(on=0)) 666 output.append(fmt.text(" - ")) 667 output.append(fmt.span(on=1)) 668 output.append(fmt.text("%04d-%02d-%02d" % event_details["end"])) 669 output.append(fmt.span(on=0)) 670 output.append(fmt.paragraph(on=0)) 671 672 # Location. 673 674 if event_details.has_key("location"): 675 output.append(fmt.paragraph(on=1)) 676 output.append(fmt.text(event_details["location"])) 677 output.append(fmt.paragraph(on=1)) 678 679 # Topics. 680 681 if event_details.has_key("topics") or event_details.has_key("categories"): 682 output.append(fmt.bullet_list(on=1, attr={"class" : "event-topics"})) 683 684 for topic in event_details.get("topics") or event_details.get("categories"): 685 output.append(fmt.listitem(on=1)) 686 output.append(fmt.text(topic)) 687 output.append(fmt.listitem(on=0)) 688 689 output.append(fmt.bullet_list(on=0)) 690 691 output.append(fmt.listitem(on=0)) 692 693 output.append(fmt.bullet_list(on=0)) 694 695 # Output top-level information. 696 697 # End of list view output. 698 699 if mode == "list": 700 output.append(fmt.bullet_list(on=0)) 701 702 return ''.join(output) 703 704 # vim: tabstop=4 expandtab shiftwidth=4