Modify

Opened 11 years ago

Last modified 4 years ago

#10703 new enhancement

[PATCH] support of fully anonymous polls

Reported by: falkb Owned by:
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 (12)

comment:1 Changed 11 years ago by Ryan J Ollos

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 11 years ago by Ryan J Ollos

Priority: normalhigh
Status: newassigned

comment:3 in reply to:  1 ; Changed 11 years 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 11 years ago by Ryan J Ollos

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 11 years 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 11 years ago by Ryan J Ollos

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 11 years 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 11 years 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 ; Changed 11 years ago by Olemis Lang

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 11 years ago by Ryan J Ollos

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 11 years 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?

comment:12 Changed 4 years ago by Ryan J Ollos

Owner: Ryan J Ollos deleted
Status: assignednew

Modify Ticket

Change Properties
Set your email in Preferences
Action
as new The ticket will remain with no owner.

Add Comment


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

 
Note: See TracTickets for help on using tickets.