EventAggregator

Annotated EventAggregatorSupport.py

73:155ceab3c122
2010-02-03 Paul Boddie Introduced basic and advanced modes to the new event action.
paul@10 1
# -*- coding: iso-8859-1 -*-
paul@10 2
"""
paul@10 3
    MoinMoin - EventAggregator library
paul@10 4
paul@67 5
    @copyright: 2008, 2009, 2010 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@69 19
try:
paul@69 20
    set
paul@69 21
except NameError:
paul@69 22
    from sets import Set as set
paul@69 23
paul@65 24
__version__ = "0.5"
paul@10 25
paul@22 26
# Date labels.
paul@22 27
paul@22 28
month_labels = ["January", "February", "March", "April", "May", "June",
paul@22 29
    "July", "August", "September", "October", "November", "December"]
paul@22 30
weekday_labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
paul@22 31
paul@10 32
# Regular expressions where MoinMoin does not provide the required support.
paul@10 33
paul@10 34
category_regexp = None
paul@47 35
paul@47 36
# Page parsing.
paul@47 37
paul@47 38
definition_list_regexp = re.compile(ur'(?P<wholeterm>^(?P<optcomment>#*)\s+(?P<term>.*?)::\s)(?P<desc>.*?)$', re.UNICODE | re.MULTILINE)
paul@47 39
category_membership_regexp = re.compile(ur"^\s*((Category\S+)(\s+Category\S+)*)\s*$", re.MULTILINE | re.UNICODE)
paul@47 40
paul@47 41
# Value parsing.
paul@47 42
paul@10 43
date_regexp = re.compile(ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})', re.UNICODE)
paul@10 44
month_regexp = re.compile(ur'(?P<year>[0-9]{4})-(?P<month>[0-9]{2})', re.UNICODE)
paul@19 45
verbatim_regexp = re.compile(ur'(?:'
paul@19 46
    ur'<<Verbatim\((?P<verbatim>.*?)\)>>'
paul@19 47
    ur'|'
paul@19 48
    ur'\[\[Verbatim\((?P<verbatim2>.*?)\)\]\]'
paul@19 49
    ur'|'
paul@19 50
    ur'`(?P<monospace>.*?)`'
paul@19 51
    ur'|'
paul@19 52
    ur'{{{(?P<preformatted>.*?)}}}'
paul@19 53
    ur')', re.UNICODE)
paul@10 54
paul@10 55
# Utility functions.
paul@10 56
paul@10 57
def isMoin15():
paul@10 58
    return version.release.startswith("1.5.")
paul@10 59
paul@10 60
def getCategoryPattern(request):
paul@10 61
    global category_regexp
paul@10 62
paul@10 63
    try:
paul@10 64
        return request.cfg.cache.page_category_regexact
paul@10 65
    except AttributeError:
paul@10 66
paul@10 67
        # Use regular expression from MoinMoin 1.7.1 otherwise.
paul@10 68
paul@10 69
        if category_regexp is None:
paul@10 70
            category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE)
paul@10 71
        return category_regexp
paul@10 72
paul@67 73
# Textual representations.
paul@67 74
paul@67 75
def getHTTPTimeString(tmtuple):
paul@67 76
    return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (
paul@67 77
        weekday_labels[tmtuple.tm_wday],
paul@67 78
        tmtuple.tm_mday,
paul@67 79
        month_labels[tmtuple.tm_mon -1], # zero-based labels
paul@67 80
        tmtuple.tm_year,
paul@67 81
        tmtuple.tm_hour,
paul@67 82
        tmtuple.tm_min,
paul@67 83
        tmtuple.tm_sec
paul@67 84
        )
paul@67 85
paul@67 86
def getSimpleWikiText(text):
paul@67 87
paul@67 88
    """
paul@67 89
    Return the plain text representation of the given 'text' which may employ
paul@67 90
    certain Wiki syntax features, such as those providing verbatim or monospaced
paul@67 91
    text.
paul@67 92
    """
paul@67 93
paul@67 94
    # NOTE: Re-implementing support for verbatim text and linking avoidance.
paul@67 95
paul@67 96
    return "".join([s for s in verbatim_regexp.split(text) if s is not None])
paul@67 97
paul@67 98
def getEncodedWikiText(text):
paul@67 99
paul@67 100
    "Encode the given 'text' in a verbatim representation."
paul@67 101
paul@67 102
    return "<<Verbatim(%s)>>" % text
paul@67 103
paul@67 104
def getPrettyTitle(title):
paul@67 105
paul@67 106
    "Return a nicely formatted version of the given 'title'."
paul@67 107
paul@67 108
    return title.replace("_", " ").replace("/", u" ? ")
paul@67 109
paul@67 110
def getMonthLabel(month):
paul@67 111
paul@67 112
    "Return an unlocalised label for the given 'month'."
paul@67 113
paul@67 114
    return month_labels[month - 1] # zero-based labels
paul@67 115
paul@67 116
def getDayLabel(weekday):
paul@67 117
paul@67 118
    "Return an unlocalised label for the given 'weekday'."
paul@67 119
paul@67 120
    return weekday_labels[weekday]
paul@67 121
paul@19 122
# Action support functions.
paul@19 123
paul@67 124
def getPageRevision(page):
paul@67 125
paul@67 126
    "Return the revision details dictionary for the given 'page'."
paul@67 127
paul@67 128
    # From Page.edit_info...
paul@67 129
paul@67 130
    if hasattr(page, "editlog_entry"):
paul@67 131
        line = page.editlog_entry()
paul@67 132
    else:
paul@67 133
        line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x
paul@67 134
paul@67 135
    timestamp = line.ed_time_usecs
paul@67 136
    mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x
paul@67 137
    return {"timestamp" : time.gmtime(mtime), "comment" : line.comment}
paul@67 138
paul@67 139
# Category discovery and searching.
paul@67 140
paul@19 141
def getCategories(request):
paul@19 142
paul@19 143
    """
