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