Changeset 2979

Show
Ignore:
Timestamp:
01/04/08 19:26:34 (11 months ago)
Author:
athomas
Message:

First commit of new code.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trachacksplugin/0.11/setup.py

    r529 r2979  
    11from setuptools import setup 
    22 
    3 setup(name='TracHacks', 
    4       version='0.1', 
    5       packages=['trachacks'], 
    6       entry_points={'trac.plugins': 'TracHacks = trachacks'}, 
    7       #install_requires=['TracXMLRPC', 'TracAccountManager'], 
    8       ) 
     3setup( 
     4    name='TracHacks', 
     5    license='GPL', 
     6    version='2.0', 
     7    packages=['trachacks'], 
     8    package_data={'trachacks' : ['templates/*.html', 'htdocs/js/*.js', 'htdocs/css/*.css']}, 
     9    dependency_links=[ 
     10        'http://trac-hacks.org/svn/tagsplugin/trunk#egg=TracTags-0.6', 
     11        'http://trac-hacks.org/svn/accountmanagerplugin/trunk#egg=TracAccountManager', 
     12        'http://trac-hacks.org/svn/voteplugin/0.11#egg=TracVote-0.1', 
     13        ], 
     14    entry_points={ 
     15        'trac.plugins': 'trachacks = trachacks' 
     16        }, 
     17    install_requires=[ 
     18        'TracAccountManager', 
     19        'TracTags >= 0.6', 
     20        'TracVote >= 0.1', 
     21        ], 
     22    ) 
  • trachacksplugin/0.11/trachacks/trachacks.py

    r2071 r2979  
    1 # vim: expandtab 
     1# -*- coding: utf-8 -*- 
     2
     3# Copyright (C) 2006 Alec Thomas <alec@swapoff.org> 
     4
     5# This software is licensed as described in the file COPYING, which 
     6# you should have received as part of this distribution. 
     7
     8 
     9import re 
    210from trac.core import * 
    3 from trac.wiki.api import IWikiMacroProvider 
    4 from tracrpc.api import IXMLRPCHandler 
     11from trac.config import * 
    512from acct_mgr.htfile import HtPasswdStore 
    613from acct_mgr.api import IPasswordStore 
    7 import sys, inspect 
    8  
    9  
    10 def try_int(s): 
    11     "Convert to integer if possible." 
    12     try: return int(s) 
    13     except: return s 
    14  
    15 def natsort_key(s): 
    16     "Used internally to get a tuple by which s is sorted." 
    17     import re 
    18     return map(try_int, re.findall(r'(\d+|\D+)', s)) 
    19  
    20 def natcmp(a, b): 
    21     "Natural string comparison, case sensitive." 
    22     return cmp(natsort_key(a), natsort_key(b)) 
    23  
    24 def natcasecmp(a, b): 
    25     "Natural string comparison, ignores case." 
    26     return natcmp(a.lower(), b.lower()) 
    27  
    28 def natsort(seq, cmp=natcmp): 
    29     "In-place natural string sort." 
    30     seq.sort(cmp) 
    31  
    32 def natsorted(seq, cmp=natcmp): 
    33     "Returns a copy of seq, sorted by natural string sort." 
    34     temp = list(seq) 
    35     natsort(temp, cmp) 
    36     return temp 
    37  
    38  
    39 class TracHacksMacros(Component): 
    40     """ List of meta types """ 
    41     implements(IWikiMacroProvider) 
    42  
    43     # IWikiMacroProvider methods 
    44     def get_macros(self): 
    45         yield 'ListTypes' 
    46  
    47     def get_macro_description(self, name): 
    48         return "Main hack type listing" 
    49  
    50     def render_macro(self, req, name, content): 
    51         from StringIO import StringIO 
    52         from trac.wiki import wiki_to_html 
    53         from trac.wiki.model import WikiPage 
    54         from trac.util import Markup 
    55         from tractags.api import TagEngine 
    56         import re 
    57  
    58         tagspace = TagEngine(self.env).tagspace.wiki 
    59  
    60         out = StringIO() 
    61         pages = tagspace.get_tagged_names(tags=['type']) 
    62         pages = sorted(pages) 
    63  
    64         out.write('<form style="text-align: right; padding-top: 1em; margin-right: 5em;" method="get">') 
    65         out.write('<span style="font-size: xx-small">') 
    66         out.write('Show hacks for releases: ') 
    67         releases = natsorted(tagspace.get_tagged_names(tags=['release'])) 
    68         if 'update_th_filter' in req.args: 
    69             show_releases = req.args.get('release', ['0.10']) 
    70             if isinstance(show_releases, basestring): 
    71                 show_releases = [show_releases] 
    72             req.session['th_release_filter'] = ','.join(show_releases) 
     14from trac.wiki.model import WikiPage 
     15from trac.util.compat import sorted 
     16from trac.web.api import IRequestHandler 
     17from trac.web.chrome import ITemplateProvider, INavigationContributor, \ 
     18                            add_stylesheet, add_script, add_ctxtnav 
     19from trac.resource import get_resource_url 
     20from tractags.api import TagSystem 
     21from tractags.macros import render_cloud 
     22from tracvote import VoteSystem 
     23from genshi.builder import tag as builder 
     24 
     25 
     26 
     27def pluralise(n, word): 
     28    """Return a (naively) pluralised phrase from a count and a singular 
     29    word.""" 
     30    if n == 0: 
     31        return 'No %ss' % word 
     32    elif n == 1: 
     33        return '%i %s' % (n, word) 
     34    else: 
     35        return '%i %ss' % (n, word) 
     36 
     37 
     38def natural_sort(l): 
     39  """Sort the given list in the way that humans expect.""" 
     40  convert = lambda text: int(text) if text.isdigit() else text 
     41  alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] 
     42  return sorted(l, key=alphanum_key) 
     43 
     44 
     45class TracHacksHandler(Component): 
     46    """Trac-Hacks request handler.""" 
     47    implements(INavigationContributor, IRequestHandler, ITemplateProvider) 
     48 
     49    limit = IntOption('trachacks', 'limit', 25, 
     50        'Default maximum number of hacks to display.') 
     51 
     52    path_match = re.compile(r'/hacks/?(.+)?') 
     53    title_extract = re.compile(r'=\s+([^=]*)=', re.MULTILINE | re.UNICODE) 
     54 
     55    # IRequestHandler methods 
     56    def match_request(self, req): 
     57        return self.path_match.match(req.path_info) 
     58 
     59    def process_request(self, req): 
     60        data = {} 
     61        tag_system = TagSystem(self.env) 
     62 
     63        match = self.path_match.match(req.path_info) 
     64        view = 'cloud' 
     65        if match.group(1): 
     66            view = match.group(1) 
     67 
     68        # Hack types 
     69        types = [r.id for r, _ in tag_system.query(req, 'realm:wiki type')] 
     70        # Trac releases 
     71        releases = natural_sort([r.id for r, _ in 
     72                                 tag_system.query(req, 'realm:wiki release')]) 
     73 
     74        hacks = self.fetch_hacks(req, data, types) 
     75 
     76        add_stylesheet(req, 'tags/css/tractags.css') 
     77        add_stylesheet(req, 'hacks/css/trachacks.css') 
     78        add_script(req, 'hacks/js/trachacks.js') 
     79 
     80        views = ['cloud', 'list'] 
     81        for v in views: 
     82            if v != view: 
     83                add_ctxtnav(req, builder.a(v.title(), href=req.href.hacks(v))) 
     84            else: 
     85                add_ctxtnav(req, v.title()) 
     86        if view == 'cloud': 
     87            return self.render_cloud(req, data, hacks) 
     88        elif view == 'list': 
     89            return self.render_list(req, data, hacks) 
     90 
     91    def fetch_hacks(self, req, data, types): 
     92        """Return a list of hacks in the form 
     93 
     94        [votes, rank, resource, tags, title] 
     95        """ 
     96        tag_system = TagSystem(self.env) 
     97        vote_system = VoteSystem(self.env) 
     98        hacks = [] 
     99        global limit 
     100        ALL = 9999 
     101        limit = req.args.get('limit', self.limit) 
     102 
     103        # Custom tag query modifiers 
     104        def top_modifier(name, node, context): 
     105            """top:<n> Only show the top N results.""" 
     106            global limit 
     107            if node.value == 'all': 
     108                limit = ALL 
     109                return True 
     110            try: 
     111                assert node.type == node.TERM 
     112                limit = int(node.value) 
     113            except (AssertionError, ValueError): 
     114                raise TracError('top: expects an integer') 
     115            return True 
     116 
     117        data['tag_query'] = req.args.get('q', '') 
     118 
     119        # Get list of hacks from tag system 
     120        query = 'realm:wiki (%s)' % ' or '.join(types) 
     121        if req.args.get('q'): 
     122            query += ' (' + req.args.get('q', '') + ')' 
     123        self.env.log.debug('Hack query: %s', query) 
     124        attribute_handlers={'top': top_modifier,} 
     125        try: 
     126            tagged = list(tag_system.query(req, query, 
     127                                           attribute_handlers=attribute_handlers)) 
     128        except TracError, e: 
     129            tagged = [] 
     130            tagged = tag_system.query(req, 'realm:wiki (#s)' % ' or '.join(types), 
     131                                      attribute_handlers=attribute_handlers) 
     132            data['tag_query_error'] = str(e) 
     133 
     134        self.env.log.debug(limit) 
     135        if limit != ALL: 
     136            data['limit'] = 'top %s' % limit 
    73137        else: 
    74             show_releases = req.session.get('th_release_filter', '0.10').split(',') 
    75         for version in releases: 
    76             checked = version in show_releases 
    77             out.write('<input type="checkbox" name="release" value="%s"%s>%s\n' % (version, checked and ' checked' or '', version)) 
    78         out.write('<input name="update_th_filter" type="submit" style="font-size: xx-small; padding: 0; border: solid 1px black" value="Update"/>') 
    79         out.write('</span>') 
    80         out.write('</form>') 
    81         for i, pagename in enumerate(pages): 
    82             page = WikiPage(self.env, pagename) 
    83             if page.text: 
    84                 topmargin = '0em' 
    85                 if i < len(pages) - 1: 
    86                     bottommargin = '0em' 
    87                 else: 
    88                     bottommargin = '2em' 
    89  
    90                 out.write('<fieldset style="padding: 1em; margin: %s 5em %s 5em; border: 1px solid #999;">\n' % (topmargin, bottommargin)) 
    91                 body = page.text 
    92                 title = re.search('=+\s([^=]*)=+', body) 
    93                 if title: 
    94                     title = title.group(1).strip() 
    95                     body = re.sub('=+\s([^=]*)=+', '', body, 1) 
    96                 else: 
    97                     title = pagename 
    98                 body = re.sub('\\[\\[TagIt.*', '', body) 
    99                 out.write('<legend style="color: #999;"><a href="%s">%s</a></legend>\n' % (self.env.href.wiki(pagename), title)) 
    100                 body = wiki_to_html(body, self.env, req) 
    101                 # Dear God, the horror! 
    102                 for line in body.splitlines(): 
    103                     show = False 
    104                     for release in show_releases: 
    105                         self.env.log.debug(release) 
    106                         if '>%s</a>' % release in line: 
    107                             show = True 
    108                             break 
    109                     if show or not '<li>' in line: 
    110                         out.write(line) 
    111  
    112                 out.write('</fieldset>\n') 
    113  
    114         return out.getvalue() 
     138            data['limit'] = 'all' 
     139 
     140        # Build hacks list 
     141        for resource, tags in tagged: 
     142            page = WikiPage(self.env, resource.id) 
     143            _, count, _ = vote_system.get_vote_counts(resource) 
     144            match = self.title_extract.search(page.text) 
     145            count_string = pluralise(count, 'vote') 
     146            if match: 
     147                title = '%s (%s)' % (match.group(1).strip(), count_string) 
     148            else: 
     149                title = '%s' % count_string 
     150            hacks.append([count, None, resource, tags, title]) 
     151 
     152        # Rank 
     153        hacks = sorted(hacks, key=lambda i: -i[0])[:limit] 
     154        for i, hack in enumerate(hacks): 
     155            hack[1] = i 
     156        return hacks 
     157 
     158    def render_list(self, req, data, hacks): 
     159        ul = builder.ul() 
     160        for votes, rank, resource, tags, title in sorted(hacks, key=lambda h: h[2].id): 
     161            li = builder.li(builder.a(resource.id, 
     162                                      href=req.href.wiki(resource.id)), 
     163                            ' - ', title) 
     164            ul(li) 
     165        data['tag_body'] = ul 
     166        return 'hacks_view.html', data, None 
     167 
     168    def render_cloud(self, req, data, hacks): 
     169        by_name = dict([(r[2].id, r) for r in hacks]) 
     170 
     171        def link_renderer(tag, count, percent): 
     172            votes, rank, resource, tags, title = by_name[tag] 
     173            href = req.href.wiki(resource.id) 
     174            font_size = 10.0 + (percent * 20.0) 
     175            colour = 128.0 - (percent * 128.0) 
     176            colour = '#%02x%02x%02x' % ((colour,) * 3) 
     177            a = builder.a(tag, rel='tag', title=title, href=href, class_='tag', 
     178                style='font-size: %ipx; color: %s' % (font_size, colour)) 
     179            return a 
     180 
     181        cloud_hacks = dict([(hack[2].id, hack[0]) for hack in hacks]) 
     182        data['tag_body'] = render_cloud(self.env, req, cloud_hacks, link_renderer) 
     183 
     184        return 'hacks_view.html', data, None 
     185 
     186    # INavigationContributor methods 
     187    def get_active_navigation_item(self, req): 
     188        return 'hacks' 
     189 
     190    def get_navigation_items(self, req): 
     191        yield ('mainnav', 'hacks', 
     192                builder.a('Hacks', href=req.href.hacks(), accesskey='H')) 
     193 
     194    # ITemplateProvider methods 
     195    def get_templates_dirs(self): 
     196        """ 
     197        Return the absolute path of the directory containing the provided 
     198        ClearSilver templates. 
     199        """ 
     200        from pkg_resources import resource_filename 
     201        return [resource_filename(__name__, 'templates')] 
     202 
     203    def get_htdocs_dirs(self): 
     204        """Return the absolute path of a directory containing additional 
     205        static resources (such as images, style sheets, etc). 
     206        """ 
     207        from pkg_resources import resource_filename 
     208        return [('hacks', resource_filename(__name__, 'htdocs'))] 
     209 
     210 
    115211 
    116212class TracHacksAccountManager(HtPasswdStore): 
     
    147243    def delete_user(self, user): 
    148244        HtPasswdStore.delete_user(self, user) 
    149  
    150 class TracHacksRPC(Component): 
    151     """ Allow inspection of hacks on TracHacks. """ 
    152     implements(IXMLRPCHandler) 
    153  
    154     def xmlrpc_namespace(self): 
    155         return 'trachacks' 
    156  
    157     def xmlrpc_methods(self): 
    158         yield ('XML_RPC', ((list, str, str),), self.getHacks) 
    159         yield ('XML_RPC', ((list,),), self.getReleases) 
    160         yield ('XML_RPC', ((list,),), self.getTypes) 
    161         yield ('XML_RPC', ((dict,str),), self.getDetails) 
    162  
    163     # Other methods 
    164     def getReleases(self, req): 
    165         """ Return a list of Trac releases TracHacks is aware of. """ 
    166         from tractags.api import TagEngine 
    167         return TagEngine(self.env).tagspace.wiki.get_tagged_names(['release']) 
    168  
    169     def getTypes(self, req): 
    170         """ Return a list of known Hack types. """ 
    171         from tractags.api import TagEngine 
    172         return TagEngine(self.env).tagspace.wiki.get_tagged_names(['type']) 
    173  
    174     def getHacks(self, req, release, type): 
    175         """ Fetch a list of hacks for Trac release, of type. """ 
    176         from trac.versioncontrol.api import Node 
    177         from tractags.api import TagEngine 
    178         repo = self.env.get_repository(req.authname) 
    179         wikitags = TagEngine(self.env).tagspace.wiki 
    180         repo_rev = repo.get_youngest_rev() 
    181         releases = wikitags.get_tagged_names([release]) 
    182         types = wikitags.get_tagged_names([type]) 
    183         for plugin in releases.intersection(types): 
    184             if plugin.startswith('tags/'): continue 
    185             path = '%s/%s' % (plugin.lower(), release) 
    186             rev = 0 
    187             if repo.has_node(str(path), repo_rev): 
    188                 node = repo.get_node(path) 
    189                 rev = node.rev 
    190             yield (plugin, rev) 
    191  
    192     def getDetails(self, req, hack): 
    193         """ Fetch hack details. Returns dict with name, dependencies and 
    194             description. """ 
    195         from tractags.api import TagEngine 
    196         wikitags = TagEngine(self.env).tagspace.wiki 
    197         tags = wikitags.get_tags(hack) 
    198         types = self.getTypes() 
    199         hacks = wikitags.get_tagged_names(types) 
    200  
    201         dependencies = hacks.intersection(tags) 
    202         href, htmllink, description = wikitags.name_details(hack) 
    203         return {'name': hack, 'dependencies': tuple(dependencies), 
    204                 'description': description}