EventAggregator

Annotated EventAggregatorSupport.py

18:83350447c606
2009-03-26 Paul Boddie Removed build dependencies.
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@10 25
paul@10 26
# Utility functions.
paul@10 27
paul@10 28
def isMoin15():
paul@10 29
    return version.release.startswith("1.5.")
paul@10 30
paul@10 31
def getCategoryPattern(request):
paul@10 32
    global category_regexp
paul@10 33
paul@10 34
    try:
paul@10 35
        return request.cfg.cache.page_category_regexact
paul@10 36
    except AttributeError:
paul@10 37
paul@10 38
        # Use regular expression from MoinMoin 1.7.1 otherwise.
paul@10 39
paul@10 40
        if category_regexp is None:
paul@10 41
            category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE)
paul@10 42
        return category_regexp
paul@10 43
paul@10 44
# The main activity functions.
paul@10 45
paul@10 46
def getPages(pagename, request):
paul@10 47
paul@10 48
    "Return the links minus category links for 'pagename' using the 'request'."
paul@10 49
paul@10 50
    query = search.QueryParser().parse_query('category:%s' % pagename)
paul@10 51
    if isMoin15():
paul@10 52
        results = search.searchPages(request, query)
paul@10 53
        results.sortByPagename()
paul@10 54
    else:
paul@10 55
        results = search.searchPages(request, query, "page_name")
paul@10 56
paul@10 57
    cat_pattern = getCategoryPattern(request)
paul@10 58
    pages = []
paul@10 59
    for page in results.hits:
paul@10 60
        if not cat_pattern.match(page.page_name):
paul@10 61
            pages.append(page)
paul@10 62
    return pages
paul@10 63
paul@10 64
def getPrettyPageName(page):
paul@10 65
paul@10 66
    "Return a nicely formatted title/name for the given 'page'."
paul@10 67
paul@10 68
    return page.split_title(force=1).replace("_", " ").replace("/", u" ? ")
paul@10 69
paul@10 70
def getEventDetails(page):
paul@10 71
paul@10 72
    "Return a dictionary of event details from the given 'page'."
paul@10 73
paul@10 74
    event_details = {}
paul@10 75
paul@10 76
    if page.pi["format"] == "wiki":
paul@10 77
        for match in definition_list_regexp.finditer(page.body):
paul@10 78
paul@10 79
            # Permit case-insensitive list terms.
paul@10 80
paul@10 81
            term = match.group("term").lower()
paul@10 82
            desc = match.group("desc")
paul@10 83
paul@10 84
            # Special value type handling.
paul@10 85
paul@10 86
            if term in ("start", "end"):
paul@10 87
                desc = getDate(desc)
paul@10 88
            elif term in ("topics",):
paul@10 89
                desc = [value.strip() for value in desc.split(",")]
paul@10 90
paul@10 91
            if desc is not None:
paul@10 92
                event_details[term] = desc
paul@10 93
paul@10 94
    return event_details
paul@10 95
paul@10 96
def getDate(s):
paul@10 97
paul@10 98
    "Parse the string 's', extracting and returning a date string."
paul@10 99
paul@10 100
    m = date_regexp.search(s)
paul@10 101
    if m:
paul@10 102
        return tuple(map(int, m.groups()))
paul@10 103
    else:
paul@10 104
        return None
paul@10 105
paul@10 106
def getMonth(s):
paul@10 107
paul@10 108
    "Parse the string 's', extracting and returning a month string."
paul@10 109
paul@10 110
    m = month_regexp.search(s)
paul@10 111
    if m:
paul@10 112
        return tuple(map(int, m.groups()))
paul@10 113
    else:
paul@10 114
        return None
paul@10 115
paul@11 116
def getCurrentMonth():
paul@11 117
paul@11 118
    "Return the current month as a (year, month) tuple."
paul@11 119
paul@11 120
    today = datetime.date.today()
paul@11 121
    return (today.year, today.month)
paul@11 122
paul@17 123
def getCurrentYear():
paul@17 124
paul@17 125
    "Return the current year."
