Modify

Opened 22 months ago

Last modified 20 months ago

#10703 assigned enhancement

[PATCH] support of fully anonymous polls

Reported by: falkb Owned by: rjollos
Priority: high Component: PollMacro
Severity: normal Keywords:
Cc: Trac Release:

Description

This patch adds the ability to make fully anonymous polls, simply by the use of pattern (*) at the end of the poll question:

  • pollmacro/trunk/tracpoll/tracpoll.py

     
     1import hashlib 
    12import os 
    23import re 
    34import pickle 
     
    4950        finally: 
    5051            fd.close() 
    5152 
    52     def populate(self, req): 
     53    def populate(self, req, isAnonymousPoll): 
    5354        """ Update poll based on HTTP request. """ 
    5455        if req.args.get('poll', '') == self.key: 
    5556            vote = req.args.get('vote', '') 
     
    5859            if vote not in self.votes: 
    5960                raise TracError('No such vote %s' % vote) 
    6061            username = req.authname or 'anonymous' 
     62            if isAnonymousPoll: 
     63                username = hashlib.sha1(username).hexdigest() 
    6164            for v, voters in self.votes.items(): 
    6265                if username in voters: 
    6366                    self.votes[v].remove(username) 
    6467            self.votes[vote] = self.votes[vote] + [username] 
    6568            self.save() 
    6669 
    67     def render(self, env, req): 
     70    def render(self, env, req, isAnonymousPoll): 
    6871        out = StringIO() 
    6972        can_vote = req.perm.has_permission('POLL_VOTE') 
    7073        if can_vote: 
     
    7679                  ' <ul>\n' 
    7780                  % escape(self.title)) 
    7881        username = req.authname or 'anonymous' 
     82        if isAnonymousPoll: 
     83            username = hashlib.sha1(username).hexdigest() 
    7984        for id, style, vote in self.vote_defs: 
    8085            hid = escape(str(id)) 
    8186            out.write('<li%s>\n' % (style and ' class="%s"' % style or '')) 
     
    9095            else: 
    9196                out.write(vote) 
    9297            if self.votes[id]: 
    93                 out.write(' <span class="voters">(<span class="voter">' + 
    94                           '</span>, <span class="voter">'.join(self.votes[id]) + 
    95                           '</span>)</span>') 
     98                if isAnonymousPoll: 
     99                    out.write(' <span class="voters">(%s)</span>' % len(self.votes[id])) 
     100                else: 
     101                    out.write(' <span class="voters">(<span class="voter">' + 
     102                              '</span>, <span class="voter">'.join(self.votes[id]) + 
     103                              '</span>)</span>') 
    96104            out.write('</li>\n') 
    97105        if can_vote: 
    98106            out.write('<input type="submit" value="Vote"/>') 
    99107        else: 
    100108            out.write("<br/><i>You don't have permission to vote. You may need to login.</i>") 
    101109        out.write(' </ul>\n</fieldset>\n') 
     
    128136                      'Path where poll pickle dumps should be stored.') 
    129137 
    130138    def expand_macro(self, formatter, name, content): 
     139        isAnonymousPoll = False 
    131140        req = formatter.req 
    132141        if not content: 
    133142            return system_message("A title must be provided as the first argument to the poll macro") 
     
    136145        if len(content) < 2: 
    137146            return system_message("One or more options must be provided to vote on.") 
    138147        title = content.pop(0) 
    139         return self.render_poll(req, title, content) 
     148        if title.endswith('(*)'): 
     149            isAnonymousPoll = True 
     150        return self.render_poll(req, title, content, isAnonymousPoll) 
    140151 
    141     def render_poll(self, req, title, votes): 
     152    def render_poll(self, req, title, votes, isAnonymousPoll): 
    142153        add_stylesheet(req, 'poll/css/poll.css') 
    143154        if not req.perm.has_permission('POLL_VIEW'): 
    144155            return '' 
     
    184195 
    185196        poll = Poll(self.base_dir, title, all_votes) 
    186197        if req.perm.has_permission('POLL_VOTE'): 
    187             poll.populate(req) 
    188         return poll.render(self.env, req) 
     198            poll.populate(req, isAnonymousPoll) 
     199        return poll.render(self.env, req, isAnonymousPoll) 
    189200 
    190201    # IPermissionRequestor methods 
    191202    def get_permission_actions(self): 

Attachments (0)

Change History (11)

comment:1 follow-up: Changed 21 months ago by rjollos

