Christian Boos wrote:
> Peter Dimov wrote:
>> The RequestHandler idea is great! The definition of RequestHandler
>> should be moved elsewhere though. You'd know better than me which module
>> is most suitable.
>>   
> 
> Well, ideally it should go in trac/web/api.py, next to the IRequestHandler.
> Hm, probably a more distinctive name would be useful then, like 
> RequestProcessor.

I did that.

> We usually use posts to trigger some side-effect in the database, so 
> after having processed the POST we usually send a redirect so that the 
> client GETs the latest version of the resource. 

OK, that makes perfect sense!

> Ok, so I'd like to restart the sandbox/vc-refactoring branch, and I'd 
> like that Peter could commit there as well.

Who should I talk to regarding permission rights?

> The idea would be to get that last patch there, then move the 
> RequestHandler to trac.web.api.RequestProcessor and do a similar 
> refactoring for the Browser and Changeset modules. 

Attached is a patch containing Christian's modifications of the
LogRequest refactoring + RequestHandler moved to
trac.web.api.RequestProcessor + refactored BrowserModule. Please have a
look and comment!

I'm not sure why the colorization doesn't work for me but it didn't work
even before the refactoring. Any idea?

Just to make sure I got it right, what is the semantic of
Node.last_modified? Is it the modification time of the entry [EMAIL PROTECTED]
If that is the case then we don't need to do the lookup
changes[node.rev].date when sorting the entries but we can use
node.last_modified instead... seems to work at least (see
_date_file_order()).


> This cleanup would be
> beneficial to 0.11 and will enable Peter to become familiar with the 
> existing code; Peter who has already volunteered for providing us with 
> the perfect versioncontrol API for 0.12 (as I understood it, right? ;-) ).

:-) volunteer I do and we'll definitely need to discuss here on the list
what "perfect" means ;-)


Regards,

Peter

--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups "Trac 
Development" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/trac-dev?hl=en
-~----------~----~----~----~------~----~------~--~---

Index: trac/versioncontrol/templates/revisionlog.html
===================================================================
--- trac/versioncontrol/templates/revisionlog.html      (revision 4941)
+++ trac/versioncontrol/templates/revisionlog.html      (working copy)
@@ -30,7 +30,6 @@
 
       <form id="prefs" action="" method="get">
         <div>
-          <input type="hidden" name="action" value="$mode" />
           <div class="choice">
             <fieldset>
               <legend>Revision Log Mode:</legend>
Index: trac/versioncontrol/web_ui/browser.py
===================================================================
--- trac/versioncontrol/web_ui/browser.py       (revision 4941)
+++ trac/versioncontrol/web_ui/browser.py       (working copy)
@@ -33,7 +33,7 @@
 from trac.util.datefmt import http_date
 from trac.util.html import escape, html, Markup
 from trac.util.text import shorten_line
-from trac.web import IRequestHandler, RequestDone
+from trac.web import IRequestHandler, RequestProcessor, RequestDone
 from trac.web.chrome import add_link, add_script, add_stylesheet, \
                             INavigationContributor
 from trac.wiki.api import IWikiSyntaxProvider
@@ -294,175 +294,8 @@
             return True
 
     def process_request(self, req):
-        go_to_preselected = req.args.get('preselected')
-        if go_to_preselected:
-            req.redirect(go_to_preselected)
-            
-        path = req.args.get('path', '/')
-        rev = req.args.get('rev', None)
-        order = req.args.get('order', None)
-        desc = req.args.get('desc', None)
+        return BrowserRequest(self, req).process()
 