paul@17 126
paul@17 127
    today = datetime.date.today()
paul@17 128
    return today.year
paul@17 129
paul@11 130
def monthupdate(date, n):
paul@11 131
paul@11 132
    "Return 'date' updated by 'n' months."
paul@11 133
paul@11 134
    if n < 0:
paul@11 135
        fn = prevmonth
paul@11 136
    else:
paul@11 137
        fn = nextmonth
paul@11 138
paul@11 139
    i = 0
paul@11 140
    while i < abs(n):
paul@11 141
        date = fn(date)
paul@11 142
        i += 1
paul@11 143
        
paul@11 144
    return date
paul@11 145
paul@13 146
def daterange(first, last, step=1):
paul@11 147
paul@13 148
    """
paul@13 149
    Get the range of dates starting at 'first' and ending on 'last', using the
paul@13 150
    specified 'step'.
paul@13 151
    """
paul@11 152
paul@10 153
    results = []
paul@10 154
paul@10 155
    months_only = len(first) == 2
paul@10 156
    start_year = first[0]
paul@10 157
    end_year = last[0]
paul@10 158
paul@11 159
    for year in range(start_year, end_year + step, step):
paul@11 160
        if step == 1 and year < end_year:
paul@10 161
            end_month = 12
paul@11 162
        elif step == -1 and year > end_year:
paul@11 163
            end_month = 1
paul@10 164
        else:
paul@10 165
            end_month = last[1]
paul@10 166
paul@11 167
        if step == 1 and year > start_year:
paul@10 168
            start_month = 1
paul@11 169
        elif step == -1 and year < start_year:
paul@11 170
            start_month = 12
paul@10 171
        else:
paul@10 172
            start_month = first[1]
paul@10 173
paul@11 174
        for month in range(start_month, end_month + step, step):
paul@10 175
            if months_only:
paul@10 176
                results.append((year, month))
paul@10 177
            else:
paul@11 178
                if step == 1 and month < end_month:
paul@10 179
                    _wd, end_day = calendar.monthrange(year, month)
paul@11 180
                elif step == -1 and month > end_month:
paul@11 181
                    end_day = 1
paul@10 182
                else:
paul@10 183
                    end_day = last[2]
paul@10 184
paul@11 185
                if step == 1 and month > start_month:
paul@10 186
                    start_day = 1
paul@11 187
                elif step == -1 and month < start_month:
paul@11 188
                    _wd, start_day = calendar.monthrange(year, month)
paul@10 189
                else:
paul@10 190
                    start_day = first[2]
paul@10 191
paul@11 192
                for day in range(start_day, end_day + step, step):
paul@10 193
                    results.append((year, month, day))
paul@10 194
paul@10 195
    return results
paul@10 196
paul@10 197
def nextdate(date):
paul@11 198
paul@11 199
    "Return the date following the given 'date'."
paul@11 200
paul@10 201
    year, month, day = date
paul@10 202
    _wd, end_day = calendar.monthrange(year, month)
paul@10 203
    if day == end_day:
paul@10 204
        if month == 12:
paul@10 205
            return (year + 1, 1, 1)
paul@10 206
        else:
paul@10 207
            return (year, month + 1, 1)
paul@10 208
    else:
paul@10 209
        return (year, month, day + 1)
paul@10 210
paul@11 211
def prevdate(date):
paul@11 212
paul@11 213
    "Return the date preceding the given 'date'."
paul@11 214
paul@11 215
    year, month, day = date
paul@11 216
    if day == 1:
paul@11 217
        if month == 1:
paul@11 218
            return (year - 1, 12, 31)
paul@11 219
        else:
paul@11 220
            _wd, end_day = calendar.monthrange(year, month - 1)
paul@11 221
            return (year, month - 1, end_day)
paul@11 222
    else:
paul@11 223
        return (year, month, day - 1)
paul@11 224
paul@11 225
def nextmonth(date):
paul@11 226
paul@11 227
    "Return the (year, month) tuple following 'date'."
paul@11 228
paul@11 229
    year, month = date
paul@11 230
    if month == 12:
paul@11 231
        return (year + 1, 1)
paul@11 232
    else:
paul@11 233
        return year, month + 1
paul@11 234
paul@11 235
def prevmonth(date):
paul@11 236
paul@11 237
    "Return the (year, month) tuple preceding 'date'."
paul@11 238
paul@11 239
    year, month = date
paul@11 240
    if month == 1:
paul@11 241
        return (year - 1, 12)
paul@11 242
    else:
paul@11 243
        return year, month - 1
paul@11 244
paul@13 245
def span(start, end):
paul@13 246
paul@13 247
    "Return the difference between 'start' and 'end'."
paul@13 248
paul@13 249
    return end[0] - start[0], end[1] - start[1]
paul@13 250
paul@10 251
def getEvents(request, category_names, calendar_start=None, calendar_end=None):
paul@10 252
paul@10 253
    """
paul@10 254
    Using the 'request', generate a list of events found on pages belonging to
paul@10 255
    the specified 'category_names', using the optional 'calendar_start' and
paul@10 256
    'calendar_end' month tuples of the form (year, month) to indicate a window
paul@10 257
    of interest.
paul@10 258
paul@10 259
    Return a list of events, a dictionary mapping months to event lists (within
paul@10 260
    the window of interest), a list of all events within the window of interest,
paul@10 261
    the earliest month of an event within the window of interest, and the latest
paul@10 262
    month of an event within the window of interest.
paul@10 263
    """
paul@10 264
paul@12 265
    # Re-order the window, if appropriate.
paul@12 266
paul@12 267
    if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end:
paul@12 268
        calendar_start, calendar_end = calendar_end, calendar_start
paul@12 269
paul@10 270
    events = []
paul@10 271
    shown_events = {}
paul@10 272
    all_shown_events = []
paul@17 273
    processed_pages = set()
paul@10 274
paul@10 275
    earliest = None
paul@10 276
    latest = None
paul@10 277
paul@10 278
    for category_name in category_names:
paul@10 279
paul@10 280
        # Get the pages and page names in the category.
paul@10 281
paul@10 282
        pages_in_category = getPages(category_name, request)
paul@10 283
paul@10 284
        # Visit each page in the category.
paul@10 285
paul@10 286
        for page_in_category in pages_in_category:
paul@10 287
            pagename = page_in_category.page_name
paul@10 288
paul@17 289
            # Only process each page once.
paul@17 290
paul@17 291
            if pagename in processed_pages:
paul@17 292
                continue
paul@17 293
            else:
paul@17 294
                processed_pages.add(pagename)
paul@17 295
paul@10 296
            # Get a real page, not a result page.
paul@10 297
paul@10 298
            real_page_in_category = Page(request, pagename)
paul@10 299
            event_details = getEventDetails(real_page_in_category)
paul@10 300
paul@10 301
            # Define the event as the page together with its details.
paul@10 302
paul@10 303
            event = (real_page_in_category, event_details)
paul@10 304
            events.append(event)
paul@10 305
paul@10 306
            # Test for the suitability of the event.
paul@10 307
paul@10 308
            if event_details.has_key("start") and event_details.has_key("end"):
paul@10 309
paul@10 310
                start_month = event_details["start"][:2]
paul@10 311
                end_month = event_details["end"][:2]
paul@10 312
paul@10 313
                # Compare the months of the dates to the requested calendar
paul@10 314
                # window, if any.
paul@10 315
paul@10 316
                if (calendar_start is None or end_month >= calendar_start) and \
paul@10 317
                    (calendar_end is None or start_month <= calendar_end):
paul@10 318
paul@10 319
                    all_shown_events.append(event)
paul@10 320
paul@10 321
                    if earliest is None or start_month < earliest:
paul@10 322
                        earliest = start_month
paul@10 323
                    if latest is None or end_month > latest:
paul@10 324
                        latest = end_month
paul@10 325
paul@10 326
                    # Store the event in the month-specific dictionary.
paul@10 327
paul@10 328
                    first = max(start_month, calendar_start or start_month)
