summaryrefslogtreecommitdiff
path: root/lib/VNDB
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2015-11-10 12:48:47 +0100
committerYorhel <git@yorhel.nl>2015-11-10 12:48:47 +0100
commitfd9f224ad7e1d6ebe4f7abba75526b5190c963ba (patch)
treee23f4975f5282b49fc685ba0b9125402f0fe10c5 /lib/VNDB
parent339f47375136b8abcb418cbdeae8adeaa2d3e73d (diff)
parent721f22030f2f87c291bde088bf96648e9cdedc0d (diff)
Merge branch 'poll' of https://github.com/morkt/vndb into polls
Diffstat (limited to 'lib/VNDB')
-rw-r--r--lib/VNDB/DB/Discussions.pm2
-rw-r--r--lib/VNDB/DB/Polls.pm95
-rw-r--r--lib/VNDB/Handler/Discussions.pm161
-rw-r--r--lib/VNDB/Util/FormHTML.pm2
4 files changed, 259 insertions, 1 deletions
diff --git a/lib/VNDB/DB/Discussions.pm b/lib/VNDB/DB/Discussions.pm
index 48859611..c380feeb 100644
--- a/lib/VNDB/DB/Discussions.pm
+++ b/lib/VNDB/DB/Discussions.pm
@@ -40,6 +40,7 @@ sub dbThreadGet {
my @select = (
qw|t.id t.title t.count t.locked t.hidden|,
$o{what} =~ /lastpost/ ? ('tpl.uid AS luid', q|EXTRACT('epoch' from tpl.date) AS ldate|, 'ul.username AS lusername') : (),
+ 'p.id AS poll',
);
my @join = (
@@ -49,6 +50,7 @@ sub dbThreadGet {
) : (),
$o{type} && $o{iid} ?
'JOIN threads_boards tb ON tb.tid = t.id' : (),
+ 'LEFT JOIN polls p ON p.tid = t.id',
);
my $order = sprintf {
diff --git a/lib/VNDB/DB/Polls.pm b/lib/VNDB/DB/Polls.pm
new file mode 100644
index 00000000..0b8ff81d
--- /dev/null
+++ b/lib/VNDB/DB/Polls.pm
@@ -0,0 +1,95 @@
+
+package VNDB::DB::Polls;
+
+use strict;
+use warnings;
+use Exporter 'import';
+
+our @EXPORT = qw|dbPollGet dbPollVote dbPollAdd dbPollEdit|;
+
+
+# Options: id, tid, uid, what
+# What: votes
+sub dbPollGet {
+ my($self, %o) = @_;
+ $o{what} ||= '';
+ $o{uid} ||= $self->authInfo->{id};
+
+ my %where = (
+ $o{id} ? ('p.id = ?' => $o{id}) :
+ $o{tid} ? ('p.tid = ?' => $o{tid}) : (),
+ );
+
+ my @select = (
+ qw|p.id p.question p.max_options p.preview p.recast|,
+ $o{what} =~ /votes/ ?
+ ('(SELECT COUNT(DISTINCT uid) FROM polls_votes pv WHERE pv.pid = p.id) AS votes') : (),
+ );
+ my $p = $self->dbRow(q|
+ SELECT !s
+ FROM polls p
+ !W|,
+ join(', ', @select), \%where
+ );
+ return $p unless %$p;
+
+ my $options_query = $o{what} =~ /votes/ ?
+ q|SELECT id, option, COUNT(pv.optid) AS votes
+ FROM polls_options po
+ LEFT JOIN polls_votes pv ON po.id = pv.optid
+ WHERE po.pid = ? GROUP BY id ORDER BY id| :
+ q|SELECT id, option
+ FROM polls_options po
+ WHERE po.pid = ? ORDER BY id|;
+ $p->{options} = $self->dbAll($options_query, $p->{id});
+
+ $p->{user} = $o{uid} ? [
+ map $_->{optid}, @{$self->dbAll(q|
+ SELECT optid FROM polls_votes
+ WHERE pid = ? AND uid = ?|, $p->{id}, $o{uid})}
+ ] : [];
+
+ return $p;
+}
+
+
+sub dbPollVote {
+ my($self, $id, %o) = @_;
+
+ $self->dbExec('DELETE FROM polls_votes WHERE pid = ? AND uid = ?', $id, $o{uid});
+ $self->dbExec('INSERT INTO polls_votes (pid, uid, optid) VALUES (?, ?, ?)',
+ $id, $o{uid}, $_) for @{$o{options}};
+}
+
+
+sub dbPollAdd {
+ my($self, %o) = @_;
+
+ my $id = $self->dbRow(q|
+ INSERT INTO polls (tid, question, max_options, preview, recast)
+ VALUES (?, ?, ?, ?, ?) RETURNING id|,
+ $o{tid}, $o{question}, $o{max_options}, $o{preview}, $o{recast}
+ )->{id};
+
+ $self->dbExec('INSERT INTO polls_options (pid, option) VALUES (?, ?)', $id, $_)
+ for @{$o{options}};
+
+ return $id;
+}
+
+
+sub dbPollEdit {
+ my($self, $id, %o) = @_;
+
+ my %set = map exists $o{$_} ? ("$_ = ?" => $o{$_}) : (),
+ qw|question max_options preview recast|;
+
+ $self->dbExec('UPDATE polls !H WHERE id = ?', \%set, $id);
+ $self->dbExec('DELETE FROM polls_options WHERE pid = ?', $id);
+ $self->dbExec('INSERT INTO polls_options (pid, option) VALUES (?, ?)', $id, $_)
+ for @{$o{options}};
+}
+
+
+1;
+
diff --git a/lib/VNDB/Handler/Discussions.pm b/lib/VNDB/Handler/Discussions.pm
index 421a998d..4542a5fb 100644
--- a/lib/VNDB/Handler/Discussions.pm
+++ b/lib/VNDB/Handler/Discussions.pm
@@ -6,10 +6,12 @@ use warnings;
use TUWF ':html', 'xml_escape', 'uri_escape';
use POSIX 'ceil';
use VNDB::Func;
+use List::Util qw(first max);
TUWF::register(
qr{t([1-9]\d*)(?:/([1-9]\d*))?} => \&thread,
+ qr{t([1-9]\d*)(/[1-9]\d*)?/vote} => \&vote,
qr{t([1-9]\d*)\.([1-9]\d*)} => \&redirect,
qr{t/(all|db|an|ge|[vpu])([1-9]\d*)?} => \&board,
qr{t([1-9]\d*)/reply} => \&edit,
@@ -37,6 +39,8 @@ sub thread {
my $p = $self->dbPostGet(tid => $tid, results => 25, page => $page, what => 'user');
return $self->resNotFound if !$p->[0];
+ my $poll = $self->dbPollGet(id => $t->{poll}, what => 'votes') if $t->{poll};
+
$self->htmlHeader(title => $t->{title}, noindex => 1);
div class => 'mainbox';
h1 $t->{title};
@@ -56,6 +60,8 @@ sub thread {
end;
end 'div';
+ _poll($self, "/t$tid".($page > 1 ? "/$page" : ''), $poll) if $poll;
+
$self->htmlBrowseNavigate("/t$tid/", $page, [ $t->{count}, 25 ], 't', 1);
div class => 'mainbox thread';
table class => 'stripe';
@@ -154,6 +160,8 @@ sub edit {
my $p = $num && $self->dbPostGet(tid => $tid, num => $num, what => 'user')->[0];
return $self->resNotFound if $num && !$p->{num};
+ $t->{poll} = $self->dbPollGet(id => $t->{poll}) if $tid && $t->{poll};
+
# are we allowed to perform this action?
return $self->htmlDenied if !$self->authCan('board')
|| ($tid && ($t->{locked} || $t->{hidden}) && !$self->authCan('boardmod'))
@@ -167,6 +175,11 @@ sub edit {
!$tid || $num == 1 ? (
{ post => 'title', maxlength => 50 },
{ post => 'boards', maxlength => 50 },
+ { post => 'poll_q', required => 0, maxlength => 100 },
+ { post => 'poll_opt', required => 0, maxlength => 100*$self->{poll_options} },
+ { post => 'poll_max', required => 0, default => 1, template => 'uint', min => 1, max => $self->{poll_options} },
+ { post => 'poll_preview', required => 0 },
+ { post => 'poll_recast', required => 0 },
) : (),
$self->authCan('boardmod') ? (
{ post => 'locked', required => 0 },
@@ -205,6 +218,22 @@ sub edit {
}
}
+ # validate poll options
+ my %poll;
+ if(!$frm->{_err} && $frm->{poll_opt}) {
+ # split by lines, trimming whitespace
+ my @options = split /\s*\n\s*/, $frm->{poll_opt};
+ %poll = (
+ question => $frm->{poll_q},
+ preview => $frm->{poll_preview}?1:0,
+ recast => $frm->{poll_recast}?1:0,
+ options => \@options,
+ max_options => $frm->{poll_max},
+ );
+ push @{$frm->{_err}}, [ 'poll_max', 'max', scalar @options ] if @options > 1 && @options < $poll{max_options};
+ push @{$frm->{_err}}, 'poll' if @options > $self->{poll_options} || @options < 2;
+ }
+
if(!$frm->{_err}) {
my($ntid, $nnum) = ($tid, $num);
@@ -218,6 +247,16 @@ sub edit {
);
$self->dbThreadEdit($tid, %thread) if $tid;
$ntid = $self->dbThreadAdd(%thread) if !$tid;
+ if(%poll) {
+ $poll{tid} = $ntid;
+ if($tid && $t->{poll}) {
+ my $same = (!first { !($t->{poll}{$_} ~~ $poll{$_}) } qw|question preview recast max_options|)
+ && [ map $_->{option}, @{$t->{poll}{options}} ] ~~ $poll{options};
+ $self->dbPollEdit($t->{poll}{id}, %poll) unless $same;
+ } else {
+ $self->dbPollAdd(%poll);
+ }
+ }
}
# create/edit post
@@ -242,10 +281,18 @@ sub edit {
$frm->{title} ||= $t->{title};
$frm->{locked} = $t->{locked} if !exists $frm->{locked};
$frm->{hidden} = $t->{hidden} if !exists $frm->{hidden};
+ if($t->{poll}) {
+ $frm->{poll_q} ||= $t->{poll}{question};
+ $frm->{poll_max} ||= $t->{poll}{max_options};
+ $frm->{poll_preview} = $t->{poll}{preview} if !exists $frm->{poll_preview};
+ $frm->{poll_recast} = $t->{poll}{recast} if !exists $frm->{poll_recast};
+ $frm->{poll_opt} ||= join "\n", map $_->{option}, @{$t->{poll}{options}};
+ }
}
}
delete $frm->{_err} unless ref $frm->{_err};
$frm->{boards} ||= $board;
+ $frm->{poll_max} ||= 1;
# generate html
my $url = !$tid ? "/t/$board/new" : !$num ? "/t$tid/reply" : "/t$tid.$num/edit";
@@ -273,11 +320,54 @@ sub edit {
) : (),
[ text => name => mt('_postedit_form_msg').'<br /><b class="standout">'.mt('_inenglish').'</b>', short => 'msg', rows => 25, cols => 75 ],
[ static => content => mt('_postedit_form_msg_format') ],
+ (!$tid || $num == 1) ? (
+ [ input => short => 'poll_q', name => mt('_postedit_form_poll_q'), width => 250 ],
+ $num && $frm->{poll_opt} ? (
+ [ static => content => '<b class="standout">'.mt('_postedit_form_poll_warning').'</b>' ]
+ ) : (),
+ [ text => short => 'poll_opt', name => mt('_postedit_form_poll_opt').'<br /><i>'.mt('_postedit_form_poll_optmax', $self->{poll_options}).'</i>', rows => 8, cols => 35 ],
+ [ input => short => 'poll_max', width => 16, post => ' '.mt('_postedit_form_poll_max') ],
+ [ check => short => 'poll_preview', name => mt('_postedit_form_poll_view') ],
+ [ check => short => 'poll_recast', name => mt('_postedit_form_poll_recast') ],
+ !$frm->{poll_opt} ? (
+ [ static => content => '<br /><a id="poll_add" class="hidden" href="#poll_q">'.mt('_postedit_form_poll_add').'</a>' ]
+ ) : (),
+ ) : (),
]);
$self->htmlFooter;
}
+sub vote {
+ my($self, $tid, $page) = @_;
+ return $self->htmlDenied if !$self->authCan('board');
+ return if !$self->authCheckCode;
+
+ my $url = '/t'.$tid.($page ? "/$page" : '');
+ my $poll = $self->dbPollGet(tid => $tid);
+ return $self->resNotFound if !%$poll;
+
+ # user has already voted and poll doesn't allow to change a vote.
+ return $self->resRedirect($url, 'post') if @{$poll->{user}} && !$poll->{recast};
+
+ my $f = $self->formValidate({
+ post => 'option', multi => 1, enum => [ map $_->{id}, @{$poll->{options}} ],
+ });
+ if(!$f->{_err} && (!@{$f->{option}} || @{$f->{option}} > $poll->{max_options})) {
+ push @{$f->{_err}}, 'poll';
+ }
+ if($f->{_err}) {
+ $self->htmlHeader(title => mt '_poll_error');
+ $self->htmlFormError($f, 1);
+ $self->htmlFooter;
+ return;
+ }
+
+ $self->dbPollVote($poll->{id}, uid => $self->authInfo->{id}, options => $f->{option});
+ $self->resRedirect($url, 'post');
+}
+
+
sub board {
my($self, $type, $iid) = @_;
$iid ||= '';
@@ -510,7 +600,10 @@ sub _threadlist {
my($self, $n, $o) = @_;
Tr;
td class => 'tc1';
- a $o->{locked} ? ( class => 'locked' ) : (), href => "/t$o->{id}", shorten $o->{title}, 50;
+ a $o->{locked} ? ( class => 'locked' ) : (), href => "/t$o->{id}";
+ span class => 'pollflag', '['.mt('_threadlist_poll').']' if $o->{poll};
+ txt shorten $o->{title}, 50;
+ end;
b class => 'boards';
my $i = 1;
my @boards = sort { $a->{type}.$a->{iid} cmp $b->{type}.$b->{iid} } grep $_->{type}.($_->{iid}||'') ne $board, @{$o->{boards}};
@@ -541,5 +634,71 @@ sub _threadlist {
}
+sub _poll {
+ my($self, $url, $poll) = @_;
+ my %own_votes = map +($_ => 1), @{$poll->{user}} if @{$poll->{user}};
+ my $preview = !%own_votes && $self->reqGet('pollview') && $poll->{preview};
+
+ div class => 'mainbox poll';
+ form action => $url.'/vote', method => 'post';
+ h1 class => 'question', $poll->{question} if $poll->{question};
+ input type => 'hidden', name => 'formcode', value => $self->authGetCode($url.'/vote');
+ table class => 'votebooth';
+ my $allow_vote = $self->authCan('board') && (!%own_votes || $poll->{recast});
+ if($allow_vote && $poll->{max_options} > 1) {
+ thead; Tr; td colspan => 3;
+ i mt('_poll_choose', $poll->{max_options});
+ end; end; end;
+ }
+ tfoot; Tr;
+ td class => 'tc1';
+ input type => 'submit', class => 'submit', value => mt('_poll_vote') if $allow_vote;
+ if(!$self->authCan('board')) {
+ b class => 'standout', mt('_poll_novote_login');
+ }
+ end;
+ td class => 'tc2', colspan => 2;
+ if($poll->{preview} || %own_votes) {
+ if(!$poll->{votes}) {
+ i mt('_poll_no_votes');
+ } elsif(!$preview && !%own_votes) {
+ a href => $url.'?pollview=1', id => 'pollpreview', mt('_poll_results');
+ } else {
+ txt mt('_poll_total_votes', $poll->{votes});
+ }
+ }
+ end;
+ end; end;
+ tbody;
+ my $max = max map $_->{votes}, @{$poll->{options}};
+ my $show_graph = $max && (%own_votes || $preview);
+ my $graph_width = 200;
+ for my $opt (@{$poll->{options}}) {
+ my $own = exists $own_votes{$opt->{id}} ? ' own' : '';
+ Tr $own ? (class => 'odd') : ();
+ td class => 'tc1';
+ label;
+ input type => $poll->{max_options} > 1 ? 'checkbox' : 'radio', name => 'option', class => 'option', value => $opt->{id}, $own ? (checked => '') : () if $allow_vote;
+ span class => 'option'.$own, $opt->{option};
+ end;
+ end;
+ if($show_graph) {
+ td class => 'tc2';
+ div class => 'graph', style => sprintf('width: %dpx', $opt->{votes}/$max*$graph_width), ' ';
+ div class => 'number', $opt->{votes};
+ end;
+ td class => 'tc3', sprintf('%.3g%%', $poll->{votes} ? $opt->{votes}/$poll->{votes}*100 : 0);
+ } else {
+ td class => 'tc2', colspan => 2, '';
+ }
+ end;
+ }
+ end;
+ end 'table';
+ end 'form';
+ end 'div';
+}
+
+
1;
diff --git a/lib/VNDB/Util/FormHTML.pm b/lib/VNDB/Util/FormHTML.pm
index afecbe08..e1f3bdfc 100644
--- a/lib/VNDB/Util/FormHTML.pm
+++ b/lib/VNDB/Util/FormHTML.pm
@@ -34,6 +34,8 @@ sub htmlFormError {
if($type eq 'required') {
li; lit mt $field eq 'editsum' ?'_formerr_tpl_editsum' : '_formerr_required', $field; end;
}
+ li mt '_formerr_min', $field, $rule if $type eq 'min';
+ li mt '_formerr_max', $field, $rule if $type eq 'max';
li mt '_formerr_minlength', $field, $rule if $type eq 'minlength';
li mt '_formerr_maxlength', $field, $rule if $type eq 'maxlength';
li mt '_formerr_enum', $field, join ', ', @$rule if $type eq 'enum';