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
RevLine 
[1633]1# -*- coding: utf-8 -*-
2#
[3491]3# Copyright (C) 2006-2008 Emmanuel Blot <emmanuel.blot@free.fr>
[1633]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
[2109]19from genshi import Markup
20from genshi.builder import tag
[1694]21from revtree.api import EmptyRangeError, RevtreeSystem
22from revtree.model import Repository
[4012]23from trac.config import Option, IntOption, BoolOption, ListOption, \
24                        Section, ConfigurationError
[1633]25from trac.core import *
[1666]26from trac.perm import IPermissionRequestor
[1908]27from trac.util import TracError
[2564]28from trac.util.datefmt import format_datetime, pretty_timedelta, to_timestamp
[2920]29from trac.web import IRequestFilter, IRequestHandler
30from trac.web.chrome import add_ctxtnav, add_script, add_stylesheet, \
[1666]31                            INavigationContributor, ITemplateProvider
[1633]32from trac.web.href import Href
[1669]33from trac.wiki import wiki_to_html, WikiSystem
[1666]34
[1908]35__all__ = ['RevtreeModule']
[1633]36
37class RevtreeStore(object):
38    """User revtree properties"""
39   
[1908]40    FIELDS = ( 'revmin', 'revmax', 'period', 'branch', 'author',
41               'limits', 'showdel', 'style' )
42   
[1652]43    def __init__(self, env, authname, revspan, timebase, style):
[1633]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
[1908]50        self.authname = (authname != 'anonymous') and authname or None
[1633]51        self.timebase = timebase
52        self['revmin'] = str(self.revspan[0])
53        self['revmax'] = str(self.revspan[1])
[2832]54        self['period'] = '31'
[1633]55        self['limits'] = 'limperiod'
[1908]56        self['style'] = style
57        self['branch'] = None
58        self['author'] = self.authname
59        self['showdel'] = None
[1633]60
[1908]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
[1633]68       
69    def load(self, session):
70        """Load user parameters from a previous session"""
[1908]71        for field in RevtreeStore.FIELDS:
[1633]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"""
[1908]78        for field in RevtreeStore.FIELDS:
[1633]79            key = 'revtree.%s' % field
80            if self[field]:
[2832]81                session[key] = str(self[field])
[1633]82            else:
83                if session.has_key(key):
84                    del session[key]
[1908]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           
[1633]92    def populate(self, values):
[1908]93        """Populate the store from the request"""
94        for name in filter(lambda v: v in RevtreeStore.FIELDS, values.keys()):
[1633]95            self[name] = values.get(name, '')
[1908]96        # checkboxes need to be postprocessed
[2832]97        self['showdel'] = values.has_key('showdel') and values['showdel']
[1633]98
[1711]99    def compute_range(self, timebase):
[1633]100        """Computes the range of revisions to show"""
[1707]101        self.revrange = self.revspan
[1633]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:
[1711]107                now = timebase
[1633]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
[1908]118               
119    def get_values(self):
120        """Returns a dictionary of the stored values"""
121        return self.values       
[1633]122
123
[4012]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   
[1633]161class RevtreeModule(Component):
162    """Implements the revision tree feature"""
163   
164    implements(IPermissionRequestor, INavigationContributor, \
[2920]165               IRequestFilter, IRequestHandler, ITemplateProvider)
[4012]166             
167    # Timeline ranges
[1908]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' }
[4012]171 
172    # Configuration Options
173    branchre = Option('revtree', 'branch_re',
[4024]174        r'^(?:(?P<branch>trunk|(?:branches|sandboxes|vendor)/'
175        r'(?P<branchname>[^/]+))|'
176        r'(?P<tag>tags/(?P<tagname>[^/]+)))(?:/(?P<path>.*))?$',
[4012]177        doc = """Regular expression to extract branches from paths""")
[1633]178   
[4012]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       
[1633]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
[2920]213        if self.contexts:
214            return
[2109]215        yield ('mainnav', 'revtree', 
216               tag.a('Rev Tree', href=req.href.revtree()))
[1633]217
[2920]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'):
[4012]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())
[2920]229        return (template, data, content_type)
230
[1633]231    # IRequestHandler methods
232
233    def match_request(self, req):
[1911]234        match = re.match(r'/revtree(_log)?(?:/([^/]+))?', req.path_info)
[1666]235        if match:
236            if match.group(1):
[1911]237                req.args['logrev'] = match.group(2)
[1666]238            return True
[1633]239
240    def process_request(self, req):
241        req.perm.assert_permission('REVTREE_VIEW')
242           
[1911]243        if req.args.has_key('logrev'):
[1666]244            return self._process_log(req)
245        else:
246            return self._process_revtree(req)
[1694]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
[4012]259        Genshi templates.
[1694]260        """
261        from pkg_resources import resource_filename
262        return [resource_filename(__name__, 'templates')]
263   
264    # end of interface implementation
[1666]265           
[1694]266    def __init__(self):
267        """Reads the configuration and run sanity checks"""
[4697]268        self.env.log.debug('Revtree RE: %s' % self.branchre)
[4012]269        self.bcre = re.compile(self.branchre)
[2147]270        self.rt = RevtreeSystem(self.env)
[1694]271
[1666]272    def _process_log(self, req):
273        """Handle AJAX log requests"""
274        try:
[1911]275            rev = int(req.args['logrev'])
[1666]276            repos = self.env.get_repository(req.authname)
277            chgset = repos.get_changeset(rev)
[1669]278            wikimsg = wiki_to_html(chgset.message, self.env, req, None, 
279                                   True, False)
[1908]280            data = {
[1669]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            }
[1908]288            return 'revtree_log.html', {'log': data}, 'application/xhtml+xml'
[1669]289        except Exception, e:
290            raise TracError, "Invalid revision log request: %s" % e
[1666]291       
292    def _process_revtree(self, req):
293        """Handle revtree generation requests"""
[1711]294        tracrepos = self.env.get_repository()
295        youngest = int(tracrepos.get_youngest_rev())
[2841]296        oldest = max(self.oldest, int(tracrepos.get_oldest_rev()))
[1711]297        if self.abstime:
298            timebase = int(time.time())
299        else:
[2564]300            timebase = to_timestamp(tracrepos.get_changeset(youngest).date)
[1633]301        revstore = RevtreeStore(self.env, req.authname, \
[2841]302                                (oldest, youngest), 
[1711]303                                timebase, self.style)
[1908]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)
[1711]310        revstore.compute_range(timebase)
[1908]311        data = revstore.get_values()
312               
[1633]313        try:
314            if not revstore.can_be_rendered():
[1694]315                raise EmptyRangeError
[1633]316            repos = Repository(self.env, req.authname)
317            repos.build(self.bcre, revstore.revrange, revstore.timerange)
[1707]318            (branches, authors) = \
[1633]319                self._select_parameters(repos, req, revstore)
[2147]320            svgrevtree = self.rt.get_revtree(repos, req)
[1908]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
[2832]328            if revstore['showdel']:
329                hidetermbranch = False
330            else:
331                hidetermbranch = True
[1908]332            svgrevtree.create(req, 
333                              revisions=revstore.revrange, 
334                              branches=sbranches, authors=sauthors, 
[2832]335                              hidetermbranch=hidetermbranch, 
[1908]336                              style=revstore['style'])
[1633]337            svgrevtree.build()
[1669]338            svgrevtree.render(self.scale*0.6)
[1916]339            style = req.href.chrome('revtree/css/revtree.css')
340            svgstyle = '<?xml-stylesheet href="%s" type="text/css"?>' % style
341            data.update({
[2178]342                'svg': Markup(unicode(str(svgrevtree), 'utf-8')),
[1916]343                'svgstyle': Markup(svgstyle)
344            })
[1633]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)
[1694]351        except EmptyRangeError:
[1908]352            data.update({'errormsg': \
353                         "Selected filters cannot render a revision tree"})
[1633]354            # restore default parameters
355            repos = Repository(self.env, req.authname)
[2841]356            repos.build(self.bcre, revrange=(oldest, youngest))
[1633]357            branches = repos.branches().keys()
358            authors = repos.authors()
359           
360        revrange = repos.revision_range()
[2841]361        revisions = self._get_ui_revisions((oldest, youngest), revrange)
[1908]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, '')
[1633]371
[1908]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        })
[1633]384                                                                               
[1908]385        # add javascript for AJAX tooltips
386        add_script(req, 'revtree/js/svgtip.js')
387        # add custom stylesheet
[1633]388        add_stylesheet(req, 'revtree/css/revtree.css')
[1908]389        return 'revtree.html', {'rt': data}, 'application/xhtml+xml'
[1633]390
391    def _get_periods(self):
392        """Generates a list of periods"""
[1908]393        periods = RevtreeModule.PERIODS
394        days = periods.keys()
[1633]395        days.sort()
[1908]396        return [dict(name=str(d), label=periods[d]) for d in days]
[1633]397
[1707]398    def _get_ui_revisions(self, revspan, revrange):
[1633]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:
[1908]407            if len(revisions) > 50:
408                if int(rev)%100 and (rev not in revrange):
409                    continue
410            elif len(revisions) > 30:
[1707]411                if int(rev)%20 and (rev not in revrange):
[1633]412                    continue
413            elif len(revisions) > 10:
[1707]414                if int(rev)%10 and (rev not in revrange):
[1633]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):
[1675]422        """Calculates the revisions/branches/authors to show as selectable
423           properties for the revtree generation"""
[1633]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
[1908]432        brfilter = revstore['branch']
433        authfilter = revstore['author']
[1633]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)
[1707]443        return (vbranches, authors)
Note: See TracBrowser for help on using the repository browser.