paul@19 144
    From the AdvancedSearch macro, return a list of category page names using
paul@19 145
    the given 'request'.
paul@19 146
    """
paul@19 147
paul@19 148
    # This will return all pages with "Category" in the title.
paul@19 149
paul@19 150
    cat_filter = getCategoryPattern(request).search
paul@19 151
    return request.rootpage.getPageList(filter=cat_filter)
paul@19 152
paul@19 153
def getCategoryMapping(category_pagenames, request):
paul@19 154
paul@19 155
    """
paul@19 156
    For the given 'category_pagenames' return a list of tuples of the form
paul@19 157
    (category name, category page name) using the given 'request'.
paul@19 158
    """
paul@19 159
paul@19 160
    cat_pattern = getCategoryPattern(request)
paul@19 161
    mapping = []
paul@19 162
    for pagename in category_pagenames:
paul@19 163
        name = cat_pattern.match(pagename).group("key")
paul@19 164
        if name != "Category":
paul@19 165
            mapping.append((name, pagename))
paul@19 166
    mapping.sort()
paul@19 167
    return mapping
paul@19 168
paul@67 169
def getCategoryPages(pagename, request):
paul@29 170
paul@67 171
    """
paul@67 172
    Return the pages associated with the given category 'pagename' using the
paul@67 173
    'request'.
