Source

tracvatar / tracvatar / web_ui.py

from trac.core import Component, implements
from trac.config import Option
from trac.web.chrome import ITemplateProvider, add_stylesheet
from trac.web.api import ITemplateStreamFilter
from genshi.filters.transform import Transformer
from genshi.builder import tag
import tracvatar
from pkg_resources import resource_filename
import itertools
import hashlib

class AvatarModule(Component):
    implements(ITemplateStreamFilter, ITemplateProvider)

    ticket_reporter_size = Option("tracvatar", "ticket_reporter_size", default="60")
    ticket_comment_size = Option("tracvatar", "ticket_comment_size", default="40")
    timeline_size = Option("tracvatar", "timeline_size", default="30")
    browser_lineitem_size = Option("tracvatar", "browser_lineitem_size", default="20")
    browser_changeset_size = Option("tracvatar", "browser_changeset_size", default="40")

    default = Option('tracvatar', 'gravatar_default', default='default',
                            doc="The default value to pass along to gravatar to "
                            "use if the email address does not match.")

    def filter_stream(self, req, method, filename, stream, data):
        filter_ = []
        author_data = {}
        if req.path_info.startswith("/ticket"):
            filter_.append(self._ticket_reporter_filter(data, author_data))
            filter_.append(self._ticket_comment_filter(data, author_data))
        elif req.path_info.startswith("/timeline"):
            filter_.append(self._timeline_filter(data, author_data))
        elif req.path_info.startswith("/browser"):
            filter_.append(self._browser_filter(data, author_data))
        elif req.path_info.startswith("/log"):
            filter_.append(self._log_filter(data, author_data))

        filter_.append(self._footer())
        self._lookup_email(author_data)
        for f in filter_:
            if f is not None:
                stream |= f
        add_stylesheet(req, 'tracvatar/tracvatar.css')
        return stream

    # ITemplateProvider methods
    def get_htdocs_dirs(self):
        yield 'tracvatar', resource_filename(__name__, 'htdocs')

    def get_templates_dirs(self):
        return []

    def _footer(self):
        return Transformer('//div[@id="footer"]/p[@class="left"]').append(tag.p(
            "Gravatar support by ",
            tag.a("Tracvatar %s" % tracvatar.__version__, 
                    href="https://bitbucket.org/zzzeek/tracvatar"),
            class_="left",
        ))

    def _generate_avatar(self, author, author_data, class_, size):
        email_hash = author_data.get(author, None) or author
        href = "http://www.gravatar.com/avatar/" + email_hash
        href += "?size=%s" % size
        # for some reason sizing doesn't work if you pass "default=default"
        if self.default != 'default':
            href += "&default=%s" % (self.default,)
        return tag.img(src=href, class_='tracvatar %s' % class_).generate()

    def _browser_filter(self, data, author_data):
        if not data.get('dir'):
            return self._browser_changeset_filter(data, author_data)
        else:
            return self._browser_lineitem_filter(data, author_data)

    def _browser_changeset_filter(self, data, author_data):
        if 'file' not in data or 'changeset' not in data['file']:
            return
        author = data['file']['changeset'].author
        author_data[author]  = None
        return lambda stream: Transformer('//table[@id="info"]//th').prepend(
            self._generate_avatar(
                    author, 
                    author_data, 
                    "browser-changeset", 
                    self.browser_changeset_size)
        )(stream)

    def _log_filter(self, data, author_data):
        if 'changes' not in data:
            return
        for change in data['changes'].values():
            author_data[change.author] = None
        return self._browser_lineitem_render_filter(author_data)

    def _browser_lineitem_filter(self, data, author_data):
        if 'dir' not in data or 'changes' not in data['dir']:
            return
        for trac_cset in data['dir']['changes'].values():
            author_data[trac_cset.author] = None
        return self._browser_lineitem_render_filter(author_data)

    def _browser_lineitem_render_filter(self, author_data):
        def find_change(stream):
            author = stream[1][1]
            tag = self._generate_avatar(
                author,
                author_data,
                'browser-lineitem',
                self.browser_lineitem_size)
            return itertools.chain([stream[0]], tag, stream[1:])

        return Transformer('//td[@class="author"]').filter(find_change)

    def _ticket_reporter_filter(self, data, author_data):
        if 'ticket' not in data:
            return
        author = data['ticket'].values['reporter']
        author_data[author] = None

        return lambda stream: Transformer('//div[@id="ticket"]').\
                    prepend(
                        self._generate_avatar(
                            author, 
                            author_data, 
                            'ticket-reporter', 
                            self.ticket_reporter_size)
                        )(stream)

    def _ticket_comment_filter(self, data, author_data):
        if 'changes' not in data:
            return

        apply_authors = []
        for change in data['changes']:
            author = change['author']
            author_data[author] = None
            apply_authors.append(author)

        def find_change(stream):
            stream = iter(stream)
            author = apply_authors.pop()
            tag = self._generate_avatar(
                        author, 
                        author_data, 
                        'ticket-comment', 
                        self.ticket_comment_size)
            return itertools.chain([next(stream)], tag, stream)

        return Transformer('//div[@id="changelog"]/div[@class="change"]/h3[@class="change"]').\
                        filter(find_change)

    def _timeline_filter(self, data, author_data):
        if 'events' not in data:
            return
        apply_authors = []
        for event in reversed(data['events']):
            author = event['author']
            author_data[author] = None
            apply_authors.append(author)

        def find_change(stream):
            stream = iter(stream)
            author = apply_authors.pop()
            tag = self._generate_avatar(
                        author, 
                        author_data, 
                        'timeline', 
                        self.timeline_size)
            return itertools.chain([next(stream)], tag, stream)

        return Transformer('//div[@id="content"]/dl/dt/a/span[@class="time"]').\
                            filter(find_change)

    def _lookup_email(self, author_data):
        if not author_data:
            return
        authors = sorted(author_data)
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute(
            "select sid, value from session_attribute where name=%%s and sid in (%s)" % (
                ",".join(["%s" for author in authors])
            ), ("email",) + tuple(authors)
        )
        for sid, email in cursor.fetchall():
            author_data[sid] = hashlib.md5(email.lower()).hexdigest()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.