diff options
Diffstat (limited to 'extensions/Voting')
21 files changed, 1829 insertions, 0 deletions
diff --git a/extensions/Voting/Extension.pm b/extensions/Voting/Extension.pm new file mode 100644 index 000000000..e111ac785 --- /dev/null +++ b/extensions/Voting/Extension.pm @@ -0,0 +1,861 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Bugzilla Bug Tracking System. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are +# Copyright (C) 1998 Netscape Communications Corporation. All +# Rights Reserved. +# +# Contributor(s): Terry Weissman <terry@mozilla.org> +# Stephan Niemz <st.n@gmx.net> +# Christopher Aillon <christopher@aillon.com> +# Gervase Markham <gerv@gerv.net> +# Frédéric Buclin <LpSolit@gmail.com> +# Max Kanat-Alexander <mkanat@bugzilla.org> + +package Bugzilla::Extension::Voting; +use strict; +use base qw(Bugzilla::Extension); + +use Bugzilla::Bug; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Field; +use Bugzilla::Mailer; +use Bugzilla::User; +use Bugzilla::Util qw(detaint_natural); + +use List::Util qw(min); + +use constant NAME => 'Voting'; +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 # +################ + +our $VERSION = BUGZILLA_VERSION; + +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 3.8, 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->id); + + 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); + # 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 $template = Bugzilla->template; + 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 $template = Bugzilla->template; + 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($bug_id) 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 @bugs; + my @bug_ids; + my $total = 0; + my $onevoteonly = 0; + + my $vote_list = + $dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count, + bugs.short_desc + FROM votes + INNER JOIN bugs + ON votes.bug_id = bugs.bug_id + WHERE votes.who = ? + AND bugs.product_id = ? + ORDER BY votes.bug_id', + undef, ($who->id, $product->id)); + + foreach (@$vote_list) { + my ($id, $count, $summary) = @$_; + $total += $count; + + # Next if user can't see this bug. So, the totals will be correct + # and they can see there are votes 'missing', but not on what bug + # they are. This seems a reasonable compromise; the alternative is + # to lie in the totals. + next if !$user->can_see_bug($id); + + push (@bugs, { id => $id, + summary => $summary, + count => $count }); + push (@bug_ids, $id); + push (@all_bug_ids, $id); + } + + $onevoteonly = 1 if (min($product->{votesperuser}, + $product->{maxvotesperbug}) == 1); + + # Only add the product for display if there are any bugs in it. + if ($#bugs > -1) { + push (@products, { name => $product->name, + bugs => \@bugs, + bug_ids => \@bug_ids, + onevoteonly => $onevoteonly, + total => $total, + maxvotes => $product->{votesperuser}, + maxperbug => $product->{maxvotesperbug} }); + } + } + + $dbh->do('DELETE FROM votes WHERE vote_count <= 0'); + $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; + + # 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; + } + } + + # Call check() on each bug ID to make sure it is a positive + # integer representing an existing bug that the user is authorized + # to access, and make sure the number of votes submitted is also + # a non-negative integer (a series of digits not preceded by a + # minus sign). + my (%votes, @bugs); + foreach my $id (@buglist) { + my $bug = Bugzilla::Bug->check($id); + push(@bugs, $bug); + $id = $bug->id; + $votes{$id} = $input->{$id}; + detaint_natural($votes{$id}) + || ThrowUserError("voting_must_be_nonnegative"); + } + + ############################################################################ + # 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 @bugs) { + my (%prodcount, %products); + foreach my $bug (@bugs) { + my $bug_id = $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. If the user did not submit + # any votes, they may be using a form with checkboxes to remove all their + # votes (checkboxes are not submitted along with other form data when + # they are not checked, and Bugzilla uses them to represent single votes + # for products that only allow one vote per bug). In that case, we still + # need to clear the user's votes from the database. + my %affected; + $dbh->bz_start_transaction(); + + # Take note of, and delete the user's old votes from the database. + my $bug_list = $dbh->selectcol_arrayref('SELECT bug_id FROM votes + WHERE who = ?', undef, $who); + + foreach my $id (@$bug_list) { + $affected{$id} = 1; + } + $dbh->do('DELETE FROM votes WHERE who = ?', undef, $who); + + my $sth_insertVotes = $dbh->prepare('INSERT INTO votes (who, bug_id, vote_count) + VALUES (?, ?, ?)'); + + # Insert the new values in their place + foreach my $id (@buglist) { + if ($votes{$id} > 0) { + $sth_insertVotes->execute($who, $id, $votes{$id}); + } + $affected{$id} = 1; + } + + # Update the cached values in the bugs table + print $cgi->header(); + my @updated_bugs = (); + + my $sth_getVotes = $dbh->prepare("SELECT SUM(vote_count) FROM votes + WHERE bug_id = ?"); + + my $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); + + my $confirmed = _confirm_if_vote_confirmed($id); + push (@updated_bugs, $id) if $confirmed; + } + + $dbh->bz_commit_transaction(); + + $vars->{'type'} = "votes"; + $vars->{'mailrecipients'} = { 'changer' => $user->login }; + $vars->{'title_tag'} = 'change_votes'; + foreach my $bug_id (@updated_bugs) { + $vars->{'id'} = $bug_id; + $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->{'votes_recorded'} = 1; +} + +###################### +# 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 = user_id_to_login($who); + + 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); + + 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')); + my $name = user_id_to_login($who); + + 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); + } +} + +# 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->settings->{'lang'}->{'value'}); + + my $msg; + $template->process("voting/votes-removed.txt.tmpl", $vars, \$msg); + push(@messages, $msg); + } + Bugzilla->template_inner(""); + + 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 = new Bugzilla::Bug($id); + + 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('no_open_bug_status') unless $new_status; + + # We cannot call $bug->set_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; diff --git a/extensions/Voting/template/en/default/hook/account/prefs/email-relationships.html.tmpl b/extensions/Voting/template/en/default/hook/account/prefs/email-relationships.html.tmpl new file mode 100644 index 000000000..0bd81eae1 --- /dev/null +++ b/extensions/Voting/template/en/default/hook/account/prefs/email-relationships.html.tmpl @@ -0,0 +1,22 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% relationships.push({ id = constants.REL_VOTER, description = "Voter" }) %] +[% no_added_removed.push(constants.REL_VOTER) %] diff --git a/extensions/Voting/template/en/default/hook/admin/products/edit-common-rows.html.tmpl b/extensions/Voting/template/en/default/hook/admin/products/edit-common-rows.html.tmpl new file mode 100644 index 000000000..fbbda3ea0 --- /dev/null +++ b/extensions/Voting/template/en/default/hook/admin/products/edit-common-rows.html.tmpl @@ -0,0 +1,60 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% DEFAULT + product.maxvotesperbug = constants.DEFAULT_VOTES_PER_BUG + product.votesperuser = 0 + product.votestoconfirm = 0 +%] + +<tr> + <th align="right">Maximum votes per person:</th> + <td><input size="5" maxlength="5" name="votesperuser" id="votesperuser" + value="[% product.votesperuser FILTER html %]"> + </td> +</tr> + +<tr> + <th align="right"> + Maximum votes a person can put on a single [% terms.bug %]: + </th> + <td><input size="5" maxlength="5" name="maxvotesperbug" id="maxvotesperbug" + value="[% product.maxvotesperbug FILTER html %]"> + </td> +</tr> + +<tr id="votes_to_confirm_container" + [%- ' class="bz_default_hidden"' IF !product.allows_unconfirmed %]> + <th align="right"> + Confirm [% terms.abug %] if it gets this many votes: + </th> + <td> + <input size="3" maxlength="5" name="votestoconfirm" id="votestoconfirm" + value="[% product.votestoconfirm FILTER html %]"> + <br>(Setting this to 0 disables auto-confirming [% terms.bugs %] + by vote.) + <script type="text/javascript"> + YAHOO.util.Event.addListener('allows_unconfirmed', 'change', + function() { bz_toggleClass('votes_to_confirm_container', + 'bz_default_hidden'); }); + </script> + </td> +</tr> + diff --git a/extensions/Voting/template/en/default/hook/admin/products/updated-changes.html.tmpl b/extensions/Voting/template/en/default/hook/admin/products/updated-changes.html.tmpl new file mode 100644 index 000000000..876c51187 --- /dev/null +++ b/extensions/Voting/template/en/default/hook/admin/products/updated-changes.html.tmpl @@ -0,0 +1,102 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% SET checkvotes = 0 %] + +[% IF changes.votesperuser.defined %] + <p> + Updated votes per user from + [%+ changes.votesperuser.0 FILTER html %] to + [%+ product.votesperuser FILTER html %]. + </p> + [% checkvotes = 1 %] +[% END %] + +[% IF changes.maxvotesperbug.defined %] + <p> + Updated maximum votes per [% terms.bug %] from + [%+ changes.maxvotesperbug.0 FILTER html %] to + [%+ product.maxvotesperbug FILTER html %]. + </p> + [% checkvotes = 1 %] +[% END %] + +[% IF changes.votestoconfirm.defined %] + <p> + Updated number of votes needed to confirm a [% terms.bug %] from + [%+ changes.votestoconfirm.0 FILTER html %] to + [%+ product.votestoconfirm FILTER html %]. + </p> + [% checkvotes = 1 %] +[% END %] + +[%# Note that this display of changed votes and/or confirmed bugs is + not very scalable. We could have a _lot_, and we just list them all. + One day we should limit this perhaps, or have a more scalable display %] + +[% IF checkvotes %] + <hr> + + <p>Checking existing votes in this product for anybody who now + has too many votes for [% terms.abug %]...<br> + [% IF changes.too_many_votes.size %] + [% FOREACH detail = changes.too_many_votes %] + →removed votes for [% terms.bug %] <a href="show_bug.cgi?id= + [%- detail.id FILTER url_quote %]"> + [%- detail.id FILTER html %]</a> from [% detail.name FILTER html %]<br> + [% END %] + [% ELSE %] + →there were none. + [% END %] + </p> + + <p>Checking existing votes in this product for anybody + who now has too many total votes...<br> + [% IF changes.too_many_total_votes.size %] + [% FOREACH detail = changes.too_many_total_votes %] + →removed votes for [% terms.bug %] <a href="show_bug.cgi?id= + [%- detail.id FILTER url_quote %]"> + [%- detail.id FILTER html %]</a> from [% detail.name FILTER html %]<br> + [% END %] + [% ELSE %] + →there were none. + [% END %] + </p> + + <p>Checking unconfirmed [% terms.bugs %] in this product for any which now have + sufficient votes...<br> + [% IF changes.confirmed_bugs.size %] + [% FOREACH id = changes.confirmed_bugs %] + + [%# This is INCLUDED instead of PROCESSED to avoid variables getting + overwritten, which happens otherwise %] + [% INCLUDE bug/process/results.html.tmpl + type = 'votes' + mailrecipients = { 'changer' => user.login } + header_done = 1 + id = id + %] + [% END %] + [% ELSE %] + →there were none. + [% END %] + </p> + +[% END %] diff --git a/extensions/Voting/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl b/extensions/Voting/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl new file mode 100644 index 000000000..afb81d34c --- /dev/null +++ b/extensions/Voting/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl @@ -0,0 +1,40 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% IF san_tag == "voting_cache_rebuild_fix" %] + <a href="sanitycheck.cgi?rebuild_vote_cache=1">Click here to + rebuild the vote cache</a> + +[% ELSIF san_tag == "voting_cache_alert" %] + Bad vote cache for [% PROCESS bug_link bug_id = id %] + +[% ELSIF san_tag == "voting_count_start" %] + Checking cached vote counts. + +[% ELSIF san_tag == "voting_count_alert" %] + Bad vote sum for [% terms.bug %] [%+ id FILTER html %]. + +[% ELSIF san_tag == "voting_cache_rebuild_start" %] + OK, now rebuilding vote cache. + +[% ELSIF san_tag == "voting_cache_rebuild_end" %] + Vote cache has been rebuilt + +[% END %] diff --git a/extensions/Voting/template/en/default/hook/admin/users/confirm-delete-warn_safe.html.tmpl b/extensions/Voting/template/en/default/hook/admin/users/confirm-delete-warn_safe.html.tmpl new file mode 100644 index 000000000..f799f1254 --- /dev/null +++ b/extensions/Voting/template/en/default/hook/admin/users/confirm-delete-warn_safe.html.tmpl @@ -0,0 +1,38 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% IF votes %] + <li> + [% otheruser.login FILTER html %] has voted on + [% IF votes == 1 %] + [%+ terms.abug %] + [% ELSE %] + [%+ votes %] [%+ terms.bugs %] + [% END %]. + + If you delete the user account, + [% IF votes == 1 %] + this vote + [% ELSE %] + these votes + [% END %] + will be deleted along with the user account. + </li> +[% END %] diff --git a/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl b/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl new file mode 100644 index 000000000..7952442da --- /dev/null +++ b/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl @@ -0,0 +1,41 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] +[% IF bug.product_obj.votesperuser %] + <style type="text/css"> + #votes_container { white-space: nowrap; } + </style> + + <span id="votes_container"> + [% IF bug.votes %] + with + <a href="page.cgi?id=voting/bug.html?bug_id= + [%- bug.id FILTER url_quote %]"> + [%- bug.votes %] + [% IF bug.votes == 1 %] + vote + [% ELSE %] + votes + [% END %]</a> + [% END %] + (<a href="page.cgi?id=voting/user.html&bug_id= + [%- bug.id FILTER url_quote %]#vote_ + [%- bug.id FILTER url_quote %]">vote</a>) + </span> +[% END %] diff --git a/extensions/Voting/template/en/default/hook/bug/format_comment-type.txt.tmpl b/extensions/Voting/template/en/default/hook/bug/format_comment-type.txt.tmpl new file mode 100644 index 000000000..ebba6fcab --- /dev/null +++ b/extensions/Voting/template/en/default/hook/bug/format_comment-type.txt.tmpl @@ -0,0 +1,23 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% IF comment.type == constants.CMT_POPULAR_VOTES %] +*** This [% terms.bug %] has been confirmed by popular vote. *** +[% END %] diff --git a/extensions/Voting/template/en/default/hook/bug/process/header-title.html.tmpl b/extensions/Voting/template/en/default/hook/bug/process/header-title.html.tmpl new file mode 100644 index 000000000..a4530653b --- /dev/null +++ b/extensions/Voting/template/en/default/hook/bug/process/header-title.html.tmpl @@ -0,0 +1,24 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% IF title_tag == "change_votes" %] + [% title = "Change Votes" %] +[% END %] + diff --git a/extensions/Voting/template/en/default/hook/bug/process/results-title.html.tmpl b/extensions/Voting/template/en/default/hook/bug/process/results-title.html.tmpl new file mode 100644 index 000000000..ae0d465dc --- /dev/null +++ b/extensions/Voting/template/en/default/hook/bug/process/results-title.html.tmpl @@ -0,0 +1,21 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% title.votes = "$Link confirmed by number of votes" %] diff --git a/extensions/Voting/template/en/default/hook/global/field-descs-end.none.tmpl b/extensions/Voting/template/en/default/hook/global/field-descs-end.none.tmpl new file mode 100644 index 000000000..2fd798084 --- /dev/null +++ b/extensions/Voting/template/en/default/hook/global/field-descs-end.none.tmpl @@ -0,0 +1,22 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% field_descs.votes = "Votes" %] + diff --git a/extensions/Voting/template/en/default/hook/global/reason-descs-end.none.tmpl b/extensions/Voting/template/en/default/hook/global/reason-descs-end.none.tmpl new file mode 100644 index 000000000..3a1f5a189 --- /dev/null +++ b/extensions/Voting/template/en/default/hook/global/reason-descs-end.none.tmpl @@ -0,0 +1,23 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% reason_descs.${constants.REL_VOTER} = "You voted for the ${terms.bug}." %] +[% watch_reason_descs.${constants.REL_VOTER} = + "You are watching a voter for the ${terms.bug}." %] diff --git a/extensions/Voting/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Voting/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..c2ff70728 --- /dev/null +++ b/extensions/Voting/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,55 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% IF error == "voting_must_be_nonnegative" %] + [% title = "Votes Must Be Non-negative" %] + [% admindocslinks = {'voting.html' => 'Setting up the voting feature'} %] + Only use non-negative numbers for your [% terms.bug %] votes. + +[% ELSIF error == "voting_product_illegal_votes" %] + [% title = "Votes Must Be Non-negative" %] + [% admindocslinks = {'voting.html' => 'Setting up the voting feature'} %] + '[% votes FILTER html %]' is an invalid value for the + <em> + [% IF field == "votesperuser" %] + Votes Per User + [% ELSIF field == "maxvotesperbug" %] + Maximum Votes Per [% terms.Bug %] + [% ELSIF field == "votestoconfirm" %] + Votes To Confirm + [% END %] + </em> field, which should contain a non-negative number. + +[% ELSIF error == "voting_too_many_votes_for_bug" %] + [% title = "Illegal Vote" %] + [% admindocslinks = {'voting.html' => 'Setting up the voting feature'} %] + You may only use at most [% max FILTER html %] votes for a single + [%+ terms.bug %] in the + <tt>[% product FILTER html %]</tt> product, but you are trying to + use [% votes FILTER html %]. + +[% ELSIF error == "voting_too_many_votes_for_product" %] + [% title = "Illegal Vote" %] + [% admindocslinks = {'voting.html' => 'Setting up the voting feature'} %] + You tried to use [% votes FILTER html %] votes in the + <tt>[% product FILTER html %]</tt> product, which exceeds the maximum of + [%+ max FILTER html %] votes for this product. + +[% END %] diff --git a/extensions/Voting/template/en/default/hook/search/form-email_numbering_end.html.tmpl b/extensions/Voting/template/en/default/hook/search/form-email_numbering_end.html.tmpl new file mode 100644 index 000000000..5acfff14c --- /dev/null +++ b/extensions/Voting/template/en/default/hook/search/form-email_numbering_end.html.tmpl @@ -0,0 +1,31 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +<tr> + <td align="right"> + <label for="votes">Only [% terms.bugs %] with at least</label>: + </td> + <td> + <input name="votes" id="votes" size="3" + value="[% default.votes.0 FILTER html %]"> votes + <input type="hidden" name="votes_type" value="greaterthaneq"> + </td> +</tr> + diff --git a/extensions/Voting/template/en/default/hook/search/search-report-select-rep_fields.html.tmpl b/extensions/Voting/template/en/default/hook/search/search-report-select-rep_fields.html.tmpl new file mode 100644 index 000000000..ca74f6d2d --- /dev/null +++ b/extensions/Voting/template/en/default/hook/search/search-report-select-rep_fields.html.tmpl @@ -0,0 +1,21 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by the Initial Developer are Copyright (C) 2010 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% rep_fields.push('votes') %] diff --git a/extensions/Voting/template/en/default/pages/voting.html.tmpl b/extensions/Voting/template/en/default/pages/voting.html.tmpl new file mode 100644 index 000000000..99026c0d5 --- /dev/null +++ b/extensions/Voting/template/en/default/pages/voting.html.tmpl @@ -0,0 +1,69 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Terry Weissman <terry@mozilla.org> + # Gervase Markham <gerv@gerv.net> + #%] + +[% PROCESS global/variables.none.tmpl %] +[% INCLUDE global/header.html.tmpl title = "Voting" %] + +<p>[% terms.Bugzilla %] has a "voting" feature. Each product allows users to +have a certain number of votes. (Some products may not allow any, which means +you can't vote on things in those products at all.) With your vote, you +indicate which [% terms.bugs %] you think are the most important and +would like to see fixed. Note that voting is nowhere near as effective +as providing a fix yourself.</p> + +<p>Depending on how the administrator has configured the relevant product, +you may be able to vote for the same [% terms.bug %] more than once. +Remember that you have a limited number of votes. When weighted voting +is allowed and a limited number of votes are available to you, you will +have to decide whether you want to distribute your votes among a large +number of [% terms.bugs %] indicating your minimal interest or focus on +a few [% terms.bugs %] indicating your strong support for them. +</p> + +<p>To look at votes:</p> + +<ul> + <li>Go to the query page. Do a normal query, but enter 1 in the "At least + ___ votes" field. This will show you items that match your query that + have at least one vote.</li> +</ul> + +<p>To vote for [% terms.abug %]:</p> + +<ul> + <li>Bring up the [% terms.bug %] in question.</li> + + <li>Click on the "(vote)" link that appears on the right of the "Importance" + fields. (If no such link appears, then voting may not be allowed in + this [% terms.bug %]'s product.)</li> + + <li>Indicate how many votes you want to give this [% terms.bug %]. This page + also displays how many votes you've given to other [% terms.bugs %], so you + may rebalance your votes as necessary.</li> +</ul> + +<p>You will automatically get email notifying you of any changes that occur +on [% terms.bugs %] you vote for.</p> + +<p>You may review your votes at any time by clicking on the "<a href= +"page.cgi?id=voting/user.html">My Votes</a>" link in the page footer.</p> + +[% INCLUDE global/footer.html.tmpl %] diff --git a/extensions/Voting/template/en/default/pages/voting/bug.html.tmpl b/extensions/Voting/template/en/default/pages/voting/bug.html.tmpl new file mode 100644 index 000000000..03434a505 --- /dev/null +++ b/extensions/Voting/template/en/default/pages/voting/bug.html.tmpl @@ -0,0 +1,61 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[%# INTERFACE: + # bug: Bugzilla::Bug that we are listing the votes for. + # users: list of hashes. May be empty. Each hash has two members: + # login_name: string. The login name of the user whose vote is attached + # vote_count: integer. The number of times that user has votes for this bug. + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Show Votes" + subheader = "$terms.Bug $bug.id" FILTER bug_link(bug) + %] + +[% total = 0 %] +<table cellspacing="4"> + <tr> + <th>Who</th> + <th>Number of votes</th> + </tr> + + [% FOREACH voter = users %] + [% total = total + voter.vote_count %] + <tr> + <td> + <a href="page.cgi?id=voting/user.html&user_id= + [%- voter.id FILTER url_quote %]"> + [% voter.login_name FILTER email FILTER html %] + </a> + </td> + <td align="right"> + [% voter.vote_count FILTER html %] + </td> + </tr> + [% END %] +</table> + +<p>Total votes: [% total FILTER html %]</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/Voting/template/en/default/pages/voting/user.html.tmpl b/extensions/Voting/template/en/default/pages/voting/user.html.tmpl new file mode 100644 index 000000000..800079224 --- /dev/null +++ b/extensions/Voting/template/en/default/pages/voting/user.html.tmpl @@ -0,0 +1,185 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + #%] + +[%# INTERFACE: + # voting_user: hash containing a 'login' field + # + # products: list of hashes containing details of products relating to + # voting: + # name: name of product + # bugs: list of bugs the user has voted for + # bug_ids: list of bug ids the user has voted for + # onevoteonly: one or more votes allowed per bug? + # total: users current vote count for the product + # maxvotes: max votes allowed for a user in this product + # maxperbug: max votes per bug allowed for a user in this product + # + # this_bug: Bugzilla::Bug; if the user is voting for a bug, this is the bug + # + # canedit: boolean; Should the votes be presented in a form, or readonly? + # + # all_bug_ids: List of all bug ids the user has voted for, across all products + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% IF !header_done %] + [% subheader = voting_user.login FILTER html %] + [% IF canedit %] + [% title = "Change Votes" %] + [% IF this_bug %] + [%# We .select and .focus the input so it works for textbox and + checkbox %] + [% onload = "document.forms['voting_form'].bug_" _ this_bug.id _ + ".select();document.forms['voting_form'].bug_" _ this_bug.id _ + ".focus()" %] + [% END %] + [% ELSE %] + [% title = "Show Votes" %] + [% END %] + [% PROCESS global/header.html.tmpl + style_urls = [ "extensions/Voting/web/style.css" ] + %] +[% ELSE %] + <hr> +[% END %] + +[% IF votes_recorded %] + <p> + <font color="red"> + The changes to your votes have been saved. + </font> + </p> +[% ELSE %] + <br> +[% END %] + +[% IF products.size %] + <form name="voting_form" method="post" action="page.cgi?id=voting/user.html"> + <input type="hidden" name="action" value="vote"> + <table cellspacing="4"> + <tr> + <td></td> + <th>Votes</th> + <th>[% terms.Bug %] #</th> + <th>Summary</th> + </tr> + + [% onevoteproduct = 0 %] + [% multivoteproduct = 0 %] + [% FOREACH product = products %] + [% IF product.onevoteonly %] + [% onevoteproduct = 1 %] + [% ELSE %] + [% multivoteproduct = 1 %] + [% END %] + <tr> + <th>[% product.name FILTER html %]</th> + <td colspan="2" ><a href="buglist.cgi?bug_id= + [%- product.bug_ids.join(",") FILTER url_quote %]">([% terms.bug %] list)</a> + </td> + <td> + [% IF product.maxperbug < product.maxvotes AND + product.maxperbug > 1 %] + <font size="-1"> + (Note: only [% product.maxperbug %] vote + [% "s" IF product.maxperbug != 1 %] allowed per [% terms.bug %] in + this product.) + </font> + [% END %] + </td> + </tr> + + [% FOREACH bug = product.bugs %] + <tr [% IF bug.id == this_bug.id && canedit %] + class="bz_bug_being_voted_on" [% END %]> + <td>[% IF bug.id == this_bug.id && canedit %]Enter New Vote here → + [%- END %]</td> + <td align="right"><a name="vote_[% bug.id %]"> + [% IF canedit %] + [% IF product.onevoteonly %] + <input type="checkbox" name="[% bug.id %]" value="1" + [% " checked" IF bug.count %] id="bug_[% bug.id %]"> + [% ELSE %] + <input name="[% bug.id %]" value="[% bug.count %]" + size="2" id="bug_[% bug.id %]"> + [% END %] + [% ELSE %] + [% bug.count %] + [% END %] + </a></td> + <td align="center"> + [% bug.id FILTER bug_link(bug) FILTER none %] + </td> + <td> + [% bug.summary FILTER html %] + (<a href="page.cgi?id=voting/bug.html&bug_id=[% bug.id %]">Show Votes</a>) + </td> + </tr> + [% END %] + + <tr> + <td></td> + <td colspan="3">[% product.total %] vote + [% "s" IF product.total != 1 %] used out of [% product.maxvotes %] + allowed. + <br> + <br> + </td> + </tr> + [% END %] + </table> + + [% IF canedit %] + <input type="submit" value="Change My Votes" id="change"> or + <a href="buglist.cgi?bug_id=[% all_bug_ids.join(",") FILTER url_quote %]">view all + as [% terms.bug %] list</a> + <br> + <br> + To change your votes, + [% IF multivoteproduct %] + type in new numbers (using zero to mean no votes) + [% " or " IF onevoteproduct %] + [% END %] + [% IF onevoteproduct %] + change the checkbox + [% END %] + and then click <b>Change My Votes</b>. + [% ELSE %] + <a href="buglist.cgi?bug_id=[% all_bug_ids.join(",") FILTER url_quote %]">View all + as [% terms.bug %] list</a> + [% END %] + </form> +[% ELSE %] + <p> + [% IF canedit %] + You are + [% ELSE %] + This user is + [% END %] + currently not voting on any [% terms.bugs %]. + </p> +[% END %] + +<p> + <a href="page.cgi?id=voting.html">Help with voting</a>. +</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/Voting/template/en/default/voting/delete-all.html.tmpl b/extensions/Voting/template/en/default/voting/delete-all.html.tmpl new file mode 100644 index 000000000..82ddc3596 --- /dev/null +++ b/extensions/Voting/template/en/default/voting/delete-all.html.tmpl @@ -0,0 +1,51 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + #%] + +[%# INTERFACE: + # This template has no interface. + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Remove your votes?" + %] + +<p> + You are about to remove all of your [% terms.bug %] votes. Are you sure you wish to + remove your vote from every [% terms.bug %] you've voted on? +</p> + +<form action="page.cgi?id=voting/user.html" method="post"> + <input type="hidden" name="action" value="vote"> + <p> + <input type="radio" name="delete_all_votes" value="1"> + Yes, delete all my votes + </p> + <p> + <input type="radio" name="delete_all_votes" value="0" checked="checked"> + No, go back and review my votes + </p> + <p> + <input type="submit" id="vote" value="Submit"> + </p> +</form> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/Voting/template/en/default/voting/votes-removed.txt.tmpl b/extensions/Voting/template/en/default/voting/votes-removed.txt.tmpl new file mode 100644 index 000000000..bfb37c90d --- /dev/null +++ b/extensions/Voting/template/en/default/voting/votes-removed.txt.tmpl @@ -0,0 +1,55 @@ +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Emmanuel Seyman <eseyman@linagora.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + +From: [% Param('mailfrom') %] +To: [% to %] +Subject: [% terms.Bug %] [%+ bugid %] Some or all of your votes have been removed. +X-Bugzilla-Type: voteremoved + +Some or all of your votes have been removed from [% terms.bug %] [%+ bugid %]. + +You had [% votesold FILTER html %] [%+ IF votesold == 1 %]vote[% ELSE %]votes[% END +%] on this [% terms.bug %], but [% votesremoved FILTER html %] have been removed. + +[% IF votesnew %] +You still have [% votesnew FILTER html %] [%+ IF votesnew == 1 %]vote[% ELSE %]votes[% END %] on this [% terms.bug %]. +[% ELSE %] +You have no more votes remaining on this [% terms.bug %]. +[% END %] + +Reason: +[% IF reason == "votes_bug_moved" %] + This [% terms.bug %] has been moved to a different product. + +[% ELSIF reason == "votes_too_many_per_bug" %] + The rules for voting on this product has changed; + you had too many votes for a single [% terms.bug %]. + +[% ELSIF reason == "votes_too_many_per_user" %] + The rules for voting on this product has changed; you had + too many total votes, so all votes have been removed. +[% END %] + + + +[% urlbase %]show_bug.cgi?id=[% bugid %] + diff --git a/extensions/Voting/web/style.css b/extensions/Voting/web/style.css new file mode 100644 index 000000000..5d9c9afe6 --- /dev/null +++ b/extensions/Voting/web/style.css @@ -0,0 +1,24 @@ +/* The contents of this file are subject to the Mozilla Public + * License Version 1.1 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * The Original Code is the Bugzilla Bug Tracking System. + * + * Contributor(s): Gavin Shelley <bugzilla@chimpychompy.org> + */ + +/* Highlight the row for the bug being voted on */ +tr.bz_bug_being_voted_on { + background-color: #e2e2e2; +} + +tr.bz_bug_being_voted_on td { + border-style: solid none solid none; + border-width: thin; +} |