-        # Find node for the requested path/rev
-        repos = self.env.get_repository(req.authname)
-        if rev:
-            rev = repos.normalize_rev(rev)
-        # If `rev` is `None`, we'll try to reuse `None` consistently,
-        # as a special shortcut to the latest revision.
-        rev_or_latest = rev or repos.youngest_rev
-        node = get_existing_node(req, repos, path, rev_or_latest)
-
-        context = Context(self.env, req, 'source', path,
-                          version=node.created_rev) # resource=node
-
-        path_links = get_path_links(req.href, path, rev, order, desc)
-        if len(path_links) > 1:
-            add_link(req, 'up', path_links[-2]['href'], 'Parent directory')
-
-        data = {
-            'context': context,
-            'path': path, 'rev': node.rev, 'stickyrev': rev,
-            'created_path': node.created_path,
-            'created_rev': node.created_rev,
-            'properties': self.render_properties('browser', context,
-                                                 node.get_properties()),
-            'path_links': path_links,
-            'dir': node.isdir and self._render_dir(req, repos, node, rev),
-            'file': node.isfile and self._render_file(context, repos,
-                                                      node, rev),
-            'quickjump_entries': list(repos.get_quickjump_entries(rev)),
-            'wiki_format_messages':
-            self.config['changeset'].getbool('wiki_format_messages')
-        }
-        add_stylesheet(req, 'common/css/browser.css')
-        return 'browser.html', data, None
-
-    # Internal methods
-
-    def _render_dir(self, req, repos, node, rev=None):
-        req.perm.require('BROWSER_VIEW')
-
-        # Entries metadata
-        entries = list(node.get_entries())
-        changes = get_changes(repos, [i.rev for i in entries])
-
-        if rev:
-            newest = repos.get_changeset(rev).date
-        else:
-            newest = datetime.now(req.tz)
-
-        # Color scale for the age column
-        timerange = custom_colorizer = None
-        if self.color_scale:
-            timerange = TimeRange(newest)
-            for c in changes.values():
-                if c:
-                    timerange.insert(c.date)
-            custom_colorizer = self.get_custom_colorizer()
-
-        # Ordering of entries
-        order = req.args.get('order', 'name').lower()
-        desc = req.args.has_key('desc')
-
-        if order == 'date':
-            def file_order(a):
-                return changes[a.rev].date
-        elif order == 'size':
-            def file_order(a):
-                return (a.content_length,
-                        embedded_numbers(a.name.lower()))
-        else:
-            def file_order(a):
-                return embedded_numbers(a.name.lower())
-
-        dir_order = desc and 1 or -1
-
-        def browse_order(a):
-            return a.isdir and dir_order or 0, file_order(a)
-        entries = sorted(entries, key=browse_order, reverse=desc)
-
-        # ''Zip Archive'' alternate link
-        patterns = self.downloadable_paths
-        if node.path and patterns and \
-               filter(None, [fnmatchcase(node.path, p) for p in patterns]):
-            zip_href = req.href.changeset(rev or repos.youngest_rev, node.path,
-                                          old=rev, old_path='/', format='zip')
-            add_link(req, 'alternate', zip_href, 'Zip Archive',
-                     'application/zip', 'zip')
-
-        return {'order': order, 'desc': desc and 1 or None,
-                'entries': entries, 'changes': changes,
-                'timerange': timerange, 'colorize_age': custom_colorizer}
-
-    def _render_file(self, context, repos, node, rev=None):
-        req = context.req
-        req.perm.require('FILE_VIEW')
-
-        mimeview = Mimeview(self.env)
-
-        # MIME type detection
-        content = node.get_content()
-        chunk = content.read(CHUNK_SIZE)
-        mime_type = node.content_type
-        if not mime_type or mime_type == 'application/octet-stream':
-            mime_type = mimeview.get_mimetype(node.name, chunk) or \
-                        mime_type or 'text/plain'
-
-        # Eventually send the file directly
-        format = req.args.get('format')
-        if format in ('raw', 'txt'):
-            req.send_response(200)
-            req.send_header('Content-Type',
-                            format == 'txt' and 'text/plain' or mime_type)
-            req.send_header('Content-Length', node.content_length)
-            req.send_header('Last-Modified', http_date(node.last_modified))
-            req.end_headers()
-
-            while 1:
-                if not chunk:
-                    raise RequestDone
-                req.write(chunk)
-                chunk = content.read(CHUNK_SIZE)
-        else:
-            # The changeset corresponding to the last change on `node` 
-            # is more interesting than the `rev` changeset.
-            changeset = repos.get_changeset(node.rev)
-
-            # add ''Plain Text'' alternate link if needed
-            if not is_binary(chunk) and mime_type != 'text/plain':
-                plain_href = req.href.browser(node.path, rev=rev, format='txt')
-                add_link(req, 'alternate', plain_href, 'Plain Text',
-                         'text/plain')
-
-            # add ''Original Format'' alternate link (always)
-            raw_href = req.href.export(rev or repos.youngest_rev, node.path)
-            add_link(req, 'alternate', raw_href, 'Original Format', mime_type)
-
-            self.log.debug("Rendering preview of node [EMAIL PROTECTED] with 
mime-type %s"
-                           % (node.name, str(rev), mime_type))
-
-            del content # the remainder of that content is not needed
-
-            add_stylesheet(req, 'common/css/code.css')
-
-            annotations = ['lineno']
-            force_source = False
-            if 'annotate' in req.args:
-                force_source = True
-                annotations.insert(0, 'blame')
-            preview_data = mimeview.preview_data(context, node.get_content(),
-                                                 node.get_content_length(),
-                                                 mime_type, node.created_path,
-                                                 raw_href,
-                                                 annotations=annotations,
-                                                 force_source=force_source)
-            return {
-                'changeset': changeset,
-                'size': node.content_length ,
-                'preview': preview_data,
-                'annotate': force_source,
-                }
-
     # public methods
     
     def render_properties(self, mode, context, props):