paul@67 174
    """
paul@10 175
paul@10 176
    query = search.QueryParser().parse_query('category:%s' % pagename)
paul@10 177
    if isMoin15():
paul@10 178
        results = search.searchPages(request, query)
paul@10 179
        results.sortByPagename()
paul@10 180
    else:
paul@10 181
        results = search.searchPages(request, query, "page_name")
paul@10 182
paul@10 183
    cat_pattern = getCategoryPattern(request)
paul@10 184
    pages = []
paul@10 185
    for page in results.hits:
paul@10 186
        if not cat_pattern.match(page.page_name):
paul@10 187
            pages.append(page)
paul@10 188
    return pages
paul@10 189
paul@67 190
# The main activity functions.
paul@67 191
paul@67 192
class EventPage:
paul@67 193
paul@67 194
    "An event page."
paul@67 195
paul@67 196
    def __init__(self, page):
paul@67 197
        self.page = page
paul@69 198
        self.events = None
paul@67 199
        self.body = None
paul@67 200
        self.categories = None
paul@67 201
paul@67 202
    def copyPage(self, page):
paul@67 203
paul@67 204
        "Copy the body of the given 'page'."
paul@67 205
paul@67 206
        self.body = page.getBody()
paul@67 207
paul@67 208
    def getPageURL(self, request):
paul@67 209
paul@67 210
        "Using 'request', return the URL of this page."
paul@24 211
paul@67 212
        page = self.page
paul@67 213
paul@67 214
        if isMoin15():
paul@67 215
            return request.getQualifiedURL(page.url(request))
paul@67 216
        else:
paul@67 217
            return request.getQualifiedURL(page.url(request, relative=0))
paul@67 218
paul@67 219
    def getFormat(self):
paul@67 220
paul@67 221
        "Get the format used on this page."
paul@24 222
paul@67 223
        if isMoin15():
paul@67 224
            return "wiki" # page.pi_format
paul@67 225
        else:
paul@67 226
            return self.page.pi["format"]
paul@67 227
paul@67 228
    def getRevisions(self):
paul@67 229
paul@67 230
        "Return a list of page revisions."
paul@67 231
paul@67 232
        return self.page.getRevList()
paul@67 233
paul@67 234
    def getPageRevision(self):
paul@24 235
paul@67 236
        "Return the revision details dictionary for this page."
paul@67 237
paul@67 238
        return getPageRevision(self.page)
paul@67 239
paul@67 240
    def getPageName(self):
paul@67 241
paul@67 242
        "Return the page name."
paul@67 243
paul@67 244
        return self.page.page_name
paul@24 245
paul@67 246
    def getPrettyPageName(self):
paul@67 247
paul@67 248
        "Return a nicely formatted title/name for this page."
paul@67 249
paul@67 250
        return getPrettyPageName(self.page)
paul@67 251
paul@67 252
    def getBody(self):
paul@67 253
paul@67 254
        "Get the current page body."
paul@47 255
paul@67 256
        if self.body is None:
paul@67 257
            self.body = self.page.get_raw_body()
paul@67 258
        return self.body
paul@67 259
paul@69 260
    def getEvents(self):
paul@69 261
paul@69 262
        "Return a list of events from this page."
paul@67 263
paul@69 264
        if self.events is None:
paul@69 265
            details = {}
paul@69 266
            self.events = [Event(self, details)]
paul@47 267
paul@67 268
            if self.getFormat() == "wiki":
paul@67 269
                for match in definition_list_regexp.finditer(self.getBody()):
paul@67 270
paul@67 271
                    # Skip commented-out items.
paul@47 272
paul@67 273
                    if match.group("optcomment"):
paul@67 274
                        continue
paul@67 275
paul@67 276
                    # Permit case-insensitive list terms.
paul@67 277
paul@67 278
                    term = match.group("term").lower()
paul@67 279
                    desc = match.group("desc")
paul@67 280
paul@67 281
                    # Special value type handling.
paul@27 282
paul@67 283
                    # Dates.
paul@67 284
paul@67 285
                    if term in ("start", "end"):
paul@67 286
                        desc = getDate(desc)
paul@67 287
paul@67 288
                    # Lists (whose elements may be quoted).
paul@67 289
paul@67 290
                    elif term in ("topics", "categories"):
paul@67 291
                        desc = [getSimpleWikiText(value.strip()) for value in desc.split(",")]
paul@67 292
paul@67 293
                    # Labels which may well be quoted.
paul@67 294
paul@67 295
                    elif term in ("title", "summary", "description"):
paul@67 296
                        desc = getSimpleWikiText(desc)
paul@67 297
paul@67 298
                    if desc is not None:
paul@69 299
paul@69 300
                        # Handle apparent duplicates by creating a new set of
paul@69 301
                        # details.
paul@69 302
paul@69 303
                        if details.has_key(term):
paul@69 304
                            details = {}
paul@69 305
                            self.events.append(Event(self, details))
paul@67 306
paul@69 307
                        details[term] = desc
paul@69 308
paul@69 309
        return self.events
paul@69 310
paul@69 311
    def setEvents(self, events):
paul@69 312
paul@69 313
        "Set the given 'events' on this page."
paul@69 314
paul@69 315
        self.events = events
paul@67 316
paul@67 317
    def getCategoryMembership(self):
paul@27 318
paul@67 319
        "Get the category names from this page."
paul@67 320
paul@67 321
        if self.categories is None:
paul@67 322
            body = self.getBody()
paul@67 323
            match = category_membership_regexp.search(body)
paul@67 324
            self.categories = match.findall().split()
paul@67 325
paul@67 326
        return self.categories
paul@67 327
paul@67 328
    def setCategoryMembership(self, category_names):
paul@10 329
paul@67 330
        """
paul@67 331
        Set the category membership for the page using the specified
paul@67 332
        'category_names'.
