EventAggregator

Annotated EventAggregatorSupport.py

20:cca0278232ac
2009-03-28 Paul Boddie Fixed and updated the documentation. Improved the example event category page. Added a help page for the macro.
paul@10 1
# -*- coding: iso-8859-1 -*-
paul@10 2
"""
paul@10 3
    MoinMoin - EventAggregator library
paul@10 4
paul@10 5
    @copyright: 2008, 2009 by Paul Boddie <paul@boddie.org.uk>
paul@10 6
    @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
paul@10 7
                2005-2008 MoinMoin:ThomasWaldmann.
paul@10 8
    @license: GNU GPL (v2 or later), see COPYING.txt for details.
paul@10 9
"""
paul@10 10
paul@10 11
from MoinMoin.Page import Page
paul@10 12
from MoinMoin import search, version
paul@10 13
import calendar
paul@11 14
import datetime
paul@10 15
import re
paul@10 16
paul@10 17
__version__ = "0.1"
paul@10 18
paul@10 19
# Regular expressions where MoinMoin does not provide the required support.
paul@10 20
paul@10 21
category_regexp = None
paul@10 22
definition_list_regexp = re.compile(ur'^\s+(?P<term>.*?)::\s(?P<desc>.*?)$', re.UNICODE | re.MULTILINE)
paul@10 23
date_regexp = re.compile(ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})', re.UNICODE)
paul@10 24
month_regexp = re.compile(ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})', re.UNICODE)
paul@19 25
verbatim_regexp = re.compile(ur'(?:'
paul@19 26
    ur'<<Verbatim\((?P<verbatim>.*?)\)>>'
paul@19 27
    ur'|'
paul@19 28
    ur'\[\[Verbatim\((?P<verbatim2>.*?)\)\]\]'
paul@19 29
    ur'|'
paul@19 30
    ur'`(?P<monospace>.*?)`'
paul@19 31
    ur'|'
paul@19 32
    ur'{{{(?P<preformatted>.*?)}}}'
paul@19 33
    ur')', re.UNICODE)
paul@10 34
paul@10 35
# Utility functions.
paul@10 36
paul@10 37
def isMoin15():
paul@10 38
    return version.release.startswith("1.5.")
paul@10 39
paul@10 40
def getCategoryPattern(request):
paul@10 41
    global category_regexp
paul@10 42
paul@10 43
    try:
paul@10 44
        return request.cfg.cache.page_category_regexact
paul@10 45
    except AttributeError:
paul@10 46
paul@10 47
        # Use regular expression from MoinMoin 1.7.1 otherwise.
paul@10 48
paul@10 49
        if category_regexp is None:
paul@10 50
            category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE)
paul@10 51
        return category_regexp
paul@10 52
paul@19 53
# Action support functions.
paul@19 54
paul@19 55
def getCategories(request):
paul@19 56
paul@19 57
    """
paul@19 58
    From the AdvancedSearch macro, return a list of category page names using
paul@19 59
    the given 'request'.
paul@19 60
    """
paul@19 61
paul@19 62
    # This will return all pages with "Category" in the title.
paul@19 63
paul@19 64
    cat_filter = getCategoryPattern(request).search
paul@19 65
    return request.rootpage.getPageList(filter=cat_filter)
paul@19 66
paul@19 67
def getCategoryMapping(category_pagenames, request):
paul@19 68
paul@19 69
    """
paul@19 70
    For the given 'category_pagenames' return a list of tuples of the form
paul@19 71
    (category name, category page name) using the given 'request'.
paul@19 72
    """
paul@19 73
paul@19 74
    cat_pattern = getCategoryPattern(request)
paul@19 75
    mapping = []
paul@19 76
    for pagename in category_pagenames:
paul@19 77
        name = cat_pattern.match(pagename).group("key")
paul@19 78
        if name != "Category":
paul@19 79
            mapping.append((name, pagename))
paul@19 80
    mapping.sort()
paul@19 81
    return mapping
paul@19 82
paul@10 83
# The main activity functions.
paul@10 84
paul@10 85
def getPages(pagename, request):
paul@10 86
paul@10 87
    "Return the links minus category links for 'pagename' using the 'request'."
paul@10 88
paul@10 89
    query = search.QueryParser().parse_query('category:%s' % pagename)
paul@10 90
    if isMoin15():
paul@10 91
        results = search.searchPages(request, query)
paul@10 92
        results.sortByPagename()
