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 try: 16 set 17 except NameError: 18 from sets import Set as set 19 20 Dependencies = ['pages'] 21 22 # Date labels. 23 24 month_labels = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] 25 weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 26 27 # HTML-related functions. 28 29 def getColour(s): 30 colour = [0, 0, 0] 31 digit = 0 32 for c in s: 33 colour[digit] += ord(c) 34 colour[digit] = colour[digit] % 256 35 digit += 1 36 digit = digit % 3 37 return tuple(colour) 38 39 def getBlackOrWhite(colour): 40 if sum(colour) / 3.0 > 127: 41 return (0, 0, 0) 42 else: 43 return (255, 255, 255) 44 45 # Macro functions. 46 47 def getMonth(arg): 48 if arg.startswith("current"): 49 date = EventAggregatorSupport.getCurrentMonth() 50 if len(arg) > 8: 51 n = int(arg[7:]) 52 date = EventAggregatorSupport.monthupdate(date, n) 53 else: 54 date = EventAggregatorSupport.getMonth(arg) 55 return date 56 57 def getFormMonth(request, calendar_name, argname): 58 arg = request.form.get("%s-%s" % (calendar_name, argname), [None])[0] 59 if arg is not None: 60 return getMonth(arg) 61 else: 62 return None 63 64 def execute(macro, args): 65 66 """ 67 Execute the 'macro' with the given 'args': an optional list of selected 68 category names (categories whose pages are to be shown), together with 69 optional named arguments of the following forms: 70 71 start=YYYY-MM shows event details starting from the specified month 72 start=current-N shows event details relative to the current month 73 end=YYYY-MM shows event details ending at the specified month 74 end=current+N shows event details relative to the current month 75 76 mode=calendar shows a calendar view of events 77 mode=list shows a list of events by month 78 mode=ics provides iCalendar data for the events 79 80 names=daily shows the name of an event on every day of that event 81 names=weekly shows the name of an event once per week 82 83 calendar=NAME uses the given NAME to provide request parameters which 84 can be used to control the calendar view 85 """ 86 87 request = macro.request 88 fmt = macro.formatter 89 page = fmt.page 90 _ = request.getText 91 92 # Interpret the arguments. 93 94 try: 95 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 96 except AttributeError: 97 parsed_args = args.split(",") 98 99 parsed_args = [arg for arg in parsed_args if arg] 100 101 # Get special arguments. 102 103 category_names = [] 104 calendar_start = None 105 calendar_end = None 106 mode = "calendar" 107 name_usage = "daily" 108 calendar_name = None 109 110 for arg in parsed_args: 111 if arg.startswith("start="): 112 calendar_start = getMonth(arg[6:]) 113 114 elif arg.startswith("end="): 115 calendar_end = getMonth(arg[4:]) 116 117 elif arg.startswith("mode="): 118 mode = arg[5:] 119 120 elif arg.startswith("names="): 121 name_usage = arg[6:] 122 123 elif arg.startswith("calendar="): 124 calendar_name = arg[9:] 125 126 else: 127 category_names.append(arg) 128 129 # Find request parameters to override settings. 130 131 if calendar_name is not None: 132 calendar_start = getFormMonth(request, calendar_name, "start") or calendar_start 133 calendar_end = getFormMonth(request, calendar_name, "end") or calendar_end 134 135 # Get the events. 136 137 events, shown_events, all_shown_events, earliest, latest = \ 138 EventAggregatorSupport.getEvents(request, category_names, calendar_start, calendar_end) 139 140 # Get a concrete period of time. 141 142 first, last = EventAggregatorSupport.getConcretePeriod(calendar_start, calendar_end, earliest, latest) 143 144 # Define some useful navigation months. 145 146 if calendar_name is not None: 147 span = EventAggregatorSupport.span(first, last) 148 number_of_months = span[0] * 12 + span[1] + 1 149 150 previous_month_start = EventAggregatorSupport.prevmonth(first) 151 next_month_start = EventAggregatorSupport.nextmonth(first) 152 previous_month_end = EventAggregatorSupport.prevmonth(last) 153 next_month_end = EventAggregatorSupport.nextmonth(last) 154 155 previous_set_start = EventAggregatorSupport.monthupdate(first, -number_of_months) 156 next_set_start = EventAggregatorSupport.monthupdate(first, number_of_months) 157 previous_set_end = EventAggregatorSupport.monthupdate(last, -number_of_months) 158 next_set_end = EventAggregatorSupport.monthupdate(last, number_of_months) 159 160 # Make a calendar. 161 162 output = [] 163 164 # Output top-level information. 165 166 if mode == "list": 167 output.append(fmt.bullet_list(on=1, attr={"class" : "event-listings"})) 168 169 # Visit all months in the requested range, or across known events. 170 171 for year, month in EventAggregatorSupport.daterange(first, last): 172 173 # Either output a calendar view... 174 175 if mode == "calendar": 176 177 # Output a month. 178 179 output.append(fmt.table(on=1, attrs={"tableclass" : "event-month"})) 180 181 output.append(fmt.table_row(on=1)) 182 output.append(fmt.table_cell(on=1, attrs={"class" : "event-month-heading", "colspan" : "7"})) 183 184 # Either write a month heading or produce a link for navigable 185 # calendars. 186 187 month_label = _(month_labels[month - 1]) # zero-based labels 188 189 if calendar_name is not None: 190 191 # Links to the previous set of months and to a calendar shifted 192 # back one month. 193 194 previous_set_link = "%s-start=%04d-%02d&%s-end=%04d-%02d" % ( 195 (calendar_name,) + previous_set_start + (calendar_name,) + previous_set_end 196 ) 197 previous_month_link = "%s-start=%04d-%02d&%s-end=%04d-%02d" % ( 198 (calendar_name,) + previous_month_start + (calendar_name,) + previous_month_end 199 ) 200 201 output.append(fmt.span(on=1, css_class="previous-month")) 202 output.append(page.link_to_raw(request, wikiutil.escape("<<"), previous_set_link)) 203 output.append(fmt.text(" ")) 204 output.append(page.link_to_raw(request, wikiutil.escape("<"), previous_month_link)) 205 output.append(fmt.span(on=0)) 206 207 # Links to the next set of months and to a calendar shifted 208 # forward one month. 209 210 next_set_link = "%s-start=%04d-%02d&%s-end=%04d-%02d" % ( 211 (calendar_name,) + next_set_start + (calendar_name,) + next_set_end 212 ) 213 next_month_link = "%s-start=%04d-%02d&%s-end=%04d-%02d" % ( 214 (calendar_name,) + next_month_start + (calendar_name,) + next_month_end 215 ) 216 217 output.append(fmt.span(on=1, css_class="next-month")) 218 output.append(page.link_to_raw(request, wikiutil.escape(">"), next_month_link)) 219 output.append(fmt.text(" ")) 220 output.append(page.link_to_raw(request, wikiutil.escape(">>"), next_set_link)) 221 output.append(fmt.span(on=0)) 222 223 # A link leading to this month being at the top of the calendar. 224 225 full_month_label = "%s %s" % (month_label, year) 226 month_link = "%s-start=%04d-%02d&%s-end=%04d-%02d" % ( 227 (calendar_name, year, month, calendar_name) + 228 EventAggregatorSupport.monthupdate((year, month), number_of_months - 1) 229 ) 230 output.append(page.link_to_raw(request, wikiutil.escape(full_month_label), month_link)) 231 232 else: 233 output.append(fmt.span(on=1)) 234 output.append(fmt.text(month_label)) 235 output.append(fmt.span(on=0)) 236 output.append(fmt.text(" ")) 237 output.append(fmt.span(on=1)) 238 output.append(fmt.text(year)) 239 output.append(fmt.span(on=0)) 240 241 output.append(fmt.table_cell(on=0)) 242 output.append(fmt.table_row(on=0)) 243 244 # Weekday headings. 245 246 output.append(fmt.table_row(on=1)) 247 248 for weekday in range(0, 7): 249 output.append(fmt.table_cell(on=1, attrs={"class" : "event-weekday-heading"})) 250 output.append(fmt.text(_(weekday_labels[weekday]))) 251 output.append(fmt.table_cell(on=0)) 252 253 output.append(fmt.table_row(on=0)) 254 255 # Process the days of the month. 256 257 start_weekday, number_of_days = calendar.monthrange(year, month) 258 259 # The start weekday is the weekday of day number 1. 260 # Find the first day of the week, counting from below zero, if 261 # necessary, in order to land on the first day of the month as 262 # day number 1. 263 264 first_day = 1 - start_weekday 265 266 while first_day <= number_of_days: 267 268 # Find events in this week and determine how to mark them on the 269 # calendar. 270 271 week_events = [] 272 week_coverage = set() 273 274 week_start = (year, month, max(first_day, 1)) 275 week_end = (year, month, min(first_day + 6, number_of_days)) 276 277 # Get event details. 278 279 for event in shown_events.get((year, month), []): 280 event_page, event_details = event 281 282 # Test for the event in the current week. 283 284 if event_details["start"] <= week_end and event_details["end"] >= week_start: 285 286 # Find the coverage of this week for the event. 287 288 event_start = max(event_details["start"], week_start) 289 event_end = min(event_details["end"], week_end) 290 event_coverage = set(EventAggregatorSupport.daterange(event_start, event_end)) 291 292 # Update the overall coverage. 293 294 week_coverage.update(event_coverage) 295 296 # Try and fit the event into the events list. 297 298 for i, (coverage, events) in enumerate(week_events): 299 300 # Where the event does not overlap with the current 301 # element, add it alongside existing events. 302 303 if not coverage.intersection(event_coverage): 304 events.append(event) 305 week_events[i] = coverage.union(event_coverage), events 306 break 307 308 # Make a new element in the list if the event cannot be 309 # marked alongside existing events. 310 311 else: 312 week_events.append((event_coverage, [event])) 313 314 # Output a week, starting with the day numbers. 315 316 output.append(fmt.table_row(on=1)) 317 318 for weekday in range(0, 7): 319 day = first_day + weekday 320 date = (year, month, day) 321 322 # Output out-of-month days. 323 324 if day < 1 or day > number_of_days: 325 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-excluded"})) 326 output.append(fmt.table_cell(on=0)) 327 328 # Output normal days. 329 330 else: 331 if date in week_coverage: 332 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-busy"})) 333 else: 334 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day-heading event-day-empty"})) 335 336 output.append(fmt.div(on=1)) 337 output.append(fmt.span(on=1, css_class="event-day-number")) 338 output.append(fmt.text(day)) 339 output.append(fmt.span(on=0)) 340 output.append(fmt.div(on=0)) 341 342 # End of day. 343 344 output.append(fmt.table_cell(on=0)) 345 346 # End of day numbers. 347 348 output.append(fmt.table_row(on=0)) 349 350 # Either generate empty days... 351 352 if not week_events: 353 output.append(fmt.table_row(on=1)) 354 355 for weekday in range(0, 7): 356 day = first_day + weekday 357 date = (year, month, day) 358 359 # Output out-of-month days. 360 361 if day < 1 or day > number_of_days: 362 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day event-day-excluded"})) 363 output.append(fmt.table_cell(on=0)) 364 365 # Output empty days. 366 367 else: 368 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day event-day-empty"})) 369 370 output.append(fmt.table_row(on=0)) 371 372 # Or visit each set of scheduled events... 373 374 else: 375 for coverage, events in week_events: 376 377 # Output each set. 378 379 output.append(fmt.table_row(on=1)) 380 381 # Then, output day details. 382 383 for weekday in range(0, 7): 384 day = first_day + weekday 385 date = (year, month, day) 386 387 # Skip out-of-month days. 388 389 if day < 1 or day > number_of_days: 390 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day event-day-excluded"})) 391 output.append(fmt.table_cell(on=0)) 392 continue 393 394 # Output the day. 395 396 if date in coverage: 397 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day event-day-busy"})) 398 else: 399 output.append(fmt.table_cell(on=1, attrs={"class" : "event-day event-day-empty"})) 400 401 # Get event details for the current day. 402 403 for event_page, event_details in events: 404 if not (event_details["start"] <= date <= event_details["end"]): 405 continue 406 407 # Get a pretty version of the page name. 408 409 pretty_pagename = EventAggregatorSupport.getPrettyPageName(event_page) 410 411 # Generate a colour for the event. 412 413 bg = getColour(event_page.page_name) 414 fg = getBlackOrWhite(bg) 415 416 css_classes = ["event-summary"] 417 418 if event_details["start"] == date: 419 css_classes.append("event-starts") 420 start_of_event = 1 421 else: 422 start_of_event = 0 423 424 if event_details["end"] == date: 425 css_classes.append("event-ends") 426 427 # Output the event. 428 429 if name_usage == "daily" or start_of_event or weekday == 0 or day == 1: 430 hide_text = 0 431 else: 432 hide_text = 1 433 434 output.append(fmt.div(on=1, css_class=(" ".join(css_classes)), 435 style=("background-color: rgb(%d, %d, %d); color: rgb(%d, %d, %d);" % (bg + fg)))) 436 437 if not hide_text: 438 output.append(event_page.link_to_raw(request, wikiutil.escape(pretty_pagename))) 439 440 output.append(fmt.div(on=0)) 441 442 # End of day. 443 444 output.append(fmt.table_cell(on=0)) 445 446 # End of set. 447 448 output.append(fmt.table_row(on=0)) 449 450 # Process the next week... 451 452 first_day += 7 453 454 # End of month. 455 456 output.append(fmt.table(on=0)) 457 458 # Or output a summary view... 459 460 elif mode == "list": 461 462 output.append(fmt.listitem(on=1, attr={"class" : "event-listings-month"})) 463 output.append(fmt.div(on=1, attr={"class" : "event-listings-month-heading"})) 464 output.append(fmt.span(on=1)) 465 output.append(fmt.text(_(month_labels[month - 1]))) # zero-based labels 466 output.append(fmt.span(on=0)) 467 output.append(fmt.text(" ")) 468 output.append(fmt.span(on=1)) 469 output.append(fmt.text(year)) 470 output.append(fmt.span(on=0)) 471 output.append(fmt.div(on=0)) 472 473 output.append(fmt.bullet_list(on=1, attr={"class" : "event-month-listings"})) 474 475 for event_page, event_details in shown_events.get((year, month), []): 476 477 # Get a pretty version of the page name. 478 479 pretty_pagename = EventAggregatorSupport.getPrettyPageName(event_page) 480 481 output.append(fmt.listitem(on=1, attr={"class" : "event-listing"})) 482 483 # Link to the page using the pretty name. 484 485 output.append(event_page.link_to_raw(request, wikiutil.escape(pretty_pagename))) 486 487 # Add the event details. 488 489 output.append(fmt.definition_list(on=1, attr={"class" : "event-details"})) 490 491 for key, value in event_details.items(): 492 output.append(fmt.definition_term(on=1)) 493 output.append(fmt.text(key)) 494 output.append(fmt.definition_term(on=0)) 495 output.append(fmt.definition_desc(on=1)) 496 output.append(fmt.text(value)) 497 output.append(fmt.definition_desc(on=0)) 498 499 output.append(fmt.definition_list(on=0)) 500 output.append(fmt.listitem(on=0)) 501 502 output.append(fmt.bullet_list(on=0)) 503 504 # Output top-level information. 505 506 # End of list view output. 507 508 if mode == "list": 509 output.append(fmt.bullet_list(on=0)) 510 511 return ''.join(output) 512 513 # vim: tabstop=4 expandtab shiftwidth=4