paul@67 333
        """
paul@67 334
paul@67 335
        self.categories = category_names
paul@67 336
paul@67 337
    def flushEventDetails(self):
paul@67 338
paul@67 339
        "Flush the current event details to this page's body text."
paul@10 340
paul@67 341
        new_body_parts = []
paul@67 342
        end_of_last_match = 0
paul@67 343
        body = self.getBody()
paul@69 344
paul@69 345
        events = iter(self.getEvents())
paul@69 346
paul@69 347
        event = events.next()
paul@69 348
        event_details = event.getDetails()
paul@69 349
        replaced_terms = set()
paul@67 350
paul@67 351
        for match in definition_list_regexp.finditer(body):
paul@47 352
paul@67 353
            # Add preceding text to the new body.
paul@67 354
paul@67 355
            new_body_parts.append(body[end_of_last_match:match.start()])
paul@67 356
paul@67 357
            # Get the matching regions, adding the term to the new body.
paul@67 358
paul@67 359
            new_body_parts.append(match.group("wholeterm"))
paul@47 360
paul@10 361
            # Permit case-insensitive list terms.
paul@10 362
paul@10 363
            term = match.group("term").lower()
paul@10 364
            desc = match.group("desc")
paul@10 365
paul@69 366
            # Check that the term has not already been substituted. If so,
paul@69 367
            # get the next event.
paul@69 368
paul@69 369
            if term in replaced_terms:
paul@69 370
                try:
paul@69 371
                    event = events.next()
paul@69 372
paul@69 373
                # No more events.
paul@69 374
paul@69 375
                except StopIteration:
paul@69 376
                    break
paul@69 377
paul@69 378
                event_details = event.getDetails()
paul@69 379
                replaced_terms = set()
paul@69 380
paul@10 381
            # Special value type handling.
paul@10 382
paul@67 383
            if event_details.has_key(term):
paul@19 384
paul@67 385
                # Dates.
paul@47 386
paul@67 387
                if term in ("start", "end"):
paul@67 388
                    desc = desc.replace("YYYY-MM-DD", str(event_details[term]))
paul@47 389
paul@67 390
                # Lists (whose elements may be quoted).
paul@47 391
paul@67 392
                elif term in ("topics", "categories"):
paul@67 393
                    desc = ", ".join(getEncodedWikiText(event_details[term]))
paul@47 394
paul@67 395
                # Labels which may well be quoted.
paul@47 396
paul@67 397
                elif term in ("title", "summary"):
paul@67 398
                    desc = getEncodedWikiText(event_details[term])
paul@47 399
paul@67 400
                # Text which need not be quoted, but it will be Wiki text.
paul@55 401
paul@67 402
                elif term in ("description",):
paul@67 403
                    desc = event_details[term]
paul@55 404
paul@69 405
                replaced_terms.add(term)
paul@69 406
paul@67 407
            new_body_parts.append(desc)
paul@10 408
paul@69 409
            # Remember where in the page has been processed.
paul@69 410
paul@69 411
            end_of_last_match = match.end()
paul@69 412
paul@69 413
        # Write the rest of the page.
paul@69 414
paul@69 415
        new_body_parts.append(body[end_of_last_match:])
paul@10 416
paul@67 417
        self.body = "".join(new_body_parts)
paul@11 418
paul@67 419
    def flushCategoryMembership(self):
paul@17 420
paul@67 421
        "Flush the category membership to the page body."
paul@11 422
paul@67 423
        body = self.getBody()
paul@67 424
        category_names = self.getCategoryMembership()
paul@67 425
        match = category_membership_regexp.search(body)
paul@10 426
paul@67 427
        if match:
paul@67 428
            self.body = "".join([body[:match.start()], " ".join(category_names), body[match.end():]])
paul@10 429
paul@67 430
    def saveChanges(self):
paul@10 431
paul@67 432
        "Save changes to the event."
paul@10 433
paul@67 434
        self.flushEventDetails()
paul@67 435
        self.flushCategoryMembership()
paul@67 436
        self.page.saveText(self.getBody(), 0)
paul@10 437
paul@67 438
    def linkToPage(self, request, text, query_string=None):
paul@11 439
paul@67 440
        """
paul@67 441
        Using 'request', return a link to this page with the given link 'text'
paul@67 442
        and optional 'query_string'.
paul@67 443
        """
paul@11 444
paul@67 445
        return linkToPage(request, self.page, text, query_string)
paul@13 446
paul@69 447
class Event:
paul@69 448
paul@69 449
    "A description of an event."
paul@69 450
paul@69 451
    def __init__(self, page, details):
paul@69 452
        self.page = page
paul@69 453
        self.details = details
paul@69 454
paul@69 455
    def __cmp__(self, other):
paul@69 456
paul@69 457
        """
paul@69 458
        Compare this object with 'other' using the event start and end details.
paul@69 459
        """
paul@69 460
paul@69 461
        details1 = self.details
paul@69 462
        details2 = other.details
paul@69 463
        return cmp(
paul@69 464
            (details1["start"], details1["end"]),
paul@69 465
            (details2["start"], details2["end"])
paul@69 466
            )
paul@69 467
paul@69 468
    def getPage(self):
paul@69 469
paul@69 470
        "Return the page describing this event."
paul@69 471
paul@69 472
        return self.page
paul@69 473
paul@69 474
    def setPage(self, page):
paul@69 475
paul@69 476
        "Set the 'page' describing this event."
paul@69 477
paul@69 478
        self.page = page
paul@69 479
paul@69 480
    def getSummary(self, event_parent=None):
paul@69 481
paul@69 482
        """
paul@69 483
        Return either the given title or summary of the event according to the
paul@69 484
        event details, or a summary made from using the pretty version of the
paul@69 485
        page name.
paul@69 486
paul@69 487
        If the optional 'event_parent' is specified, any page beneath the given
paul@69 488
        'event_parent' page in the page hierarchy will omit this parent information
paul@69 489
        if its name is used as the summary.
paul@69 490
        """
paul@69 491
paul@69 492
        event_details = self.details
paul@69 493
paul@69 494
        if event_details.has_key("title"):
paul@69 495
            return event_details["title"]
paul@69 496
        elif event_details.has_key("summary"):
paul@69 497
            return event_details["summary"]
paul@69 498
        else:
paul@69 499
            # If appropriate, remove the parent details and "/" character.
paul@69 500
paul@69 501
            title = self.page.getPageName()
paul@69 502
paul@69 503
            if event_parent is not None and title.startswith(event_parent):
paul@69 504
                title = title[len(event_parent.rstrip("/")) + 1:]
paul@69 505
paul@69 506
            return getPrettyTitle(title)
paul@69 507
paul@69 508
    def getDetails(self):
paul@69 509
paul@69 510
        "Return the details for this event."
paul@69 511
paul@69 512
        return self.details
paul@69 513
paul@69 514
    def setDetails(self, event_details):
paul@69 515
paul@69 516
        "Set the 'event_details' for this event."
paul@69 517
paul@69 518
        self.details = event_details
paul@69 519
paul@10 520
def getEvents(request, category_names, calendar_start=None, calendar_end=None):
paul@10 521
paul@10 522
    """