paul@10 93
    else:
paul@10 94
        results = search.searchPages(request, query, "page_name")
paul@10 95
paul@10 96
    cat_pattern = getCategoryPattern(request)
paul@10 97
    pages = []
paul@10 98
    for page in results.hits:
paul@10 99
        if not cat_pattern.match(page.page_name):
paul@10 100
            pages.append(page)
paul@10 101
    return pages
paul@10 102
paul@10 103
def getEventDetails(page):
paul@10 104
paul@10 105
    "Return a dictionary of event details from the given 'page'."
paul@10 106
paul@10 107
    event_details = {}
paul@10 108
paul@10 109
    if page.pi["format"] == "wiki":
paul@10 110
        for match in definition_list_regexp.finditer(page.body):
paul@10 111
paul@10 112
            # Permit case-insensitive list terms.
paul@10 113
paul@10 114
            term = match.group("term").lower()
paul@10 115
            desc = match.group("desc")
paul@10 116
paul@10 117
            # Special value type handling.
paul@10 118
paul@19 119
            # Dates.
paul@19 120
paul@10 121
            if term in ("start", "end"):
paul@10 122
                desc = getDate(desc)
paul@19 123
paul@19 124
            # Lists.
paul@19 125
paul@10 126
            elif term in ("topics",):
paul@10 127
                desc = [value.strip() for value in desc.split(",")]
paul@10 128
paul@19 129
            # Labels which may well be quoted.
paul@19 130
            # NOTE: Re-implementing support for verbatim text and linking
paul@19 131
            # NOTE: avoidance.
paul@19 132
paul@19 133
            elif term in ("title", "summary"):
paul@19 134
                desc = "".join([s for s in verbatim_regexp.split(desc) if s is not None])
paul@19 135
paul@10 136
            if desc is not None:
paul@10 137
                event_details[term] = desc
paul@10 138
paul@10 139
    return event_details
paul@10 140
paul@19 141
def getEventSummary(event_page, event_details):
paul@19 142
paul@19 143
    """
paul@19 144
    Return either the given title or summary of the event described by the given
paul@19 145
    'event_page', according to the given 'event_details', or return the pretty
paul@19 146
    version of the page name.
paul@19 147
    """
paul@19 148
paul@19 149
    if event_details.has_key("title"):
paul@19 150
        return event_details["title"]
paul@19 151
    elif event_details.has_key("summary"):
paul@19 152
        return event_details["summary"]
paul@19 153
    else:
paul@19 154
        return getPrettyPageName(event_page)
paul@19 155
paul@10 156
def getDate(s):
paul@10 157
paul@10 158
    "Parse the string 's', extracting and returning a date string."
paul@10 159
paul@10 160
    m = date_regexp.search(s)
paul@10 161
    if m:
paul@10 162
        return tuple(map(int, m.groups()))
paul@10 163
    else:
paul@10 164
        return None
paul@10 165
paul@10 166
def getMonth(s):
paul@10 167
paul@10 168
    "Parse the string 's', extracting and returning a month string."
paul@10 169
paul@10 170
    m = month_regexp.search(s)
paul@10 171
    if m:
paul@10 172
        return tuple(map(int, m.groups()))
paul@10 173
    else:
paul@10 174
        return None
paul@10 175
paul@11 176
def getCurrentMonth():
paul@11 177
paul@11 178
    "Return the current month as a (year, month) tuple."
paul@11 179
paul@11 180
    today = datetime.date.today()
paul@11 181
    return (today.year, today.month)
paul@11 182
paul@17 183
def getCurrentYear():
paul@17 184
paul@17 185
    "Return the current year."
paul@17 186
paul@17 187
    today = datetime.date.today()
paul@17 188
    return today.year
paul@17 189
paul@11 190
def monthupdate(date, n):
paul@11 191
paul@11 192
    "Return 'date' updated by 'n' months."
paul@11 193
paul@11 194
    if n < 0:
paul@11 195
        fn = prevmonth
paul@11 196
    else:
paul@11 197
        fn = nextmonth
paul@11 198
paul@11 199
    i = 0
paul@11 200
    while i < abs(n):
paul@11 201
        date = fn(date)
paul@11 202
        i += 1
paul@11 203
        
paul@11 204
    return date
paul@11 205
paul@13 206
def daterange(first, last, step=1):
paul@11 207
paul@13 208
    """
paul@13 209
    Get the range of dates starting at 'first' and ending on 'last', using the
paul@13 210
    specified 'step'.
paul@13 211
    """
