paul@0 | 1 | # -*- coding: iso-8859-1 -*- |
paul@0 | 2 | """ |
paul@0 | 3 | MoinMoin - MoinSupport library (derived from EventAggregatorSupport) |
paul@0 | 4 | |
paul@0 | 5 | @copyright: 2008, 2009, 2010, 2011, 2012 by Paul Boddie <paul@boddie.org.uk> |
paul@0 | 6 | @copyright: 2000-2004 Juergen Hermann <jh@web.de>, |
paul@0 | 7 | 2005-2008 MoinMoin:ThomasWaldmann. |
paul@0 | 8 | @license: GNU GPL (v2 or later), see COPYING.txt for details. |
paul@0 | 9 | """ |
paul@0 | 10 | |
paul@0 | 11 | from DateSupport import * |
paul@1 | 12 | from MoinMoin.Page import Page |
paul@0 | 13 | from MoinMoin import wikiutil |
paul@10 | 14 | from StringIO import StringIO |
paul@10 | 15 | from shlex import shlex |
paul@0 | 16 | import re |
paul@0 | 17 | import time |
paul@0 | 18 | |
paul@10 | 19 | __version__ = "0.2" |
paul@0 | 20 | |
paul@0 | 21 | # Content type parsing. |
paul@0 | 22 | |
paul@0 | 23 | encoding_regexp_str = ur'(?P<content_type>[^\s;]*)(?:;\s*charset=(?P<encoding>[-A-Za-z0-9]+))?' |
paul@0 | 24 | encoding_regexp = re.compile(encoding_regexp_str) |
paul@0 | 25 | |
paul@2 | 26 | # Accept header parsing. |
paul@2 | 27 | |
paul@2 | 28 | accept_regexp_str = ur';\s*q=' |
paul@2 | 29 | accept_regexp = re.compile(accept_regexp_str) |
paul@2 | 30 | |
paul@0 | 31 | # Utility functions. |
paul@0 | 32 | |
paul@0 | 33 | def getContentTypeAndEncoding(content_type): |
paul@2 | 34 | |
paul@2 | 35 | """ |
paul@2 | 36 | Return a tuple with the content/media type and encoding, extracted from the |
paul@2 | 37 | given 'content_type' header value. |
paul@2 | 38 | """ |
paul@2 | 39 | |
paul@0 | 40 | m = encoding_regexp.search(content_type) |
paul@0 | 41 | if m: |
paul@0 | 42 | return m.group("content_type"), m.group("encoding") |
paul@0 | 43 | else: |
paul@0 | 44 | return None, None |
paul@0 | 45 | |
paul@0 | 46 | def int_or_none(x): |
paul@0 | 47 | if x is None: |
paul@0 | 48 | return x |
paul@0 | 49 | else: |
paul@0 | 50 | return int(x) |
paul@0 | 51 | |
paul@10 | 52 | def parseAttributes(s, escape=True): |
paul@10 | 53 | |
paul@10 | 54 | """ |
paul@10 | 55 | Parse the section attributes string 's', returning a mapping of names to |
paul@10 | 56 | values. If 'escape' is set to a true value, the attributes will be suitable |
paul@10 | 57 | for use with the formatter API. If 'escape' is set to a false value, the |
paul@10 | 58 | attributes will have any quoting removed. |
paul@10 | 59 | """ |
paul@10 | 60 | |
paul@10 | 61 | attrs = {} |
paul@10 | 62 | f = StringIO(s) |
paul@10 | 63 | name = None |
paul@10 | 64 | need_value = False |
paul@10 | 65 | |
paul@10 | 66 | for token in shlex(f): |
paul@10 | 67 | |
paul@10 | 68 | # Capture the name if needed. |
paul@10 | 69 | |
paul@10 | 70 | if name is None: |
paul@10 | 71 | name = escape and wikiutil.escape(token) or strip_token(token) |
paul@10 | 72 | |
paul@10 | 73 | # Detect either an equals sign or another name. |
paul@10 | 74 | |
paul@10 | 75 | elif not need_value: |
paul@10 | 76 | if token == "=": |
paul@10 | 77 | need_value = True |
paul@10 | 78 | else: |
paul@10 | 79 | attrs[name.lower()] = escape and "true" or True |
paul@10 | 80 | name = wikiutil.escape(token) |
paul@10 | 81 | |
paul@10 | 82 | # Otherwise, capture a value. |
paul@10 | 83 | |
paul@10 | 84 | else: |
paul@10 | 85 | # Quoting of attributes done similarly to wikiutil.parseAttributes. |
paul@10 | 86 | |
paul@10 | 87 | if token: |
paul@10 | 88 | if escape: |
paul@10 | 89 | if token[0] in ("'", '"'): |
paul@10 | 90 | token = wikiutil.escape(token) |
paul@10 | 91 | else: |
paul@10 | 92 | token = '"%s"' % wikiutil.escape(token, 1) |
paul@10 | 93 | else: |
paul@10 | 94 | token = strip_token(token) |
paul@10 | 95 | |
paul@10 | 96 | attrs[name.lower()] = token |
paul@10 | 97 | name = None |
paul@10 | 98 | need_value = False |
paul@10 | 99 | |
paul@13 | 100 | # Handle any name-only attributes at the end of the collection. |
paul@13 | 101 | |
paul@13 | 102 | if name and not need_value: |
paul@13 | 103 | attrs[name.lower()] = escape and "true" or True |
paul@13 | 104 | |
paul@10 | 105 | return attrs |
paul@10 | 106 | |
paul@10 | 107 | def strip_token(token): |
paul@10 | 108 | |
paul@10 | 109 | "Return the given 'token' stripped of quoting." |
paul@10 | 110 | |
paul@10 | 111 | if token[0] in ("'", '"') and token[-1] == token[0]: |
paul@10 | 112 | return token[1:-1] |
paul@10 | 113 | else: |
paul@10 | 114 | return token |
paul@10 | 115 | |
paul@0 | 116 | # Utility classes and associated functions. |
paul@0 | 117 | |
paul@0 | 118 | class Form: |
paul@0 | 119 | |
paul@0 | 120 | """ |
paul@0 | 121 | A wrapper preserving MoinMoin 1.8.x (and earlier) behaviour in a 1.9.x |
paul@0 | 122 | environment. |
paul@0 | 123 | """ |
paul@0 | 124 | |
paul@0 | 125 | def __init__(self, form): |
paul@0 | 126 | self.form = form |
paul@0 | 127 | |
paul@0 | 128 | def has_key(self, name): |
paul@0 | 129 | return not not self.form.getlist(name) |
paul@0 | 130 | |
paul@0 | 131 | def get(self, name, default=None): |
paul@0 | 132 | values = self.form.getlist(name) |
paul@0 | 133 | if not values: |
paul@0 | 134 | return default |
paul@0 | 135 | else: |
paul@0 | 136 | return values |
paul@0 | 137 | |
paul@0 | 138 | def __getitem__(self, name): |
paul@0 | 139 | return self.form.getlist(name) |
paul@0 | 140 | |
paul@0 | 141 | class ActionSupport: |
paul@0 | 142 | |
paul@0 | 143 | """ |
paul@0 | 144 | Work around disruptive MoinMoin changes in 1.9, and also provide useful |
paul@0 | 145 | convenience methods. |
paul@0 | 146 | """ |
paul@0 | 147 | |
paul@0 | 148 | def get_form(self): |
paul@0 | 149 | return get_form(self.request) |
paul@0 | 150 | |
paul@0 | 151 | def _get_selected(self, value, input_value): |
paul@0 | 152 | |
paul@0 | 153 | """ |
paul@0 | 154 | Return the HTML attribute text indicating selection of an option (or |
paul@0 | 155 | otherwise) if 'value' matches 'input_value'. |
paul@0 | 156 | """ |
paul@0 | 157 | |
paul@0 | 158 | return input_value is not None and value == input_value and 'selected="selected"' or '' |
paul@0 | 159 | |
paul@0 | 160 | def _get_selected_for_list(self, value, input_values): |
paul@0 | 161 | |
paul@0 | 162 | """ |
paul@0 | 163 | Return the HTML attribute text indicating selection of an option (or |
paul@0 | 164 | otherwise) if 'value' matches one of the 'input_values'. |
paul@0 | 165 | """ |
paul@0 | 166 | |
paul@0 | 167 | return value in input_values and 'selected="selected"' or '' |
paul@0 | 168 | |
paul@0 | 169 | def _get_input(self, form, name, default=None): |
paul@0 | 170 | |
paul@0 | 171 | """ |
paul@0 | 172 | Return the input from 'form' having the given 'name', returning either |
paul@0 | 173 | the input converted to an integer or the given 'default' (optional, None |
paul@0 | 174 | if not specified). |
paul@0 | 175 | """ |
paul@0 | 176 | |
paul@0 | 177 | value = form.get(name, [None])[0] |
paul@0 | 178 | if not value: # true if 0 obtained |
paul@0 | 179 | return default |
paul@0 | 180 | else: |
paul@0 | 181 | return int(value) |
paul@0 | 182 | |
paul@0 | 183 | def get_form(request): |
paul@0 | 184 | |
paul@0 | 185 | "Work around disruptive MoinMoin changes in 1.9." |
paul@0 | 186 | |
paul@0 | 187 | if hasattr(request, "values"): |
paul@0 | 188 | return Form(request.values) |
paul@0 | 189 | else: |
paul@0 | 190 | return request.form |
paul@0 | 191 | |
paul@0 | 192 | class send_headers_cls: |
paul@0 | 193 | |
paul@0 | 194 | """ |
paul@0 | 195 | A wrapper to preserve MoinMoin 1.8.x (and earlier) request behaviour in a |
paul@0 | 196 | 1.9.x environment. |
paul@0 | 197 | """ |
paul@0 | 198 | |
paul@0 | 199 | def __init__(self, request): |
paul@0 | 200 | self.request = request |
paul@0 | 201 | |
paul@0 | 202 | def __call__(self, headers): |
paul@0 | 203 | for header in headers: |
paul@0 | 204 | parts = header.split(":") |
paul@0 | 205 | self.request.headers.add(parts[0], ":".join(parts[1:])) |
paul@0 | 206 | |
paul@0 | 207 | def get_send_headers(request): |
paul@0 | 208 | |
paul@0 | 209 | "Return a function that can send response headers." |
paul@0 | 210 | |
paul@0 | 211 | if hasattr(request, "http_headers"): |
paul@0 | 212 | return request.http_headers |
paul@0 | 213 | elif hasattr(request, "emit_http_headers"): |
paul@0 | 214 | return request.emit_http_headers |
paul@0 | 215 | else: |
paul@0 | 216 | return send_headers_cls(request) |
paul@0 | 217 | |
paul@0 | 218 | def escattr(s): |
paul@0 | 219 | return wikiutil.escape(s, 1) |
paul@0 | 220 | |
paul@0 | 221 | def getPathInfo(request): |
paul@0 | 222 | if hasattr(request, "getPathinfo"): |
paul@0 | 223 | return request.getPathinfo() |
paul@0 | 224 | else: |
paul@0 | 225 | return request.path |
paul@0 | 226 | |
paul@14 | 227 | def getHeader(request, header_name, prefix=None): |
paul@14 | 228 | if hasattr(request, "getHeader"): |
paul@14 | 229 | return request.getHeader(header_name) |
paul@14 | 230 | elif hasattr(request, "headers"): |
paul@14 | 231 | return request.headers[header_name] |
paul@14 | 232 | else: |
paul@14 | 233 | return request.env[(prefix and prefix + "_" or "") + header_name.upper()] |
paul@14 | 234 | |
paul@2 | 235 | # Content/media type and preferences support. |
paul@2 | 236 | |
paul@2 | 237 | class MediaRange: |
paul@2 | 238 | |
paul@2 | 239 | "A content/media type value which supports whole categories of data." |
paul@2 | 240 | |
paul@2 | 241 | def __init__(self, media_range, accept_parameters=None): |
paul@2 | 242 | self.media_range = media_range |
paul@2 | 243 | self.accept_parameters = accept_parameters or {} |
paul@2 | 244 | |
paul@2 | 245 | parts = media_range.split(";") |
paul@2 | 246 | self.media_type = parts[0] |
paul@2 | 247 | self.parameters = getMappingFromParameterStrings(parts[1:]) |
paul@2 | 248 | |
paul@2 | 249 | # The media type is divided into category and subcategory. |
paul@2 | 250 | |
paul@2 | 251 | parts = self.media_type.split("/") |
paul@2 | 252 | self.category = parts[0] |
paul@2 | 253 | self.subcategory = "/".join(parts[1:]) |
paul@2 | 254 | |
paul@2 | 255 | def get_parts(self): |
paul@3 | 256 | |
paul@3 | 257 | "Return the category, subcategory parts." |
paul@3 | 258 | |
paul@2 | 259 | return self.category, self.subcategory |
paul@2 | 260 | |
paul@2 | 261 | def get_specificity(self): |
paul@3 | 262 | |
paul@3 | 263 | """ |
paul@3 | 264 | Return the specificity of the media type in terms of the scope of the |
paul@3 | 265 | category and subcategory, and also in terms of any qualifying |
paul@3 | 266 | parameters. |
paul@3 | 267 | """ |
paul@3 | 268 | |
paul@2 | 269 | if "*" in self.get_parts(): |
paul@2 | 270 | return -list(self.get_parts()).count("*") |
paul@2 | 271 | else: |
paul@2 | 272 | return len(self.parameters) |
paul@2 | 273 | |
paul@2 | 274 | def permits(self, other): |
paul@3 | 275 | |
paul@3 | 276 | """ |
paul@3 | 277 | Return whether this media type permits the use of the 'other' media type |
paul@3 | 278 | if suggested as suitable content. |
paul@3 | 279 | """ |
paul@3 | 280 | |
paul@2 | 281 | if not isinstance(other, MediaRange): |
paul@2 | 282 | other = MediaRange(other) |
paul@2 | 283 | |
paul@2 | 284 | category = categoryPermits(self.category, other.category) |
paul@2 | 285 | subcategory = categoryPermits(self.subcategory, other.subcategory) |
paul@2 | 286 | |
paul@2 | 287 | if category and subcategory: |
paul@2 | 288 | if "*" not in (category, subcategory): |
paul@2 | 289 | return not self.parameters or self.parameters == other.parameters |
paul@2 | 290 | else: |
paul@2 | 291 | return True |
paul@2 | 292 | else: |
paul@2 | 293 | return False |
paul@2 | 294 | |
paul@2 | 295 | def __eq__(self, other): |
paul@3 | 296 | |
paul@3 | 297 | """ |
paul@3 | 298 | Return whether this media type is effectively the same as the 'other' |
paul@3 | 299 | media type. |
paul@3 | 300 | """ |
paul@3 | 301 | |
paul@2 | 302 | if not isinstance(other, MediaRange): |
paul@2 | 303 | other = MediaRange(other) |
paul@2 | 304 | |
paul@2 | 305 | category = categoryMatches(self.category, other.category) |
paul@2 | 306 | subcategory = categoryMatches(self.subcategory, other.subcategory) |
paul@2 | 307 | |
paul@2 | 308 | if category and subcategory: |
paul@2 | 309 | if "*" not in (category, subcategory): |
paul@2 | 310 | return self.parameters == other.parameters or \ |
paul@2 | 311 | not self.parameters or not other.parameters |
paul@2 | 312 | else: |
paul@2 | 313 | return True |
paul@2 | 314 | else: |
paul@2 | 315 | return False |
paul@2 | 316 | |
paul@2 | 317 | def __ne__(self, other): |
paul@2 | 318 | return not self.__eq__(other) |
paul@2 | 319 | |
paul@2 | 320 | def __hash__(self): |
paul@2 | 321 | return hash(self.media_range) |
paul@2 | 322 | |
paul@2 | 323 | def __repr__(self): |
paul@2 | 324 | return "MediaRange(%r)" % self.media_range |
paul@2 | 325 | |
paul@2 | 326 | def categoryMatches(this, that): |
paul@2 | 327 | |
paul@2 | 328 | """ |
paul@2 | 329 | Return the basis of a match between 'this' and 'that' or False if the given |
paul@2 | 330 | categories do not match. |
paul@2 | 331 | """ |
paul@2 | 332 | |
paul@2 | 333 | return (this == "*" or this == that) and this or \ |
paul@2 | 334 | that == "*" and that or False |
paul@2 | 335 | |
paul@2 | 336 | def categoryPermits(this, that): |
paul@2 | 337 | |
paul@2 | 338 | """ |
paul@2 | 339 | Return whether 'this' category permits 'that' category. Where 'this' is a |
paul@2 | 340 | wildcard ("*"), 'that' should always match. A value of False is returned if |
paul@2 | 341 | the categories do not otherwise match. |
paul@2 | 342 | """ |
paul@2 | 343 | |
paul@2 | 344 | return (this == "*" or this == that) and this or False |
paul@2 | 345 | |
paul@2 | 346 | def getMappingFromParameterStrings(l): |
paul@2 | 347 | |
paul@2 | 348 | """ |
paul@2 | 349 | Return a mapping representing the list of "name=value" strings given by 'l'. |
paul@2 | 350 | """ |
paul@2 | 351 | |
paul@2 | 352 | parameters = {} |
paul@2 | 353 | |
paul@2 | 354 | for parameter in l: |
paul@2 | 355 | parts = parameter.split("=") |
paul@2 | 356 | name = parts[0].strip() |
paul@2 | 357 | value = "=".join(parts[1:]).strip() |
paul@2 | 358 | parameters[name] = value |
paul@2 | 359 | |
paul@2 | 360 | return parameters |
paul@2 | 361 | |
paul@2 | 362 | def getContentPreferences(accept): |
paul@2 | 363 | |
paul@2 | 364 | """ |
paul@2 | 365 | Return a mapping from media types to parameters for content/media types |
paul@2 | 366 | extracted from the given 'accept' header value. The mapping is returned in |
paul@2 | 367 | the form of a list of (media type, parameters) tuples. |
paul@2 | 368 | |
paul@2 | 369 | See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 |
paul@2 | 370 | """ |
paul@2 | 371 | |
paul@2 | 372 | preferences = [] |
paul@2 | 373 | |
paul@2 | 374 | for field in accept.split(","): |
paul@2 | 375 | |
paul@2 | 376 | # The media type with parameters (defined by the "media-range") is |
paul@2 | 377 | # separated from any other parameters (defined as "accept-extension" |
paul@2 | 378 | # parameters) by a quality parameter. |
paul@2 | 379 | |
paul@2 | 380 | fparts = accept_regexp.split(field) |
paul@2 | 381 | |
paul@2 | 382 | # The first part is always the media type. |
paul@2 | 383 | |
paul@2 | 384 | media_type = fparts[0].strip() |
paul@2 | 385 | |
paul@2 | 386 | # Any other parts can be interpreted as extension parameters. |
paul@2 | 387 | |
paul@2 | 388 | if len(fparts) > 1: |
paul@2 | 389 | fparts = ("q=" + ";q=".join(fparts[1:])).split(";") |
paul@2 | 390 | else: |
paul@2 | 391 | fparts = [] |
paul@2 | 392 | |
paul@2 | 393 | # Each field in the preferences can incorporate parameters separated by |
paul@2 | 394 | # semicolon characters. |
paul@2 | 395 | |
paul@2 | 396 | parameters = getMappingFromParameterStrings(fparts) |
paul@2 | 397 | media_range = MediaRange(media_type, parameters) |
paul@2 | 398 | preferences.append(media_range) |
paul@2 | 399 | |
paul@2 | 400 | return ContentPreferences(preferences) |
paul@2 | 401 | |
paul@2 | 402 | class ContentPreferences: |
paul@2 | 403 | |
paul@2 | 404 | "A wrapper around content preference information." |
paul@2 | 405 | |
paul@2 | 406 | def __init__(self, preferences): |
paul@2 | 407 | self.preferences = preferences |
paul@2 | 408 | |
paul@2 | 409 | def __iter__(self): |
paul@2 | 410 | return iter(self.preferences) |
paul@2 | 411 | |
paul@2 | 412 | def get_ordered(self, by_quality=0): |
paul@2 | 413 | |
paul@2 | 414 | """ |
paul@2 | 415 | Return a list of content/media types in descending order of preference. |
paul@2 | 416 | If 'by_quality' is set to a true value, the "q" value will be used as |
paul@2 | 417 | the primary measure of preference; otherwise, only the specificity will |
paul@2 | 418 | be considered. |
paul@2 | 419 | """ |
paul@2 | 420 | |
paul@2 | 421 | ordered = {} |
paul@2 | 422 | |
paul@2 | 423 | for media_range in self.preferences: |
paul@2 | 424 | specificity = media_range.get_specificity() |
paul@2 | 425 | |
paul@2 | 426 | if by_quality: |
paul@2 | 427 | q = float(media_range.accept_parameters.get("q", "1")) |
paul@2 | 428 | key = q, specificity |
paul@2 | 429 | else: |
paul@2 | 430 | key = specificity |
paul@2 | 431 | |
paul@2 | 432 | if not ordered.has_key(key): |
paul@2 | 433 | ordered[key] = [] |
paul@2 | 434 | |
paul@2 | 435 | ordered[key].append(media_range) |
paul@2 | 436 | |
paul@2 | 437 | # Return the preferences in descending order of quality and specificity. |
paul@2 | 438 | |
paul@2 | 439 | keys = ordered.keys() |
paul@2 | 440 | keys.sort(reverse=True) |
paul@2 | 441 | return [ordered[key] for key in keys] |
paul@2 | 442 | |
paul@14 | 443 | def get_acceptable_types(self, available): |
paul@2 | 444 | |
paul@2 | 445 | """ |
paul@14 | 446 | Return content/media types from those in the 'available' list supported |
paul@14 | 447 | by the known preferences grouped by preference level in descending order |
paul@14 | 448 | of preference. |
paul@2 | 449 | """ |
paul@2 | 450 | |
paul@2 | 451 | matches = {} |
paul@2 | 452 | available = set(available[:]) |
paul@2 | 453 | |
paul@2 | 454 | for level in self.get_ordered(): |
paul@2 | 455 | for media_range in level: |
paul@2 | 456 | |
paul@2 | 457 | # Attempt to match available types. |
paul@2 | 458 | |
paul@2 | 459 | found = set() |
paul@2 | 460 | for available_type in available: |
paul@2 | 461 | if media_range.permits(available_type): |
paul@2 | 462 | q = float(media_range.accept_parameters.get("q", "1")) |
paul@2 | 463 | if not matches.has_key(q): |
paul@2 | 464 | matches[q] = [] |
paul@2 | 465 | matches[q].append(available_type) |
paul@2 | 466 | found.add(available_type) |
paul@2 | 467 | |
paul@2 | 468 | # Stop looking for matches for matched available types. |
paul@2 | 469 | |
paul@2 | 470 | if found: |
paul@2 | 471 | available.difference_update(found) |
paul@2 | 472 | |
paul@2 | 473 | # Sort the matches in descending order of quality. |
paul@2 | 474 | |
paul@2 | 475 | all_q = matches.keys() |
paul@2 | 476 | |
paul@2 | 477 | if all_q: |
paul@2 | 478 | all_q.sort(reverse=True) |
paul@14 | 479 | return [matches[q] for q in all_q] |
paul@2 | 480 | else: |
paul@14 | 481 | return [] |
paul@14 | 482 | |
paul@14 | 483 | def get_preferred_types(self, available): |
paul@14 | 484 | |
paul@14 | 485 | """ |
paul@14 | 486 | Return the preferred content/media types from those in the 'available' |
paul@14 | 487 | list, given the known preferences. |
paul@14 | 488 | """ |
paul@14 | 489 | |
paul@14 | 490 | preferred = self.get_acceptable_types(available) |
paul@14 | 491 | if preferred: |
paul@14 | 492 | return preferred[0] |
paul@14 | 493 | else: |
paul@14 | 494 | return [] |
paul@2 | 495 | |
paul@1 | 496 | # Page access functions. |
paul@1 | 497 | |
paul@1 | 498 | def getPageURL(page): |
paul@1 | 499 | |
paul@1 | 500 | "Return the URL of the given 'page'." |
paul@1 | 501 | |
paul@1 | 502 | request = page.request |
paul@1 | 503 | return request.getQualifiedURL(page.url(request, relative=0)) |
paul@1 | 504 | |
paul@1 | 505 | def getFormat(page): |
paul@1 | 506 | |
paul@1 | 507 | "Get the format used on the given 'page'." |
paul@1 | 508 | |
paul@1 | 509 | return page.pi["format"] |
paul@1 | 510 | |
paul@1 | 511 | def getMetadata(page): |
paul@1 | 512 | |
paul@1 | 513 | """ |
paul@1 | 514 | Return a dictionary containing items describing for the given 'page' the |
paul@1 | 515 | page's "created" time, "last-modified" time, "sequence" (or revision number) |
paul@1 | 516 | and the "last-comment" made about the last edit. |
paul@1 | 517 | """ |
paul@1 | 518 | |
paul@1 | 519 | request = page.request |
paul@1 | 520 | |
paul@1 | 521 | # Get the initial revision of the page. |
paul@1 | 522 | |
paul@1 | 523 | revisions = page.getRevList() |
paul@1 | 524 | event_page_initial = Page(request, page.page_name, rev=revisions[-1]) |
paul@1 | 525 | |
paul@1 | 526 | # Get the created and last modified times. |
paul@1 | 527 | |
paul@1 | 528 | initial_revision = getPageRevision(event_page_initial) |
paul@1 | 529 | |
paul@1 | 530 | metadata = {} |
paul@1 | 531 | metadata["created"] = initial_revision["timestamp"] |
paul@1 | 532 | latest_revision = getPageRevision(page) |
paul@1 | 533 | metadata["last-modified"] = latest_revision["timestamp"] |
paul@1 | 534 | metadata["sequence"] = len(revisions) - 1 |
paul@1 | 535 | metadata["last-comment"] = latest_revision["comment"] |
paul@1 | 536 | |
paul@1 | 537 | return metadata |
paul@0 | 538 | |
paul@0 | 539 | def getPageRevision(page): |
paul@0 | 540 | |
paul@0 | 541 | "Return the revision details dictionary for the given 'page'." |
paul@0 | 542 | |
paul@0 | 543 | # From Page.edit_info... |
paul@0 | 544 | |
paul@0 | 545 | if hasattr(page, "editlog_entry"): |
paul@0 | 546 | line = page.editlog_entry() |
paul@0 | 547 | else: |
paul@0 | 548 | line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x |
paul@0 | 549 | |
paul@0 | 550 | # Similar to Page.mtime_usecs behaviour... |
paul@0 | 551 | |
paul@0 | 552 | if line: |
paul@0 | 553 | timestamp = line.ed_time_usecs |
paul@0 | 554 | mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x |
paul@0 | 555 | comment = line.comment |
paul@0 | 556 | else: |
paul@0 | 557 | mtime = 0 |
paul@0 | 558 | comment = "" |
paul@0 | 559 | |
paul@0 | 560 | # Leave the time zone empty. |
paul@0 | 561 | |
paul@0 | 562 | return {"timestamp" : DateTime(time.gmtime(mtime)[:6] + (None,)), "comment" : comment} |
paul@0 | 563 | |
paul@11 | 564 | # Page parsing and formatting of embedded content. |
paul@11 | 565 | |
paul@15 | 566 | def getPageParserClass(request): |
paul@15 | 567 | |
paul@15 | 568 | "Using 'request', return a parser class for the current page's format." |
paul@15 | 569 | |
paul@15 | 570 | return getParserClass(request, request.page.pi["format"]) |
paul@15 | 571 | |
paul@11 | 572 | def getParserClass(request, format): |
paul@11 | 573 | |
paul@11 | 574 | """ |
paul@11 | 575 | Return a parser class using the 'request' for the given 'format', returning |
paul@11 | 576 | a plain text parser if no parser can be found for the specified 'format'. |
paul@11 | 577 | """ |
paul@11 | 578 | |
paul@11 | 579 | try: |
paul@11 | 580 | return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") |
paul@11 | 581 | except wikiutil.PluginMissingError: |
paul@11 | 582 | return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") |
paul@11 | 583 | |
paul@15 | 584 | def getFormatterClass(request, format): |
paul@15 | 585 | |
paul@15 | 586 | """ |
paul@15 | 587 | Return a formatter class using the 'request' for the given output 'format', |
paul@15 | 588 | returning a plain text formatter if no formatter can be found for the |
paul@15 | 589 | specified 'format'. |
paul@15 | 590 | """ |
paul@15 | 591 | |
paul@15 | 592 | try: |
paul@15 | 593 | return wikiutil.searchAndImportPlugin(request.cfg, "formatter", format or "plain") |
paul@15 | 594 | except wikiutil.PluginMissingError: |
paul@15 | 595 | return wikiutil.searchAndImportPlugin(request.cfg, "formatter", "plain") |
paul@15 | 596 | |
paul@15 | 597 | def formatText(text, request, fmt, parser_cls=None): |
paul@15 | 598 | |
paul@15 | 599 | """ |
paul@15 | 600 | Format the given 'text' using the specified 'request' and formatter 'fmt'. |
paul@15 | 601 | Suppress line anchors in the output, and fix lists by indicating that a |
paul@15 | 602 | paragraph has already been started. |
paul@15 | 603 | """ |
paul@15 | 604 | |
paul@15 | 605 | if not parser_cls: |
paul@15 | 606 | parser_cls = getPageParserClass(request) |
paul@15 | 607 | parser = parser_cls(text, request, line_anchors=False) |
paul@15 | 608 | |
paul@15 | 609 | old_fmt = request.formatter |
paul@15 | 610 | request.formatter = fmt |
paul@15 | 611 | try: |
paul@15 | 612 | return redirectedOutput(request, parser, fmt, inhibit_p=True) |
paul@15 | 613 | finally: |
paul@15 | 614 | request.formatter = old_fmt |
paul@15 | 615 | |
paul@11 | 616 | def redirectedOutput(request, parser, fmt, **kw): |
paul@11 | 617 | |
paul@11 | 618 | "A fixed version of the request method of the same name." |
paul@11 | 619 | |
paul@11 | 620 | buf = StringIO() |
paul@11 | 621 | request.redirect(buf) |
paul@11 | 622 | try: |
paul@11 | 623 | parser.format(fmt, **kw) |
paul@11 | 624 | if hasattr(fmt, "flush"): |
paul@11 | 625 | buf.write(fmt.flush(True)) |
paul@11 | 626 | finally: |
paul@11 | 627 | request.redirect() |
paul@11 | 628 | text = buf.getvalue() |
paul@11 | 629 | buf.close() |
paul@11 | 630 | return text |
paul@11 | 631 | |
paul@0 | 632 | # User interface functions. |
paul@0 | 633 | |
paul@0 | 634 | def getParameter(request, name, default=None): |
paul@0 | 635 | |
paul@0 | 636 | """ |
paul@0 | 637 | Using the given 'request', return the value of the parameter with the given |
paul@0 | 638 | 'name', returning the optional 'default' (or None) if no value was supplied |
paul@0 | 639 | in the 'request'. |
paul@0 | 640 | """ |
paul@0 | 641 | |
paul@0 | 642 | return get_form(request).get(name, [default])[0] |
paul@0 | 643 | |
paul@0 | 644 | def getQualifiedParameter(request, prefix, argname, default=None): |
paul@0 | 645 | |
paul@0 | 646 | """ |
paul@0 | 647 | Using the given 'request', 'prefix' and 'argname', retrieve the value of the |
paul@0 | 648 | qualified parameter, returning the optional 'default' (or None) if no value |
paul@0 | 649 | was supplied in the 'request'. |
paul@0 | 650 | """ |
paul@0 | 651 | |
paul@0 | 652 | argname = getQualifiedParameterName(prefix, argname) |
paul@0 | 653 | return getParameter(request, argname, default) |
paul@0 | 654 | |
paul@0 | 655 | def getQualifiedParameterName(prefix, argname): |
paul@0 | 656 | |
paul@0 | 657 | """ |
paul@0 | 658 | Return the qualified parameter name using the given 'prefix' and 'argname'. |
paul@0 | 659 | """ |
paul@0 | 660 | |
paul@0 | 661 | if prefix is None: |
paul@0 | 662 | return argname |
paul@0 | 663 | else: |
paul@0 | 664 | return "%s-%s" % (prefix, argname) |
paul@0 | 665 | |
paul@0 | 666 | # Page-related functions. |
paul@0 | 667 | |
paul@0 | 668 | def getPrettyPageName(page): |
paul@0 | 669 | |
paul@0 | 670 | "Return a nicely formatted title/name for the given 'page'." |
paul@0 | 671 | |
paul@0 | 672 | title = page.split_title(force=1) |
paul@0 | 673 | return getPrettyTitle(title) |
paul@0 | 674 | |
paul@3 | 675 | def linkToPage(request, page, text, query_string=None, **kw): |
paul@0 | 676 | |
paul@0 | 677 | """ |
paul@0 | 678 | Using 'request', return a link to 'page' with the given link 'text' and |
paul@0 | 679 | optional 'query_string'. |
paul@0 | 680 | """ |
paul@0 | 681 | |
paul@0 | 682 | text = wikiutil.escape(text) |
paul@3 | 683 | return page.link_to_raw(request, text, query_string, **kw) |
paul@0 | 684 | |
paul@0 | 685 | def linkToResource(url, request, text, query_string=None): |
paul@0 | 686 | |
paul@0 | 687 | """ |
paul@0 | 688 | Using 'request', return a link to 'url' with the given link 'text' and |
paul@0 | 689 | optional 'query_string'. |
paul@0 | 690 | """ |
paul@0 | 691 | |
paul@0 | 692 | if query_string: |
paul@0 | 693 | query_string = wikiutil.makeQueryString(query_string) |
paul@0 | 694 | url = "%s?%s" % (url, query_string) |
paul@0 | 695 | |
paul@0 | 696 | formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter |
paul@0 | 697 | |
paul@0 | 698 | output = [] |
paul@0 | 699 | output.append(formatter.url(1, url)) |
paul@0 | 700 | output.append(formatter.text(text)) |
paul@0 | 701 | output.append(formatter.url(0)) |
paul@0 | 702 | return "".join(output) |
paul@0 | 703 | |
paul@0 | 704 | def getFullPageName(parent, title): |
paul@0 | 705 | |
paul@0 | 706 | """ |
paul@0 | 707 | Return a full page name from the given 'parent' page (can be empty or None) |
paul@0 | 708 | and 'title' (a simple page name). |
paul@0 | 709 | """ |
paul@0 | 710 | |
paul@0 | 711 | if parent: |
paul@0 | 712 | return "%s/%s" % (parent.rstrip("/"), title) |
paul@0 | 713 | else: |
paul@0 | 714 | return title |
paul@0 | 715 | |
paul@0 | 716 | # vim: tabstop=4 expandtab shiftwidth=4 |