source: revtreeplugin/0.11/revtree/web_ui.py

Last change on this file was 4697, checked in by Emmanuel Blot, 15 years ago

Fixes #3985. No more error when pretty branch name groups are not defined within the regular expression

  • Property svn:eol-style set to native
File size: 16.7 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2008 Emmanuel Blot <emmanuel.blot@free.fr>
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
8# are also available at http://trac.edgewall.com/license.html.
9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
12# history and logs, available at http://projects.edgewall.com/trac/.
13#
14
15import re
16import os
17import time
18
19from genshi import Markup
20from genshi.builder import tag
21from revtree.api import EmptyRangeError, RevtreeSystem
22from revtree.model import Repository
23from trac.config import Option, IntOption, BoolOption, ListOption, \
24                        Section, ConfigurationError
25from trac.core import *
26from trac.perm import IPermissionRequestor
27from trac.util import TracError
28from trac.util.datefmt import format_datetime, pretty_timedelta, to_timestamp
29from trac.web import IRequestFilter, IRequestHandler
30from trac.web.chrome import add_ctxtnav, add_script, add_stylesheet, \
31                            INavigationContributor, ITemplateProvider
32from trac.web.href import Href
33from trac.wiki import wiki_to_html, WikiSystem
34
35__all__ = ['RevtreeModule']
36
37class RevtreeStore(object):
38    """User revtree properties"""
39   
40    FIELDS = ( 'revmin', 'revmax', 'period', 'branch', 'author',
41               'limits', 'showdel', 'style' )
42   
43    def __init__(self, env, authname, revspan, timebase, style):
44        """Initialize the instance with default values"""
45        self.env = env
46        self.values = {}
47        self.revrange = None
48        self.timerange = None
49        self.revspan = revspan
50        self.authname = (authname != 'anonymous') and authname or None
51        self.timebase = timebase
52        self['revmin'] = str(self.revspan[0])
53        self['revmax'] = str(self.revspan[1])
54        self['period'] = '31'
55        self['limits'] = 'limperiod'
56        self['style'] = style
57        self['branch'] = None
58        self['author'] = self.authname
59        self['showdel'] = None
60
61    def __getitem__(self, name):
62        """Getter (dictionary)"""
63        return self.values[name]
64   
65    def __setitem__(self, name, value):
66        """Setter (dictionary)"""
67        self.values[name] = value
68       
69    def load(self, session):
70        """Load user parameters from a previous session"""
71        for field in RevtreeStore.FIELDS:
72            key = 'revtree.%s' % field
73            if session.has_key(key):
74                self[field] = session.get(key, '')
75
76    def save(self, session):
77        """Store user parameters"""
78        for field in RevtreeStore.FIELDS:
79            key = 'revtree.%s' % field
80            if self[field]:
81                session[key] = str(self[field])
82            else:
83                if session.has_key(key):
84                    del session[key]
85                   
86    def clear(self, session):
87        """Remove all the revtree data from the user session"""
88        for key in filter(lambda k: k.startswith('revtree'), session.keys()):
89            del session[key]
90        self.env.log.debug('Revtree data removed from user session')
91           
92    def populate(self, values):
93        """Populate the store from the request"""
94        for name in filter(lambda v: v in RevtreeStore.FIELDS, values.keys()):
95            self[name] = values.get(name, '')
96        # checkboxes need to be postprocessed
97        self['showdel'] = values.has_key('showdel') and values['showdel']
98
99    def compute_range(self, timebase):
100        """Computes the range of revisions to show"""
101        self.revrange = self.revspan
102        if self['limits'] == 'limrev':
103            self.revrange = (int(self['revmin']), int(self['revmax']))
104        elif self['limits'] == 'limperiod':
105            period = int(self['period'])
106            if period:
107                now = timebase
108                self.timerange = (now-period*86400, now)
109
110    def can_be_rendered(self):
111        """Reports whether the revtree has enough items to produce a valid
112           representation, based on the revision range"""
113        if self.timerange:
114            return True
115        if self.revrange and (self.revrange[0] < self.revrange[1]):
116            return True
117        return False
118               
119    def get_values(self):
120        """Returns a dictionary of the stored values"""
121        return self.values       
122
123
124class FloatOption(Option):
125    """Descriptor for float configuration options.
126       Option for real number is missing in Trac
127    """
128   
129    def accessor(self, section, name, default=''):
130        """Return the value of the specified option as float.
131       
132        If the specified option can not be converted to a float, a
133        `ConfigurationError` exception is raised.
134       
135        Valid default input is a string or a float. Returns an float.
136        """
137        value = section.get(name, default)
138        if not value:
139            return 0.0
140        try:
141            return float(value)
142        except ValueError:
143            raise ConfigurationError('expected real number, got %s' % \
144                                     repr(value))
145
146class ChoiceOption(Option):
147    """Descriptor for choice configuration options."""
148
149    def __init__(self, section, name, default=None, choices='', doc=''):
150        Option.__init__(self, section, name, default, doc)
151        self.choices = filter(None, [c.strip() for c in choices.split(',')])
152
153    def accessor(self, section, name, default):
154        value = section.get(name, default)
155        if value not in self.choices:
156            raise ConfigurationError('expected a choice among "%s", got %s' % \
157                                     (', '.join(self.choices), repr(value)))
158        return value
159
160   
161class RevtreeModule(Component):
162    """Implements the revision tree feature"""
163   
164    implements(IPermissionRequestor, INavigationContributor, \
165               IRequestFilter, IRequestHandler, ITemplateProvider)
166             
167    # Timeline ranges
168    PERIODS = { 1: 'day', 2: '2 days', 3: '3 days', 5: '5 days', 7:'week',
169                14: 'fortnight', 31: 'month', 61: '2 months', 
170                91: 'quarter', 183: 'semester', 366: 'year', 0: 'all' }
171 
172    # Configuration Options
173    branchre = Option('revtree', 'branch_re',
174        r'^(?:(?P<branch>trunk|(?:branches|sandboxes|vendor)/'
175        r'(?P<branchname>[^/]+))|'
176        r'(?P<tag>tags/(?P<tagname>[^/]+)))(?:/(?P<path>.*))?$',
177        doc = """Regular expression to extract branches from paths""")
178   
179    abstime = BoolOption('revtree', 'abstime', 'true',
180        doc = """Timeline filters start on absolute time or on the youngest
181                 revision.""")
182
183    contexts = ListOption('revtree', 'contexts',
184        doc = """Navigation contexts where the Revtree item appears.
185                 If empty, the Revtree item appears in the main navigation
186                 bar.""")
187                 
188    trunks = ListOption('revtree', 'trunks',
189        doc = """Branches that are considered as trunks""")
190   
191    oldest = IntOption('revtree', 'revbase', '1',
192        doc = """Oldest revision to consider (older revisions are ignored)""")
193   
194    style = ChoiceOption('revtree', 'style', 'compact', 'compact,timeline',
195        doc = """Revtree style, 'compact' or 'timeline'""")
196       
197    scale = FloatOption('revtree', 'scale', '1',
198        doc = """Default rendering scale for the SVG graph""")
199       
200    # IPermissionRequestor methods
201
202    def get_permission_actions(self):
203        return ['REVTREE_VIEW']
204   
205    # INavigationContributor methods
206
207    def get_active_navigation_item(self, req):
208        return 'revtree'
209
210    def get_navigation_items(self, req):
211        if not req.perm.has_permission('REVTREE_VIEW'):
212            return
213        if self.contexts:
214            return
215        yield ('mainnav', 'revtree', 
216               tag.a('Rev Tree', href=req.href.revtree()))
217
218    # IRequestFilter methods
219
220    def pre_process_request(self, req, handler):
221        return handler
222
223    def post_process_request(self, req, template, data, content_type):
224        if req.perm.has_permission('REVTREE_VIEW'):
225            url_parts = filter(None, req.path_info.split(u'/'))
226            if url_parts and (url_parts[0] in self.contexts):
227                add_ctxtnav(req, 'Revtree' % self.contexts, 
228                            href=req.href.revtree())
229        return (template, data, content_type)
230
231    # IRequestHandler methods
232
233    def match_request(self, req):
234        match = re.match(r'/revtree(_log)?(?:/([^/]+))?', req.path_info)
235        if match:
236            if match.group(1):
237                req.args['logrev'] = match.group(2)
238            return True
239
240    def process_request(self, req):
241        req.perm.assert_permission('REVTREE_VIEW')
242           
243        if req.args.has_key('logrev'):
244            return self._process_log(req)
245        else:
246            return self._process_revtree(req)
247
248    # ITemplateProvider
249   
250    def get_htdocs_dirs(self):
251        """Return the absolute path of a directory containing additional
252        static resources (such as images, style sheets, etc).
253        """
254        from pkg_resources import resource_filename
255        return [('revtree', resource_filename(__name__, 'htdocs'))]
256   
257    def get_templates_dirs(self):
258        """Return the absolute path of the directory containing the provided
259        Genshi templates.
260        """
261        from pkg_resources import resource_filename
262        return [resource_filename(__name__, 'templates')]
263   
264    # end of interface implementation
265           
266    def __init__(self):
267        """Reads the configuration and run sanity checks"""
268        self.env.log.debug('Revtree RE: %s' % self.branchre)
269        self.bcre = re.compile(self.branchre)
270        self.rt = RevtreeSystem(self.env)
271
272    def _process_log(self, req):
273        """Handle AJAX log requests"""
274        try:
275            rev = int(req.args['logrev'])
276            repos = self.env.get_repository(req.authname)
277            chgset = repos.get_changeset(rev)
278            wikimsg = wiki_to_html(chgset.message, self.env, req, None, 
279                                   True, False)
280            data = {
281                'chgset': True,
282                'revision': rev,
283                'time': format_datetime(chgset.date),
284                'age': pretty_timedelta(chgset.date, None, 3600),
285                'author': chgset.author or 'anonymous',
286                'message': wikimsg, 
287            }
288            return 'revtree_log.html', {'log': data}, 'application/xhtml+xml'
289        except Exception, e:
290            raise TracError, "Invalid revision log request: %s" % e
291       
292    def _process_revtree(self, req):
293        """Handle revtree generation requests"""
294        tracrepos = self.env.get_repository()
295        youngest = int(tracrepos.get_youngest_rev())
296        oldest = max(self.oldest, int(tracrepos.get_oldest_rev()))
297        if self.abstime:
298            timebase = int(time.time())
299        else:
300            timebase = to_timestamp(tracrepos.get_changeset(youngest).date)
301        revstore = RevtreeStore(self.env, req.authname, \
302                                (oldest, youngest), 
303                                timebase, self.style)
304        if req.args.has_key('reset') and req.args['reset']:
305            revstore.clear(req.session)
306        else:
307            revstore.load(req.session)
308        if req.args:
309            revstore.populate(req.args)
310        revstore.compute_range(timebase)
311        data = revstore.get_values()
312               
313        try:
314            if not revstore.can_be_rendered():
315                raise EmptyRangeError
316            repos = Repository(self.env, req.authname)
317            repos.build(self.bcre, revstore.revrange, revstore.timerange)
318            (branches, authors) = \
319                self._select_parameters(repos, req, revstore)
320            svgrevtree = self.rt.get_revtree(repos, req)
321            if revstore['branch']:
322                sbranches = [revstore['branch']]
323                sbranches.extend(filter(lambda t: t not in sbranches, 
324                                        self.trunks))
325            else:
326                sbranches = None
327            sauthors = revstore['author'] and [revstore['author']] or None
328            if revstore['showdel']:
329                hidetermbranch = False
330            else:
331                hidetermbranch = True
332            svgrevtree.create(req, 
333                              revisions=revstore.revrange, 
334                              branches=sbranches, authors=sauthors, 
335                              hidetermbranch=hidetermbranch, 
336                              style=revstore['style'])
337            svgrevtree.build()
338            svgrevtree.render(self.scale*0.6)
339            style = req.href.chrome('revtree/css/revtree.css')
340            svgstyle = '<?xml-stylesheet href="%s" type="text/css"?>' % style
341            data.update({
342                'svg': Markup(unicode(str(svgrevtree), 'utf-8')),
343                'svgstyle': Markup(svgstyle)
344            })
345            # create and order the drop-down list content, starting with the
346            # global values
347            branches = repos.branches().keys()
348            authors = repos.authors()
349            # save the user parameters only if the tree can be rendered
350            revstore.save(req.session)
351        except EmptyRangeError:
352            data.update({'errormsg': \
353                         "Selected filters cannot render a revision tree"})
354            # restore default parameters
355            repos = Repository(self.env, req.authname)
356            repos.build(self.bcre, revrange=(oldest, youngest))
357            branches = repos.branches().keys()
358            authors = repos.authors()
359           
360        revrange = repos.revision_range()
361        revisions = self._get_ui_revisions((oldest, youngest), revrange)
362        branches.sort()
363        # prepend the trunks to the selected branches
364        for b in filter(lambda t: t not in branches, self.trunks):
365                branches.insert(0, b)
366        branches = filter(None, branches)
367        branches.insert(0, '')
368        authors.sort()
369        authors = filter(None, authors)
370        authors.insert(0, '')
371
372        dauthors = [dict(name=a, label=a or 'All') for a in authors]
373        dbranches = [dict(name=b, label=b or 'All') for b in branches]
374       
375        data.update({
376            'title': 'Revision Tree',
377            'periods': self._get_periods(),
378            'revmin': str(revrange[0]),
379            'revmax': str(revrange[1]),
380            'revisions': revisions,
381            'branches': dbranches,
382            'authors': dauthors
383        })
384                                                                               
385        # add javascript for AJAX tooltips
386        add_script(req, 'revtree/js/svgtip.js')
387        # add custom stylesheet
388        add_stylesheet(req, 'revtree/css/revtree.css')
389        return 'revtree.html', {'rt': data}, 'application/xhtml+xml'
390
391    def _get_periods(self):
392        """Generates a list of periods"""
393        periods = RevtreeModule.PERIODS
394        days = periods.keys()
395        days.sort()
396        return [dict(name=str(d), label=periods[d]) for d in days]
397
398    def _get_ui_revisions(self, revspan, revrange):
399        """Generates the list of displayable revisions"""
400        (revmin, revmax) = revspan
401        allrevisions = range(revmin, revmax+1)
402        allrevisions.sort()
403        revs = [c for c in allrevisions]
404        revs.reverse()
405        revisions = []
406        for rev in revs:
407            if len(revisions) > 50:
408                if int(rev)%100 and (rev not in revrange):
409                    continue
410            elif len(revisions) > 30:
411                if int(rev)%20 and (rev not in revrange):
412                    continue
413            elif len(revisions) > 10:
414                if int(rev)%10 and (rev not in revrange):
415                    continue
416            revisions.append(str(rev))
417        if revisions[-1] != str(revmin):
418            revisions.append(str(revmin))
419        return revisions
420
421    def _select_parameters(self, repos, req, revstore):
422        """Calculates the revisions/branches/authors to show as selectable
423           properties for the revtree generation"""
424        revs = [c for c in repos.changesets()]
425        revs.reverse()
426        brnames = [bn for bn in repos.branches().keys() \
427                      if bn not in self.trunks]
428        brnames.sort()
429        branches = []
430        authors = repos.authors()
431        vbranches = None
432        brfilter = revstore['branch']
433        authfilter = revstore['author']
434        for b in brnames:
435            if brfilter and brfilter != b:
436                continue
437            if authfilter and authfilter not in repos.branch(b).authors():
438                continue
439            branches.append(b)
440        if brfilter or authfilter:
441            vbranches = self.trunks
442            vbranches.extend(branches)
443        return (vbranches, authors)
Note: See TracBrowser for help on using the repository browser.