| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | |
|---|
| 3 | # 2011 Steffen Hoffmann |
|---|
| 4 | |
|---|
| 5 | import htmlentitydefs |
|---|
| 6 | import re |
|---|
| 7 | import codecs |
|---|
| 8 | |
|---|
| 9 | from trac.resource import ResourceSystem |
|---|
| 10 | from trac.util.html import html as tag |
|---|
| 11 | from trac.util.text import to_unicode |
|---|
| 12 | |
|---|
| 13 | from api import tag_ |
|---|
| 14 | from compat import json |
|---|
| 15 | |
|---|
| 16 | __all__ = ['parse_history', 'resource_from_page', 'xml_escape', |
|---|
| 17 | 'xml_unescape'] |
|---|
| 18 | |
|---|
| 19 | |
|---|
| 20 | def parse_history(changes, fieldwise=False): |
|---|
| 21 | """Versatile history parser for TracForms. |
|---|
| 22 | |
|---|
| 23 | Returns either a list of dicts for changeset display in form view or |
|---|
| 24 | a dict of field change lists for stepwise form reset. |
|---|
| 25 | """ |
|---|
| 26 | field_history = {} |
|---|
| 27 | history = [] |
|---|
| 28 | if fieldwise is not False: |
|---|
| 29 | def _add_change(field_history, field, author, time, old, new): |
|---|
| 30 | if field not in field_history.keys(): |
|---|
| 31 | field_history[field] = [{'author': author, 'time': time, |
|---|
| 32 | 'old': old, 'new': new}] |
|---|
| 33 | else: |
|---|
| 34 | field_history[field].append({'author': author, 'time': time, |
|---|
| 35 | 'old': old, 'new': new}) |
|---|
| 36 | return field_history |
|---|
| 37 | |
|---|
| 38 | new_fields = None |
|---|
| 39 | last_author = last_change = None |
|---|
| 40 | for changeset in changes: |
|---|
| 41 | # break down old and new version |
|---|
| 42 | try: |
|---|
| 43 | old_fields = json.loads(changeset.get('old_state', '{}')) |
|---|
| 44 | except ValueError: |
|---|
| 45 | # skip invalid history |
|---|
| 46 | old_fields = {} |
|---|
| 47 | pass |
|---|
| 48 | if new_fields is None: |
|---|
| 49 | # first loop cycle: only load values for comparison next time |
|---|
| 50 | new_fields = old_fields |
|---|
| 51 | last_author = changeset['author'] |
|---|
| 52 | last_change = changeset['time'] |
|---|
| 53 | continue |
|---|
| 54 | updated_fields = {} |
|---|
| 55 | for field, old_value in old_fields.iteritems(): |
|---|
| 56 | new_value = new_fields.get(field) |
|---|
| 57 | if new_value != old_value: |
|---|
| 58 | if fieldwise is False: |
|---|
| 59 | change = _render_change(old_value, new_value) |
|---|
| 60 | if change is not None: |
|---|
| 61 | updated_fields[field] = change |
|---|
| 62 | else: |
|---|
| 63 | field_history = _add_change(field_history, field, |
|---|
| 64 | last_author, last_change, |
|---|
| 65 | old_value, new_value) |
|---|
| 66 | for field in new_fields: |
|---|
| 67 | if old_fields.get(field) is None: |
|---|
| 68 | if fieldwise is False: |
|---|
| 69 | change = _render_change(None, new_fields[field]) |
|---|
| 70 | if change is not None: |
|---|
| 71 | updated_fields[field] = change |
|---|
| 72 | else: |
|---|
| 73 | field_history = _add_change(field_history, field, |
|---|
| 74 | last_author, last_change, |
|---|
| 75 | None, new_fields[field]) |
|---|
| 76 | new_fields = old_fields |
|---|
| 77 | history.append({'author': last_author, |
|---|
| 78 | 'time': last_change, |
|---|
| 79 | 'changes': updated_fields}) |
|---|
| 80 | last_author = changeset['author'] |
|---|
| 81 | last_change = changeset['time'] |
|---|
| 82 | return fieldwise is False and history or field_history |
|---|
| 83 | |
|---|
| 84 | |
|---|
| 85 | def _render_change(old, new): |
|---|
| 86 | rendered = None |
|---|
| 87 | if old and not new: |
|---|
| 88 | rendered = tag_("%(value)s reset to default value", |
|---|
| 89 | value=tag.em(old)) |
|---|
| 90 | elif new and not old: |
|---|
| 91 | rendered = tag_("from default value set to %(value)s", |
|---|
| 92 | value=tag.em(new)) |
|---|
| 93 | elif old and new: |
|---|
| 94 | if len(old) < 20 and len(new) < 20: |
|---|
| 95 | rendered = tag_("changed from %(old)s to %(new)s", |
|---|
| 96 | old=tag.em(old), new=tag.em(new)) |
|---|
| 97 | else: |
|---|
| 98 | nbsp = tag.br() |
|---|
| 99 | # TRANSLATOR: same as before, but with additional line breaks |
|---|
| 100 | rendered = tag_("changed from %(old)s to %(new)s", |
|---|
| 101 | old=tag.em(nbsp, old), new=tag.em(nbsp, new)) |
|---|
| 102 | return rendered |
|---|
| 103 | |
|---|
| 104 | |
|---|
| 105 | # adapted from code published by Daniel Goldberg on 09-Dec-2008 at |
|---|
| 106 | # http://stackoverflow.com/questions/354038 |
|---|
| 107 | def is_number(s): |
|---|
| 108 | try: |
|---|
| 109 | float(s) |
|---|
| 110 | return True |
|---|
| 111 | except (TypeError, ValueError): |
|---|
| 112 | return False |
|---|
| 113 | |
|---|
| 114 | |
|---|
| 115 | # code from an article published by Uche Ogbuji on 15-Jun-2005 at |
|---|
| 116 | # http://www.xml.com/pub/a/2005/06/15/py-xml.html |
|---|
| 117 | def xml_escape(text): |
|---|
| 118 | enc = codecs.getencoder('us-ascii') |
|---|
| 119 | return enc(to_unicode(text), 'xmlcharrefreplace')[0] |
|---|
| 120 | |
|---|
| 121 | |
|---|
| 122 | # adapted from code published by John J. Lee on 06-Jun-2007 at |
|---|
| 123 | # http://www.velocityreviews.com/forums |
|---|
| 124 | # /t511850-how-do-you-htmlentities-in-python.html |
|---|
| 125 | unichresc_RE = re.compile(r'&#?[A-Za-z0-9]+?;') |
|---|
| 126 | |
|---|
| 127 | |
|---|
| 128 | def xml_unescape(text): |
|---|
| 129 | return unichresc_RE.sub(_replace_entities, text) |
|---|
| 130 | |
|---|
| 131 | |
|---|
| 132 | def _unescape_charref(ref): |
|---|
| 133 | name = ref[2:-1] |
|---|
| 134 | base = 10 |
|---|
| 135 | # DEVEL: gain 20 % performance by omitting hex references |
|---|
| 136 | if name.startswith("x"): |
|---|
| 137 | name = name[1:] |
|---|
| 138 | base = 16 |
|---|
| 139 | return unichr(int(name, base)) |
|---|
| 140 | |
|---|
| 141 | |
|---|
| 142 | def _replace_entities(match): |
|---|
| 143 | ent = match.group() |
|---|
| 144 | if ent[1] == "#": |
|---|
| 145 | return _unescape_charref(ent) |
|---|
| 146 | repl = htmlentitydefs.name2codepoint.get(ent[1:-1]) |
|---|
| 147 | if repl is not None: |
|---|
| 148 | repl = unichr(repl) |
|---|
| 149 | else: |
|---|
| 150 | repl = ent |
|---|
| 151 | return repl |
|---|
| 152 | |
|---|
| 153 | |
|---|
| 154 | def resource_from_page(env, page): |
|---|
| 155 | resource_realm = None |
|---|
| 156 | resources = ResourceSystem(env) |
|---|
| 157 | for realm in resources.get_known_realms(): |
|---|
| 158 | if page.startswith('/' + realm): |
|---|
| 159 | resource_realm = realm |
|---|
| 160 | break |
|---|
| 161 | if resource_realm is not None: |
|---|
| 162 | return (resource_realm, |
|---|
| 163 | re.sub('/' + resource_realm, '', page).lstrip('/')) |
|---|
| 164 | else: |
|---|
| 165 | return page, None |
|---|