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@2 | 227 | # Content/media type and preferences support. |
paul@2 | 228 | |
paul@2 | 229 | class MediaRange: |
paul@2 | 230 | |
paul@2 | 231 | "A content/media type value which supports whole categories of data." |
paul@2 | 232 | |
paul@2 | 233 | def __init__(self, media_range, accept_parameters=None): |
paul@2 | 234 | self.media_range = media_range |
paul@2 | 235 | self.accept_parameters = accept_parameters or {} |
paul@2 | 236 | |
paul@2 | 237 | parts = media_range.split(";") |
paul@2 | 238 | self.media_type = parts[0] |
paul@2 | 239 | self.parameters = getMappingFromParameterStrings(parts[1:]) |
paul@2 | 240 | |
paul@2 | 241 | # The media type is divided into category and subcategory. |
paul@2 | 242 | |
paul@2 | 243 | parts = self.media_type.split("/") |
paul@2 | 244 | self.category = parts[0] |
paul@2 | 245 | self.subcategory = "/".join(parts[1:]) |
paul@2 | 246 | |
paul@2 | 247 | def get_parts(self): |
paul@3 | 248 | |
paul@3 | 249 | "Return the category, subcategory parts." |
paul@3 | 250 | |
paul@2 | 251 | return self.category, self.subcategory |
paul@2 | 252 | |
paul@2 | 253 | def get_specificity(self): |
paul@3 | 254 | |
paul@3 | 255 | """ |
paul@3 | 256 | Return the specificity of the media type in terms of the scope of the |
paul@3 | 257 | category and subcategory, and also in terms of any qualifying |
paul@3 | 258 | parameters. |
paul@3 | 259 | """ |
paul@3 | 260 | |
paul@2 | 261 | if "*" in self.get_parts(): |
paul@2 | 262 | return -list(self.get_parts()).count("*") |
paul@2 | 263 | else: |
paul@2 | 264 | return len(self.parameters) |
paul@2 | 265 | |
paul@2 | 266 | def permits(self, other): |
paul@3 | 267 | |
paul@3 | 268 | """ |
paul@3 | 269 | Return whether this media type permits the use of the 'other' media type |
paul@3 | 270 | if suggested as suitable content. |
paul@3 | 271 | """ |
paul@3 | 272 | |
paul@2 | 273 | if not isinstance(other, MediaRange): |
paul@2 | 274 | other = MediaRange(other) |
paul@2 | 275 | |
paul@2 | 276 | category = categoryPermits(self.category, other.category) |
paul@2 | 277 | subcategory = categoryPermits(self.subcategory, other.subcategory) |
paul@2 | 278 | |
paul@2 | 279 | if category and subcategory: |
paul@2 | 280 | if "*" not in (category, subcategory): |
paul@2 | 281 | return not self.parameters or self.parameters == other.parameters |
paul@2 | 282 | else: |
paul@2 | 283 | return True |
paul@2 | 284 | else: |
paul@2 | 285 | return False |
paul@2 | 286 | |
paul@2 | 287 | def __eq__(self, other): |
paul@3 | 288 | |
paul@3 | 289 | """ |
paul@3 | 290 | Return whether this media type is effectively the same as the 'other' |
paul@3 | 291 | media type. |
paul@3 | 292 | """ |
paul@3 | 293 | |
paul@2 | 294 | if not isinstance(other, MediaRange): |
paul@2 | 295 | other = MediaRange(other) |
paul@2 | 296 | |
paul@2 | 297 | category = categoryMatches(self.category, other.category) |
paul@2 | 298 | subcategory = categoryMatches(self.subcategory, other.subcategory) |
paul@2 | 299 | |
paul@2 | 300 | if category and subcategory: |
paul@2 | 301 | if "*" not in (category, subcategory): |
paul@2 | 302 | return self.parameters == other.parameters or \ |
paul@2 | 303 | not self.parameters or not other.parameters |
paul@2 | 304 | else: |
paul@2 | 305 | return True |
paul@2 | 306 | else: |
paul@2 | 307 | return False |
paul@2 | 308 | |
paul@2 | 309 | def __ne__(self, other): |
paul@2 | 310 | return not self.__eq__(other) |
paul@2 | 311 | |
paul@2 | 312 | def __hash__(self): |
paul@2 | 313 | return hash(self.media_range) |
paul@2 | 314 | |
paul@2 | 315 | def __repr__(self): |
paul@2 | 316 | return "MediaRange(%r)" % self.media_range |
paul@2 | 317 | |
paul@2 | 318 | def categoryMatches(this, that): |
paul@2 | 319 | |
paul@2 | 320 | """ |
paul@2 | 321 | Return the basis of a match between 'this' and 'that' or False if the given |
paul@2 | 322 | categories do not match. |
paul@2 | 323 | """ |
paul@2 | 324 | |
paul@2 | 325 | return (this == "*" or this == that) and this or \ |
paul@2 | 326 | that == "*" and that or False |
paul@2 | 327 | |
paul@2 | 328 | def categoryPermits(this, that): |
paul@2 | 329 | |
paul@2 | 330 | """ |
paul@2 | 331 | Return whether 'this' category permits 'that' category. Where 'this' is a |
paul@2 | 332 | wildcard ("*"), 'that' should always match. A value of False is returned if |
paul@2 | 333 | the categories do not otherwise match. |
paul@2 | 334 | """ |
paul@2 | 335 | |
paul@2 | 336 | return (this == "*" or this == that) and this or False |
paul@2 | 337 | |
paul@2 | 338 | def getMappingFromParameterStrings(l): |
paul@2 | 339 | |
paul@2 | 340 | """ |
paul@2 | 341 | Return a mapping representing the list of "name=value" strings given by 'l'. |
paul@2 | 342 | """ |
paul@2 | 343 | |
paul@2 | 344 | parameters = {} |
paul@2 | 345 | |
paul@2 | 346 | for parameter in l: |
paul@2 | 347 | parts = parameter.split("=") |
paul@2 | 348 | name = parts[0].strip() |
paul@2 | 349 | value = "=".join(parts[1:]).strip() |
paul@2 | 350 | parameters[name] = value |
paul@2 | 351 | |
paul@2 | 352 | return parameters |
paul@2 | 353 | |
paul@2 | 354 | def getContentPreferences(accept): |
paul@2 | 355 | |
paul@2 | 356 | """ |
paul@2 | 357 | Return a mapping from media types to parameters for content/media types |
paul@2 | 358 | extracted from the given 'accept' header value. The mapping is returned in |
paul@2 | 359 | the form of a list of (media type, parameters) tuples. |
paul@2 | 360 | |
paul@2 | 361 | See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 |
paul@2 | 362 | """ |
paul@2 | 363 | |
paul@2 | 364 | preferences = [] |
paul@2 | 365 | |
paul@2 | 366 | for field in accept.split(","): |
paul@2 | 367 | |
paul@2 | 368 | # The media type with parameters (defined by the "media-range") is |
paul@2 | 369 | # separated from any other parameters (defined as "accept-extension" |
paul@2 | 370 | # parameters) by a quality parameter. |
paul@2 | 371 | |
paul@2 | 372 | fparts = accept_regexp.split(field) |
paul@2 | 373 | |
paul@2 | 374 | # The first part is always the media type. |
paul@2 | 375 | |
paul@2 | 376 | media_type = fparts[0].strip() |
paul@2 | 377 | |
paul@2 | 378 | # Any other parts can be interpreted as extension parameters. |
paul@2 | 379 | |
paul@2 | 380 | if len(fparts) > 1: |
paul@2 | 381 | fparts = ("q=" + ";q=".join(fparts[1:])).split(";") |
paul@2 | 382 | else: |
paul@2 | 383 | fparts = [] |
paul@2 | 384 | |
paul@2 | 385 | # Each field in the preferences can incorporate parameters separated by |
paul@2 | 386 | # semicolon characters. |
paul@2 | 387 | |
paul@2 | 388 | parameters = getMappingFromParameterStrings(fparts) |
paul@2 | 389 | media_range = MediaRange(media_type, parameters) |
paul@2 | 390 | preferences.append(media_range) |
paul@2 | 391 | |
paul@2 | 392 | return ContentPreferences(preferences) |
paul@2 | 393 | |
paul@2 | 394 | class ContentPreferences: |
paul@2 | 395 | |
paul@2 | 396 | "A wrapper around content preference information." |
paul@2 | 397 | |
paul@2 | 398 | def __init__(self, preferences): |
paul@2 | 399 | self.preferences = preferences |
paul@2 | 400 | |
paul@2 | 401 | def __iter__(self): |
paul@2 | 402 | return iter(self.preferences) |
paul@2 | 403 | |
paul@2 | 404 | def get_ordered(self, by_quality=0): |
paul@2 | 405 | |
paul@2 | 406 | """ |
paul@2 | 407 | Return a list of content/media types in descending order of preference. |
paul@2 | 408 | If 'by_quality' is set to a true value, the "q" value will be used as |
paul@2 | 409 | the primary measure of preference; otherwise, only the specificity will |
paul@2 | 410 | be considered. |
paul@2 | 411 | """ |
paul@2 | 412 | |
paul@2 | 413 | ordered = {} |
paul@2 | 414 | |
paul@2 | 415 | for media_range in self.preferences: |
paul@2 | 416 | specificity = media_range.get_specificity() |
paul@2 | 417 | |
paul@2 | 418 | if by_quality: |
paul@2 | 419 | q = float(media_range.accept_parameters.get("q", "1")) |
paul@2 | 420 | key = q, specificity |
paul@2 | 421 | else: |
paul@2 | 422 | key = specificity |
paul@2 | 423 | |
paul@2 | 424 | if not ordered.has_key(key): |
paul@2 | 425 | ordered[key] = [] |
paul@2 | 426 | |
paul@2 | 427 | ordered[key].append(media_range) |
paul@2 | 428 | |
paul@2 | 429 | # Return the preferences in descending order of quality and specificity. |
paul@2 | 430 | |
paul@2 | 431 | keys = ordered.keys() |
paul@2 | 432 | keys.sort(reverse=True) |
paul@2 | 433 | return [ordered[key] for key in keys] |
paul@2 | 434 | |
paul@2 | 435 | def get_preferred_type(self, available): |
paul@2 | 436 | |
paul@2 | 437 | """ |
paul@2 | 438 | Return the preferred content/media type from those in the 'available' |
paul@2 | 439 | list, given the known preferences. |
paul@2 | 440 | """ |
paul@2 | 441 | |
paul@2 | 442 | matches = {} |
paul@2 | 443 | available = set(available[:]) |
paul@2 | 444 | |
paul@2 | 445 | for level in self.get_ordered(): |
paul@2 | 446 | for media_range in level: |
paul@2 | 447 | |
paul@2 | 448 | # Attempt to match available types. |
paul@2 | 449 | |
paul@2 | 450 | found = set() |
paul@2 | 451 | for available_type in available: |
paul@2 | 452 | if media_range.permits(available_type): |
paul@2 | 453 | q = float(media_range.accept_parameters.get("q", "1")) |
paul@2 | 454 | if not matches.has_key(q): |
paul@2 | 455 | matches[q] = [] |
paul@2 | 456 | matches[q].append(available_type) |
paul@2 | 457 | found.add(available_type) |
paul@2 | 458 | |
paul@2 | 459 | # Stop looking for matches for matched available types. |
paul@2 | 460 | |
paul@2 | 461 | if found: |
paul@2 | 462 | available.difference_update(found) |
paul@2 | 463 | |
paul@2 | 464 | # Sort the matches in descending order of quality. |
paul@2 | 465 | |
paul@2 | 466 | all_q = matches.keys() |
paul@2 | 467 | |
paul@2 | 468 | if all_q: |
paul@2 | 469 | all_q.sort(reverse=True) |
paul@2 | 470 | return matches[all_q[0]] |
paul@2 | 471 | else: |
paul@2 | 472 | return None |
paul@2 | 473 | |
paul@1 | 474 | # Page access functions. |
paul@1 | 475 | |
paul@1 | 476 | def getPageURL(page): |
paul@1 | 477 | |
paul@1 | 478 | "Return the URL of the given 'page'." |
paul@1 | 479 | |
paul@1 | 480 | request = page.request |
paul@1 | 481 | return request.getQualifiedURL(page.url(request, relative=0)) |
paul@1 | 482 | |
paul@1 | 483 | def getFormat(page): |
paul@1 | 484 | |
paul@1 | 485 | "Get the format used on the given 'page'." |
paul@1 | 486 | |
paul@1 | 487 | return page.pi["format"] |
paul@1 | 488 | |
paul@1 | 489 | def getMetadata(page): |
paul@1 | 490 | |
paul@1 | 491 | """ |
paul@1 | 492 | Return a dictionary containing items describing for the given 'page' the |
paul@1 | 493 | page's "created" time, "last-modified" time, "sequence" (or revision number) |
paul@1 | 494 | and the "last-comment" made about the last edit. |
paul@1 | 495 | """ |
paul@1 | 496 | |
paul@1 | 497 | request = page.request |
paul@1 | 498 | |
paul@1 | 499 | # Get the initial revision of the page. |
paul@1 | 500 | |
paul@1 | 501 | revisions = page.getRevList() |
paul@1 | 502 | event_page_initial = Page(request, page.page_name, rev=revisions[-1]) |
paul@1 | 503 | |
paul@1 | 504 | # Get the created and last modified times. |
paul@1 | 505 | |
paul@1 | 506 | initial_revision = getPageRevision(event_page_initial) |
paul@1 | 507 | |
paul@1 | 508 | metadata = {} |
paul@1 | 509 | metadata["created"] = initial_revision["timestamp"] |
paul@1 | 510 | latest_revision = getPageRevision(page) |
paul@1 | 511 | metadata["last-modified"] = latest_revision["timestamp"] |
paul@1 | 512 | metadata["sequence"] = len(revisions) - 1 |
paul@1 | 513 | metadata["last-comment"] = latest_revision["comment"] |
paul@1 | 514 | |
paul@1 | 515 | return metadata |
paul@0 | 516 | |
paul@0 | 517 | def getPageRevision(page): |
paul@0 | 518 | |
paul@0 | 519 | "Return the revision details dictionary for the given 'page'." |
paul@0 | 520 | |
paul@0 | 521 | # From Page.edit_info... |
paul@0 | 522 | |
paul@0 | 523 | if hasattr(page, "editlog_entry"): |
paul@0 | 524 | line = page.editlog_entry() |
paul@0 | 525 | else: |
paul@0 | 526 | line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x |
paul@0 | 527 | |
paul@0 | 528 | # Similar to Page.mtime_usecs behaviour... |
paul@0 | 529 | |
paul@0 | 530 | if line: |
paul@0 | 531 | timestamp = line.ed_time_usecs |
paul@0 | 532 | mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x |
paul@0 | 533 | comment = line.comment |
paul@0 | 534 | else: |
paul@0 | 535 | mtime = 0 |
paul@0 | 536 | comment = "" |
paul@0 | 537 | |
paul@0 | 538 | # Leave the time zone empty. |
paul@0 | 539 | |
paul@0 | 540 | return {"timestamp" : DateTime(time.gmtime(mtime)[:6] + (None,)), "comment" : comment} |
paul@0 | 541 | |
paul@11 | 542 | # Page parsing and formatting of embedded content. |
paul@11 | 543 | |
paul@11 | 544 | def getParserClass(request, format): |
paul@11 | 545 | |
paul@11 | 546 | """ |
paul@11 | 547 | Return a parser class using the 'request' for the given 'format', returning |
paul@11 | 548 | a plain text parser if no parser can be found for the specified 'format'. |
paul@11 | 549 | """ |
paul@11 | 550 | |
paul@11 | 551 | try: |
paul@11 | 552 | return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") |
paul@11 | 553 | except wikiutil.PluginMissingError: |
paul@11 | 554 | return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") |
paul@11 | 555 | |
paul@11 | 556 | def redirectedOutput(request, parser, fmt, **kw): |
paul@11 | 557 | |
paul@11 | 558 | "A fixed version of the request method of the same name." |
paul@11 | 559 | |
paul@11 | 560 | buf = StringIO() |
paul@11 | 561 | request.redirect(buf) |
paul@11 | 562 | try: |
paul@11 | 563 | parser.format(fmt, **kw) |
paul@11 | 564 | if hasattr(fmt, "flush"): |
paul@11 | 565 | buf.write(fmt.flush(True)) |
paul@11 | 566 | finally: |
paul@11 | 567 | request.redirect() |
paul@11 | 568 | text = buf.getvalue() |
paul@11 | 569 | buf.close() |
paul@11 | 570 | return text |
paul@11 | 571 | |
paul@0 | 572 | # User interface functions. |
paul@0 | 573 | |
paul@0 | 574 | def getParameter(request, name, default=None): |
paul@0 | 575 | |
paul@0 | 576 | """ |
paul@0 | 577 | Using the given 'request', return the value of the parameter with the given |
paul@0 | 578 | 'name', returning the optional 'default' (or None) if no value was supplied |
paul@0 | 579 | in the 'request'. |
paul@0 | 580 | """ |
paul@0 | 581 | |
paul@0 | 582 | return get_form(request).get(name, [default])[0] |
paul@0 | 583 | |
paul@0 | 584 | def getQualifiedParameter(request, prefix, argname, default=None): |
paul@0 | 585 | |
paul@0 | 586 | """ |
paul@0 | 587 | Using the given 'request', 'prefix' and 'argname', retrieve the value of the |
paul@0 | 588 | qualified parameter, returning the optional 'default' (or None) if no value |
paul@0 | 589 | was supplied in the 'request'. |
paul@0 | 590 | """ |
paul@0 | 591 | |
paul@0 | 592 | argname = getQualifiedParameterName(prefix, argname) |
paul@0 | 593 | return getParameter(request, argname, default) |
paul@0 | 594 | |
paul@0 | 595 | def getQualifiedParameterName(prefix, argname): |
paul@0 | 596 | |
paul@0 | 597 | """ |
paul@0 | 598 | Return the qualified parameter name using the given 'prefix' and 'argname'. |
paul@0 | 599 | """ |
paul@0 | 600 | |
paul@0 | 601 | if prefix is None: |
paul@0 | 602 | return argname |
paul@0 | 603 | else: |
paul@0 | 604 | return "%s-%s" % (prefix, argname) |
paul@0 | 605 | |
paul@0 | 606 | # Page-related functions. |
paul@0 | 607 | |
paul@0 | 608 | def getPrettyPageName(page): |
paul@0 | 609 | |
paul@0 | 610 | "Return a nicely formatted title/name for the given 'page'." |
paul@0 | 611 | |
paul@0 | 612 | title = page.split_title(force=1) |
paul@0 | 613 | return getPrettyTitle(title) |
paul@0 | 614 | |
paul@3 | 615 | def linkToPage(request, page, text, query_string=None, **kw): |
paul@0 | 616 | |
paul@0 | 617 | """ |
paul@0 | 618 | Using 'request', return a link to 'page' with the given link 'text' and |
paul@0 | 619 | optional 'query_string'. |
paul@0 | 620 | """ |
paul@0 | 621 | |
paul@0 | 622 | text = wikiutil.escape(text) |
paul@3 | 623 | return page.link_to_raw(request, text, query_string, **kw) |
paul@0 | 624 | |
paul@0 | 625 | def linkToResource(url, request, text, query_string=None): |
paul@0 | 626 | |
paul@0 | 627 | """ |
paul@0 | 628 | Using 'request', return a link to 'url' with the given link 'text' and |
paul@0 | 629 | optional 'query_string'. |
paul@0 | 630 | """ |
paul@0 | 631 | |
paul@0 | 632 | if query_string: |
paul@0 | 633 | query_string = wikiutil.makeQueryString(query_string) |
paul@0 | 634 | url = "%s?%s" % (url, query_string) |
paul@0 | 635 | |
paul@0 | 636 | formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter |
paul@0 | 637 | |
paul@0 | 638 | output = [] |
paul@0 | 639 | output.append(formatter.url(1, url)) |
paul@0 | 640 | output.append(formatter.text(text)) |
paul@0 | 641 | output.append(formatter.url(0)) |
paul@0 | 642 | return "".join(output) |
paul@0 | 643 | |
paul@0 | 644 | def getFullPageName(parent, title): |
paul@0 | 645 | |
paul@0 | 646 | """ |
paul@0 | 647 | Return a full page name from the given 'parent' page (can be empty or None) |
paul@0 | 648 | and 'title' (a simple page name). |
paul@0 | 649 | """ |
paul@0 | 650 | |
paul@0 | 651 | if parent: |
paul@0 | 652 | return "%s/%s" % (parent.rstrip("/"), title) |
paul@0 | 653 | else: |
paul@0 | 654 | return title |
paul@0 | 655 | |
paul@0 | 656 | # vim: tabstop=4 expandtab shiftwidth=4 |