paul@11 212
paul@10 213
    results = []
paul@10 214
paul@10 215
    months_only = len(first) == 2
paul@10 216
    start_year = first[0]
paul@10 217
    end_year = last[0]
paul@10 218
paul@11 219
    for year in range(start_year, end_year + step, step):
paul@11 220
        if step == 1 and year < end_year:
paul@10 221
            end_month = 12
paul@11 222
        elif step == -1 and year > end_year:
paul@11 223
            end_month = 1
paul@10 224
        else:
paul@10 225
            end_month = last[1]
paul@10 226
paul@11 227
        if step == 1 and year > start_year:
paul@10 228
            start_month = 1
paul@11 229
        elif step == -1 and year < start_year:
paul@11 230
            start_month = 12
paul@10 231
        else:
paul@10 232
            start_month = first[1]
paul@10 233
paul@11 234
        for month in range(start_month, end_month + step, step):
paul@10 235
            if months_only:
paul@10 236
                results.append((year, month))
paul@10 237
            else:
paul@11 238
                if step == 1 and month < end_month:
paul@10 239
                    _wd, end_day = calendar.monthrange(year, month)
paul@11 240
                elif step == -1 and month > end_month:
paul@11 241
                    end_day = 1
paul@10 242
                else:
paul@10 243
                    end_day = last[2]
paul@10 244
paul@11 245
                if step == 1 and month > start_month:
paul@10 246
                    start_day = 1
paul@11 247
                elif step == -1 and month < start_month:
paul@11 248
                    _wd, start_day = calendar.monthrange(year, month)
paul@10 249
                else:
paul@10 250
                    start_day = first[2]
paul@10 251
paul@11 252
                for day in range(start_day, end_day + step, step):
paul@10 253
                    results.append((year, month, day))
paul@10 254
paul@10 255
    return results
paul@10 256
paul@10 257
def nextdate(date):
paul@11 258
paul@11 259
    "Return the date following the given 'date'."
paul@11 260
paul@10 261
    year, month, day = date
paul@10 262
    _wd, end_day = calendar.monthrange(year, month)
paul@10 263
    if day == end_day:
paul@10 264
        if month == 12:
paul@10 265
            return (year + 1, 1, 1)
paul@10 266
        else:
paul@10 267
            return (year, month + 1, 1)
paul@10 268
    else:
paul@10 269
        return (year, month, day + 1)
paul@10 270
paul@11 271
def prevdate(date):
paul@11 272
paul@11 273
    "Return the date preceding the given 'date'."
paul@11 274
paul@11 275
    year, month, day = date
paul@11 276
    if day == 1:
paul@11 277
        if month == 1:
paul@11 278
            return (year - 1, 12, 31)
paul@11 279
        else:
paul@11 280
            _wd, end_day = calendar.monthrange(year, month - 1)
paul@11 281
            return (year, month - 1, end_day)
paul@11 282
    else:
paul@11 283
        return (year, month, day - 1)
paul@11 284
paul@11 285
def nextmonth(date):
paul@11 286
paul@11 287
    "Return the (year, month) tuple following 'date'."
paul@11 288
paul@11 289
    year, month = date
paul@11 290
    if month == 12:
paul@11 291
        return (year + 1, 1)
paul@11 292
    else:
paul@11 293
        return year, month + 1
paul@11 294
paul@11 295
def prevmonth(date):
paul@11 296
paul@11 297
    "Return the (year, month) tuple preceding 'date'."
paul@11 298
paul@11 299
    year, month = date
paul@11 300
    if month == 1:
paul@11 301
        return (year - 1, 12)
paul@11 302
    else:
paul@11 303
        return year, month - 1
paul@11 304
paul@13 305
def span(start, end):
paul@13 306
paul@13 307
    "Return the difference between 'start' and 'end'."
paul@13 308
paul@13 309
    return end[0] - start[0], end[1] - start[1]
paul@13 310
paul@10 311
def getEvents(request, category_names, calendar_start=None, calendar_end=None):
paul@10 312
paul@10 313
    """
paul@10 314
    Using the 'request', generate a list of events found on pages belonging to
paul@10 315
    the specified 'category_names', using the optional 'calendar_start' and
paul@10 316
    'calendar_end' month tuples of the form (year, month) to indicate a window
paul@10 317
    of interest.
paul@10 318
paul@10 319
    Return a list of events, a dictionary mapping months to event lists (within
paul@10 320
    the window of interest), a list of all events within the window of interest,
paul@10 321
    the earliest month of an event within the window of interest, and the latest
paul@10 322
    month of an event within the window of interest.
paul@10 323
    """