paul@10 523
    Using the 'request', generate a list of events found on pages belonging to
paul@10 524
    the specified 'category_names', using the optional 'calendar_start' and
paul@10 525
    'calendar_end' month tuples of the form (year, month) to indicate a window
paul@10 526
    of interest.
paul@10 527
paul@10 528
    Return a list of events, a dictionary mapping months to event lists (within
paul@10 529
    the window of interest), a list of all events within the window of interest,
paul@10 530
    the earliest month of an event within the window of interest, and the latest
paul@10 531
    month of an event within the window of interest.
paul@10 532
    """
paul@10 533
paul@12 534
    # Re-order the window, if appropriate.
paul@12 535
paul@12 536
    if calendar_start is not None and calendar_end is not None and calendar_start > calendar_end:
paul@12 537
        calendar_start, calendar_end = calendar_end, calendar_start
paul@12 538
paul@10 539
    events = []
paul@10 540
    shown_events = {}
paul@10 541
    all_shown_events = []
paul@17 542
    processed_pages = set()
paul@10 543
paul@10 544
    earliest = None
paul@10 545
    latest = None
paul@10 546
paul@10 547
    for category_name in category_names:
paul@10 548
paul@10 549
        # Get the pages and page names in the category.
paul@10 550
paul@67 551
        pages_in_category = getCategoryPages(category_name, request)
paul@10 552
paul@10 553
        # Visit each page in the category.
paul@10 554
paul@10 555
        for page_in_category in pages_in_category:
paul@10 556
            pagename = page_in_category.page_name
paul@10 557
paul@17 558
            # Only process each page once.
paul@17 559
paul@17 560
            if pagename in processed_pages:
paul@17 561
                continue
paul@17 562
            else:
paul@17 563
                processed_pages.add(pagename)
paul@17 564
paul@10 565
            # Get a real page, not a result page.
paul@10 566
paul@67 567
            event_page = EventPage(Page(request, pagename))
paul@10 568
paul@69 569
            # Get all events described in the page.
paul@10 570
paul@69 571
            for event in event_page.getEvents():
paul@69 572
                event_details = event.getDetails()
paul@10 573
paul@69 574
                # Remember the event.
paul@69 575
paul@69 576
                events.append(event)
paul@10 577
paul@69 578
                # Test for the suitability of the event.
paul@69 579
paul@69 580
                if event_details.has_key("start") and event_details.has_key("end"):
paul@10 581
paul@69 582
                    start_month = event_details["start"].as_month()
paul@69 583
                    end_month = event_details["end"].as_month()
paul@10 584
paul@69 585
                    # Compare the months of the dates to the requested calendar
paul@69 586
                    # window, if any.
paul@10 587
paul@69 588
                    if (calendar_start is None or end_month >= calendar_start) and \
paul@69 589
                        (calendar_end is None or start_month <= calendar_end):
paul@10 590
paul@69 591
                        all_shown_events.append(event)
paul@10 592
paul@69 593
                        if earliest is None or start_month < earliest:
paul@69 594
                            earliest = start_month
paul@69 595
                        if latest is None or end_month > latest:
paul@69 596
                            latest = end_month
paul@10 597
paul@69 598
                        # Store the event in the month-specific dictionary.
paul@10 599
paul@69 600
                        first = max(start_month, calendar_start or start_month)
paul@69 601
                        last = min(end_month, calendar_end or end_month)
paul@10 602
paul@69 603
                        for event_month in first.months_until(last):
paul@69 604
                            if not shown_events.has_key(event_month):
paul@69 605
                                shown_events[event_month] = []
paul@69 606
                            shown_events[event_month].append(event)
paul@10 607
paul@10 608
    return events, shown_events, all_shown_events, earliest, latest
paul@10 609
paul@29 610
def setEventTimestamps(request, events):
paul@29 611
paul@29 612
    """
paul@29 613
    Using 'request', set timestamp details in the details dictionary of each of
paul@67 614
    the 'events'.
paul@29 615
paul@29 616
    Retutn the latest timestamp found.
paul@29 617
    """
paul@29 618
paul@29 619
    latest = None
paul@29 620
paul@69 621
    for event in events:
paul@69 622
        event_details = event.getDetails()
paul@69 623
        event_page = event.getPage()
paul@29 624
paul@29 625
        # Get the initial revision of the page.
paul@29 626
paul@67 627
        revisions = event_page.getRevisions()
paul@67 628
        event_page_initial = Page(request, event_page.getPageName(), rev=revisions[-1])
paul@29 629
paul@29 630
        # Get the created and last modified times.
paul@29 631
paul@30 632
        initial_revision = getPageRevision(event_page_initial)
paul@30 633
        event_details["created"] = initial_revision["timestamp"]
paul@67 634
        latest_revision = event_page.getPageRevision()
paul@30 635
        event_details["last-modified"] = latest_revision["timestamp"]
paul@29 636
        event_details["sequence"] = len(revisions) - 1
paul@30 637
        event_details["last-comment"] = latest_revision["comment"]
paul@29 638
paul@29 639
        if latest is None or latest < event_details["last-modified"]:
paul@29 640
            latest = event_details["last-modified"]
paul@29 641
paul@29 642
    return latest
paul@29 643
paul@26 644
def getOrderedEvents(events):
paul@26 645
paul@26 646
    """
