add user names in polls and poll() pagespec to match against
authorJulien Moutinho <julm+ikiwiki+poll@autogeree.net>
Tue, 11 Feb 2014 03:30:54 +0000 (04:30 +0100)
committerJulien Moutinho <julm@autogeree.net>
Thu, 13 Feb 2014 18:59:55 +0000 (19:59 +0100)
poll.pm

diff --git a/poll.pm b/poll.pm
index 3bd4af2..612b49d 100644 (file)
--- a/poll.pm
+++ b/poll.pm
@@ -7,94 +7,151 @@ use IkiWiki 3.00;
 use Encode;
 
 sub import {
-       hook(type => "getsetup", id => "poll", call => \&getsetup);
+       hook(type => "getsetup",   id => "poll", call => \&getsetup);
        hook(type => "preprocess", id => "poll", call => \&preprocess);
+       hook(type => "scan",       id => "poll", call => \&scan);
        hook(type => "sessioncgi", id => "poll", call => \&sessioncgi);
-}
-
-sub getsetup () {
-       return 
-               plugin => {
-                       safe => 1,
-                       rebuild => undef,
-                       section => "widget",
-               },
-}
+ }
 
 my %pagenum;
+sub getsetup () {
+       return
+        plugin =>
+                { safe => 1
+                , rebuild => undef
+                , section => "widget"
+                };
+ }
+sub scan (@) {
+       my %params = @_;
+       my $page = $params{page};
+       my $content = $params{content};
+       my $prefix = $config{prefix_directives} ? "!poll" : "poll";
+       my $type = IkiWiki::pagetype($pagesources{$page});
+       if (defined $type and $type eq "mdwn") {
+               my %polls = ();
+               while ($content =~ m{(\\?)\[\[\Q$prefix\E(\s+id="([^"]*)")?\s+(.+?)\s*\]\]}gs) {
+                       my ($escape, $poll, $directive) = ($1, $3, $4);
+                       next if $escape;
+                       $poll = '' unless defined $poll;
+                       error("poll id=`$poll' must match (|[a-z][a-z0-9_-]*) on page=`$page'")
+                               unless $poll =~              m/^(|[a-z][a-z0-9_-]*)$/;
+                       my %poll = ();
+                       while ($directive =~ m/(^|\s+)(\d+)(="([^"]*)")?\s+"?([^"]*)"?/gs) {
+                               my ($unknown_votes, $known_votes, $choice) = ($2, $4, $5);
+                               my @known_votes = defined $known_votes ? split(/\s+/, $known_votes) : ();
+                                       $poll{$choice} =
+                                        { unknown_votes => $unknown_votes
+                                        , known_votes => \@known_votes
+                                        };
+                               foreach my $user (@known_votes) {
+                                       my $userpage = linkpage(($config{userdir}?$config{userdir}.'/':'').$user);
+                                       add_link($page, $userpage);
+                                }
+                        }
+                       error("poll id=`$poll' already exists on page=`$page'")
+                               if exists $polls{$poll};
+                       $polls{$poll} = \%poll;
+                }
+               $IkiWiki::pagestate{$page}{poll} = \%polls;
+        }
+ }
 sub preprocess (@) {
-       my %params=(open => "yes", total => "yes", percent => "yes",
-               expandable => "no", @_);
-
+       my %params=
+        ( open => "yes"
+        , total => "yes"
+        , percent => "yes"
+        , expandable => "no"
+        , @_ );
+       
        my $open=IkiWiki::yesno($params{open});
        my $showtotal=IkiWiki::yesno($params{total});
        my $showpercent=IkiWiki::yesno($params{percent});
        my $expandable=IkiWiki::yesno($params{expandable});
        my $num=++$pagenum{$params{page}}{$params{destpage}};
-
+       
        my %choices;
        my @choices;
        my $total=0;
        while (@_) {
-               my $key=shift;
-               my $value=shift;
-
-               next unless $key =~ /^\d+/;
-
-               my $num=$key;
-               $key=shift;
-               $value=shift;
-
-               $choices{$key}=$num;
-               push @choices, $key;
-               $total+=$num;
-       }
-
+               my $unknown_votes = shift;
+               my $known_votes = shift;
+               next
+                       unless $unknown_votes =~ /^\d+$/;
+               my @users = $known_votes ? split(/\s+/, $known_votes) : ();
+               my $choice = shift;
+               shift;
+               my $tot = ($unknown_votes + @users);
+               $choices{$choice} =
+                { unknown_votes => $unknown_votes
+                , users => \@users
+                , total => $tot
+                };
+               push @choices, $choice;
+               $total += $tot;
+        }
+       use URI::Escape;
+       my $uri_page = URI::Escape::uri_escape_utf8($params{page}, '^A-Za-z0-9\-\._~/');
        my $ret="";
        foreach my $choice (@choices) {
                if ($open && exists $config{cgiurl}) {
                        # use POST to avoid robots
                        $ret.="<form method=\"POST\" action=\"".IkiWiki::cgiurl()."\">\n";
-               }
-               my $percent=$total > 0 ? int($choices{$choice} / $total * 100) : 0;
-               $ret.="<p>\n";
-               if ($showpercent) {
-                       $ret.="$choice ($percent%)\n";
-               }
+                }
+               $ret.="<dt class='choice'>";
+               my $percent = $total > 0 ? int($choices{$choice}{total} / $total * 100) : 0;
+               my $votes = $choices{$choice}{total};
+               $votes .= '/'.$total
+                       if $showtotal;
+               $votes .= " ($percent%)"
+                       if $showpercent;
+               if (@{$choices{$choice}{users}} > 0) {
+                       $votes .= " : ".join(', ', map {
+                                       my $userpage = linkpage(($config{userdir}?$config{userdir}.'/':'').$_);
+                                       htmllink($params{page}, $params{destpage}, '/'.$userpage, linktext => pagetitle($_))
+                                } @{$choices{$choice}{users}});
+                       $votes .= " + ".$choices{$choice}{unknown_votes}." "
+                        . ($choices{$choice}{unknown_votes} > 1 ? gettext("unknowns") : gettext("unknown"))
+                               if $choices{$choice}{unknown_votes};
+                }
                else {
-                       $ret.="$choice ($choices{$choice})\n";
-               }
+                       $votes .= " : ".($choices{$choice}{unknown_votes}." ".gettext("unknowns"))
+                               if $choices{$choice}{unknown_votes};
+                }
                if ($open && exists $config{cgiurl}) {
                        $ret.="<input type=\"hidden\" name=\"do\" value=\"poll\" />\n";
                        $ret.="<input type=\"hidden\" name=\"num\" value=\"$num\" />\n";
-                       $ret.="<input type=\"hidden\" name=\"page\" value=\"$params{page}\" />\n";
+                       $ret.="<input type=\"hidden\" name=\"page\" value=\"$uri_page\" />\n";
                        $ret.="<input type=\"hidden\" name=\"choice\" value=\"$choice\" />\n";
                        $ret.="<input type=\"submit\" value=\"".gettext("vote")."\" />\n";
-               }
-               $ret.="</p>\n<hr class=poll align=left width=\"$percent%\"/>\n";
+                }
+               $ret.="<span class='description'>$choice</span>";
+               $ret.="</dt>";
+               $ret.="<dd class='votes'>";
+               $ret.=$votes;
+               $ret.="<hr class='poll' align=left width=\"$percent%\"/>\n";
                if ($open && exists $config{cgiurl}) {
                        $ret.="</form>\n";
-               }
-       }
+                }
+               $ret.="</dd>\n";
+        }
        
        if ($expandable && $open && exists $config{cgiurl}) {
-               $ret.="<p>\n";
+               $ret.="<dt>";
                $ret.="<form method=\"POST\" action=\"".IkiWiki::cgiurl()."\">\n";
                $ret.="<input type=\"hidden\" name=\"do\" value=\"poll\" />\n";
                $ret.="<input type=\"hidden\" name=\"num\" value=\"$num\" />\n";
-               $ret.="<input type=\"hidden\" name=\"page\" value=\"$params{page}\" />\n";
+               $ret.="<input type=\"hidden\" name=\"page\" value=\"$uri_page\" />\n";
                $ret.=gettext("Write in").": <input name=\"choice\" size=50 />\n";
                $ret.="<input type=\"submit\" value=\"".gettext("vote")."\" />\n";
+               $ret.="</dt>\n";
+               $ret.="<dd>";
+               $ret.="</dd>\n";
                $ret.="</form>\n";
                $ret.="</p>\n";
-       }
-
-       if ($showtotal) {
-               $ret.="<span>".gettext("Total votes:")." $total</span>\n";
-       }
-       return "<div class=poll>$ret</div>";
-}
-
+        }
+       return "<dl class='poll'>$ret</dl>";
+ }
 sub sessioncgi ($$) {
        my $cgi=shift;
        my $session=shift;
@@ -102,28 +159,27 @@ sub sessioncgi ($$) {
                my $choice=decode_utf8($cgi->param('choice'));
                if (! defined $choice || not length $choice) {
                        error("no choice specified");
-               }
+                }
                my $num=$cgi->param('num');
                if (! defined $num) {
                        error("no num specified");
-               }
-               my $page=IkiWiki::possibly_foolish_untaint($cgi->param('page'));
+                }
+               my $page=Encode::decode_utf8(URI::Escape::uri_unescape(IkiWiki::possibly_foolish_untaint($cgi->param('page'))));
                if (! defined $page || ! exists $pagesources{$page}) {
+                       use Data::Dumper;
                        error("bad page name");
-               }
-
+                }
+               
                # Did they vote before? If so, let them change their vote,
                # and check for dups.
                my $choice_param="poll_choice_${page}_$num";
                my $oldchoice=$session->param($choice_param);