paul@10 324
paul@12 325
    # Re-order the window, if appropriate.
paul@12 326
paul@12 327
    if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end:
paul@12 328
        calendar_start, calendar_end = calendar_end, calendar_start
paul@12 329
paul@10 330
    events = []
paul@10 331
    shown_events = {}
paul@10 332
    all_shown_events = []
paul@17 333
    processed_pages = set()
paul@10 334
paul@10 335
    earliest = None
paul@10 336
    latest = None
paul@10 337
paul@10 338
    for category_name in category_names:
paul@10 339
paul@10 340
        # Get the pages and page names in the category.
paul@10 341
paul@10 342
        pages_in_category = getPages(category_name, request)
paul@10 343
paul@10 344
        # Visit each page in the category.
paul@10 345
paul@10 346
        for page_in_category in pages_in_category:
paul@10 347
            pagename = page_in_category.page_name
paul@10 348
paul@17 349
            # Only process each page once.
paul@17 350
paul@17 351
            if pagename in processed_pages:
paul@17 352
                continue
paul@17 353
            else:
paul@17 354
                processed_pages.add(pagename)
paul@17 355
paul@10 356
            # Get a real page, not a result page.
paul@10 357
paul@10 358
            real_page_in_category = Page(request, pagename)
paul@10 359
            event_details = getEventDetails(real_page_in_category)
paul@10 360
paul@10 361
            # Define the event as the page together with its details.
paul@10 362
paul@10 363
            event = (real_page_in_category, event_details)
paul@10 364
            events.append(event)
paul@10 365
paul@10 366
            # Test for the suitability of the event.
paul@10 367
paul@10 368
            if event_details.has_key("start") and event_details.has_key("end"):
paul@10 369
paul@10 370
                start_month = event_details["start"][:2]
paul@10 371
                end_month = event_details["end"][:2]
paul@10 372
paul@10 373
                # Compare the months of the dates to the requested calendar
paul@10 374
                # window, if any.
paul@10 375
paul@10 376
                if (calendar_start is None or end_month >= calendar_start) and \
paul@10 377
                    (calendar_end is None or start_month <= calendar_end):
paul@10 378
paul@10 379
                    all_shown_events.append(event)
paul@10 380
paul@10 381
                    if earliest is None or start_month < earliest:
paul@10 382
                        earliest = start_month
paul@10 383
                    if latest is None or end_month > latest:
paul@10 384
                        latest = end_month
paul@10 385
paul@10 386
                    # Store the event in the month-specific dictionary.
paul@10 387
paul@10 388
                    first = max(start_month, calendar_start or start_month)
paul@10 389
                    last = min(end_month, calendar_end or end_month)
paul@10 390
paul@10 391
                    for event_month in daterange(first, last):
paul@10 392
                        if not shown_events.has_key(event_month):
paul@10 393
                            shown_events[event_month] = []
paul@10 394
                        shown_events[event_month].append(event)
paul@10 395
paul@10 396
    return events, shown_events, all_shown_events, earliest, latest
paul@10 397
paul@13 398
def getConcretePeriod(calendar_start, calendar_end, earliest, latest):
paul@13 399
paul@13 400
    """
paul@13 401
    From the requested 'calendar_start' and 'calendar_end', which may be None,
paul@13 402
    indicating that no restriction is imposed on the period for each of the
paul@13 403
    boundaries, use the 'earliest' and 'latest' event months to define a
paul@13 404
    specific period of interest.
paul@13 405
    """
paul@13 406
paul@13 407
    # Define the period as starting with any specified start month or the
paul@13 408
    # earliest event known, ending with any specified end month or the latest
paul@13 409
    # event known.
paul@13 410
paul@13 411
    first = calendar_start or earliest
paul@13 412
    last = calendar_end or latest
paul@13 413
paul@13 414
    # If there is no range of months to show, perhaps because there are no
paul@13 415
    # events in the requested period, and there was no start or end month
paul@13 416
    # specified, show only the month indicated by the start or end of the
paul@13 417
    # requested period. If all events were to be shown but none were found show
paul@13 418
    # the current month.
paul@13 419
paul@13 420
    if first is None:
paul@13 421
        first = last or getCurrentMonth()
paul@13 422
    if last is None:
paul@13 423
        last = first or getCurrentMonth()
paul@13 424
paul@13 425
    # Permit "expiring" periods (where the start date approaches the end date).
paul@13 426
paul@13 427
    return min(first, last), last
paul@13 428
paul@15 429
def getCoverage(start, end, events):
paul@15 430
paul@15 431
    """
paul@15 432
    Within the period defined by the 'start' and 'end' dates, determine the
paul@15 433
    coverage of the days in the period by the given 'events', returning a set of
paul@15 434
    covered days, along with a list of slots, where each slot contains a tuple
paul@15 435
    of the form (set of covered days, events).
paul@15 436
    """
paul@15 437
paul@15 438
    all_events = []
paul@15 439
    full_coverage = set()
paul@15 440
paul@15 441
    # Get event details.
paul@15 442
paul@15 443
    for event in events:
paul@15 444
        event_page, event_details = event
paul@15 445
paul@15 446
        # Test for the event in the period.
paul@15 447
paul@15 448
        if event_details["start"] <= end and event_details["end"] >= start:
paul@15 449
paul@15 450
            # Find the coverage of this period for the event.
paul@15 451
paul@15 452
            event_start = max(event_details["start"], start)
paul@15 453
            event_end = min(event_details["end"], end)
paul@15 454
            event_coverage = set(daterange(event_start, event_end))
paul@15 455
paul@15 456
            # Update the overall coverage.
paul@15 457
paul@15 458
            full_coverage.update(event_coverage)
paul@15 459
paul@15 460
            # Try and fit the event into the events list.
paul@15 461
paul@15 462
            for i, (coverage, covered_events) in enumerate(all_events):
paul@15 463
paul@15 464
                # Where the event does not overlap with the current
paul@15 465
                # element, add it alongside existing events.
paul@15 466
paul@15 467
                if not coverage.intersection(event_coverage):
paul@15 468
                    covered_events.append(event)
paul@15 469
                    all_events[i] = coverage.union(event_coverage), covered_events
paul@15 470
                    break
paul@15 471
paul@15 472
            # Make a new element in the list if the event cannot be
paul@15 473
            # marked alongside existing events.
paul@15 474
paul@15 475
            else:
paul@15 476
                all_events.append((event_coverage, [event]))
paul@15 477
paul@15 478
    return full_coverage, all_events
paul@15 479
paul@19 480
# User interface functions.
paul@19 481
paul@19 482
def getParameterMonth(arg):
paul@19 483
    n = None
paul@19 484
paul@19 485
    if arg.startswith("current"):
paul@19 486
        date = getCurrentMonth()
paul@19 487
        if len(arg) > 8:
paul@19 488
            n = int(arg[7:])
paul@19 489
paul@19 490
    elif arg.startswith("yearstart"):
paul@19 491
        date = (getCurrentYear(), 1)
paul@19 492
        if len(arg) > 10:
paul@19 493
            n = int(arg[9:])
paul@19 494
paul@19 495
    elif arg.startswith("yearend"):
paul@19 496
        date = (getCurrentYear(), 12)
paul@19 497
        if len(arg) > 8:
paul@19 498
            n = int(arg[7:])
paul@19 499
paul@19 500
    else:
paul@19 501
        date = getMonth(arg)
paul@19 502
paul@19 503
    if n is not None:
paul@19 504
        date = monthupdate(date, n)
paul@19 505
paul@19 506
    return date
paul@19 507
paul@19 508
def getFormMonth(request, calendar_name, argname):
paul@19 509
    if calendar_name is None:
paul@19 510
        calendar_prefix = argname
paul@19 511
    else:
paul@19 512
        calendar_prefix = "%s-%s" % (calendar_name, argname)
paul@19 513
paul@19 514
    arg = request.form.get(calendar_prefix, [None])[0]
paul@19 515
    if arg is not None:
paul@19 516
        return getParameterMonth(arg)
paul@19 517
    else:
paul@19 518
        return None
paul@19 519
paul@19 520
def getPrettyPageName(page):
paul@19 521
paul@19 522
    "Return a nicely formatted title/name for the given 'page'."
paul@19 523
paul@19 524
    return page.split_title(force=1).replace("_", " ").replace("/", u" ? ")
paul@19 525
paul@10 526
# vim: tabstop=4 expandtab shiftwidth=4