paul@26 647
    Return a list with the given 'events' ordered according to their start and
paul@67 648
    end dates.
paul@26 649
    """
paul@26 650
paul@26 651
    ordered_events = events[:]
paul@68 652
    ordered_events.sort()
paul@26 653
    return ordered_events
paul@26 654
paul@13 655
def getConcretePeriod(calendar_start, calendar_end, earliest, latest):
paul@13 656
paul@13 657
    """
paul@13 658
    From the requested 'calendar_start' and 'calendar_end', which may be None,
paul@13 659
    indicating that no restriction is imposed on the period for each of the
paul@13 660
    boundaries, use the 'earliest' and 'latest' event months to define a
paul@13 661
    specific period of interest.
paul@13 662
    """
paul@13 663
paul@13 664
    # Define the period as starting with any specified start month or the
paul@13 665
    # earliest event known, ending with any specified end month or the latest
paul@13 666
    # event known.
paul@13 667
paul@13 668
    first = calendar_start or earliest
paul@13 669
    last = calendar_end or latest
paul@13 670
paul@13 671
    # If there is no range of months to show, perhaps because there are no
paul@13 672
    # events in the requested period, and there was no start or end month
paul@13 673
    # specified, show only the month indicated by the start or end of the
paul@13 674
    # requested period. If all events were to be shown but none were found show
paul@13 675
    # the current month.
paul@13 676
paul@13 677
    if first is None:
paul@13 678
        first = last or getCurrentMonth()
paul@13 679
    if last is None:
paul@13 680
        last = first or getCurrentMonth()
paul@13 681
paul@13 682
    # Permit "expiring" periods (where the start date approaches the end date).
paul@13 683
paul@13 684
    return min(first, last), last
paul@13 685
paul@15 686
def getCoverage(start, end, events):
paul@15 687
paul@15 688
    """
paul@15 689
    Within the period defined by the 'start' and 'end' dates, determine the
paul@15 690
    coverage of the days in the period by the given 'events', returning a set of
paul@15 691
    covered days, along with a list of slots, where each slot contains a tuple
paul@15 692
    of the form (set of covered days, events).
paul@15 693
    """
paul@15 694
paul@15 695
    all_events = []
paul@15 696
    full_coverage = set()
paul@15 697
paul@15 698
    # Get event details.
paul@15 699
paul@69 700
    for event in events:
paul@69 701
        event_details = event.getDetails()
paul@15 702
paul@15 703
        # Test for the event in the period.
paul@15 704
paul@15 705
        if event_details["start"] <= end and event_details["end"] >= start:
paul@15 706
paul@15 707
            # Find the coverage of this period for the event.
paul@15 708
paul@15 709
            event_start = max(event_details["start"], start)
paul@15 710
            event_end = min(event_details["end"], end)
paul@67 711
            event_coverage = set(event_start.days_until(event_end))
paul@15 712
paul@15 713
            # Update the overall coverage.
paul@15 714
paul@15 715
            full_coverage.update(event_coverage)
paul@15 716
paul@15 717
            # Try and fit the event into the events list.
paul@15 718
paul@15 719
            for i, (coverage, covered_events) in enumerate(all_events):
paul@15 720
paul@15 721
                # Where the event does not overlap with the current
paul@15 722
                # element, add it alongside existing events.
paul@15 723
paul@15 724
                if not coverage.intersection(event_coverage):
paul@69 725
                    covered_events.append(event)
paul@15 726
                    all_events[i] = coverage.union(event_coverage), covered_events
paul@15 727
                    break
paul@15 728
paul@15 729
            # Make a new element in the list if the event cannot be
paul@15 730
            # marked alongside existing events.
paul@15 731
paul@15 732
            else:
paul@69 733
                all_events.append((event_coverage, [event]))
paul@15 734
paul@15 735
    return full_coverage, all_events
paul@15 736
paul@67 737
# Date-related functions.
paul@67 738
paul@67 739
class Period:
paul@67 740
paul@67 741
    "A simple period of time."
paul@67 742
paul@67 743
    def __init__(self, data):
paul@67 744
        self.data = data
paul@67 745
paul@67 746
    def months(self):
paul@67 747
        return self.data[0] * 12 + self.data[1]
paul@67 748
paul@67 749
class Month:
paul@67 750
paul@67 751
    "A simple year-month representation."
paul@67 752
paul@67 753
    def __init__(self, data):
paul@67 754
        self.data = tuple(data)
paul@67 755
paul@67 756
    def __repr__(self):
paul@67 757
        return "%s(%r)" % (self.__class__.__name__, self.data)
paul@67 758
paul@67 759
    def __str__(self):
paul@67 760
        return "%04d-%02d" % self.as_tuple()[:2]
paul@67 761
paul@67 762
    def __hash__(self):
paul@67 763
        return hash(self.as_tuple())
paul@67 764
paul@67 765
    def as_tuple(self):
paul@67 766
        return self.data
paul@67 767
paul@67 768
    def as_date(self, day):
paul@67 769
        return Date(self.as_tuple() + (day,))
paul@67 770
paul@67 771
    def year(self):
paul@67 772
        return self.data[0]
paul@67 773
paul@67 774
    def month(self):
paul@67 775
        return self.data[1]
paul@67 776
paul@67 777
    def month_properties(self):
paul@67 778
paul@67 779
        """
