import os import re import pickle from StringIO import StringIO from trac.core import * from trac.perm import IPermissionRequestor from trac.util import sorted, escape from trac.wiki.formatter import wiki_to_oneliner from trac.web.chrome import ITemplateProvider, add_stylesheet class Poll(object): def __init__(self, base_dir, title, vote_defs): self.vote_defs = vote_defs self.title = title # Perhaps the Wiki page name should be included? self.key = ''.join(re.findall(r'(\w+)', title)).lower() self.store = os.path.join(base_dir, self.key + '.poll') self.load() def load(self): """ Load pickled representation of votes. """ self.votes = {} if os.path.isfile(self.store): poll = pickle.load(open(self.store, 'r')) assert self.title == poll['title'], \ 'Stored poll is not the same as this one.' self.votes = dict([(k, v) for k, v in poll['votes'].iteritems() if k in [d[0] for d in self.vote_defs]]) self.votes.update(dict([(k[0], []) for k in self.vote_defs if k[0] not in self.votes])) def save(self): data = {'title': self.title, 'votes': self.votes} pickle.dump(data, open(self.store, 'w')) def populate(self, req): """ Update poll based on HTTP request. """ if req.args.get('poll', '') == self.key: vote = req.args.get('vote', '') if not vote: return if vote not in self.votes: raise TracError('No such vote %s' % vote) username = req.authname or 'anonymous' for v, voters in self.votes.items(): if username in voters: self.votes[v].remove(username) self.votes[vote] = self.votes[vote] + [username] self.save() def render(self, env, req): out = StringIO() can_vote = req.perm.has_permission('POLL_VOTE') if can_vote: out.write('
\n' '\n' % {'id': self.key, 'href': env.href(req.path_info)}) out.write('
\n' ' %s\n' ' \n
\n') can_vote and out.write('
\n') return out.getvalue() try: from trac.wiki.macros import WikiMacroBase except ImportError: # TODO Remove this when ported to 0.10 from trac.wiki.api import IWikiMacroProvider class WikiMacroBase(Component): """Abstract base class for wiki macros.""" implements(IWikiMacroProvider) abstract = True def get_macros(self): """Yield the name of the macro based on the class name.""" name = self.__class__.__name__ if name.endswith('Macro'): name = name[:-5] yield name def get_macro_description(self, name): """Return the subclass's docstring.""" return inspect.getdoc(self.__class__) def render_macro(self, req, name, content): raise NotImplementedError class PollMacro(WikiMacroBase): """ Add a poll. Each argument must be separated by a semi-colon (;) or new-line (if used as a processor). The first argument is the title of the poll, which is also the identifier for each poll. Usage: `[[TicketPoll(; <arg> [; <arg>] ...)]]` Where <arg> conforms to the following: || '''<arg>''' || '''Description''' || || `query:<ticket-query>` || Add tickets from a query to the poll. || || `#<n>` || Add an individual ticket to the poll. || || `<text>` || A poll question. || eg. [[Poll(Which of these do you prefer?; #1; #2; #3; query:component-Request-a-Hack&status!=closed; Cheese dip)]] """ implements(IPermissionRequestor, ITemplateProvider) base_dir = property(lambda self: self.env.config.get('poll', 'base_dir', '/tmp')) def render_macro(self, req, name, content): content = filter(None, [i.strip() for i in content.replace(';', '\n').split('\n')]) title = content.pop(0) return self.render_poll(req, title, content) def render_poll(self, req, title, votes): from trac.ticket.model import Ticket, Priority from trac.ticket.query import Query add_stylesheet(req, 'poll/css/poll.css') if not req.perm.has_permission('POLL_VIEW') or \ not req.perm.has_permission('TICKET_VIEW'): return '' all_votes = [] ticket_count = 0 for vote in votes: tickets = [] if vote.startswith('#'): try: tickets.append(int(vote.strip('#'))) except ValueError: raise TracError('Invalid ticket number %s' % vote) elif vote.startswith('query:'): query = vote[6:] tickets = [q['id'] for q in Query.from_string(self.env, query).execute(req)] else: all_votes.append(('%08x' % abs(hash(vote)), None, wiki_to_oneliner(vote, self.env))) # Make tickets look pretty for idx, id in enumerate(tickets): try: ticket = Ticket(self.env, id) except Exception: continue summary = ticket['summary'] + ' (#%i)' % id priority = Priority(self.env, ticket['priority']).value summary = wiki_to_oneliner(summary, self.env) all_votes.append((str(id), "ticket prio%s%s%s" % (priority, ticket_count % 2 and ' even' or '', ticket['status'] == 'closed' and ' closed' or ''), summary)) ticket_count += 1 if not all_votes: raise TracError('No votes provided') poll = Poll(self.base_dir, title, all_votes) if req.perm.has_permission('POLL_VOTE'): poll.populate(req) return poll.render(self.env, req) # IPermissionRequestor methods def get_permission_actions(self): return ('POLL_VIEW', ('POLL_VOTE', ('POLL_VIEW',))) # ITemplateProvider methods def get_htdocs_dirs(self): from pkg_resources import resource_filename return [('poll', resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): return []