| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | |
|---|
| 3 | import re |
|---|
| 4 | from pkg_resources import resource_filename |
|---|
| 5 | |
|---|
| 6 | from trac.core import implements |
|---|
| 7 | from trac.resource import get_resource_description, \ |
|---|
| 8 | get_resource_shortname, get_resource_url |
|---|
| 9 | from trac.search.api import ISearchSource, shorten_result |
|---|
| 10 | from trac.util.datefmt import to_datetime |
|---|
| 11 | from trac.util.html import html as tag |
|---|
| 12 | from trac.web.api import IRequestFilter, IRequestHandler |
|---|
| 13 | from trac.web.chrome import ITemplateProvider, add_ctxtnav, add_stylesheet |
|---|
| 14 | |
|---|
| 15 | from api import FormDBUser, _, dgettext, tag_ |
|---|
| 16 | from compat import json |
|---|
| 17 | from formdb import format_author |
|---|
| 18 | from model import Form |
|---|
| 19 | from util import parse_history, resource_from_page |
|---|
| 20 | |
|---|
| 21 | tf_page_re = re.compile('/form(/\d+|$)') |
|---|
| 22 | |
|---|
| 23 | |
|---|
| 24 | class FormUI(FormDBUser): |
|---|
| 25 | """Provides form views for reviewing and managing TracForm data. |
|---|
| 26 | |
|---|
| 27 | Extensions for the Trac web user interface display saved field values, |
|---|
| 28 | metadata and history. TracSearch support for TracForms is included here |
|---|
| 29 | and administrative actions to revert form changes are embedded as well. |
|---|
| 30 | """ |
|---|
| 31 | |
|---|
| 32 | implements(IRequestFilter, IRequestHandler, ISearchSource, |
|---|
| 33 | ITemplateProvider) |
|---|
| 34 | |
|---|
| 35 | # IRequestFilter methods |
|---|
| 36 | |
|---|
| 37 | def pre_process_request(self, req, handler): |
|---|
| 38 | return handler |
|---|
| 39 | |
|---|
| 40 | def post_process_request(self, req, template, data, content_type): |
|---|
| 41 | env = self.env |
|---|
| 42 | page = req.path_info |
|---|
| 43 | realm, resource_id = resource_from_page(env, page) |
|---|
| 44 | # break (recursive) search for form in forms realm |
|---|
| 45 | if tf_page_re.match(page) is None and resource_id is not None: |
|---|
| 46 | if page == '/wiki' or page == '/wiki/': |
|---|
| 47 | page = '/wiki/WikiStart' |
|---|
| 48 | form = Form(env, realm, resource_id) |
|---|
| 49 | if 'FORM_VIEW' in req.perm(form.resource): |
|---|
| 50 | if len(form.siblings) == 0: |
|---|
| 51 | # no form record found for this parent resource |
|---|
| 52 | return template, data, content_type |
|---|
| 53 | elif form.resource.id is not None: |
|---|
| 54 | # single form record found |
|---|
| 55 | href = req.href.form(form.resource.id) |
|---|
| 56 | else: |
|---|
| 57 | # multiple form records found |
|---|
| 58 | href = req.href.form(action='select', realm=realm, |
|---|
| 59 | resource_id=resource_id) |
|---|
| 60 | add_ctxtnav(req, _("Form details"), href=href, |
|---|
| 61 | title=_("Review form data")) |
|---|
| 62 | elif page.startswith('/form') and not resource_id == '': |
|---|
| 63 | form = Form(env, form_id=resource_id) |
|---|
| 64 | parent = form.resource.parent |
|---|
| 65 | if len(form.siblings) > 1: |
|---|
| 66 | href = req.href.form(action='select', realm=parent.realm, |
|---|
| 67 | resource_id=parent.id) |
|---|
| 68 | add_ctxtnav(req, _("Back to forms list"), href=href) |
|---|
| 69 | return template, data, content_type |
|---|
| 70 | |
|---|
| 71 | # ITemplateProvider methods |
|---|
| 72 | |
|---|
| 73 | def get_htdocs_dirs(self): |
|---|
| 74 | """Return static resources for TracForms.""" |
|---|
| 75 | return [('tracforms', resource_filename(__name__, 'htdocs'))] |
|---|
| 76 | |
|---|
| 77 | def get_templates_dirs(self): |
|---|
| 78 | """Return template directory for TracForms.""" |
|---|
| 79 | return [resource_filename(__name__, 'templates')] |
|---|
| 80 | |
|---|
| 81 | # IRequestHandler methods |
|---|
| 82 | |
|---|
| 83 | def match_request(self, req): |
|---|
| 84 | if req.path_info == '/form': |
|---|
| 85 | return True |
|---|
| 86 | match = re.match('/form/(\d+)$', req.path_info) |
|---|
| 87 | if match: |
|---|
| 88 | if match.group(1): |
|---|
| 89 | req.args['id'] = match.group(1) |
|---|
| 90 | return True |
|---|
| 91 | |
|---|
| 92 | def process_request(self, req): |
|---|
| 93 | env = self.env |
|---|
| 94 | id_hint = req.args.get('id') |
|---|
| 95 | if id_hint is not None and Form.id_is_valid(id_hint): |
|---|
| 96 | form_id = int(id_hint) |
|---|
| 97 | else: |
|---|
| 98 | form_id = None |
|---|
| 99 | if form_id is not None: |
|---|
| 100 | form = Form(env, form_id=form_id) |
|---|
| 101 | if req.method == 'POST': |
|---|
| 102 | req.perm(form.resource).require('FORM_RESET') |
|---|
| 103 | return self._do_reset(env, req, form) |
|---|
| 104 | |
|---|
| 105 | req.perm(form.resource).require('FORM_VIEW') |
|---|
| 106 | return self._do_view(env, req, form) |
|---|
| 107 | |
|---|
| 108 | if req.args.get('action') == 'select': |
|---|
| 109 | realm = req.args.get('realm') |
|---|
| 110 | resource_id = req.args.get('resource_id') |
|---|
| 111 | if realm is not None and resource_id is not None: |
|---|
| 112 | form = Form(env, realm, resource_id) |
|---|
| 113 | req.perm(form.resource).require('FORM_VIEW') |
|---|
| 114 | return self._do_switch(env, req, form) |
|---|
| 115 | |
|---|
| 116 | def _do_view(self, env, req, form): |
|---|
| 117 | data = {'_dgettext': dgettext} |
|---|
| 118 | form_id = form.resource.id |
|---|
| 119 | data['page_title'] = get_resource_description(env, form.resource, |
|---|
| 120 | href=req.href) |
|---|
| 121 | data['title'] = get_resource_shortname(env, form.resource) |
|---|
| 122 | # prime list with current state |
|---|
| 123 | subcontext, author, time = self.get_tracform_meta(form_id)[3:6] |
|---|
| 124 | author = format_author(self.env, req, author, 'change') |
|---|
| 125 | if not subcontext == '': |
|---|
| 126 | data['subcontext'] = subcontext |
|---|
| 127 | state = self.get_tracform_state(form_id) |
|---|
| 128 | data['fields'] = self._render_fields(req, form_id, state) |
|---|
| 129 | history = [{'author': author, 'time': time, |
|---|
| 130 | 'old_state': state}] |
|---|
| 131 | # add recorded old_state |
|---|
| 132 | records = self.get_tracform_history(form_id) |
|---|
| 133 | for author, time, old_state in records: |
|---|
| 134 | author = format_author(self.env, req, author, 'change') |
|---|
| 135 | history.append({'author': author, 'time': time, |
|---|
| 136 | 'old_state': old_state}) |
|---|
| 137 | data['history'] = parse_history(history) |
|---|
| 138 | # show reset button in case of existing data and proper permission |
|---|
| 139 | data['allow_reset'] = req.perm(form.resource) \ |
|---|
| 140 | .has_permission( |
|---|
| 141 | 'FORM_RESET') and form.has_data |
|---|
| 142 | add_stylesheet(req, 'tracforms/tracforms.css') |
|---|
| 143 | return 'form.html', data, None |
|---|
| 144 | |
|---|
| 145 | def _do_switch(self, env, req, form): |
|---|
| 146 | data = { |
|---|
| 147 | '_dgettext': dgettext, |
|---|
| 148 | 'page_title': get_resource_description(env, form.resource, |
|---|
| 149 | href=req.href), |
|---|
| 150 | 'title': get_resource_shortname(env, form.resource), |
|---|
| 151 | 'siblings': [] |
|---|
| 152 | } |
|---|
| 153 | for sibling in form.siblings: |
|---|
| 154 | form_id = tag.strong(tag.a( |
|---|
| 155 | _("Form %(form_id)s", form_id=sibling[0]), |
|---|
| 156 | href=req.href.form(sibling[0]))) |
|---|
| 157 | if sibling[1] == '': |
|---|
| 158 | data['siblings'].append(form_id) |
|---|
| 159 | else: |
|---|
| 160 | # TRANSLATOR: Form list entry for form select page |
|---|
| 161 | data['siblings'].append(tag_( |
|---|
| 162 | "%(form_id)s (subcontext = '%(subcontext)s')", |
|---|
| 163 | form_id=form_id, subcontext=sibling[1])) |
|---|
| 164 | add_stylesheet(req, 'tracforms/tracforms.css') |
|---|
| 165 | return 'switch.html', data, None |
|---|
| 166 | |
|---|
| 167 | def _do_reset(self, env, req, form): |
|---|
| 168 | author = req.authname |
|---|
| 169 | step = None |
|---|
| 170 | if 'rewind' in req.args: |
|---|
| 171 | step = -1 |
|---|
| 172 | elif 'reset' in req.args: |
|---|
| 173 | step = 0 |
|---|
| 174 | if form.resource.id is not None: |
|---|
| 175 | self.reset_tracform(form.resource.id, author=author, step=step) |
|---|
| 176 | else: |
|---|
| 177 | self.reset_tracform(tuple([form.parent.realm, form.parent.id]), |
|---|
| 178 | author=author, step=step) |
|---|
| 179 | return self._do_view(env, req, form) |
|---|
| 180 | |
|---|
| 181 | def _render_fields(self, req, form_id, state): |
|---|
| 182 | fields = json.loads(state is not None and state or '{}') |
|---|
| 183 | rendered = [] |
|---|
| 184 | for name, value in fields.iteritems(): |
|---|
| 185 | if value == 'on': |
|---|
| 186 | value = _("checked (checkbox)") |
|---|
| 187 | elif value == '': |
|---|
| 188 | value = _("empty (text field)") |
|---|
| 189 | elif isinstance(value, basestring): |
|---|
| 190 | value = "'".join(['', value, '']) |
|---|
| 191 | else: |
|---|
| 192 | # Still try to display something useful instead of corrupting |
|---|
| 193 | # parent page beyond hope of recovery through the web_ui. |
|---|
| 194 | value = "'".join(['', repr(value), '']) |
|---|
| 195 | author, time = self.get_tracform_fieldinfo(form_id, name) |
|---|
| 196 | author = format_author(self.env, req, author, 'value') |
|---|
| 197 | rendered.append( |
|---|
| 198 | {'name': name, 'value': value, |
|---|
| 199 | 'author': tag.span(tag(_("by %(author)s", author=author)), |
|---|
| 200 | class_='author'), |
|---|
| 201 | 'time': time}) |
|---|
| 202 | return rendered |
|---|
| 203 | |
|---|
| 204 | # ISearchSource methods |
|---|
| 205 | |
|---|
| 206 | def get_search_filters(self, req): |
|---|
| 207 | if 'FORM_VIEW' in req.perm: |
|---|
| 208 | # TRANSLATOR: The realm name used as TracSearch filter label |
|---|
| 209 | yield ('form', _("Forms")) |
|---|
| 210 | |
|---|
| 211 | def get_search_results(self, req, terms, filters): |
|---|
| 212 | if 'form' not in filters: |
|---|
| 213 | return |
|---|
| 214 | env = self.env |
|---|
| 215 | results = self.search_tracforms(terms) |
|---|
| 216 | |
|---|
| 217 | for id_, realm, parent, subctxt, state, author, updated_on in results: |
|---|
| 218 | # DEVEL: support for handling form revisions not implemented yet |
|---|
| 219 | # form = Form(env, realm, parent, subctxt, id, version) |
|---|
| 220 | form = Form(env, realm, parent, subctxt, id_) |
|---|
| 221 | if 'FORM_VIEW' in req.perm(form.resource): |
|---|
| 222 | form = form.resource |
|---|
| 223 | # build a more human-readable form values representation, |
|---|
| 224 | # especially with unicode character escapes removed |
|---|
| 225 | state = _render_values(state) |
|---|
| 226 | yield (get_resource_url(env, form, req.href), |
|---|
| 227 | get_resource_description(env, form), |
|---|
| 228 | to_datetime(updated_on), author, |
|---|
| 229 | shorten_result(state, terms)) |
|---|
| 230 | |
|---|
| 231 | |
|---|
| 232 | def _render_values(state, delimiter=': '): |
|---|
| 233 | fields = [] |
|---|
| 234 | for name, value in json.loads(state or '{}').iteritems(): |
|---|
| 235 | fields.append(''.join([name, delimiter, value])) |
|---|
| 236 | return '; '.join(fields) |
|---|