paul@67 780
        Return the weekday of the 1st of the month, along with the number of
paul@67 781
        days, as a tuple.
paul@67 782
        """
paul@67 783
paul@67 784
        year, month = self.data
paul@67 785
        return calendar.monthrange(year, month)
paul@67 786
paul@67 787
    def month_update(self, n=1):
paul@67 788
paul@67 789
        "Return the month updated by 'n' months."
paul@67 790
paul@67 791
        year, month = self.data
paul@67 792
        return Month((year + (month - 1 + n) / 12, (month - 1 + n) % 12 + 1))
paul@67 793
paul@67 794
    def next_month(self):
paul@67 795
paul@67 796
        "Return the month following this one."
paul@67 797
paul@67 798
        return self.month_update(1)
paul@67 799
paul@67 800
    def previous_month(self):
paul@67 801
paul@67 802
        "Return the month preceding this one."
paul@67 803
paul@67 804
        return self.month_update(-1)
paul@67 805
paul@67 806
    def __sub__(self, start):
paul@67 807
paul@67 808
        """
paul@67 809
        Return the difference in years and months between this month and the
paul@67 810
        'start' month as a period.
paul@67 811
        """
paul@67 812
paul@67 813
        return Period([(x - y) for x, y in zip(self.data, start.data)])
paul@67 814
paul@67 815
    def __cmp__(self, other):
paul@67 816
        return cmp(self.data, other.data)
paul@67 817
paul@67 818
    def until(self, end, nextfn, prevfn):
paul@67 819
        month = self
paul@67 820
        months = [month]
paul@67 821
        if month < end:
paul@67 822
            while month < end:
paul@67 823
                month = nextfn(month)
paul@67 824
                months.append(month)
paul@67 825
        elif month > end:
paul@67 826
            while month > end:
paul@67 827
                month = prevfn(month)
paul@67 828
                months.append(month)
paul@67 829
        return months
paul@67 830
paul@67 831
    def months_until(self, end):
paul@67 832
        return self.until(end, Month.next_month, Month.previous_month)
paul@67 833
paul@67 834
class Date(Month):
paul@67 835
paul@67 836
    "A simple year-month-day representation."
paul@67 837
paul@67 838
    def __str__(self):
paul@67 839
        return "%04d-%02d-%02d" % self.as_tuple()[:3]
paul@67 840
paul@67 841
    def as_month(self):
paul@67 842
        return Month(self.data[:2])
paul@67 843
paul@67 844
    def day(self):
paul@67 845
        return self.data[2]
paul@67 846
paul@67 847
    def next_day(self):
paul@67 848
paul@67 849
        "Return the date following this one."
paul@67 850
paul@67 851
        year, month, day = self.data
paul@67 852
        _wd, end_day = calendar.monthrange(year, month)
paul@67 853
        if day == end_day:
paul@67 854
            if month == 12:
paul@67 855
                return Date((year + 1, 1, 1))
paul@67 856
            else:
paul@67 857
                return Date((year, month + 1, 1))
paul@67 858
        else:
paul@67 859
            return Date((year, month, day + 1))
paul@67 860
paul@67 861
    def previous_day(self):
paul@67 862
paul@67 863
        "Return the date preceding this one."
paul@67 864
paul@67 865
        year, month, day = self.data
paul@67 866
        if day == 1:
paul@67 867
            if month == 1:
paul@67 868
                return Date((year - 1, 12, 31))
paul@67 869
            else:
paul@67 870
                _wd, end_day = calendar.monthrange(year, month - 1)
paul@67 871
                return Date((year, month - 1, end_day))
paul@67 872
        else:
paul@67 873
            return Date((year, month, day - 1))
paul@67 874
paul@67 875
    def days_until(self, end):
paul@67 876
        return self.until(end, Date.next_day, Date.previous_day)
paul@67 877
paul@67 878
def getDate(s):
paul@67 879
paul@67 880
    "Parse the string 's', extracting and returning a date string."
paul@67 881
paul@67 882
    m = date_regexp.search(s)
paul@67 883
    if m:
paul@67 884
        return Date(map(int, m.groups()))
paul@67 885
    else:
paul@67 886
        return None
paul@67 887
paul@67 888
def getMonth(s):
paul@67 889
paul@67 890
    "Parse the string 's', extracting and returning a month string."
paul@67 891
paul@67 892
    m = month_regexp.search(s)
paul@67 893
    if m:
paul@67 894
        return Month(map(int, m.groups()))
paul@67 895
    else:
paul@67 896
        return None
paul@67 897
paul@67 898
def getCurrentMonth():
paul@67 899
paul@67 900
    "Return the current month as a (year, month) tuple."
paul@67 901
paul@67 902
    today = datetime.date.today()
paul@67 903
    return Month((today.year, today.month))
paul@67 904
paul@67 905
def getCurrentYear():
paul@67 906
paul@67 907
    "Return the current year."
paul@67 908
paul@67 909
    today = datetime.date.today()
paul@67 910
    return today.year
paul@67 911
paul@19 912
# User interface functions.
paul@19 913
paul@55 914
def getParameter(request, name, default=None):
paul@55 915
    return request.form.get(name, [default])[0]
paul@23 916
paul@58 917
def getQualifiedParameter(request, calendar_name, argname, default=None):
paul@58 918
    argname = getQualifiedParameterName(calendar_name, argname)
paul@58 919
    return getParameter(request, argname, default)
paul@58 920
paul@58 921
def getQualifiedParameterName(calendar_name, argname):
paul@58 922
    if calendar_name is None:
paul@58 923
        return argname
paul@58 924
    else:
paul@58 925
        return "%s-%s" % (calendar_name, argname)
paul@58 926
paul@19 927
def getParameterMonth(arg):
paul@67 928
paul@67 929
    "Interpret 'arg', recognising keywords and simple arithmetic operations."
paul@67 930
paul@19 931
    n = None
paul@19 932
paul@19 933
    if arg.startswith("current"):
paul@19 934
        date = getCurrentMonth()
paul@19 935
        if len(arg) > 8:
paul@19 936
            n = int(arg[7:])
paul@19 937
paul@19 938
    elif arg.startswith("yearstart"):
paul@67 939
        date = Month((getCurrentYear(), 1))
paul@19 940
        if len(arg) > 10:
paul@19 941
            n = int(arg[9:])
paul@19 942
paul@19 943
    elif arg.startswith("yearend"):
paul@67 944
        date = Month((getCurrentYear(), 12))
paul@19 945
        if len(arg) > 8:
paul@19 946
            n = int(arg[7:])
paul@19 947
paul@19 948
    else:
paul@19 949
        date = getMonth(arg)
paul@19 950
paul@19 951
    if n is not None:
paul@67 952
        date = date.month_update(n)
paul@19 953
paul@19 954
    return date
paul@19 955
paul@19 956
def getFormMonth(request, calendar_name, argname):
paul@67 957
paul@67 958
    """