@@ -629,3 +462,229 @@
             blame_col.append(anchor)
             self.prev_chgset = chgset
         row.append(blame_col)
+
+class BrowserRequest(RequestProcessor):
+    """Completely handles a reqest to the BrowserModule."""
+    
+    def __init__(self, module, req):
+        """Parses the data from req into member variables for use later."""
+        RequestProcessor.__init__(self, module.env, req)
+
+        self.module = module
+        self.req = req
+        self.go_to_preselected = req.args.get('preselected')
+        if self.go_to_preselected:
+            req.redirect(go_to_preselected)
+            
+        self.path = req.args.get('path', '/')
+        self.rev = req.args.get('rev', None)
+        self.order = req.args.get('order', 'name').lower()
+        self.desc = req.args.has_key('desc')
+
+        # Find node for the requested path/rev
+        self.repos = self.module.env.get_repository(req.authname)
+        if self.rev:
+            self.rev = self.repos.normalize_rev(self.rev)
+    
+    def render_view(self):
+        node = self.get_node()
+        data = self.get_template_context(node)
+
+        add_stylesheet(self.req, 'common/css/browser.css')
+        return 'browser.html', data, None
+        
+    def get_node(self):
+        # If `rev` is `None`, we'll try to reuse `None` consistently,
+        # as a special shortcut to the latest revision.
+        rev_or_latest = self.rev or self.repos.youngest_rev
+        node = get_existing_node(self.req, self.repos, self.path, \
+                                 rev_or_latest)
+        self.context = Context(self.module.env, self.req, 'source', self.path,
+                               version=node.created_rev) # resource=node
+        return node
+        
+    def get_template_context(self, node):
+        """Prepares the template data to be returned as a response."""
+
+        path_links = get_path_links(self.req.href, self.path, self.rev, \
+                                    self.order, self.desc)
+        if len(path_links) > 1:
+            add_link(self.req, 'up', path_links[-2]['href'], 'Parent 
directory')
+
+        return {
+            'context': self.context,
+            'path': self.path, 'rev': node.rev, 'stickyrev': self.rev,
+            'created_path': node.created_path,
+            'created_rev': node.created_rev,
+            'properties': self.module.render_properties('browser', 
self.context,
+                                                        node.get_properties()),
+            'path_links': path_links,
+            'dir': node.isdir and self._render_dir(node),
+            'file': node.isfile and self._render_file(node),
+            'quickjump_entries': 
+            list(self.repos.get_quickjump_entries(self.rev)),
+            'wiki_format_messages':
+            self.module.config['changeset'].getbool('wiki_format_messages')
+        }
+
+    def _render_dir(self, node):
+        self.req.perm.require('BROWSER_VIEW')
+
+        entries = list(node.get_entries())
+        changes = self._get_changes(entries)
+        entries = self._sort_entries(entries, changes)
+        timerange, custom_colorizer = self._get_colorization(changes)
+        self._add_downloadable_link(node.path)
+
+        return {'order': self.order, 'desc': self.desc and 1 or None,
+                'entries': entries, 'changes': changes,
+                'timerange': timerange, 'colorize_age': custom_colorizer}
+
+    def _date_file_order(self, changes):
+        """Nodes sorting helper."""
+        #return lambda a: changes[a.rev].date
+        return a.last_modified
+        
+    def _size_file_order(self, a):
+        """Nodes sorting helper."""
+        return (a.content_length, embedded_numbers(a.name.lower()))
+    
+    def _name_file_order(self, a):
+        """Nodes sorting helper."""
+        return embedded_numbers(a.name.lower())
+
+    def _sort_entries(self, entries, changes):
+        """Returns a correctly ordered list of node's entries."""
+
+        if self.order == 'date':
+            # \todo After the version control refactoring we will not need to 
+            # pass 'changes' as argument because we would be able to access 
the 
+            # node modification date via node:
+            file_order = self._date_file_order#(changes)
+        elif self.order == 'size':
+            file_order = self._size_file_order
+        else:
+            file_order = self._name_file_order
+
+        dir_order = self.desc and 1 or -1
+        def browse_order(a):
+            return a.isdir and dir_order or 0, file_order(a)
+
+        return sorted(entries, key=browse_order, reverse=self.desc)
+
+    def _get_changes(self, entries):
+        return get_changes(self.repos, [i.rev for i in entries])
+
+    def _get_colorization(self, changes):
+        """Returns colorization data for the given changes."""
+        
+        if self.rev:
+            newest = self.repos.get_changeset(self.rev).date
+        else:
+            newest = datetime.now(self.req.tz)
+
+        # Color scale for the age column
+        timerange = custom_colorizer = None
+        if self.module.color_scale:
+            timerange = TimeRange(newest)
+            for c in changes.values():
+                if c:
+                    timerange.insert(c.date)
+            custom_colorizer = self.module.get_custom_colorizer()
+        
+        return timerange, custom_colorizer
+
+    def _add_downloadable_link(self, path):
+        """Add alternative link for zip download."""
+        
+        patterns = self.module.downloadable_paths
+        if path and patterns and \
+               filter(None, [fnmatchcase(path, p) for p in patterns]):
+            zip_href = self.req.href.changeset(
+                                    self.rev or self.repos.youngest_rev, path,
+                                    old=self.rev, old_path='/', format='zip')
+            add_link(self.req, 'alternate', zip_href, 'Zip Archive',
+                     'application/zip', 'zip')
+
+    def _render_file(self, node):
+        """Either sends the file contents in raw format or renders it in a 
+        template."""
+        
+        req = self.context.req
+        req.perm.require('FILE_VIEW')
+
+        mimeview, mime_type, content, chunk = self._get_mime_data (node)
+        format = req.args.get('format')
+        if format in ('raw', 'txt'):
+            self._send_file_raw (req, node, format, mime_type, content, chunk)
+        else:
+            del content # the remainder of that content is not needed
+            return self._render_file_content(req, node, mimeview, \
+                                             mime_type, chunk)
+
+    def _get_mime_data(self, node):
+        mimeview = Mimeview(self.module.env)
+        # MIME type detection
+        content = node.get_content()
+        chunk = content.read(CHUNK_SIZE)
+        mime_type = node.content_type
+        if not mime_type or mime_type == 'application/octet-stream':
+            mime_type = mimeview.get_mimetype(node.name, chunk) or \
+                        mime_type or 'text/plain'
+        return mimeview, mime_type, content, chunk
+
+    def _send_file_raw(self, req, node, format, mime_type, content, chunk):
+        """Sends the file contents in raw format and raises RequestDone."""
+        
+        req.send_response(200)
+        req.send_header('Content-Type',
+                        format == 'txt' and 'text/plain' or mime_type)
+        req.send_header('Content-Length', node.content_length)
+        req.send_header('Last-Modified', http_date(node.last_modified))
+        req.end_headers()
+
+        while 1:
+            if not chunk:
+                raise RequestDone
+            req.write(chunk)
+            chunk = content.read(CHUNK_SIZE)
+
+    def _render_file_content(self, req, node, mimeview, mime_type, chunk):
+        """Returns the rendered file content."""
+        
+        # The changeset corresponding to the last change on `node` 
+        # is more interesting than the `rev` changeset.
+        changeset = self.repos.get_changeset(node.rev)
+
+        # add ''Plain Text'' alternate link if needed
+        if not is_binary(chunk) and mime_type != 'text/plain':
+            plain_href = req.href.browser(node.path, rev=self.rev, 
format='txt')
+            add_link(req, 'alternate', plain_href, 'Plain Text',
+                     'text/plain')
+
+        # add ''Original Format'' alternate link (always)
+        raw_href = req.href.export(self.rev or self.repos.youngest_rev, 
node.path)
+        add_link(req, 'alternate', raw_href, 'Original Format', mime_type)
+
+        self.module.log.debug("Rendering preview of node [EMAIL PROTECTED] 
with mime-type %s"
+                              % (node.name, str(self.rev), mime_type))
+
+        add_stylesheet(req, 'common/css/code.css')
+
+        annotations = ['lineno']
+        force_source = False
+        if 'annotate' in req.args:
+            force_source = True
+            annotations.insert(0, 'blame')
+        preview_data = mimeview.preview_data(self.context, node.get_content(),
+                                             node.get_content_length(),
+                                             mime_type, node.created_path,
+                                             raw_href,
+                                             annotations=annotations,
+                                             force_source=force_source)
+        return {
+            'changeset': changeset,
+            'size': node.content_length ,
+            'preview': preview_data,
+            'annotate': force_source,
+            }
Index: trac/versioncontrol/web_ui/log.py
===================================================================
--- trac/versioncontrol/web_ui/log.py   (revision 4941)
+++ trac/versioncontrol/web_ui/log.py   (working copy)
@@ -30,11 +30,12 @@
 from trac.versioncontrol import Changeset
 from trac.versioncontrol.web_ui.changeset import ChangesetModule
 from trac.versioncontrol.web_ui.util import *
-from trac.web import IRequestHandler
+from trac.web import IRequestHandler, RequestProcessor
 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor, \
                             Chrome
 from trac.wiki import IWikiSyntaxProvider, WikiParser 
 
+
 class LogModule(Component):
 
     implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
@@ -69,175 +70,8 @@
     def process_request(self, req):
         req.perm.assert_permission('LOG_VIEW')
 
-        mode = req.args.get('mode', 'stop_on_copy')
-        path = req.args.get('path', '/')
-        rev = req.args.get('rev')
-        stop_rev = req.args.get('stop_rev')
-        revs = req.args.get('revs', rev)
-        format = req.args.get('format')
-        verbose = req.args.get('verbose')
-        limit = int(req.args.get('limit') or self.default_log_limit)
+        return LogRequest(self, req).process()
 
-        repos = self.env.get_repository(req.authname)
-        normpath = repos.normalize_path(path)
-        revranges = None
-        rev = revs
-        if revs:
-            try:
-                revranges = Ranges(revs)
-                rev = revranges.b
-            except ValueError:
-                pass
-        rev = unicode(repos.normalize_rev(rev))    
-        path_links = get_path_links(req.href, path, rev)
-        if path_links:
-            add_link(req, 'up', path_links[-1]['href'], 'Parent directory')
-
-        # The `history()` method depends on the mode:
-        #  * for ''stop on copy'' and ''follow copies'', it's `Node.history()`
-        #    unless explicit ranges have been specified
-        #  * for ''show only add, delete'' we're using
-        #   `Repository.get_path_history()` 
-        if mode == 'path_history':
-            rev = revranges.b
-            def history(limit):
-                for h in repos.get_path_history(path, rev, limit):
-                    yield h
-        else:
-            if not revranges or revranges.a == revranges.b:
-                history = get_existing_node(req, repos, path, rev).get_history
-            else:
-                def history(limit):
-                    prevpath = path
-                    ranges = list(revranges.pairs)
-                    ranges.reverse()
-                    for (a,b) in ranges:
-                        while b >= a:
-                            rev = repos.normalize_rev(b)
-                            node = get_existing_node(req, repos, prevpath, rev)
-                            node_history = list(node.get_history(2))
-                            p, rev, chg = node_history[0]
-                            if rev < a:
-                                yield (p, rev, None) # separator
-                                break
-                            yield node_history[0]
-                            prevpath = node_history[-1][0] # follow copy
-                            b = rev-1
-                            if b < a and len(node_history) > 1:
-                                p, rev, chg = node_history[1]
-                                yield (p, rev, None)
-
-        # -- retrieve history, asking for limit+1 results
-        info = []
-        depth = 1
-        fix_deleted_rev = False
-        previous_path = normpath
-        for old_path, old_rev, old_chg in history(limit+1):
-            if fix_deleted_rev:
-                fix_deleted_rev['existing_rev'] = old_rev
-                fix_deleted_rev = False
-            if stop_rev and repos.rev_older_than(old_rev, stop_rev):
-                break
-            old_path = repos.normalize_path(old_path)
-
-            item = {
-                'path': old_path, 'rev': old_rev, 'existing_rev': old_rev,
-                'change': old_chg, 'depth': depth,
-            }
-            
-            if old_chg == Changeset.DELETE:
-                fix_deleted_rev = item
-            if not (mode == 'path_history' and old_chg == Changeset.EDIT):
-                info.append(item)
-            if old_path and old_path != previous_path \
-               and not (mode == 'path_history' and old_path == normpath):
-                depth += 1
-                item['depth'] = depth
-                item['copyfrom_path'] = old_path
-                if mode == 'stop_on_copy':
-                    break
-            if len(info) > limit: # we want limit+1 entries
-                break
-            previous_path = old_path
-        if info == []:
-            # FIXME: we should send a 404 error here
-            raise TracError("The file or directory '%s' doesn't exist "
-                            "at revision %s or at any previous revision."
-                            % (path, rev), 'Nonexistent path')
-
-        def make_log_href(path, **args):
-            link_rev = rev
-            if rev == str(repos.youngest_rev):
-                link_rev = None
-            params = {'rev': link_rev, 'mode': mode, 'limit': limit}
-            params.update(args)
-            if verbose:
-                params['verbose'] = verbose
-            return req.href.log(path, **params)
-
-        if len(info) == limit+1: # limit+1 reached, there _might_ be some more
-            next_rev = info[-1]['rev']
-            next_path = info[-1]['path']
-            add_link(req, 'next', make_log_href(next_path, rev=next_rev),
-                     'Revision Log (restarting at %s, rev. %s)'
-                     % (next_path, next_rev))
-            # only show fully 'limit' results, use `change == None` as a marker
-            info[-1]['change'] = None
-        
-        revs = [i['rev'] for i in info]
-        changes = get_changes(repos, revs)
-        extra_changes = {}
-        email_map = {}
-        
-        if format == 'rss':
-            # Get the email addresses of all known users
-            if Chrome(self.env).show_email_addresses:
-                for username,name,email in self.env.get_known_users():
-                    if email:
-                        email_map[username] = email
-        elif format == 'changelog':
-            for rev in revs:
-                changeset = changes[rev]
-                cs = {}
-                cs['message'] = wrap(changeset.message, 70,
-                                     initial_indent='\t',
-                                     subsequent_indent='\t')
-                files = []
-                actions = []
-                for path, kind, chg, bpath, brev in changeset.get_changes():
-                    files.append(chg == Changeset.DELETE and bpath or path)
-                    actions.append(chg)
-                cs['files'] = files
-                cs['actions'] = actions
-                extra_changes[rev] = cs
-        data = {
-            'context': Context(self.env, req, 'source', path),
-            'path': path, 'rev': rev, 'stop_rev': stop_rev,
-            'mode': mode, 'verbose': verbose,
-            'path_links': path_links, 'limit' : limit,
-            'items': info, 'changes': changes,
-            'email_map': email_map, 'extra_changes': extra_changes,
-            'wiki_format_messages':
-            self.config['changeset'].getbool('wiki_format_messages')
-            }
-
-        if req.args.get('format') == 'changelog':
-            return 'revisionlog.txt', data, 'text/plain'
-        elif req.args.get('format') == 'rss':
-            return 'revisionlog.rss', data, 'application/rss+xml'
-
-        add_stylesheet(req, 'common/css/diff.css')
-        add_stylesheet(req, 'common/css/browser.css')
-
-        rss_href = make_log_href(path, format='rss', stop_rev=stop_rev)
-        add_link(req, 'alternate', rss_href, 'RSS Feed', 'application/rss+xml',
-                 'rss')
-        changelog_href = make_log_href(path, format='changelog',
-                                       stop_rev=stop_rev)
-        add_link(req, 'alternate', changelog_href, 'ChangeLog', 'text/plain')
-
-        return 'revisionlog.html', data, None
-
     # IWikiSyntaxProvider methods
 
     REV_RANGE = r"(?:%s|%s)" % (Ranges.RE_STR, ChangesetModule.CHANGESET_ID)
@@ -304,4 +138,226 @@
             ranges = ''.join([str(rev)+sep for rev, sep in zip(revs, seps)])
             revranges = Ranges(ranges)
         return str(revranges) or None
-               
+
+
+class LogRequest(RequestProcessor):
+    """Completely handles a request to the LogModule."""
+    
+    def __init__(self, module, req):
+        """Parses the data from req into member variables for use later."""
+        RequestProcessor.__init__(self, module.env, req)
+        
+        self.module = module
+        self.mode = req.args.get('mode', 'stop_on_copy')
+        self.path = req.args.get('path', '/')
+        self.rev = req.args.get('rev')
+        self.stop_rev = req.args.get('stop_rev')
+        revs = req.args.get('revs', self.rev)
+        self.format = req.args.get('format')
+        self.verbose = req.args.get('verbose')
+        self.limit = int(req.args.get('limit') or
+                         self.module.default_log_limit)
+
+        self.repos = self.module.env.get_repository(req.authname)
+        self.normpath = self.repos.normalize_path(self.path)
+        self.revranges = None
+        self.rev = revs
+        if revs:
+            try:
+                self.revranges = Ranges(revs)
+                self.rev = self.revranges.b
+            except ValueError:
+                pass
+        self.rev = unicode(self.repos.normalize_rev(self.rev))
+
+    def render_view(self):
+        history = self.get_history_provider()
+        items = self.get_log_items(history)
+        data = self.get_template_context(items)
+        
+        if len(items) == self.limit+1:
+            # limit+1 reached, there _might_ be some more
+            next_rev = items[-1]['rev']
+            next_path = items[-1]['path']
+            add_link(self.req, 'next',
+                     self._make_log_href(next_path, rev=next_rev),
+                     'Revision Log (restarting at %s, rev. %s)' %
+                     (next_path, next_rev))
+            # only show fully 'limit' results, use `change == None` as a marker
+            items[-1]['change'] = None
+
+        if self.format == 'changelog':
+            return 'revisionlog.txt', data, 'text/plain'
+        elif self.format == 'rss':
+            return 'revisionlog.rss', data, 'application/rss+xml'
+
+        add_stylesheet(self.req, 'common/css/diff.css')
+        add_stylesheet(self.req, 'common/css/browser.css')
+
+        rss_href = self._make_log_href(self.path, format='rss',
+                                       stop_rev=self.stop_rev)
+        add_link(self.req, 'alternate', rss_href, 'RSS Feed',
+                 'application/rss+xml', 'rss')
+        changelog_href = self._make_log_href(self.path, format='changelog',
+                                             stop_rev=self.stop_rev)
+        add_link(self.req, 'alternate', changelog_href, 'ChangeLog',
+                 'text/plain')
+        
+        return 'revisionlog.html', data, None
+
+    def _get_path_history(self, limit):
+        """A generator yielding the changes history of a path, i.e. possibly 
+        different files or directories that have been moved/copied to this 
+        location in different revisions."""
+        
+        for h in self.repos.get_path_history(self.path, self.rev, limit):
+            yield h
+
+    def _get_node_history(self, limit):
+        """Returns a gererator yielding the changes to a node."""
+        
+        return get_existing_node(self.req, self.repos, \
+                                 self.path, self.rev).get_history(limit)
+
+    def _get_node_history_ranges(self, limit):
+        """A generator yielding the changes to a node, restricted to certain
+        revisions. The revisions outside the given ranges are filtered."""
+        
+        prevpath = self.path
+        ranges = list(self.revranges.pairs)
+        ranges.reverse()
+        for (a,b) in ranges:
+            while b >= a:
+                rev = repos.normalize_rev(b) # \todo
+                node = get_existing_node(self.req, self.repos, prevpath, rev)
+                node_history = list(node.get_history(2))
+                p, rev, chg = node_history[0]
+                if rev < a:
+                    yield (p, rev, None) # separator
+                    break
+                yield node_history[0]
+                prevpath = node_history[-1][0] # follow copy
+                b = rev-1
+                if b < a and len(node_history) > 1:
+                    p, rev, chg = node_history[1]
+                    yield (p, rev, None)
+
+    def get_history_provider(self):
+        """Depending on the selected mode prepares and selects a generator 
+        function, which will be used to extract the history in the next step.
+        
+         * for ''stop on copy'' and ''follow copies'',
+           it's `Node.get_history()` unless explicit ranges have been
+           specified, in which case some of the entries are filtered.
+         * for ''show only add, delete'' we're using
+          `Repository.get_path_history()`"""
+        
+        if self.mode == 'path_history':
+            return self._get_path_history
+        else:
+            if not self.revranges or self.revranges.a == self.revranges.b:
+                return self._get_node_history
+            else:
+                return self._get_node_history_ranges
+
+    def get_log_items(self, history):
+        """Retrieves history and extracts info for at most limit+1 results"""
+        
+        info = []
+        depth = 1
+        fix_deleted_rev = False
+        previous_path = self.normpath
+        for old_path, old_rev, old_chg in history(self.limit+1):
+            if fix_deleted_rev:
+                fix_deleted_rev['existing_rev'] = old_rev
+                fix_deleted_rev = False
+            if self.stop_rev and self.repos.rev_older_than(old_rev,
+                                                           self.stop_rev):
+                break
+            old_path = self.repos.normalize_path(old_path)
+
+            item = {
+                'path': old_path, 'rev': old_rev, 'existing_rev': old_rev,
+                'change': old_chg, 'depth': depth,
+            }
+            
+            if old_chg == Changeset.DELETE:
+                fix_deleted_rev = item
+            if not (self.mode == 'path_history' and old_chg == Changeset.EDIT):
+                info.append(item)
+            if old_path and old_path != previous_path and \
+                   not (self.mode == 'path_history' and
+                        old_path == self.normpath):
+                depth += 1
+                item['depth'] = depth
+                item['copyfrom_path'] = old_path
+                if self.mode == 'stop_on_copy':
+                    break
+            if len(info) > self.limit: # we want limit+1 entries
+                break
+            previous_path = old_path
+        if info == []:
+            # FIXME: we should send a 404 error here
+            raise TracError("The file or directory '%s' doesn't exist "
+                            "at revision %s or at any previous revision."
+                            % (path, rev), 'Nonexistent path')
+        else:
+            return info
+
+    def get_template_context(self, items):
+        """Prepares the template data to be returned as a response."""
+        
+        # \todo I actually wanted this get_path_links part to be in response() 
+        # but path_links is needed for self.data (see below).
+        self.path_links = get_path_links(self.req.href, self.path, self.rev)
+        if self.path_links:
+            add_link(self.req, 'up', self.path_links[-1]['href'],
+                     'Parent directory')
+
+        revs = [i['rev'] for i in items]
+        changes = get_changes(self.repos, revs)
+        extra_changes = {}
+        email_map = {}
+        
+        if self.format == 'rss':
+            # Get the email addresses of all known users
+            if Chrome(self.module.env).show_email_addresses:
+                for username,name,email in self.module.env.get_known_users():
+                    if email:
+                        email_map[username] = email
+        elif self.format == 'changelog':
+            for rev in revs:
+                changeset = changes[rev]
+                cs = {}
+                cs['message'] = wrap(changeset.message, 70,
+                                     initial_indent='\t',
+                                     subsequent_indent='\t')
+                files = []
+                actions = []
+                for path, kind, chg, bpath, brev in changeset.get_changes():
+                    files.append(chg == Changeset.DELETE and bpath or path)
+                    actions.append(chg)
+                cs['files'] = files
+                cs['actions'] = actions
+                extra_changes[rev] = cs
+        return {
+            'context': Context(self.module.env, self.req, 'source', self.path),
+            'path': self.path, 'rev': self.rev, 'stop_rev': self.stop_rev,
+            'mode': self.mode, 'verbose': self.verbose,
+            'path_links': self.path_links, 'limit' : self.limit,
+            'items': items, 'changes': changes,
+            'email_map': email_map, 'extra_changes': extra_changes,
+            'wiki_format_messages':
+            self.module.config['changeset'].getbool('wiki_format_messages')
+            }
+    
+    def _make_log_href(self, path, **args):
+        link_rev = self.rev
+        if self.rev == str(self.repos.youngest_rev):
+            link_rev = None
+        params = {'rev': link_rev, 'mode': self.mode, 'limit': self.limit}
+        params.update(args)
+        if self.verbose:
+            params['verbose'] = self.verbose
+        return self.req.href.log(path, **params)
+
Index: trac/web/api.py
===================================================================
--- trac/web/api.py     (revision 4941)
+++ trac/web/api.py     (working copy)
@@ -517,6 +517,26 @@
         """
 
 
+class RequestProcessor(object):
+
+    default_action = 'view'
+    
+    def __init__(self, env, req):
+        self.env = env
+        self.req = req
+
+    def process(self):
+        if self.req.method == 'POST':
+            prefix = 'do_'
+        else:
+            prefix = 'render_'
+        method = prefix + self.req.args.get('action', self.default_action)
+        if hasattr(self, method):
+            return getattr(self, method)()
+        raise TracError("The request handler '%s.%s' doesn't exist." %
+                        (self.__class__.__name__, method))
+
+
 class IRequestFilter(Interface):
     """Extension point interface for components that want to filter HTTP
     requests, before and/or after they are processed by the main handler."""

Reply via email to