| # This Source Code Form is subject to the terms of the Mozilla Public |
| # License, v. 2.0. If a copy of the MPL was not distributed with this |
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| # |
| # This Source Code Form is "Incompatible With Secondary Licenses", as |
| # defined by the Mozilla Public License, v. 2.0. |
| |
| package Bugzilla::Extension::Voting; |
| |
| use 5.10.1; |
| use strict; |
| use warnings; |
| |
| use parent qw(Bugzilla::Extension); |
| |
| use Bugzilla::Bug; |
| use Bugzilla::BugMail; |
| use Bugzilla::Constants; |
| use Bugzilla::Error; |
| use Bugzilla::Field; |
| use Bugzilla::Mailer; |
| use Bugzilla::User; |
| use Bugzilla::Util qw(detaint_natural); |
| use Bugzilla::Token; |
| |
| use List::Util qw(min sum); |
| |
| use constant VERSION => BUGZILLA_VERSION; |
| use constant DEFAULT_VOTES_PER_BUG => 1; |
| # These came from Bugzilla itself, so they maintain the old numbers |
| # they had before. |
| use constant CMT_POPULAR_VOTES => 3; |
| use constant REL_VOTER => 4; |
| |
| ################ |
| # Installation # |
| ################ |
| |
| BEGIN { |
| *Bugzilla::Bug::votes = \&votes; |
| } |
| |
| sub votes { |
| my $self = shift; |
| my $dbh = Bugzilla->dbh; |
| |
| return $self->{votes} if exists $self->{votes}; |
| |
| $self->{votes} = $dbh->selectrow_array('SELECT votes FROM bugs WHERE bug_id = ?', |
| undef, $self->id); |
| return $self->{votes}; |
| } |
| |
| sub db_schema_abstract_schema { |
| my ($self, $args) = @_; |
| $args->{'schema'}->{'votes'} = { |
| FIELDS => [ |
| who => {TYPE => 'INT3', NOTNULL => 1, |
| REFERENCES => {TABLE => 'profiles', |
| COLUMN => 'userid', |
| DELETE => 'CASCADE'}}, |
| bug_id => {TYPE => 'INT3', NOTNULL => 1, |
| REFERENCES => {TABLE => 'bugs', |
| COLUMN => 'bug_id', |
| DELETE => 'CASCADE'}}, |
| vote_count => {TYPE => 'INT2', NOTNULL => 1}, |
| ], |
| INDEXES => [ |
| votes_who_idx => ['who'], |
| votes_bug_id_idx => ['bug_id'], |
| ], |
| }; |
| } |
| |
| sub install_update_db { |
| my $dbh = Bugzilla->dbh; |
| # Note that before Bugzilla 4.0, voting was a built-in part of Bugzilla, |
| # so updates to the columns for old versions of Bugzilla happen in |
| # Bugzilla::Install::DB, and can't safely be moved to this extension. |
| |
| my $field = new Bugzilla::Field({ name => 'votes' }); |
| if (!$field) { |
| Bugzilla::Field->create( |
| { name => 'votes', description => 'Votes', buglist => 1 }); |
| } |
| |
| $dbh->bz_add_column('products', 'votesperuser', |
| {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); |
| $dbh->bz_add_column('products', 'maxvotesperbug', |
| {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG}); |
| $dbh->bz_add_column('products', 'votestoconfirm', |
| {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); |
| |
| $dbh->bz_add_column('bugs', 'votes', |
| {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0}); |
| $dbh->bz_add_index('bugs', 'bugs_votes_idx', ['votes']); |
| |
| # maxvotesperbug used to default to 10,000, which isn't very sensible. |
| my $per_bug = $dbh->bz_column_info('products', 'maxvotesperbug'); |
| if ($per_bug->{DEFAULT} != DEFAULT_VOTES_PER_BUG) { |
| $dbh->bz_alter_column('products', 'maxvotesperbug', |
| {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG}); |
| } |
| } |
| |
| ########### |
| # Objects # |
| ########### |
| |
| sub object_columns { |
| my ($self, $args) = @_; |
| my ($class, $columns) = @$args{qw(class columns)}; |
| if ($class->isa('Bugzilla::Bug')) { |
| push(@$columns, 'votes'); |
| } |
| elsif ($class->isa('Bugzilla::Product')) { |
| push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm)); |
| } |
| } |
| |
| sub bug_fields { |
| my ($self, $args) = @_; |
| my $fields = $args->{fields}; |
| push(@$fields, 'votes'); |
| } |
| |
| sub object_update_columns { |
| my ($self, $args) = @_; |
| my ($object, $columns) = @$args{qw(object columns)}; |
| if ($object->isa('Bugzilla::Product')) { |
| push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm)); |
| } |
| } |
| |
| sub object_validators { |
| my ($self, $args) = @_; |
| my ($class, $validators) = @$args{qw(class validators)}; |
| if ($class->isa('Bugzilla::Product')) { |
| $validators->{'votesperuser'} = \&_check_votesperuser; |
| $validators->{'maxvotesperbug'} = \&_check_maxvotesperbug; |
| $validators->{'votestoconfirm'} = \&_check_votestoconfirm; |
| } |
| } |
| |
| sub object_before_create { |
| my ($self, $args) = @_; |
| my ($class, $params) = @$args{qw(class params)}; |
| if ($class->isa('Bugzilla::Bug')) { |
| # Don't ever allow people to directly specify "votes" into the bugs |
| # table. |
| delete $params->{votes}; |
| } |
| elsif ($class->isa('Bugzilla::Product')) { |
| my $input = Bugzilla->input_params; |
| $params->{votesperuser} = $input->{'votesperuser'}; |
| $params->{maxvotesperbug} = $input->{'maxvotesperbug'}; |
| $params->{votestoconfirm} = $input->{'votestoconfirm'}; |
| } |
| } |
| |
| sub object_end_of_set_all { |
| my ($self, $args) = @_; |
| my ($object) = $args->{object}; |
| if ($object->isa('Bugzilla::Product')) { |
| my $input = Bugzilla->input_params; |
| $object->set('votesperuser', $input->{'votesperuser'}); |
| $object->set('maxvotesperbug', $input->{'maxvotesperbug'}); |
| $object->set('votestoconfirm', $input->{'votestoconfirm'}); |
| } |
| } |
| |
| sub object_end_of_update { |
| my ($self, $args) = @_; |
| my ($object, $changes) = @$args{qw(object changes)}; |
| if ( $object->isa('Bugzilla::Product') |
| and ($changes->{maxvotesperbug} or $changes->{votesperuser} |
| or $changes->{votestoconfirm}) ) |
| { |
| _modify_bug_votes($object, $changes); |
| } |
| } |
| |
| sub bug_end_of_update { |
| my ($self, $args) = @_; |
| my ($bug, $changes) = @$args{qw(bug changes)}; |
| |
| if ($changes->{'product'}) { |
| my @msgs; |
| # If some votes have been removed, RemoveVotes() returns |
| # a list of messages to send to voters. |
| @msgs = _remove_votes($bug->id, 0, 'votes_bug_moved'); |
| _confirm_if_vote_confirmed($bug); |
| |
| foreach my $msg (@msgs) { |
| MessageToMTA($msg); |
| } |
| } |
| } |
| |
| ############# |
| # Templates # |
| ############# |
| |
| sub template_before_create { |
| my ($self, $args) = @_; |
| my $config = $args->{config}; |
| my $constants = $config->{CONSTANTS}; |
| $constants->{REL_VOTER} = REL_VOTER; |
| $constants->{CMT_POPULAR_VOTES} = CMT_POPULAR_VOTES; |
| $constants->{DEFAULT_VOTES_PER_BUG} = DEFAULT_VOTES_PER_BUG; |
| } |
| |
| |
| sub template_before_process { |
| my ($self, $args) = @_; |
| my ($vars, $file) = @$args{qw(vars file)}; |
| if ($file eq 'admin/users/confirm-delete.html.tmpl') { |
| my $who = $vars->{otheruser}; |
| my $votes = Bugzilla->dbh->selectrow_array( |
| 'SELECT COUNT(*) FROM votes WHERE who = ?', undef, $who->id); |
| if ($votes) { |
| $vars->{other_safe} = 1; |
| $vars->{votes} = $votes; |
| } |
| } |
| } |
| |
| ########### |
| # Bugmail # |
| ########### |
| |
| sub bugmail_recipients { |
| my ($self, $args) = @_; |
| my ($bug, $recipients) = @$args{qw(bug recipients)}; |
| my $dbh = Bugzilla->dbh; |
| |
| my $voters = $dbh->selectcol_arrayref( |
| "SELECT who FROM votes WHERE bug_id = ?", undef, $bug->id); |
| $recipients->{$_}->{+REL_VOTER} = 1 foreach (@$voters); |
| } |
| |
| sub bugmail_relationships { |
| my ($self, $args) = @_; |
| my $relationships = $args->{relationships}; |
| $relationships->{+REL_VOTER} = 'Voter'; |
| } |
| |
| ############### |
| # Sanitycheck # |
| ############### |
| |
| sub sanitycheck_check { |
| my ($self, $args) = @_; |
| my $status = $args->{status}; |
| |
| # Vote Cache |
| $status->('voting_count_start'); |
| my $dbh = Bugzilla->dbh; |
| my %cached_counts = @{ $dbh->selectcol_arrayref( |
| 'SELECT bug_id, votes FROM bugs', {Columns=>[1,2]}) }; |
| |
| my %real_counts = @{ $dbh->selectcol_arrayref( |
| 'SELECT bug_id, SUM(vote_count) FROM votes ' |
| . $dbh->sql_group_by('bug_id'), {Columns=>[1,2]}) }; |
| |
| my $needs_rebuild; |
| foreach my $id (keys %cached_counts) { |
| my $cached_count = $cached_counts{$id}; |
| my $real_count = $real_counts{$id} || 0; |
| if ($cached_count < 0) { |
| $status->('voting_count_alert', { id => $id }, 'alert'); |
| } |
| elsif ($cached_count != $real_count) { |
| $status->('voting_cache_alert', { id => $id }, 'alert'); |
| $needs_rebuild = 1; |
| } |
| } |
| |
| $status->('voting_cache_rebuild_fix') if $needs_rebuild; |
| } |
| |
| sub sanitycheck_repair { |
| my ($self, $args) = @_; |
| my $status = $args->{status}; |
| my $input = Bugzilla->input_params; |
| my $dbh = Bugzilla->dbh; |
| |
| return if !$input->{rebuild_vote_cache}; |
| |
| $status->('voting_cache_rebuild_start'); |
| $dbh->bz_start_transaction(); |
| $dbh->do('UPDATE bugs SET votes = 0'); |
| |
| my $sth = $dbh->prepare( |
| 'SELECT bug_id, SUM(vote_count) FROM votes ' |
| . $dbh->sql_group_by('bug_id')); |
| $sth->execute(); |
| |
| my $sth_update = $dbh->prepare( |
| 'UPDATE bugs SET votes = ? WHERE bug_id = ?'); |
| while (my ($id, $count) = $sth->fetchrow_array) { |
| $sth_update->execute($count, $id); |
| } |
| $dbh->bz_commit_transaction(); |
| $status->('voting_cache_rebuild_end'); |
| } |
| |
| |
| ############## |
| # Validators # |
| ############## |
| |
| sub _check_votesperuser { |
| return _check_votes(0, @_); |
| } |
| |
| sub _check_maxvotesperbug { |
| return _check_votes(DEFAULT_VOTES_PER_BUG, @_); |
| } |
| |
| sub _check_votestoconfirm { |
| return _check_votes(0, @_); |
| } |
| |
| # This subroutine is only used internally by other _check_votes_* validators. |
| sub _check_votes { |
| my ($default, $invocant, $votes, $field) = @_; |
| |
| detaint_natural($votes) if defined $votes; |
| # On product creation, if the number of votes is not a valid integer, |
| # we silently fall back to the given default value. |
| # If the product already exists and the change is illegal, we complain. |
| if (!defined $votes) { |
| if (ref $invocant) { |
| ThrowUserError('voting_product_illegal_votes', |
| { field => $field, votes => $_[2] }); |
| } |
| else { |
| $votes = $default; |
| } |
| } |
| return $votes; |
| } |
| |
| ######### |
| # Pages # |
| ######### |
| |
| sub page_before_template { |
| my ($self, $args) = @_; |
| my $page = $args->{page_id}; |
| my $vars = $args->{vars}; |
| |
| if ($page =~ m{^voting/bug\.}) { |
| _page_bug($vars); |
| } |
| elsif ($page =~ m{^voting/user\.}) { |
| _page_user($vars); |
| } |
| } |
| |
| sub _page_bug { |
| my ($vars) = @_; |
| my $dbh = Bugzilla->dbh; |
| my $input = Bugzilla->input_params; |
| |
| my $bug_id = $input->{bug_id}; |
| my $bug = Bugzilla::Bug->check($bug_id); |
| |
| $vars->{'bug'} = $bug; |
| $vars->{'users'} = |
| $dbh->selectall_arrayref('SELECT profiles.login_name, |
| profiles.userid AS id, |
| votes.vote_count |
| FROM votes |
| INNER JOIN profiles |
| ON profiles.userid = votes.who |
| WHERE votes.bug_id = ?', |
| {Slice=>{}}, $bug->id); |
| } |
| |
| sub _page_user { |
| my ($vars) = @_; |
| my $dbh = Bugzilla->dbh; |
| my $user = Bugzilla->user; |
| my $input = Bugzilla->input_params; |
| |
| my $action = $input->{action}; |
| if ($action and $action eq 'vote') { |
| _update_votes($vars); |
| } |
| |
| # If a bug_id is given, and we're editing, we'll add it to the votes list. |
| |
| my $bug_id = $input->{bug_id}; |
| my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 }) if $bug_id; |
| my $who_id = $input->{user_id} || $user->id; |
| |
| # Logged-out users must specify a user_id. |
| Bugzilla->login(LOGIN_REQUIRED) if !$who_id; |
| |
| my $who = Bugzilla::User->check({ id => $who_id }); |
| |
| my $canedit = $user->id == $who->id; |
| |
| $dbh->bz_start_transaction(); |
| |
| if ($canedit && $bug) { |
| # Make sure there is an entry for this bug |
| # in the vote table, just so that things display right. |
| my $has_votes = $dbh->selectrow_array('SELECT vote_count FROM votes |
| WHERE bug_id = ? AND who = ?', |
| undef, ($bug->id, $who->id)); |
| if (!$has_votes) { |
| $dbh->do('INSERT INTO votes (who, bug_id, vote_count) |
| VALUES (?, ?, 0)', undef, ($who->id, $bug->id)); |
| } |
| } |
| |
| my (@products, @all_bug_ids); |
| # Read the votes data for this user for each product. |
| foreach my $product (@{ $user->get_selectable_products }) { |
| next unless ($product->{votesperuser} > 0); |
| |
| my $vote_list = |
| $dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count |
| FROM votes |
| INNER JOIN bugs |
| ON votes.bug_id = bugs.bug_id |
| WHERE votes.who = ? |
| AND bugs.product_id = ?', |
| undef, ($who->id, $product->id)); |
| |
| my %votes = map { $_->[0] => $_->[1] } @$vote_list; |
| my @bug_ids = sort keys %votes; |
| # Exclude bugs that the user can no longer see. |
| @bug_ids = @{ $user->visible_bugs(\@bug_ids) }; |
| next unless scalar @bug_ids; |
| |
| push(@all_bug_ids, @bug_ids); |
| my @bugs = @{ Bugzilla::Bug->new_from_list(\@bug_ids) }; |
| $_->{count} = $votes{$_->id} foreach @bugs; |
| # We include votes from bugs that the user can no longer see. |
| my $total = sum(values %votes) || 0; |
| |
| my $onevoteonly = 0; |
| $onevoteonly = 1 if (min($product->{votesperuser}, |
| $product->{maxvotesperbug}) == 1); |
| |
| push(@products, { name => $product->name, |
| bugs => \@bugs, |
| bug_ids => \@bug_ids, |
| onevoteonly => $onevoteonly, |
| total => $total, |
| maxvotes => $product->{votesperuser}, |
| maxperbug => $product->{maxvotesperbug} }); |
| } |
| |
| if ($canedit && $bug) { |
| $dbh->do('DELETE FROM votes WHERE vote_count = 0 AND who = ?', |
| undef, $who->id); |
| } |
| $dbh->bz_commit_transaction(); |
| |
| $vars->{'canedit'} = $canedit; |
| $vars->{'voting_user'} = { "login" => $who->name }; |
| $vars->{'products'} = \@products; |
| $vars->{'this_bug'} = $bug; |
| $vars->{'all_bug_ids'} = \@all_bug_ids; |
| } |
| |
| sub _update_votes { |
| my ($vars) = @_; |
| |
| ############################################################################ |
| # Begin Data/Security Validation |
| ############################################################################ |
| |
| my $cgi = Bugzilla->cgi; |
| my $dbh = Bugzilla->dbh; |
| my $template = Bugzilla->template; |
| my $user = Bugzilla->login(LOGIN_REQUIRED); |
| my $input = Bugzilla->input_params; |
| |
| # Build a list of bug IDs for which votes have been submitted. Votes |
| # are submitted in form fields in which the field names are the bug |
| # IDs and the field values are the number of votes. |
| |
| my @buglist = grep {/^\d+$/} keys %$input; |
| my (%bugs, %votes); |
| |
| # If no bugs are in the buglist, let's make sure the user gets notified |
| # that their votes will get nuked if they continue. |
| if (scalar(@buglist) == 0) { |
| if (!defined $cgi->param('delete_all_votes')) { |
| print $cgi->header(); |
| $template->process("voting/delete-all.html.tmpl", $vars) |
| || ThrowTemplateError($template->error()); |
| exit; |
| } |
| elsif ($cgi->param('delete_all_votes') == 0) { |
| print $cgi->redirect("page.cgi?id=voting/user.html"); |
| exit; |
| } |
| } |
| else { |
| $user->visible_bugs(\@buglist); |
| my $bugs_obj = Bugzilla::Bug->new_from_list(\@buglist); |
| $bugs{$_->id} = $_ foreach @$bugs_obj; |
| } |
| |
| # Call check_is_visible() on each bug to make sure it is an existing bug |
| # that the user is authorized to access, and make sure the number of votes |
| # submitted is also an integer. |
| foreach my $id (@buglist) { |
| my $bug = $bugs{$id} |
| or ThrowUserError('bug_id_does_not_exist', { bug_id => $id }); |
| $bug->check_is_visible; |
| $id = $bug->id; |
| $votes{$id} = $input->{$id}; |
| detaint_natural($votes{$id}) |
| || ThrowUserError("voting_must_be_nonnegative"); |
| } |
| |
| my $token = $cgi->param('token'); |
| check_hash_token($token, ['vote']); |
| |
| ############################################################################ |
| # End Data/Security Validation |
| ############################################################################ |
| my $who = $user->id; |
| |
| # If the user is voting for bugs, make sure they aren't overstuffing |
| # the ballot box. |
| if (scalar @buglist) { |
| my (%prodcount, %products); |
| foreach my $bug_id (keys %bugs) { |
| my $bug = $bugs{$bug_id}; |
| my $prod = $bug->product; |
| $products{$prod} ||= $bug->product_obj; |
| $prodcount{$prod} ||= 0; |
| $prodcount{$prod} += $votes{$bug_id}; |
| |
| # Make sure we haven't broken the votes-per-bug limit |
| ($votes{$bug_id} <= $products{$prod}->{maxvotesperbug}) |
| || ThrowUserError("voting_too_many_votes_for_bug", |
| {max => $products{$prod}->{maxvotesperbug}, |
| product => $prod, |
| votes => $votes{$bug_id}}); |
| } |
| |
| # Make sure we haven't broken the votes-per-product limit |
| foreach my $prod (keys(%prodcount)) { |
| ($prodcount{$prod} <= $products{$prod}->{votesperuser}) |
| || ThrowUserError("voting_too_many_votes_for_product", |
| {max => $products{$prod}->{votesperuser}, |
| product => $prod, |
| votes => $prodcount{$prod}}); |
| } |
| } |
| |
| # Update the user's votes in the database. |
| $dbh->bz_start_transaction(); |
| |
| my $old_list = $dbh->selectall_arrayref('SELECT bug_id, vote_count FROM votes |
| WHERE who = ?', undef, $who); |
| |
| my %old_votes = map { $_->[0] => $_->[1] } @$old_list; |
| |
| my $sth_insertVotes = $dbh->prepare('INSERT INTO votes (who, bug_id, vote_count) |
| VALUES (?, ?, ?)'); |
| my $sth_updateVotes = $dbh->prepare('UPDATE votes SET vote_count = ? |
| WHERE bug_id = ? AND who = ?'); |
| |
| my %affected = map { $_ => 1 } (@buglist, keys %old_votes); |
| my @deleted_votes; |
| |
| foreach my $id (keys %affected) { |
| if (!$votes{$id}) { |
| push(@deleted_votes, $id); |
| next; |
| } |
| if ($votes{$id} == ($old_votes{$id} || 0)) { |
| delete $affected{$id}; |
| next; |
| } |
| # We use 'defined' in case 0 was accidentally stored in the DB. |
| if (defined $old_votes{$id}) { |
| $sth_updateVotes->execute($votes{$id}, $id, $who); |
| } |
| else { |
| $sth_insertVotes->execute($who, $id, $votes{$id}); |
| } |
| } |
| |
| if (@deleted_votes) { |
| $dbh->do('DELETE FROM votes WHERE who = ? AND ' . |
| $dbh->sql_in('bug_id', \@deleted_votes), undef, $who); |
| } |
| |
| # Update the cached values in the bugs table |
| my @updated_bugs = (); |
| |
| my $sth_getVotes = $dbh->prepare("SELECT SUM(vote_count) FROM votes |
| WHERE bug_id = ?"); |
| |
| $sth_updateVotes = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?'); |
| |
| foreach my $id (keys %affected) { |
| $sth_getVotes->execute($id); |
| my $v = $sth_getVotes->fetchrow_array || 0; |
| $sth_updateVotes->execute($v, $id); |
| $bugs{$id}->{votes} = $v if $bugs{$id}; |
| my $confirmed = _confirm_if_vote_confirmed($bugs{$id} || $id); |
| push (@updated_bugs, $id) if $confirmed; |
| } |
| |
| $dbh->bz_commit_transaction(); |
| |
| print $cgi->header() if scalar @updated_bugs; |
| $vars->{'type'} = "votes"; |
| $vars->{'title_tag'} = 'change_votes'; |
| foreach my $bug_id (@updated_bugs) { |
| $vars->{'id'} = $bug_id; |
| $vars->{'sent_bugmail'} = |
| Bugzilla::BugMail::Send($bug_id, { 'changer' => $user }); |
| |
| $template->process("bug/process/results.html.tmpl", $vars) |
| || ThrowTemplateError($template->error()); |
| # Set header_done to 1 only after the first bug. |
| $vars->{'header_done'} = 1; |
| } |
| $vars->{'message'} = 'votes_recorded'; |
| } |
| |
| ###################### |
| # Helper Subroutines # |
| ###################### |
| |
| sub _modify_bug_votes { |
| my ($product, $changes) = @_; |
| my $dbh = Bugzilla->dbh; |
| my @msgs; |
| |
| # 1. too many votes for a single user on a single bug. |
| my @toomanyvotes_list; |
| if ($product->{maxvotesperbug} < $product->{votesperuser}) { |
| my $votes = $dbh->selectall_arrayref( |
| 'SELECT votes.who, votes.bug_id |
| FROM votes |
| INNER JOIN bugs ON bugs.bug_id = votes.bug_id |
| WHERE bugs.product_id = ? |
| AND votes.vote_count > ?', |
| undef, ($product->id, $product->{maxvotesperbug})); |
| |
| foreach my $vote (@$votes) { |
| my ($who, $id) = (@$vote); |
| # If some votes are removed, _remove_votes() returns a list |
| # of messages to send to voters. |
| push(@msgs, _remove_votes($id, $who, 'votes_too_many_per_bug')); |
| my $name = Bugzilla::User->new($who)->login; |
| |
| push(@toomanyvotes_list, {id => $id, name => $name}); |
| } |
| } |
| |
| $changes->{'_too_many_votes'} = \@toomanyvotes_list; |
| |
| # 2. too many total votes for a single user. |
| # This part doesn't work in the general case because _remove_votes |
| # doesn't enforce votesperuser (except per-bug when it's less |
| # than maxvotesperbug). See _remove_votes(). |
| |
| my $votes = $dbh->selectall_arrayref( |
| 'SELECT votes.who, votes.vote_count |
| FROM votes |
| INNER JOIN bugs ON bugs.bug_id = votes.bug_id |
| WHERE bugs.product_id = ?', |
| undef, $product->id); |
| |
| my %counts; |
| foreach my $vote (@$votes) { |
| my ($who, $count) = @$vote; |
| if (!defined $counts{$who}) { |
| $counts{$who} = $count; |
| } else { |
| $counts{$who} += $count; |
| } |
| } |
| |
| my @toomanytotalvotes_list; |
| foreach my $who (keys(%counts)) { |
| if ($counts{$who} > $product->{votesperuser}) { |
| my $bug_ids = $dbh->selectcol_arrayref( |
| 'SELECT votes.bug_id |
| FROM votes |
| INNER JOIN bugs ON bugs.bug_id = votes.bug_id |
| WHERE bugs.product_id = ? |
| AND votes.who = ?', |
| undef, $product->id, $who); |
| |
| my $name = Bugzilla::User->new($who)->login; |
| foreach my $bug_id (@$bug_ids) { |
| # _remove_votes returns a list of messages to send |
| # in case some voters had too many votes. |
| push(@msgs, _remove_votes($bug_id, $who, |
| 'votes_too_many_per_user')); |
| |
| push(@toomanytotalvotes_list, {id => $bug_id, name => $name}); |
| } |
| } |
| } |
| |
| $changes->{'_too_many_total_votes'} = \@toomanytotalvotes_list; |
| |
| # 3. enough votes to confirm |
| my $bug_list = $dbh->selectcol_arrayref( |
| 'SELECT bug_id FROM bugs |
| WHERE product_id = ? AND bug_status = ? AND votes >= ?', |
| undef, ($product->id, 'UNCONFIRMED', $product->{votestoconfirm})); |
| |
| my @updated_bugs; |
| foreach my $bug_id (@$bug_list) { |
| my $confirmed = _confirm_if_vote_confirmed($bug_id); |
| push (@updated_bugs, $bug_id) if $confirmed; |
| } |
| $changes->{'_confirmed_bugs'} = \@updated_bugs; |
| |
| # Now that changes are done, we can send emails to voters. |
| foreach my $msg (@msgs) { |
| MessageToMTA($msg); |
| } |
| # And send out emails about changed bugs |
| foreach my $bug_id (@updated_bugs) { |
| my $sent_bugmail = Bugzilla::BugMail::Send( |
| $bug_id, { changer => Bugzilla->user }); |
| $changes->{'_confirmed_bugs_sent_bugmail'}->{$bug_id} = $sent_bugmail; |
| } |
| } |
| |
| # If a bug is moved to a product which allows less votes per bug |
| # compared to the previous product, extra votes need to be removed. |
| sub _remove_votes { |
| my ($id, $who, $reason) = (@_); |
| my $dbh = Bugzilla->dbh; |
| |
| my $whopart = ($who) ? " AND votes.who = $who" : ""; |
| |
| my $sth = $dbh->prepare("SELECT profiles.login_name, " . |
| "profiles.userid, votes.vote_count, " . |
| "products.votesperuser, products.maxvotesperbug " . |
| "FROM profiles " . |
| "LEFT JOIN votes ON profiles.userid = votes.who " . |
| "LEFT JOIN bugs ON votes.bug_id = bugs.bug_id " . |
| "LEFT JOIN products ON products.id = bugs.product_id " . |
| "WHERE votes.bug_id = ? " . $whopart); |
| $sth->execute($id); |
| my @list; |
| while (my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = $sth->fetchrow_array()) { |
| push(@list, [$name, $userid, $oldvotes, $votesperuser, $maxvotesperbug]); |
| } |
| |
| # @messages stores all emails which have to be sent, if any. |
| # This array is passed to the caller which will send these emails itself. |
| my @messages = (); |
| |
| if (scalar(@list)) { |
| foreach my $ref (@list) { |
| my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref); |
| |
| $maxvotesperbug = min($votesperuser, $maxvotesperbug); |
| |
| # If this product allows voting and the user's votes are in |
| # the acceptable range, then don't do anything. |
| next if $votesperuser && $oldvotes <= $maxvotesperbug; |
| |
| # If the user has more votes on this bug than this product |
| # allows, then reduce the number of votes so it fits |
| my $newvotes = $maxvotesperbug; |
| |
| my $removedvotes = $oldvotes - $newvotes; |
| |
| if ($newvotes) { |
| $dbh->do("UPDATE votes SET vote_count = ? " . |
| "WHERE bug_id = ? AND who = ?", |
| undef, ($newvotes, $id, $userid)); |
| } else { |
| $dbh->do("DELETE FROM votes WHERE bug_id = ? AND who = ?", |
| undef, ($id, $userid)); |
| } |
| |
| # Notice that we did not make sure that the user fit within the $votesperuser |
| # range. This is considered to be an acceptable alternative to losing votes |
| # during product moves. Then next time the user attempts to change their votes, |
| # they will be forced to fit within the $votesperuser limit. |
| |
| # Now lets send the e-mail to alert the user to the fact that their votes have |
| # been reduced or removed. |
| my $vars = { |
| 'to' => $name . Bugzilla->params->{'emailsuffix'}, |
| 'bugid' => $id, |
| 'reason' => $reason, |
| |
| 'votesremoved' => $removedvotes, |
| 'votesold' => $oldvotes, |
| 'votesnew' => $newvotes, |
| }; |
| |
| my $voter = new Bugzilla::User($userid); |
| my $template = Bugzilla->template_inner($voter->setting('lang')); |
| |
| my $msg; |
| $template->process("voting/votes-removed.txt.tmpl", $vars, \$msg); |
| push(@messages, $msg); |
| } |
| |
| my $votes = $dbh->selectrow_array("SELECT SUM(vote_count) " . |
| "FROM votes WHERE bug_id = ?", |
| undef, $id) || 0; |
| $dbh->do("UPDATE bugs SET votes = ? WHERE bug_id = ?", |
| undef, ($votes, $id)); |
| } |
| # Now return the array containing emails to be sent. |
| return @messages; |
| } |
| |
| # If a user votes for a bug, or the number of votes required to |
| # confirm a bug has been reduced, check if the bug is now confirmed. |
| sub _confirm_if_vote_confirmed { |
| my $id = shift; |
| my $bug = ref $id ? $id : new Bugzilla::Bug({ id => $id, cache => 1 }); |
| |
| my $ret = 0; |
| if (!$bug->everconfirmed |
| and $bug->product_obj->{votestoconfirm} |
| and $bug->votes >= $bug->product_obj->{votestoconfirm}) |
| { |
| $bug->add_comment('', { type => CMT_POPULAR_VOTES }); |
| |
| if ($bug->bug_status eq 'UNCONFIRMED') { |
| # Get a valid open state. |
| my $new_status; |
| foreach my $state (@{$bug->status->can_change_to}) { |
| if ($state->is_open && $state->name ne 'UNCONFIRMED') { |
| $new_status = $state->name; |
| last; |
| } |
| } |
| ThrowCodeError('voting_no_open_bug_status') unless $new_status; |
| |
| # We cannot call $bug->set_bug_status() here, because a user without |
| # canconfirm privs should still be able to confirm a bug by |
| # popular vote. We already know the new status is valid, so it's safe. |
| $bug->{bug_status} = $new_status; |
| $bug->{everconfirmed} = 1; |
| delete $bug->{'status'}; # Contains the status object. |
| } |
| else { |
| # If the bug is in a closed state, only set everconfirmed to 1. |
| # Do not call $bug->_set_everconfirmed(), for the same reason as above. |
| $bug->{everconfirmed} = 1; |
| } |
| $bug->update(); |
| |
| $ret = 1; |
| } |
| return $ret; |
| } |
| |
| |
| __PACKAGE__->NAME; |