paul@67 959
    Return the month from the 'request' for the calendar with the given
paul@67 960
    'calendar_name' using the parameter having the given 'argname'.
paul@67 961
    """
paul@67 962
paul@58 963
    arg = getQualifiedParameter(request, calendar_name, argname)
paul@19 964
    if arg is not None:
paul@19 965
        return getParameterMonth(arg)
paul@19 966
    else:
paul@19 967
        return None
paul@19 968
paul@23 969
def getFormMonthPair(request, yeararg, montharg):
paul@67 970
paul@67 971
    """
paul@67 972
    Return the month from the 'request' for the calendar with the given
paul@67 973
    'calendar_name' using the parameters having the given 'yeararg' and
paul@67 974
    'montharg' names.
paul@67 975
    """
paul@67 976
paul@23 977
    year = getParameter(request, yeararg)
paul@23 978
    month = getParameter(request, montharg)
paul@23 979
    if year and month:
paul@67 980
        return Month((int(year), int(month)))
paul@23 981
    else:
paul@23 982
        return None
paul@23 983
paul@67 984
# Page-related functions.
paul@67 985
paul@19 986
def getPrettyPageName(page):
paul@19 987
paul@19 988
    "Return a nicely formatted title/name for the given 'page'."
paul@19 989
paul@27 990
    if isMoin15():
paul@27 991
        title = page.split_title(page.request, force=1)
paul@27 992
    else:
paul@27 993
        title = page.split_title(force=1)
paul@27 994
paul@55 995
    return getPrettyTitle(title)
paul@55 996
paul@27 997
def linkToPage(request, page, text, query_string=None):
paul@27 998
paul@27 999
    """
paul@27 1000
    Using 'request', return a link to 'page' with the given link 'text' and
paul@27 1001
    optional 'query_string'.
paul@27 1002
    """
paul@27 1003
paul@27 1004
    text = wikiutil.escape(text)
paul@27 1005
paul@27 1006
    if isMoin15():
paul@27 1007
        url = wikiutil.quoteWikinameURL(page.page_name)
paul@27 1008
        if query_string is not None:
paul@27 1009
            url = "%s?%s" % (url, query_string)
paul@27 1010
        return wikiutil.link_tag(request, url, text, getattr(page, "formatter", None))
paul@27 1011
    else:
paul@27 1012
        return page.link_to_raw(request, text, query_string)
paul@27 1013
paul@10 1014
# vim: tabstop=4 expandtab shiftwidth=4