EventAggregator

Annotated EventAggregatorSupport.py

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