-               if (defined $oldchoice && $oldchoice eq $choice) {
-                       # Same vote; no-op.
-                       IkiWiki::redirect($cgi, urlto($page));
-                       exit;
-               }
-
+               #if (defined $oldchoice && $oldchoice eq $choice) {
+               #       # Same vote; no-op.
+               #       IkiWiki::redirect($cgi, urlto($page));
+               #       exit;
+               # }
                my $prefix=$config{prefix_directives} ? "!poll" : "poll";
-
                my $content=readfile(srcfile($pagesources{$page}));
                # Now parse the content, find the right poll,
                # and find the choice within it, and increment its number.
@@ -131,53 +187,132 @@ sub sessioncgi ($$) {
                my $edit=sub {
                        my $escape=shift;
                        my $params=shift;
-                       return "\\[[$prefix $params]]" if $escape;
+                       return $params
+                               if $escape;
                        if (--$num == 0) {
-                               if ($params=~s/(^|\s+)(\d+)\s+"?\Q$choice\E"?(\s+|$)/$1.($2+1)." \"$choice\"".$3/se) {
-                               }
+                               my $vote = sub {
+                                       my ($action, $unknown_votes, $known_votes) = @_;
+                                       my $user = $session->param("name");
+                                       my %users;
+                                       foreach (split(/\s+/, $known_votes)) {
+                                               $users{$_} = 1;
+                                        }
+                                       if ($action eq 'add') {
+                                               if (defined $user) {
+                                                       if (exists $users{$user} or (defined $oldchoice and $oldchoice eq $choice)) {
+                                                               delete $users{$user};
+                                                               $known_votes = join(' ', sort {lc $a <=> lc $b} (keys %users));
+                                                        }
+                                                       else {
+                                                               $known_votes = join(' ', sort {lc $a <=> lc $b} ($user, keys %users));
+                                                        }
+                                                }
+                                               else {
+                                                       $unknown_votes += 1;
+                                                }
+                                        }
+                                       elsif ($action eq 'del') {
+                                               if (defined $user) {
+                                                       if (exists $users{$user}) {
+                                                               delete $users{$user};
+                                                               $known_votes = join(' ', sort {lc $a <=> lc $b} (keys %users));
+                                                        }
+                                                }
+                                               else {
+                                                       $unknown_votes = ($unknown_votes-1 >=0 ? $unknown_votes-1 : 0);
+                                                }
+                                        }
+                                       return $unknown_votes.($known_votes?"=\"$known_votes\"":"")
+                                };
+                               if ($params=~s/(^|\s+)(\d+)(="([^"]*)")?(\s+)"?\Q$choice\E"?(\s+|$)/$1.$vote->('add', $2, $4)."$5\"$choice\"".$6/es) {
+                                }
                                elsif ($params=~/expandable=(\w+)/
                                    & &IkiWiki::yesno($1)) {
                                        $choice=~s/["\]\n\r]//g;
                                        $params.=" 1 \"$choice\""
                                                if length $choice;
-                               }
-                               if (defined $oldchoice) {
-                                       $params=~s/(^|\s+)(\d+)\s+"?\Q$oldchoice\E"?(\s+|$)/$1.($2-1 >=0 ? $2-1 : 0)." \"$oldchoice\"".$3/se;
-                               }
-                       }
-                       return "[[$prefix $params]]";
-               };
-               $content =~ s{(\\?)\[\[\Q$prefix\E\s+([^]]+)\s*\]\]}{$edit->($1, $2)}seg;
-
+                                }
+                               if (defined $oldchoice and not ($oldchoice eq $choice)) {
+                                       $params=~s/(^|\s+)(\d+)(="([^"]*)")?(\s+)"?\Q$oldchoice\E"?(\s+|$)/$1.$vote->('del', $2, $4)."$5\"$oldchoice\"".$6/es;
+                                }
+                        }
+                       return "$params";
+                };
+               my $id='';
+               $content =~ s{(\\?)\[\[\Q$prefix\E(\s+id="([^"]*)")?(\s+)(.+?)(\s*)\]\]}{$id=$3;$1.'[['.$prefix.$2.$4.$edit->($1, $5).$6.']]'}gse;
+               
                # Store their vote, update the page, and redirect to it.
                writefile($pagesources{$page}, $config{srcdir}, $content);
-               $session->param($choice_param, $choice);
+               if (defined $oldchoice and $choice eq $oldchoice) {
+                       $session->param($choice_param, undef);
+                       # TOTRY: $session->clear($choice_param);
+                }
+               else {
+                       $session->param($choice_param, $choice);
+                }
                IkiWiki::cgi_savesession($session);
-               $oldchoice=$session->param($choice_param);
                if ($config{rcs}) {
                        IkiWiki::disable_commit_hook();
-                       IkiWiki::rcs_commit(
-                               file => $pagesources{$page},
-                               message => "poll vote ($choice)",
-                               token => IkiWiki::rcs_prepedit($pagesources{$page}),
-                               session => $session,
-                       );
+                       IkiWiki::rcs_commit
+                        ( file    => $pagesources{$page}
+                        , message => "poll vote: id=$id: $choice"
+                        , session => $session
+                        , token   => IkiWiki::rcs_prepedit($pagesources{$page})
+                        );
                        IkiWiki::enable_commit_hook();
                        IkiWiki::rcs_update();
-               }
+                }
                require IkiWiki::Render;
                IkiWiki::refresh();
                IkiWiki::saveindex();
-
-               # Need to set cookie in same http response that does the
-               # redir.
+               # Need to set cookie in same http response that does the redir.
                eval q{use CGI::Cookie};
                error($@) if $@;
-               my $cookie = CGI::Cookie->new(-name=> $session->name, -value=> $session->id);
-               print $cgi->redirect(-cookie => $cookie,
-                       -url => urlto($page));
+               my $cookie = CGI::Cookie->new
+                ( -name  => $session->name
+                , -value => $session->id );
+               print $cgi->redirect
+                ( -cookie => $cookie
+                , -url    => urlto($page) );
                exit;
-       }
-}
+        }
+ }
+package IkiWiki::PageSpec;
+       sub match_poll ($$;@) {
+               my ($page, $match, %params) = @_;
+               my $polls = $IkiWiki::pagestate{$page}{poll};
+               if (defined $polls and %$polls) {
+                       my ($match_poll, $match_user, $match_choice) = $match =~ m/^id=(.*?) user=(.*?) choice=(.*?)$/;
+                       if (exists $polls->{$match_poll}) {
+                               my %poll = %{$polls->{$match_poll}};
+                               my $match_user_re   = IkiWiki::glob2re($match_user?$match_user:'*');
+                               my $match_choice_re = IkiWiki::glob2re($match_choice?$match_choice:'*');
+                               while (my ($choice, $data) = each %poll) {
+                                       next unless $choice =~ $match_choice_re;
+                                       if ($match_user eq '') {
+                                               if ($data->{unknown_votes} > 0) {
+                                                       return IkiWiki::SuccessReason->new("unkown user has voted for choice=`$choice'", $page => $IkiWiki::DEPEND_CONTENT);
+                                                }
+                                               else {
+                                                       return IkiWiki::FailReason->new("no unkown user has voted for choice=`$choice'", $page => $IkiWiki::DEPEND_CONTENT);
+                                                }
+                                        }
+                                       else {
+                                               foreach my $user (@{$data->{known_votes}}) {
+                                                       next unless $user =~ $match_user_re;
+                                                       return IkiWiki::SuccessReason->new("user=`$user' has voted for choice=`$choice'", $page => $IkiWiki::DEPEND_CONTENT);
+                                                }
+                                        }
+                                }
+                               return IkiWiki::FailReason->new("no user=`$match_user' has voted for choice=`$match_choice'", $page => $IkiWiki::DEPEND_CONTENT);
+                        }
+                       else {
+                               return IkiWiki::FailReason->new("no poll id=`$match_poll'", $page => $IkiWiki::DEPEND_CONTENT);
+                        }
+                }
+               else {
+                       return IkiWiki::FailReason->new("no poll", $page => $IkiWiki::DEPEND_CONTENT);
+                }
+        }
 
-1
+1;