paul@10 329
                    last = min(end_month, calendar_end or end_month)
paul@10 330
paul@10 331
                    for event_month in daterange(first, last):
paul@10 332
                        if not shown_events.has_key(event_month):
paul@10 333
                            shown_events[event_month] = []
paul@10 334
                        shown_events[event_month].append(event)
paul@10 335
paul@10 336
    return events, shown_events, all_shown_events, earliest, latest
paul@10 337
paul@13 338
def getConcretePeriod(calendar_start, calendar_end, earliest, latest):
paul@13 339
paul@13 340
    """
paul@13 341
    From the requested 'calendar_start' and 'calendar_end', which may be None,
paul@13 342
    indicating that no restriction is imposed on the period for each of the
paul@13 343
    boundaries, use the 'earliest' and 'latest' event months to define a
paul@13 344
    specific period of interest.
paul@13 345
    """
paul@13 346
paul@13 347
    # Define the period as starting with any specified start month or the
paul@13 348
    # earliest event known, ending with any specified end month or the latest
paul@13 349
    # event known.
paul@13 350
paul@13 351
    first = calendar_start or earliest
paul@13 352
    last = calendar_end or latest
paul@13 353
paul@13 354
    # If there is no range of months to show, perhaps because there are no
paul@13 355
    # events in the requested period, and there was no start or end month
paul@13 356
    # specified, show only the month indicated by the start or end of the
paul@13 357
    # requested period. If all events were to be shown but none were found show
paul@13 358
    # the current month.
paul@13 359
paul@13 360
    if first is None:
paul@13 361
        first = last or getCurrentMonth()
paul@13 362
    if last is None:
paul@13 363
        last = first or getCurrentMonth()
paul@13 364
paul@13 365
    # Permit "expiring" periods (where the start date approaches the end date).
paul@13 366
paul@13 367
    return min(first, last), last
paul@13 368
paul@15 369
def getCoverage(start, end, events):
paul@15 370
paul@15 371
    """
paul@15 372
    Within the period defined by the 'start' and 'end' dates, determine the
paul@15 373
    coverage of the days in the period by the given 'events', returning a set of
paul@15 374
    covered days, along with a list of slots, where each slot contains a tuple
paul@15 375
    of the form (set of covered days, events).
paul@15 376
    """
paul@15 377
paul@15 378
    all_events = []
paul@15 379
    full_coverage = set()
paul@15 380
paul@15 381
    # Get event details.
paul@15 382
paul@15 383
    for event in events:
paul@15 384
        event_page, event_details = event
paul@15 385
paul@15 386
        # Test for the event in the period.
paul@15 387
paul@15 388
        if event_details["start"] <= end and event_details["end"] >= start:
paul@15 389
paul@15 390
            # Find the coverage of this period for the event.
paul@15 391
paul@15 392
            event_start = max(event_details["start"], start)
paul@15 393
            event_end = min(event_details["end"], end)
paul@15 394
            event_coverage = set(daterange(event_start, event_end))
paul@15 395
paul@15 396
            # Update the overall coverage.
paul@15 397
paul@15 398
            full_coverage.update(event_coverage)
paul@15 399
paul@15 400
            # Try and fit the event into the events list.
paul@15 401
paul@15 402
            for i, (coverage, covered_events) in enumerate(all_events):
paul@15 403
paul@15 404
                # Where the event does not overlap with the current
paul@15 405
                # element, add it alongside existing events.
paul@15 406
paul@15 407
                if not coverage.intersection(event_coverage):
paul@15 408
                    covered_events.append(event)
paul@15 409
                    all_events[i] = coverage.union(event_coverage), covered_events
paul@15 410
                    break
paul@15 411
paul@15 412
            # Make a new element in the list if the event cannot be
paul@15 413
            # marked alongside existing events.
paul@15 414
paul@15 415
            else:
paul@15 416
                all_events.append((event_coverage, [event]))
paul@15 417
paul@15 418
    return full_coverage, all_events
paul@15 419
paul@10 420
# vim: tabstop=4 expandtab shiftwidth=4