Looks like a nice feature. I think we should consider using a positional or kw arg rather than appending a pattern to the title. isAnonymousPoll should be is_anonymous_poll or just is_anonymous per the style used in the code.

comment:2 Changed 21 months ago by rjollos

  • Priority changed from normal to high
  • Status changed from new to assigned

comment:3 in reply to: ↑ 1 ; follow-up: Changed 21 months ago by anonymous

Replying to rjollos:

Looks like a nice feature. I think we should consider using a positional or kw arg rather than appending a pattern to the title.

I wonder how such arg can be programmed in a macro, even if the whole string is splitted in a string list by separator ';'.

comment:4 Changed 21 months ago by rjollos

Good point. I seem to have remember dealing with this for another plugin. I'll do some research and get back to you.

comment:5 Changed 21 months ago by falkb

I also have another small patch that introduces a second pattern "(-)" which means the poll is closed. If one changes from "(*)" to "(-)", the anonymous polling is closed and no input is possible henceforward. I also somehow dislike that pattern trick but it's compatible with the ';'-separator divide mechanism.

comment:6 Changed 21 months ago by rjollos

Please do go ahead and post your new patch then. If we come up with a good way to handle kw args, we can always modify it. Could you take care of the style issue not in comment:1 as well?

comment:7 Changed 21 months ago by falkb

