source: tracformsplugin/tags/tracforms-0.4.1/0.11/tracforms/web_ui.py

Last change on this file was 10490, checked in by Steffen Hoffmann, 12 years ago

TracFormsPlugin: Release maintenance version 0.4.1 for compatibility with Trac 0.11, closes #9000.

These are changes cherry-picked from trunk and merged into tracforms-0.4 to
establish full compatibility with Trac 0.11 on level with plugin's own claims.

File size: 12.8 KB
Line 
1# -*- coding: utf-8 -*-
2
3import re
4
5from genshi.builder import Markup, tag
6from pkg_resources import resource_filename
7
8from trac.core import implements
9from trac.util.datefmt import format_datetime
10from trac.resource import get_resource_description, \
11                          get_resource_shortname, get_resource_url
12from trac.search.api import ISearchSource, shorten_result
13from trac.util.datefmt import to_datetime
14from trac.web.api import IRequestFilter, IRequestHandler
15from trac.web.chrome import ITemplateProvider, add_ctxtnav, add_stylesheet
16
17from api import FormDBUser, _, dgettext
18from compat import json
19from model import Form
20from util import resource_from_page
21
22tfpageRE = re.compile('/form(/\d+|$)')
23
24
25class FormUI(FormDBUser):
26    """Provides form views for reviewing and managing TracForm data.
27
28    Extensions for the Trac web user interface display saved field values,
29    metadata and history. TracSearch support for TracForms is included here
30    and administrative actions to revert form changes are embedded as well.
31    """
32
33    implements(IRequestFilter, IRequestHandler, ISearchSource,
34               ITemplateProvider)
35
36    # IRequestFilter methods
37
38    def pre_process_request(self, req, handler):
39        return handler
40
41    def post_process_request(self, req, template, data, content_type):
42        env = self.env
43        page = req.path_info
44        realm, resource_id = resource_from_page(env, page)
45        # break (recursive) search for form in forms realm
46        if tfpageRE.match(page) == None and resource_id is not None:
47            if page == '/wiki' or page == '/wiki/':
48                page = '/wiki/WikiStart'
49            form = Form(env, realm, resource_id)
50            if 'FORM_VIEW' in req.perm(form.resource):
51                if len(form.siblings) == 0:
52                    # no form record found for this parent resource
53                    return (template, data, content_type)
54                elif form.resource.id is not None:
55                    # single form record found
56                    href = req.href.form(form.resource.id)
57                else:
58                    # multiple form records found
59                    href = req.href.form(action='select', realm=realm,
60                                         resource_id=resource_id)
61                add_ctxtnav(req, _("Form details"), href=href,
62                            title=_("Review form data"))
63        elif page.startswith('/form') and not resource_id == '':
64            form = Form(env, form_id=resource_id)
65            parent = form.resource.parent
66            if len(form.siblings) > 1:
67                href = req.href.form(action='select', realm=parent.realm,
68                                     resource_id=parent.id)
69                add_ctxtnav(req, _("Back to forms list"), href=href)
70        return (template, data, content_type)
71
72    # ITemplateProvider methods
73
74    def get_htdocs_dirs(self):
75        """Return static resources for TracForms."""
76        return [('tracforms', resource_filename(__name__, 'htdocs'))]
77
78    def get_templates_dirs(self):
79        """Return template directory for TracForms."""
80        return [resource_filename(__name__, 'templates')]
81
82    # IRequestHandler methods
83
84    def match_request(self, req):
85        if req.path_info == '/form':
86            return True
87        match = re.match('/form/(\d+)$', req.path_info)
88        if match:
89            if match.group(1):
90                req.args['id'] = match.group(1)
91            return True
92
93    def process_request(self, req):
94        env = self.env
95        id_hint = req.args.get('id')
96        if id_hint is not None and Form.id_is_valid(id_hint):
97            form_id = int(id_hint)
98        else:
99            form_id = None
100        if form_id is not None:
101            form = Form(env, form_id=form_id)
102            if req.method == 'POST':
103                req.perm(form.resource).require('FORM_RESET')
104                return self._do_reset(env, req, form)
105
106            req.perm(form.resource).require('FORM_VIEW')
107            return self._do_view(env, req, form)
108
109        if req.args.get('action') == 'select':
110            realm=req.args.get('realm')
111            resource_id=req.args.get('resource_id')
112            if realm is not None and resource_id is not None: 
113                form = Form(env, realm, resource_id)
114                req.perm(form.resource).require('FORM_VIEW')
115                return self._do_switch(env, req, form)
116
117    def _do_view(self, env, req, form):
118        data = {'_dgettext': dgettext}
119        form_id = form.resource.id
120        data['page_title'] = get_resource_description(env, form.resource,
121                                                      href=req.href)
122        data['title'] = get_resource_shortname(env, form.resource)
123        # prime list with current state
124        subcontext, author, time = self.get_tracform_meta(form_id)[3:6]
125        if not subcontext == '':
126            data['subcontext'] = subcontext
127        state = self.get_tracform_state(form_id)
128        data['fields'] = self._render_fields(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            history.append({'author': author, 'time': time,
135                            'old_state': old_state})
136        data['history'] = parse_history(history)
137        # show reset button in case of existing data and proper permission
138        data['allow_reset'] = req.perm(form.resource) \
139                              .has_permission('FORM_RESET') and form.has_data
140        add_stylesheet(req, 'tracforms/tracforms.css')
141        return 'form.html', data, None
142
143    def _do_switch(self, env, req, form):
144        data = {'_dgettext': dgettext}
145        data['page_title'] = get_resource_description(env, form.resource,
146                                                      href=req.href)
147        data['title'] = get_resource_shortname(env, form.resource)
148        data['siblings'] = []
149        for sibling in form.siblings:
150            form_id = tag.strong(tag.a(
151                          _("Form %(form_id)s", form_id=sibling[0]),
152                            href=req.href.form(sibling[0])))
153            if sibling[1] == '':
154                data['siblings'].append(form_id)
155            else:
156                # TRANSLATOR: Form list entry for form select page
157                data['siblings'].append(tag(Markup(_(
158                              "%(form_id)s (subcontext = '%(subcontext)s')",
159                              form_id=form_id, subcontext = sibling[1]))))
160        add_stylesheet(req, 'tracforms/tracforms.css')
161        return 'switch.html', data, None
162
163    def _do_reset(self, env, req, form):
164        author = req.authname
165        if 'rewind' in req.args:
166            step = -1
167        elif 'reset' in req.args:
168            step = 0
169        if form.resource.id is not None:
170            self.reset_tracform(form.resource.id, author=author, step=step)
171        else:
172            self.reset_tracform(tuple([form.parent.realm, form.parent.id]),
173                                author=author, step=step)
174        return self._do_view(env, req, form)
175
176    def _render_fields(self, form_id, state):
177        fields = json.loads(state is not None and state or '{}')
178        rendered = []
179        for name, value in fields.iteritems():
180            if value == 'on':
181               value = _("checked (checkbox)")
182            elif value == '':
183               value = _("empty (text field)")
184            else:
185               value = '\'' + value + '\''
186            author, time = self.get_tracform_fieldinfo(form_id, name)
187            rendered.append(
188                {'name': name, 'value': value,
189                 'author': tag.span(tag(_("by %(author)s", author=author)),
190                                    class_='author'),
191                 'time': time is not None and tag.span(
192                         format_datetime(time), class_='date') or None})
193        return rendered
194
195    # ISearchSource methods
196
197    def get_search_filters(self, req):
198        if 'FORM_VIEW' in req.perm:
199            # TRANSLATOR: The realm name used as TracSearch filter label
200            yield ('form', _("Forms"))
201
202    def get_search_results(self, req, terms, filters):
203        if not 'form' in filters:
204            return
205        env = self.env
206        results = self.search_tracforms(env, terms)
207
208        for id, realm, parent, subctxt, state, author, updated_on in results:
209            # DEVEL: support for handling form revisions not implemented yet
210            #form = Form(env, realm, parent, subctxt, id, version)
211            form = Form(env, realm, parent, subctxt, id)
212            if 'FORM_VIEW' in req.perm(form):
213                form = form.resource
214                # build a more human-readable form values representation,
215                # especially with unicode character escapes removed
216                state = _render_values(state)
217                yield (get_resource_url(env, form, req.href),
218                       get_resource_description(env, form),
219                       to_datetime(updated_on), author,
220                       shorten_result(state, terms))
221
222
223def parse_history(changes, fieldwise=False):
224    """Versatile history parser for TracForms.
225
226    Returns either a list of dicts for changeset display in form view or
227    a dict of field change lists for stepwise form reset.
228    """
229    fieldhistory = {}
230    history = []
231    if not fieldwise == False:
232        def _add_change(fieldhistory, field, author, time, old, new):
233            if field not in fieldhistory.keys():
234                fieldhistory[field] = [{'author': author, 'time': time,
235                                        'old': old, 'new': new}]
236            else:
237                fieldhistory[field].append({'author': author, 'time': time,
238                                            'old': old, 'new': new})
239            return fieldhistory
240
241    new_fields = None
242    for changeset in changes:
243        # break down old and new version
244        try:
245            old_fields = json.loads(changeset.get('old_state', '{}'))
246        except ValueError:
247            # skip invalid history
248            old_fields = {}
249            pass
250        if new_fields is None:
251            # first loop cycle: only load values for comparison next time
252            new_fields = old_fields
253            last_author = changeset['author']
254            last_change = changeset['time']
255            continue
256        updated_fields = {}
257        for field, old_value in old_fields.iteritems():
258            new_value = new_fields.get(field)
259            if new_value != old_value:
260                if fieldwise == False:
261                    change = _render_change(old_value, new_value)
262                    if change is not None:
263                        updated_fields[field] = change
264                else:
265                    fieldhistory = _add_change(fieldhistory, field,
266                                               last_author, last_change,
267                                               old_value, new_value)
268        for field in new_fields:
269            if old_fields.get(field) is None:
270                if fieldwise == False:
271                    change = _render_change(None, new_fields[field])
272                    if change is not None:
273                        updated_fields[field] = change
274                else:
275                    fieldhistory = _add_change(fieldhistory, field,
276                                               last_author, last_change,
277                                               None, new_fields[field])
278        new_fields = old_fields
279        history.append({'author': last_author,
280                        'time': format_datetime(last_change),
281                        'changes': updated_fields})
282        last_author = changeset['author']
283        last_change = changeset['time']
284    return fieldwise == False and history or fieldhistory
285
286def _render_change(old, new):
287    rendered = None
288    if old and not new:
289        rendered = tag(Markup(_("%(value)s reset to default value",
290                                value=tag.em(old))))
291    elif new and not old:
292        rendered = tag(Markup(_("from default value set to %(value)s",
293                                value=tag.em(new))))
294    elif old and new:
295        if len(old) < 20 and len(new) < 20:
296            rendered = tag(Markup(_("changed from %(old)s to %(new)s",
297                                    old=tag.em(old), new=tag.em(new))))
298        else:
299            nbsp = Markup('<br />')
300            # TRANSLATOR: same as before, but with additional line breaks
301            rendered = tag(Markup(_("changed from %(old)s to %(new)s",
302                                    old=tag.em(nbsp, old),
303                                    new=tag.em(nbsp, new))))
304    return rendered
305
306def _render_values(state):
307    fields = []
308    for name, value in json.loads(state or '{}').iteritems():
309        fields.append(name + ': ' + value)
310    return '; '.join(fields)
311
Note: See TracBrowser for help on using the repository browser.