EventAggregator

Annotated EventAggregatorSupport.py

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