Here we go, with closed-poll support now. Hope you like it. This replaces this first patch here:

  • pollmacro/trunk/tracpoll/tracpoll.py

     
     1import hashlib 
    12import os 
    23import re 
    34import pickle 
     
    3132                poll = pickle.load(fd) 
    3233            finally: 
    3334                fd.close() 
     35            if self.title.endswith('(-)'): 
     36                self.title = self.title.replace('(-)', '(*)') 
    3437            assert self.title == poll['title'], \ 
    3538                   'Stored poll is not the same as this one.' 
    3639            self.votes = dict([(k, v) for k, v in poll['votes'].iteritems() 
     
    4952        finally: 
    5053            fd.close() 
    5154 
    52     def populate(self, req): 
     55    def populate(self, req, is_anonymous_poll): 
    5356        """ Update poll based on HTTP request. """ 
    5457        if req.args.get('poll', '') == self.key: 
    5558            vote = req.args.get('vote', '') 
     
    5861            if vote not in self.votes: 
    5962                raise TracError('No such vote %s' % vote) 
    6063            username = req.authname or 'anonymous' 
     64            if is_anonymous_poll: 
     65                username = hashlib.sha1(username).hexdigest() 
    6166            for v, voters in self.votes.items(): 
    6267                if username in voters: 
    6368                    self.votes[v].remove(username) 
    6469            self.votes[vote] = self.votes[vote] + [username] 
    6570            self.save() 
    6671 
    67     def render(self, env, req): 
     72    def render(self, env, req, is_anonymous_poll, is_closed_poll): 
    6873        out = StringIO() 
    6974        can_vote = req.perm.has_permission('POLL_VOTE') 
     75        if is_closed_poll: 
     76            can_vote = False 
    7077        if can_vote: 
    7178            out.write('<form id="%(id)s" method="get" action="%(href)s#%(id)s">\n' 
    7279                      '<input type="hidden" name="poll" value="%(id)s"/>\n' 
     
    7683                  ' <ul>\n' 
    7784                  % escape(self.title)) 
    7885        username = req.authname or 'anonymous' 
     86        if is_anonymous_poll: 
     87            username = hashlib.sha1(username).hexdigest() 
    7988        for id, style, vote in self.vote_defs: 
    8089            hid = escape(str(id)) 
    8190            out.write('<li%s>\n' % (style and ' class="%s"' % style or '')) 
    (this hunk was shorter than expected) 
    9099            else: 
    91100                out.write(vote) 
    92101            if self.votes[id]: 
    93                 out.write(' <span class="voters">(<span class="voter">' + 
    94                           '</span>, <span class="voter">'.join(self.votes[id]) + 
    95                           '</span>)</span>') 
     102                if is_anonymous_poll: 
     103                    out.write(' <span class="voters">(%s)</span>' % len(self.votes[id])) 
     104                else: 
     105                    out.write(' <span class="voters">(<span class="voter">' + 
     106                              '</span>, <span class="voter">'.join(self.votes[id]) + 
     107                              '</span>)</span>') 
    96108            out.write('</li>\n') 
    97109        if can_vote: 
    98         else: 
     110        elif not is_closed_poll: 
    99111            out.write("<br/><i>You don't have permission to vote. You may need to login.</i>") 
    100112        out.write(' </ul>\n</fieldset>\n') 
    101113        can_vote and out.write('</form>\n') 
     
    128140                      'Path where poll pickle dumps should be stored.') 
    129141 
    130142    def expand_macro(self, formatter, name, content): 
     143        is_anonymous_poll = False 
     144        is_closed_poll = False 
    131145        req = formatter.req 
    132146        if not content: 
    133147            return system_message("A title must be provided as the first argument to the poll macro") 
     
    136150        if len(content) < 2: 
    137151            return system_message("One or more options must be provided to vote on.") 
    138152        title = content.pop(0) 
    139         return self.render_poll(req, title, content) 
     153        if title.endswith('(*)'): 
     154            is_anonymous_poll = True 
     155        if title.endswith('(-)'): 
     156            is_anonymous_poll = True 
     157            is_closed_poll = True 
     158        return self.render_poll(req, title, content, is_anonymous_poll, is_closed_poll) 
    140159 
    141     def render_poll(self, req, title, votes): 
     160    def render_poll(self, req, title, votes, is_anonymous_poll, is_closed_poll): 
    142161        add_stylesheet(req, 'poll/css/poll.css') 
    143162        if not req.perm.has_permission('POLL_VIEW'): 
    144163            return '' 
     
    184203 
    185204        poll = Poll(self.base_dir, title, all_votes) 
    186205        if req.perm.has_permission('POLL_VOTE'): 
    187             poll.populate(req) 
    188         return poll.render(self.env, req) 
     206            poll.populate(req, is_anonymous_poll) 
     207        return poll.render(self.env, req, is_anonymous_poll, is_closed_poll) 
    189208 
    190209    # IPermissionRequestor methods 
    191210    def get_permission_actions(self): 

comment:8 Changed 21 months ago by falkb

Addon: Not tested with closing of non-anonymous polls. Could be a TODO but could give you an idea with the given patch.

comment:9 in reply to: ↑ 3 ; follow-ups: Changed 20 months ago by olemis

Replying to anonymous:

Replying to rjollos:

Looks like a nice feature. I think we should consider using a positional or kw arg rather than appending a pattern to the title.

AFAICT +1

I wonder how such arg can be programmed in a macro, even if the whole string is splitted in a string list by separator ';'.

If I understood correctly your implicit question , that's what trac.wiki.api.parse_args is for .

PS: Notice that commas in argument values have to be escaped

comment:10 in reply to: ↑ 9 Changed 20 months ago by rjollos

Replying to olemis:

PS: Notice that commas in argument values have to be escaped

Yeah, I think this is the best way to go. I've been trying to think about how we might better handle cases that the user hasn't escaped the args, since it will particularly cause problems for users that are upgrading and never took care to escape the poll question.

I haven't tested this out yet, but I was thinking of employing one of the following solutions:

  • Define the variable that determines is_anonymous_poll as a kwarg and then append all of the regular args together, since it's safe to assume that if more than one regular arg exists then poll question wasn't escaped properly.
  • Define is_anonymous_poll as a regular arg, but when evaluating the output of parse_args, if there isn't a case-insensitive match to true, assume that the poll question wasn't escaped properly and append the arg to the 0th arg.

A prerequisite to setting up the above behavior is to wire up unit tests, which will be very valuable here.

comment:11 in reply to: ↑ 9 Changed 20 months ago by falkb

Replying to olemis:

Replying to anonymous:

Replying to rjollos:

Looks like a nice feature. I think we should consider using a positional or kw arg rather than appending a pattern to the title.

AFAICT +1

Would it be compatible to older versions of poll, if we first split by ';', get a resulting string list, and then analyze the last one in the string list if it contains existing args?

Let's talk with the help of an example:

For instance, [[Poll(Please, tell me what do you think about foo?;Yes, it's OK;No, I don't like it;is_anonymous_poll=true)]] will then be split into 4 strings where the 4th one contains the arguments that we then analyze with trac.wiki.api.parse_args , and the check of the analyzing will have success on checking for known arguments.

If we just have [[Poll(Please, tell me what do you think about foo?;Yes, it's OK;No, I don't like it)]] , after the ';'-split, trac.wiki.api.parse_args will read the last partial string as (['No', 'I don't like it']), and then it can be decided as another poll answer instead of an argument list, because at least one of the strings is not known as valid argument.

Will this approach work?

Add Comment

Modify Ticket

Action
as assigned .
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.