]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 372979: Make voting into an extension
authorMax Kanat-Alexander <mkanat@bugzilla.org>
Mon, 15 Feb 2010 23:22:55 +0000 (15:22 -0800)
committerMax Kanat-Alexander <mkanat@bugzilla.org>
Mon, 15 Feb 2010 23:22:55 +0000 (15:22 -0800)
r=mkanat, a=mkanat, a=LpSolit

72 files changed:
.bzrignore
Bugzilla/Bug.pm
Bugzilla/BugMail.pm
Bugzilla/Comment.pm
Bugzilla/Config/BugFields.pm
Bugzilla/Constants.pm
Bugzilla/DB/Schema.pm
Bugzilla/Field.pm
Bugzilla/Install/DB.pm
Bugzilla/Object.pm
Bugzilla/Product.pm
Bugzilla/Search.pm
Bugzilla/Search/Quicksearch.pm
Bugzilla/WebService/Bug.pm
buglist.cgi
bugzilla.dtd
colchange.cgi
contrib/merge-users.pl
docs/en/images/bzLifecycle.xml
editproducts.cgi
editusers.cgi
extensions/Voting/Extension.pm [new file with mode: 0644]
extensions/Voting/template/en/default/hook/account/prefs/email-relationships.html.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/hook/admin/products/edit-common-rows.html.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/hook/admin/products/updated-changes.html.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/hook/admin/users/confirm-delete-warn_safe.html.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/hook/bug/format_comment-type.txt.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/hook/bug/process/header-title.html.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/hook/bug/process/results-title.html.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/hook/global/field-descs-end.none.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/hook/global/reason-descs-end.none.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/hook/global/user-error-errors.html.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/hook/search/form-email_numbering_end.html.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/hook/search/search-report-select-rep_fields.html.tmpl [new file with mode: 0644]
extensions/Voting/template/en/default/pages/voting.html.tmpl [moved from template/en/default/pages/voting.html.tmpl with 97% similarity]
extensions/Voting/template/en/default/pages/voting/bug.html.tmpl [moved from template/en/default/bug/votes/list-for-bug.html.tmpl with 83% similarity]
extensions/Voting/template/en/default/pages/voting/user.html.tmpl [moved from template/en/default/bug/votes/list-for-user.html.tmpl with 90% similarity]
extensions/Voting/template/en/default/voting/delete-all.html.tmpl [moved from template/en/default/bug/votes/delete-all.html.tmpl with 96% similarity]
extensions/Voting/template/en/default/voting/votes-removed.txt.tmpl [moved from template/en/default/email/votes-removed.txt.tmpl with 100% similarity]
extensions/Voting/web/style.css [moved from skins/standard/voting.css with 100% similarity]
importxml.pl
process_bug.cgi
query.cgi
report.cgi
sanitycheck.cgi
skins/standard/show_bug.css
template/en/default/account/prefs/email.html.tmpl
template/en/default/admin/params/bugfields.html.tmpl
template/en/default/admin/products/create.html.tmpl
template/en/default/admin/products/edit-common.html.tmpl
template/en/default/admin/products/list.html.tmpl
template/en/default/admin/products/updated.html.tmpl
template/en/default/admin/sanitycheck/messages.html.tmpl
template/en/default/admin/users/confirm-delete.html.tmpl
template/en/default/bug/edit.html.tmpl
template/en/default/bug/format_comment.txt.tmpl
template/en/default/bug/process/header.html.tmpl
template/en/default/bug/process/results.html.tmpl
template/en/default/email/newchangedmail.txt.tmpl
template/en/default/filterexceptions.pl
template/en/default/global/field-descs.none.tmpl
template/en/default/global/reason-descs.none.tmpl [new file with mode: 0644]
template/en/default/global/site-navigation.html.tmpl
template/en/default/global/user-error.html.tmpl
template/en/default/list/list.rdf.tmpl
template/en/default/search/form.html.tmpl
template/en/default/search/search-help.html.tmpl
template/en/default/search/search-report-select.html.tmpl
template/en/default/sidebar.xul.tmpl
votes.cgi

index 8415e356ba6fb2438232b0e85a91a609b57c3a89..5ee4b214df105645d2fa3dd58111c2edeff639d8 100644 (file)
@@ -27,6 +27,5 @@
 /skins/contrib/Dusk/show_bug.css
 /skins/contrib/Dusk/show_multiple.css
 /skins/contrib/Dusk/summarize-time.css
-/skins/contrib/Dusk/voting.css
 /skins/contrib/Dusk/yui
 .DS_Store
index 336b9cfe10573f28f4ddff5ae47c514eb269796d..b3f0fe58e2026fd2a3dc389a77f35a52e39bb825 100644 (file)
@@ -57,7 +57,6 @@ use URI::QueryParam;
 use base qw(Bugzilla::Object Exporter);
 @Bugzilla::Bug::EXPORT = qw(
     bug_alias_to_id
-    RemoveVotes CheckIfVotedConfirmed
     LogActivityEntry
     editable_bug_fields
 );
@@ -631,7 +630,6 @@ sub run_create_validators {
 
     # You can't set these fields on bug creation (or sometimes ever).
     delete $params->{resolution};
-    delete $params->{votes};
     delete $params->{lastdiffed};
     delete $params->{bug_id};
 
@@ -967,7 +965,6 @@ sub remove_from_db {
     # - flags
     # - keywords
     # - longdescs
-    # - votes
 
     # Also, the attach_data table uses attachments.attach_id as a foreign
     # key, and so indirectly depends on a bug deletion too.
@@ -983,7 +980,6 @@ sub remove_from_db {
              undef, ($bug_id, $bug_id));
     $dbh->do("DELETE FROM flags WHERE bug_id = ?", undef, $bug_id);
     $dbh->do("DELETE FROM keywords WHERE bug_id = ?", undef, $bug_id);
-    $dbh->do("DELETE FROM votes WHERE bug_id = ?", undef, $bug_id);
 
     # The attach_data table doesn't depend on bugs.bug_id directly.
     my $attach_ids =
@@ -1819,7 +1815,7 @@ sub fields {
            bug_status resolution dup_id see_also
            bug_file_loc status_whiteboard keywords
            priority bug_severity target_milestone
-           dependson blocked votes everconfirmed
+           dependson blocked everconfirmed
            reporter assigned_to cc estimated_time
            remaining_time actual_time deadline),
 
@@ -2870,14 +2866,6 @@ sub show_attachment_flags {
     return $self->{'show_attachment_flags'};
 }
 
-sub use_votes {
-    my ($self) = @_;
-    return 0 if $self->{'error'};
-
-    return Bugzilla->params->{'usevotes'} 
-           && $self->product_obj->votes_per_user > 0;
-}
-
 sub groups {
     my $self = shift;
     return $self->{'groups'} if exists $self->{'groups'};
@@ -3019,20 +3007,6 @@ sub choices {
     return $self->{'choices'};
 }
 
-sub votes {
-    my ($self) = @_;
-    return 0 if $self->{error};
-    return $self->{votes} if defined $self->{votes};
-
-    my $dbh = Bugzilla->dbh;
-    $self->{votes} = $dbh->selectrow_array(
-        'SELECT SUM(vote_count) FROM votes
-          WHERE bug_id = ? ' . $dbh->sql_group_by('bug_id'),
-        undef, $self->bug_id);
-    $self->{votes} ||= 0;
-    return $self->{votes};
-}
-
 # Convenience Function. If you need speed, use this. If you need
 # other Bug fields in addition to this, just create a new Bug with
 # the alias.
@@ -3312,136 +3286,6 @@ sub CountOpenDependencies {
     return @dependencies;
 }
 
-# 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 RemoveVotes {
-    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("email/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 CheckIfVotedConfirmed {
-    my $id = shift;
-    my $bug = new Bugzilla::Bug($id);
-
-    my $ret = 0;
-    if (!$bug->everconfirmed
-        and $bug->product_obj->votes_to_confirm
-        and $bug->votes >= $bug->product_obj->votes_to_confirm) 
-    {
-        $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;
-}
-
 ################################################################################
 # check_can_change_field() defines what users are allowed to change. You
 # can add code here for site-specific policy changes, according to the
index 204c4ba9a052cf6b1626aba9946ed3cc6f255356..e7694c32e17a59c12c1d0b7d17bc4ede48d16279 100644 (file)
@@ -377,12 +377,6 @@ sub Send {
     # the relationships in a hash. The keys are userids, the values are an
     # array of role constants.
     
-    # Voters
-    my $voters = $dbh->selectcol_arrayref(
-        "SELECT who FROM votes WHERE bug_id = ?", undef, ($id));
-        
-    $recipients{$_}->{+REL_VOTER} = BIT_DIRECT foreach (@$voters);
-
     # CCs
     $recipients{$_}->{+REL_CC} = BIT_DIRECT foreach (@ccs);
     
@@ -405,8 +399,8 @@ sub Send {
     foreach my $ref (@$diffs) {
         my ($who, $whoname, $what, $when, $old, $new) = (@$ref);
         if ($old) {
-            # You can't stop being the reporter, and mail isn't sent if you
-            # remove your vote.
+            # You can't stop being the reporter, so we don't check that
+            # relationship here.
             # Ignore people whose user account has been deleted or renamed.
             if ($what eq "CC") {
                 foreach my $cc_user (split(/[\s,]+/, $old)) {
@@ -462,7 +456,6 @@ sub Send {
     foreach my $user_id (keys %recipients) {
         my %rels_which_want;
         my $sent_mail = 0;
-
         my $user = new Bugzilla::User($user_id);
         # Deleted users must be excluded.
         next unless $user;
index 300357313e2be3f848ebbc224e699aa0a469a37e..60d26012f1df29e1db0a51911bc0d344e4efe10a 100644 (file)
@@ -148,7 +148,7 @@ sub set_type {
 sub _check_extra_data {
     my ($invocant, $extra_data, $type) = @_;
     $type = $invocant->type if ref $invocant;
-    if ($type == CMT_NORMAL or $type == CMT_POPULAR_VOTES) {
+    if ($type == CMT_NORMAL) {
         if (defined $extra_data) {
             ThrowCodeError('comment_extra_data_not_allowed',
                            { type => $type, extra_data => $extra_data });
index 9f3ddc9abb1266cfb29f9eb0983006b0358d6a7b..d0de9dac601cc795b04a25c788f8d8c10e3386b9 100644 (file)
@@ -71,12 +71,6 @@ sub get_param_list {
    default => 0
   },
 
-  {
-   name => 'usevotes',
-   type => 'b',
-   default => 0
-  },
-
   {
    name => 'usebugaliases',
    type => 'b',
index 8ab7455ff602fe6e7c9c33dcd82166f82ee104fc..d626c97497a9af2cde21cbb9297a0c5aa199ce17 100644 (file)
@@ -90,7 +90,6 @@ use File::Basename;
     CMT_NORMAL
     CMT_DUPE_OF
     CMT_HAS_DUPE
-    CMT_POPULAR_VOTES
     CMT_MOVED_TO
     CMT_ATTACHMENT_CREATED
     CMT_ATTACHMENT_UPDATED
@@ -98,7 +97,7 @@ use File::Basename;
     THROW_ERROR
     
     RELATIONSHIPS
-    REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_VOTER REL_GLOBAL_WATCHER
+    REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER
     REL_ANY
     
     POS_EVENTS
@@ -282,7 +281,7 @@ use constant MAX_COMMENT_LENGTH => 65535;
 use constant CMT_NORMAL => 0;
 use constant CMT_DUPE_OF => 1;
 use constant CMT_HAS_DUPE => 2;
-use constant CMT_POPULAR_VOTES => 3;
+# Type 3 was CMT_POPULAR_VOTES, which moved to the Voting extension.
 use constant CMT_MOVED_TO => 4;
 use constant CMT_ATTACHMENT_CREATED => 5;
 use constant CMT_ATTACHMENT_UPDATED => 6;
@@ -295,7 +294,7 @@ use constant REL_ASSIGNEE           => 0;
 use constant REL_QA                 => 1;
 use constant REL_REPORTER           => 2;
 use constant REL_CC                 => 3;
-use constant REL_VOTER              => 4;
+# REL 4 was REL_VOTER, before it was moved ino an extension.
 use constant REL_GLOBAL_WATCHER     => 5;
 
 # We need these strings for the X-Bugzilla-Reasons header
@@ -307,7 +306,6 @@ use constant RELATIONSHIPS => {
     REL_REPORTER      , "Reporter",
     REL_QA            , "QAcontact",
     REL_CC            , "CC",
-    REL_VOTER         , "Voter",
     REL_GLOBAL_WATCHER, "GlobalWatcher"
 };
                               
index 27ae3be8a3a7370a4bcf9d86510aadef78d8e4c9..21c0e7970bd05d9017ff0621db53910923bdfa8b 100644 (file)
@@ -273,8 +273,6 @@ use constant ABSTRACT_SCHEMA => {
                                                   COLUMN => 'userid'}},
             status_whiteboard   => {TYPE => 'MEDIUMTEXT', NOTNULL => 1,
                                     DEFAULT => "''"},
-            votes               => {TYPE => 'INT3', NOTNULL => 1,
-                                    DEFAULT => '0'},
             # Note: keywords field is only a cache; the real data
             # comes from the keywords table
             keywords            => {TYPE => 'MEDIUMTEXT', NOTNULL => 1,
@@ -309,7 +307,6 @@ use constant ABSTRACT_SCHEMA => {
             bugs_resolution_idx       => ['resolution'],
             bugs_target_milestone_idx => ['target_milestone'],
             bugs_qa_contact_idx       => ['qa_contact'],
-            bugs_votes_idx            => ['votes'],
         ],
     },
 
@@ -434,24 +431,6 @@ use constant ABSTRACT_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'],
-        ],
-    },
-
     attachments => {
         FIELDS => [
             attach_id    => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
@@ -1223,12 +1202,6 @@ use constant ABSTRACT_SCHEMA => {
             description       => {TYPE => 'MEDIUMTEXT'},
             isactive          => {TYPE => 'BOOLEAN', NOTNULL => 1,
                                   DEFAULT => 1},
-            votesperuser      => {TYPE => 'INT2', NOTNULL => 1,
-                                  DEFAULT => 0},
-            maxvotesperbug    => {TYPE => 'INT2', NOTNULL => 1,
-                                  DEFAULT => '10000'},
-            votestoconfirm    => {TYPE => 'INT2', NOTNULL => 1,
-                                  DEFAULT => 0},
             defaultmilestone  => {TYPE => 'varchar(20)',
                                   NOTNULL => 1, DEFAULT => "'---'"},
             allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1,
index 17e4194c2a648411306e74cd3ea1dc240d17d094..c2ab6e11b08264f4d6382a3bb7b5dc77dd8613f2 100644 (file)
@@ -188,7 +188,6 @@ use constant DEFAULT_FIELDS => (
      buglist => 1},
     {name => 'reporter',     desc => 'ReportedBy', in_new_bugmail => 1,
      buglist => 1},
-    {name => 'votes',        desc => 'Votes',      buglist => 1},
     {name => 'qa_contact',   desc => 'QAContact',  in_new_bugmail => 1,
      buglist => 1},
     {name => 'cc',           desc => 'CC',         in_new_bugmail => 1},
index 65137e593cef2d9a835a62da0933bd655fc97f4c..66461bf454661be74916d7eaa1f5d2e64fe4ccd7 100644 (file)
@@ -168,11 +168,6 @@ sub update_table_definitions {
     $dbh->bz_add_column('bugs', 'everconfirmed',
                         {TYPE => 'BOOLEAN', NOTNULL => 1}, 1);
 
-    $dbh->bz_add_column('products', 'maxvotesperbug',
-                        {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '10000'});
-    $dbh->bz_add_column('products', 'votestoconfirm',
-                        {TYPE => 'INT2', NOTNULL => 1}, 0);
-
     _populate_milestones_table();
 
     # 2000-03-22 Changed the default value for target_milestone to be "---"
@@ -363,8 +358,10 @@ sub update_table_definitions {
         {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"});
     $dbh->bz_alter_column('bugs', 'keywords',
         {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"});
-    $dbh->bz_alter_column('bugs', 'votes',
-                          {TYPE => 'INT3', NOTNULL => 1, DEFAULT => '0'});
+    if ($dbh->bz_column_info('bugs', 'votes')) {
+        $dbh->bz_alter_column('bugs', 'votes',
+                              {TYPE => 'INT3', NOTNULL => 1, DEFAULT => '0'});
+    }
 
     $dbh->bz_alter_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'});
 
@@ -469,11 +466,14 @@ sub update_table_definitions {
     if ($dbh->bz_column_info('products', 'disallownew')){
         $dbh->bz_alter_column('products', 'disallownew',
                               {TYPE => 'BOOLEAN', NOTNULL => 1,  DEFAULT => 0});
+    
+        if ($dbh->bz_column_info('products', 'votesperuser')) {
+            $dbh->bz_alter_column('products', 'votesperuser', 
+                {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
+            $dbh->bz_alter_column('products', 'votestoconfirm',
+                {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
+        }
     }
-    $dbh->bz_alter_column('products', 'votesperuser', 
-                          {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
-    $dbh->bz_alter_column('products', 'votestoconfirm',
-                          {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
 
     # 2006-08-04 LpSolit@gmail.com - Bug 305941
     $dbh->bz_drop_column('profiles', 'refreshed_when');
@@ -654,14 +654,14 @@ sub _add_bug_vote_cache {
     # (P.S. All is not lost; it appears that the latest betas of MySQL 
     # support a new table format which will allow 32 indices.)
 
-    $dbh->bz_drop_column('bugs', 'area');
-    if (!$dbh->bz_column_info('bugs', 'votes')) {
+    if ($dbh->bz_column_info('bugs', 'area')) {
+        $dbh->bz_drop_column('bugs', 'area');
         $dbh->bz_add_column('bugs', 'votes', {TYPE => 'INT3', NOTNULL => 1,
                                               DEFAULT => 0});
         $dbh->bz_add_index('bugs', 'bugs_votes_idx', [qw(votes)]);
+        $dbh->bz_add_column('products', 'votesperuser',
+                            {TYPE => 'INT2', NOTNULL => 1}, 0);
     }
-    $dbh->bz_add_column('products', 'votesperuser',
-                        {TYPE => 'INT2', NOTNULL => 1}, 0);
 }
 
 sub _update_product_name_definition {
@@ -896,9 +896,11 @@ sub _add_unique_login_name_index_to_profiles {
                            ["votes", "who"],
                            ["longdescs", "who"]) {
                 my ($table, $field) = (@$i);
-                print "   Updating $table.$field...\n";
-                $dbh->do("UPDATE $table SET $field = $u1 " .
-                          "WHERE $field = $u2");
+                if ($dbh->bz_table_info($table)) {
+                    print "   Updating $table.$field...\n";
+                    $dbh->do("UPDATE $table SET $field = $u1 " .
+                              "WHERE $field = $u2");
+                }
             }
             $dbh->do("DELETE FROM profiles WHERE userid = $u2");
         }
@@ -2206,9 +2208,9 @@ sub _rename_votes_count_and_force_group_refresh {
     #
     # Renaming the 'count' column in the votes table because Sybase doesn't
     # like it
-    if ($dbh->bz_column_info('votes', 'count')) {
-        $dbh->bz_rename_column('votes', 'count', 'vote_count');
-    }
+    return if !$dbh->bz_table_info('votes');
+    return if $dbh->bz_column_info('votes', 'count');
+    $dbh->bz_rename_column('votes', 'count', 'vote_count');
 }
 
 sub _fix_group_with_empty_name {
@@ -2266,7 +2268,9 @@ sub _migrate_email_prefs_to_new_table {
                              "Reporter"  => REL_REPORTER,
                              "QAcontact" => REL_QA,
                              "CClist"    => REL_CC,
-                             "Voter"     => REL_VOTER);
+                             # REL_VOTER was "4" before it was moved to an
+                             #  extension.
+                             "Voter"     => 4);
 
         my %events = ("Removeme"    => EVT_ADDED_REMOVED,
                       "Comments"    => EVT_COMMENT,
@@ -3343,8 +3347,10 @@ sub _add_allows_unconfirmed_to_product_table {
     if (!$dbh->bz_column_info('products', 'allows_unconfirmed')) {
         $dbh->bz_add_column('products', 'allows_unconfirmed',
             { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' });
-        $dbh->do('UPDATE products SET allows_unconfirmed = 1 
-                   WHERE votestoconfirm > 0');
+        if ($dbh->bz_column_info('products', 'votestoconfirm')) {
+            $dbh->do('UPDATE products SET allows_unconfirmed = 1 
+                       WHERE votestoconfirm > 0');
+        }
     }
 }
 
index dac8962ff3f3fb4718dfe91fd9162d79cfb6b5a7..11db7567bb46db7e67bfe909b5de95e5ea476b4b 100644 (file)
@@ -278,7 +278,8 @@ sub set {
     my ($self, $field, $value) = @_;
 
     # This method is protected. It's used to help implement set_ functions.
-    caller->isa('Bugzilla::Object')
+    my $caller = caller;
+    $caller->isa('Bugzilla::Object') || $caller->isa('Bugzilla::Extension')
         || ThrowCodeError('protection_violation', 
                           { caller     => caller,
                             superclass => __PACKAGE__,
index 6b00fcbf6a4d431803484d9166f1d603f1c30137..975af7d5d6c290ad1b5fbf19c7bedeccbcd1da0b 100644 (file)
@@ -48,9 +48,6 @@ use constant DB_COLUMNS => qw(
    classification_id
    description
    isactive
-   votesperuser
-   maxvotesperbug
-   votestoconfirm
    defaultmilestone
    allows_unconfirmed
 );
@@ -66,9 +63,6 @@ use constant UPDATE_COLUMNS => qw(
     description
     defaultmilestone
     isactive
-    votesperuser
-    maxvotesperbug
-    votestoconfirm
     allows_unconfirmed
 );
 
@@ -80,9 +74,6 @@ use constant VALIDATORS => {
     version          => \&_check_version,
     defaultmilestone => \&_check_default_milestone,
     isactive         => \&Bugzilla::Object::check_boolean,
-    votesperuser     => \&_check_votes_per_user,
-    maxvotesperbug   => \&_check_votes_per_bug,
-    votestoconfirm   => \&_check_votes_to_confirm,
     create_series    => \&Bugzilla::Object::check_boolean
 };
 
@@ -155,99 +146,6 @@ sub update {
     $dbh->bz_start_transaction();
     my ($changes, $old_self) = $self->SUPER::update(@_);
 
-    # We also have to fix votes.
-    my @msgs; # Will store emails to send to voters.
-    if ($changes->{maxvotesperbug} || $changes->{votesperuser} || $changes->{votestoconfirm}) {
-        # We cannot |use| these modules, due to dependency loops.
-        require Bugzilla::Bug;
-        import Bugzilla::Bug qw(RemoveVotes CheckIfVotedConfirmed);
-        require Bugzilla::User;
-        import Bugzilla::User qw(user_id_to_login);
-
-        # 1. too many votes for a single user on a single bug.
-        my @toomanyvotes_list = ();
-        if ($self->max_votes_per_bug < $self->votes_per_user) {
-            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, ($self->id, $self->max_votes_per_bug));
-
-            foreach my $vote (@$votes) {
-                my ($who, $id) = (@$vote);
-                # If some votes are removed, RemoveVotes() returns a list
-                # of messages to send to voters.
-                push(@msgs, RemoveVotes($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 RemoveVotes
-        # doesn't enforce votesperuser (except per-bug when it's less
-        # than maxvotesperbug).  See Bugzilla::Bug::RemoveVotes().
-
-        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, $self->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} > $self->votes_per_user) {
-                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, ($self->id, $who));
-
-                foreach my $bug_id (@$bug_ids) {
-                    # RemoveVotes() returns a list of messages to send
-                    # in case some voters had too many votes.
-                    push(@msgs, RemoveVotes($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, ($self->id, 'UNCONFIRMED', $self->votes_to_confirm));
-
-        my @updated_bugs = ();
-        foreach my $bug_id (@$bug_list) {
-            my $confirmed = CheckIfVotedConfirmed($bug_id);
-            push (@updated_bugs, $bug_id) if $confirmed;
-        }
-        $changes->{'confirmed_bugs'} = \@updated_bugs;
-    }
-
     # Also update group settings.
     if ($self->{check_group_controls}) {
         require Bugzilla::Bug;
@@ -364,11 +262,6 @@ sub update {
     delete $self->{check_group_controls};
     Bugzilla->user->clear_product_cache();
 
-    # Now that changes have been committed, we can send emails to voters.
-    foreach my $msg (@msgs) {
-        MessageToMTA($msg);
-    }
-
     return $changes;
 }
 
@@ -524,37 +417,6 @@ sub _check_milestone_url {
     return $url;
 }
 
-sub _check_votes_per_user {
-    return _check_votes(@_, 0);
-}
-
-sub _check_votes_per_bug {
-    return _check_votes(@_, 10000);
-}
-
-sub _check_votes_to_confirm {
-    return _check_votes(@_, 0);
-}
-
-# This subroutine is only used internally by other _check_votes_* validators.
-sub _check_votes {
-    my ($invocant, $votes, $field, $default) = @_;
-
-    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('product_illegal_votes', {field => $field, votes => $_[1]});
-        }
-        else {
-            $votes = $default;
-        }
-    }
-    return $votes;
-}
-
 #####################################
 # Implement Bugzilla::Field::Choice #
 #####################################
@@ -618,9 +480,6 @@ sub set_name { $_[0]->set('name', $_[1]); }
 sub set_description { $_[0]->set('description', $_[1]); }
 sub set_default_milestone { $_[0]->set('defaultmilestone', $_[1]); }
 sub set_is_active { $_[0]->set('isactive', $_[1]); }
-sub set_votes_per_user { $_[0]->set('votesperuser', $_[1]); }
-sub set_votes_per_bug { $_[0]->set('maxvotesperbug', $_[1]); }
-sub set_votes_to_confirm { $_[0]->set('votestoconfirm', $_[1]); }
 sub set_allows_unconfirmed { $_[0]->set('allows_unconfirmed', $_[1]); }
 
 sub set_group_controls {
@@ -876,9 +735,6 @@ sub flag_types {
 sub allows_unconfirmed { return $_[0]->{'allows_unconfirmed'}; }
 sub description       { return $_[0]->{'description'};       }
 sub is_active         { return $_[0]->{'isactive'};       }
-sub votes_per_user    { return $_[0]->{'votesperuser'};      }
-sub max_votes_per_bug { return $_[0]->{'maxvotesperbug'};    }
-sub votes_to_confirm  { return $_[0]->{'votestoconfirm'};    }
 sub default_milestone { return $_[0]->{'defaultmilestone'};  }
 sub classification_id { return $_[0]->{'classification_id'}; }
 
@@ -939,9 +795,6 @@ Bugzilla::Product - Bugzilla product class.
     my $name             = $product->name;
     my $description      = $product->description;
     my isactive          = $product->is_active;
-    my votesperuser      = $product->votes_per_user;
-    my maxvotesperbug    = $product->max_votes_per_bug;
-    my votestoconfirm    = $product->votes_to_confirm;
     my $defaultmilestone = $product->default_milestone;
     my $classificationid = $product->classification_id;
     my $allows_unconfirmed = $product->allows_unconfirmed;
index d85da01a459c0f2bb35c3075341d944f36025a07..52c99903d774e19ad95d5d791cd729b03625d772 100644 (file)
@@ -86,10 +86,8 @@ use constant SPECIAL_ORDER_JOIN => {
 # 3. title: The title of the column as displayed to users.
 # 
 # Note: There are a few hacks in the code that deviate from these definitions.
-#       In particular, when the list is sorted by the "votes" field the word 
-#       "DESC" is added to the end of the field to sort in descending order, 
-#       and the redundant short_desc column is removed when the client
-#       requests "all" columns.
+#       In particular, the redundant short_desc column is removed when the
+#       client requests "all" columns.
 #
 # This is really a constant--that is, once it's been called once, the value
 # will always be the same unless somebody adds a new custom field. But
@@ -281,18 +279,6 @@ sub init {
         push(@supptables, "LEFT JOIN flagtypes ON flagtypes.id = flags.type_id");
     }
 
-    my $minvotes;
-    if (defined $params->param('votes')) {
-        my $c = trim($params->param('votes'));
-        if ($c ne "") {
-            if ($c !~ /^[0-9]*$/) {
-                ThrowUserError("illegal_at_least_x_votes",
-                                  { value => $c });
-            }
-            push(@specialchart, ["votes", "greaterthan", $c - 1]);
-        }
-    }
-
     # If the user has selected all of either status or resolution, change to
     # selecting none. This is functionally equivalent, but quite a lot faster.
     # Also, if the status is __open__ or __closed__, translate those
index 2f9e0734fd2db3d726a32d01605dc8c496888bdc..1e0bdc437e0301547a82c3f4f6603016aac9211a 100644 (file)
@@ -339,12 +339,6 @@ sub _handle_special_first_chars {
 sub _handle_field_names {
     my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_;
     
-    # votes:xx ("at least xx votes")
-    if ($or_operand =~ /^votes:([0-9]+)$/) {
-        addChart('votes', 'greaterthan', $1 - 1, $negate);
-        return 1;
-    }
-    
     # Flag and requestee shortcut
     if ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) {
         addChart('flagtypes.name', 'substring', $1, $negate);
@@ -454,18 +448,6 @@ sub _special_field_syntax {
         return 1;
     }
 
-    # Votes (votes>xx)
-    if ($word =~ m/^votes>([0-9]+)$/) {
-        addChart('votes', 'greaterthan', $1, $negate);
-        return 1;
-    }
-    
-    # Votes (votes>=xx, votes=>xx)
-    if ($word =~ m/^votes(>=|=>)([0-9]+)$/) {
-        addChart('votes', 'greaterthan', $2-1, $negate);
-        return 1;
-    }
-
     return 0;    
 }
 
index 711a45f4439119cd2340fb422be9f06364295e8c..1d7b9f7d9ad9734e5f814ff28d9c76c98cc74acf 100644 (file)
@@ -386,9 +386,6 @@ sub search {
     if (my $when = delete $params->{creation_ts}) {
         $params->{WHERE}->{'creation_ts >= ?'} = $when;
     }
-    if (my $votes = delete $params->{votes}) { 
-        $params->{WHERE}->{'votes >= ?'} = $votes;
-    }
     if (my $summary = delete $params->{short_desc}) {
         my @strings = ref $summary ? @$summary : ($summary);
         my @likes = ("short_desc LIKE ?") x @strings;
@@ -1687,11 +1684,6 @@ C<string> The "URL" field of a bug.
 
 C<string> The Version field of a bug.
 
-=item C<votes>
-
-C<int> Searches for bugs with this many votes or greater. May not
-be an array.
-
 =item C<whiteboard>
 
 C<string> Search the "Status Whiteboard" field on bugs for a substring.
@@ -1722,6 +1714,8 @@ for that value.
 
 =item Added in Bugzilla B<3.4>.
 
+=item Searching by C<votes> was removed in Bugzilla B<3.8>.
+
 =back
 
 =back
index b8cfa63367724b2818855531f24241da7bca453a..48fe2d873200c33cfd9bed2ffa02ff0f92429597 100755 (executable)
@@ -653,18 +653,6 @@ else {
 # and are hard-coded into the display templates.
 @displaycolumns = grep($_ ne 'bug_id', @displaycolumns);
 
-# Add the votes column to the list of columns to be displayed
-# in the bug list if the user is searching for bugs with a certain
-# number of votes and the votes column is not already on the list.
-
-# Some versions of perl will taint 'votes' if this is done as a single
-# statement, because the votes param is tainted at this point
-my $votes = $params->param('votes');
-$votes ||= "";
-if (trim($votes) && !grep($_ eq 'votes', @displaycolumns)) {
-    push(@displaycolumns, 'votes');
-}
-
 # Remove the timetracking columns if they are not a part of the group
 # (happens if a user had access to time tracking and it was revoked/disabled)
 if (!Bugzilla->user->is_timetracker) {
@@ -806,12 +794,6 @@ if ($order) {
                 # Special handlings for certain columns
                 next if $column_name eq 'relevance' && !$fulltext;
                                 
-                # If we are sorting by votes, sort in descending order if
-                # no explicit sort order was given.
-                if ($column_name eq 'votes' && !$direction) {
-                    $direction = "DESC";
-                }
-
                 if (exists $columns->{$column_name}) {
                     $direction = " $direction" if $direction;
                     push(@order, "$column_name$direction");
index 64f575b62cf1f0e8f3471cfbda28c9ea3731b501..b449d6ba42b13aff3991b65a80298df11b262939 100644 (file)
@@ -5,7 +5,7 @@
        maintainer CDATA #REQUIRED
        exporter CDATA #IMPLIED
 >
-<!ELEMENT bug (bug_id, (alias?, creation_ts, short_desc, delta_ts, reporter_accessible, cclist_accessible, classification_id, classification, product, component, version, rep_platform, op_sys, bug_status, resolution?, dup_id?, bug_file_loc?, status_whiteboard?, keywords*, priority, bug_severity, target_milestone?, dependson*, blocked*, votes?, everconfirmed, reporter, assigned_to, qa_contact?, cc*, (estimated_time, remaining_time, actual_time, deadline)?, group*, flag*, long_desc*, attachment*)?)>
+<!ELEMENT bug (bug_id, (alias?, creation_ts, short_desc, delta_ts, reporter_accessible, cclist_accessible, classification_id, classification, product, component, version, rep_platform, op_sys, bug_status, resolution?, dup_id?, bug_file_loc?, status_whiteboard?, keywords*, priority, bug_severity, target_milestone?, dependson*, blocked*, everconfirmed, reporter, assigned_to, qa_contact?, cc*, (estimated_time, remaining_time, actual_time, deadline)?, group*, flag*, long_desc*, attachment*)?)>
 <!ATTLIST bug
        error (NotFound | NotPermitted | InvalidBugId) #IMPLIED
 >
@@ -39,7 +39,6 @@
 <!ELEMENT keywords (#PCDATA)>
 <!ELEMENT dependson (#PCDATA)>
 <!ELEMENT blocked (#PCDATA)>
-<!ELEMENT votes (#PCDATA)>
 <!ELEMENT everconfirmed (#PCDATA)>
 <!ELEMENT cc (#PCDATA)>
 <!ELEMENT group (#PCDATA)>
index 409f15e5a3ab469353ff809b09084a4228125c02..15bdac599a198db9a13e5f314dce31074a19e497 100755 (executable)
@@ -55,9 +55,6 @@ if (Bugzilla->params->{"useclassification"}) {
 
 push(@masterlist, ("product", "component", "version", "op_sys"));
 
-if (Bugzilla->params->{"usevotes"}) {
-    push (@masterlist, "votes");
-}
 if (Bugzilla->params->{"usebugaliases"}) {
     unshift(@masterlist, "alias");
 }
index 80c516e04c2927a5c3661d11e1108aa04eabd992..6c1ed13771f5bff5c6e12f7abee74880a4975d6d 100755 (executable)
@@ -129,7 +129,6 @@ my $changes = {
     flags           => ['setter_id', 'requestee_id'],
     cc              => ['who bug_id'],
     longdescs       => ['who'],
-    votes           => ['who'],
     # Tables affecting global behavior / other users.
     components      => ['initialowner', 'initialqacontact'],
     component_cc    => ['user_id component_id'],
index 1adc6b70c5f70cdbf584f5157ce8749404ab490b..dda8ac48a0c6e6073cd23229c0d50562b07c2420 100644 (file)
@@ -968,8 +968,7 @@ UNCONFIRMED state#</dia:string>
       <dia:attribute name="text">
         <dia:composite type="text">
           <dia:attribute name="string">
-            <dia:string>#Bug confirmed or
-receives enough votes#</dia:string>
+            <dia:string>#Bug confirmed#</dia:string>
           </dia:attribute>
           <dia:attribute name="font">
             <dia:font family="sans" style="0" name="Helvetica"/>
index 8433ed16bac9c38aa41314c67b1b016f55ff0f4e..4a302aa6cb6311d76bdde4d7fe736c90b3b96275 100755 (executable)
@@ -186,11 +186,6 @@ if ($action eq 'new') {
         create_series    => scalar $cgi->param('createseries'),
         allows_unconfirmed => scalar $cgi->param('allows_unconfirmed'),
     );
-    if (Bugzilla->params->{'usevotes'}) {
-        $create_params{votesperuser}   = $cgi->param('votesperuser');
-        $create_params{maxvotesperbug} = $cgi->param('maxvotesperbug');
-        $create_params{votestoconfirm} = $cgi->param('votestoconfirm');
-    }
     my $product = Bugzilla::Product->create(\%create_params);
 
     delete_token($token);
@@ -295,16 +290,13 @@ if ($action eq 'update') {
     my $product_old_name = trim($cgi->param('product_old_name') || '');
     my $product = $user->check_can_admin_product($product_old_name);
 
-    $product->set_name($product_name);
-    $product->set_description(scalar $cgi->param('description'));
-    $product->set_default_milestone(scalar $cgi->param('defaultmilestone'));
-    $product->set_is_active(scalar $cgi->param('is_active'));
-    if (Bugzilla->params->{'usevotes'}) {
-        $product->set_votes_per_user(scalar $cgi->param('votesperuser'));
-        $product->set_votes_per_bug(scalar $cgi->param('maxvotesperbug'));
-        $product->set_votes_to_confirm(scalar $cgi->param('votestoconfirm'));
-    }
-    $product->set_allows_unconfirmed(scalar $cgi->param('allows_unconfirmed'));
+    $product->set_all({
+        name        => $product_name,
+        description => scalar $cgi->param('description'),
+        is_active   => scalar $cgi->param('is_active'),
+        allows_unconfirmed => scalar $cgi->param('allows_unconfirmed'),
+        default_milestone  => scalar $cgi->param('defaultmilestone'),
+    });
 
     my $changes = $product->update();
 
index e63f29fc5c3d2dd30b67c69ff0925c29a06e9f90..7ce6bb9d24d2d73f797bb27b1ad7b58209df9006 100755 (executable)
@@ -424,9 +424,6 @@ if ($action eq 'search') {
     $vars->{'series'} = $dbh->selectrow_array(
         'SELECT COUNT(*) FROM series WHERE creator = ?',
         undef, $otherUserID);
-    $vars->{'votes'} = $dbh->selectrow_array(
-        'SELECT COUNT(*) FROM votes WHERE who = ?',
-        undef, $otherUserID);
     $vars->{'watch'}{'watched'} = $dbh->selectrow_array(
         'SELECT COUNT(*) FROM watch WHERE watched = ?',
         undef, $otherUserID);
@@ -537,7 +534,6 @@ if ($action eq 'search') {
     $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $otherUserID);
     $dbh->do('DELETE FROM user_group_map WHERE user_id = ?', undef,
              $otherUserID);
-    $dbh->do('DELETE FROM votes WHERE who = ?', undef, $otherUserID);
     $dbh->do('DELETE FROM watch WHERE watcher = ? OR watched = ?', undef,
              ($otherUserID, $otherUserID));
 
diff --git a/extensions/Voting/Extension.pm b/extensions/Voting/Extension.pm
new file mode 100644 (file)
index 0000000..e111ac7
--- /dev/null
@@ -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 (file)
index 0000000..0bd81ea
--- /dev/null
@@ -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 (file)
index 0000000..fbbda3e
--- /dev/null
@@ -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 (file)
index 0000000..876c511
--- /dev/null
@@ -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 %]
+      &rarr;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 %]
+    &rarr;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 %]
+      &rarr;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 %]
+    &rarr;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 %]
+    &rarr;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 (file)
index 0000000..afb81d3
--- /dev/null
@@ -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 (file)
index 0000000..f799f12
--- /dev/null
@@ -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 (file)
index 0000000..7952442
--- /dev/null
@@ -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&amp;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 (file)
index 0000000..ebba6fc
--- /dev/null
@@ -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 (file)
index 0000000..a453065
--- /dev/null
@@ -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 (file)
index 0000000..ae0d465
--- /dev/null
@@ -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 (file)
index 0000000..2fd7980
--- /dev/null
@@ -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 (file)
index 0000000..3a1f5a1
--- /dev/null
@@ -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 (file)
index 0000000..c2ff707
--- /dev/null
@@ -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 (file)
index 0000000..5acfff1
--- /dev/null
@@ -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 (file)
index 0000000..ca74f6d
--- /dev/null
@@ -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') %]
similarity index 97%
rename from template/en/default/pages/voting.html.tmpl
rename to extensions/Voting/template/en/default/pages/voting.html.tmpl
index 4e6fb473d649f35352a2f5fbd91b9dc739a77a58..99026c0d5550a2e4fdb3e8b597949e01d596c319 100644 (file)
@@ -64,6 +64,6 @@ a few [% terms.bugs %] indicating your strong support for them.
 on [% terms.bugs %] you vote for.</p>
 
 <p>You may review your votes at any time by clicking on the "<a href=
-"votes.cgi?action=show_user">My Votes</a>" link in the page footer.</p>
+"page.cgi?id=voting/user.html">My Votes</a>" link in the page footer.</p>
 
 [% INCLUDE global/footer.html.tmpl %]
similarity index 83%
rename from template/en/default/bug/votes/list-for-bug.html.tmpl
rename to extensions/Voting/template/en/default/pages/voting/bug.html.tmpl
index a599dc0fb9e3415c65eb866399a3d2753b83780b..03434a5053046be213891be84be450178f75b719 100644 (file)
   # Rights Reserved.
   #
   # Contributor(s): Gervase Markham <gerv@gerv.net>
+  #                 Max Kanat-Alexander <mkanat@bugzilla.org>
   #%]
 
 [%# INTERFACE:
-  # bug_id: integer. ID of the bug we are listing the votes for.
+  # 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.
@@ -29,7 +30,7 @@
 
 [% PROCESS global/header.html.tmpl
            title = "Show Votes"
-           subheader = "$terms.Bug <a href=\"show_bug.cgi?id=$bug_id\">$bug_id</a>"
+           subheader = "$terms.Bug $bug.id" FILTER bug_link(bug)
  %]
 
 [% total = 0 %]
     [% total = total + voter.vote_count %]
     <tr>
       <td>
-          <a href="votes.cgi?action=show_user&amp;user_id=
+          <a href="page.cgi?id=voting/user.html&amp;user_id=
                   [%- voter.id FILTER url_quote %]">
           [% voter.login_name FILTER email FILTER html %]
         </a>
       </td>
       <td align="right">
-        [% voter.vote_count %]
+        [% voter.vote_count FILTER html %]
       </td>
     </tr>
   [% END %]
 </table>
 
-<p>Total votes: [% total %]</p>
+<p>Total votes: [% total FILTER html %]</p>
 
 [% PROCESS global/footer.html.tmpl %]
similarity index 90%
rename from template/en/default/bug/votes/list-for-user.html.tmpl
rename to extensions/Voting/template/en/default/pages/voting/user.html.tmpl
index 2f97616ed69324a056fdff62ea1fa4a6fad81f72..8000792243315c01809727a61b8d71f4424b8467 100644 (file)
@@ -31,7 +31,7 @@
   #            maxvotes: max votes allowed for a user in this product
   #            maxperbug: max votes per bug allowed for a user in this product
   #
-  # bug_id: number; if the user is voting for a bug, this is the bug id
+  # 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? 
   #
   [% subheader = voting_user.login FILTER html %]
   [% IF canedit %]
     [% title = "Change Votes" %]
-    [% IF bug_id %]
+    [% IF this_bug %]
       [%# We .select and .focus the input so it works for textbox and 
           checkbox %]
-      [% onload = "document.forms['voting_form'].bug_" _ bug_id _
-                  ".select();document.forms['voting_form'].bug_" _ bug_id _
+      [% 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 = [ "skins/standard/voting.css" ] 
+             style_urls = [ "extensions/Voting/web/style.css" ] 
   %]
 [% ELSE %]
   <hr>
@@ -72,7 +72,7 @@
 [% END %]
 
 [% IF products.size %]
-  <form name="voting_form" method="post" action="votes.cgi">
+  <form name="voting_form" method="post" action="page.cgi?id=voting/user.html">
     <input type="hidden" name="action" value="vote">
     <table cellspacing="4">
       <tr>
         </tr>
 
         [% FOREACH bug = product.bugs %]
-          <tr [% IF bug.id == bug_id && canedit %] 
+          <tr [% IF bug.id == this_bug.id && canedit %] 
             class="bz_bug_being_voted_on" [% END %]>
-            <td>[% IF bug.id == bug_id && canedit %]Enter New Vote here &rarr;
+            <td>[% IF bug.id == this_bug.id && canedit %]Enter New Vote here &rarr;
               [%- END %]</td>
             <td align="right"><a name="vote_[% bug.id %]">
               [% IF canedit %]
             </td>
             <td>
               [% bug.summary FILTER html %]
-              (<a href="votes.cgi?action=show_bug&amp;bug_id=[% bug.id %]">Show Votes</a>)
+              (<a href="page.cgi?id=voting/bug.html&amp;bug_id=[% bug.id %]">Show Votes</a>)
             </td>
           </tr>
         [% END %]
similarity index 96%
rename from template/en/default/bug/votes/delete-all.html.tmpl
rename to extensions/Voting/template/en/default/voting/delete-all.html.tmpl
index 41b75123dd5011614ce54bbb9a7575dcfe257032..82ddc35961adaf39559e15f8a44b65efcd77c8ac 100644 (file)
@@ -33,7 +33,7 @@
   remove your vote from every [% terms.bug %] you've voted on?
 </p>
 
-<form action="votes.cgi" method="post">
+<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">
index 1a61c5eadee30a512aa85e2b9828ea91e9d39d45..aff475b70c8defee90ef16b72363bb1da54c9ac1 100755 (executable)
@@ -980,7 +980,6 @@ sub process_bug {
                     if($status eq "UNCONFIRMED"){
                         $err .= "Bug Status was UNCONFIRMED but everconfirmed was true\n";
                         $err .= "   Setting status to $initial_status\n";
-                        $err .= "Resetting votes to 0\n" if ( $bug_fields{'votes'} );
                         $status = $initial_status;
                     }
                 }
index 237588ebd624df1e87bdff1b90deb0f8ec35b630..d16298df6717f7513f5e8253efd33dca1e516234 100755 (executable)
@@ -573,27 +573,12 @@ foreach my $bug (@bug_objects) {
     # an error later.
     delete $changed_deps{''};
 
-    # @msgs will store emails which have to be sent to voters, if any.
-    my @msgs;
-    if ($changes->{'product'}) {
-        # If some votes have been removed, RemoveVotes() returns
-        # a list of messages to send to voters.
-        # We delay the sending of these messages till changes are committed.
-        @msgs = RemoveVotes($bug->id, 0, 'votes_bug_moved');
-        CheckIfVotedConfirmed($bug->id);
-    }
-
     $dbh->bz_commit_transaction();
 
     ###############
     # Send Emails #
     ###############
 
-    # Now is a good time to send email to voters.
-    foreach my $msg (@msgs) {
-        MessageToMTA($msg);
-    }
-
     my $old_qa  = $changes->{'qa_contact'}  ? $changes->{'qa_contact'}->[0] : '';
     my $old_own = $changes->{'assigned_to'} ? $changes->{'assigned_to'}->[0] : '';
     my $old_cc  = $changes->{cc}            ? $changes->{cc}->[0] : '';
index 1cbcf0a60b1680420500d8d71ece1a8f9f14cc57..101c90700fd90e546c70d3e0429268e957e2650d 100755 (executable)
--- a/query.cgi
+++ b/query.cgi
@@ -127,7 +127,7 @@ sub PrefillForm {
                       "email", "emailtype", "emailreporter",
                       "emailassigned_to", "emailcc", "emailqa_contact",
                       "emaillongdesc", "content",
-                      "changedin", "votes", "short_desc", "short_desc_type",
+                      "changedin", "short_desc", "short_desc_type",
                       "longdesc", "longdesc_type", "bug_file_loc",
                       "bug_file_loc_type", "status_whiteboard",
                       "status_whiteboard_type", "bug_id",
index 6612ded2a2c8b7064063360b942f51d4b2e5cafb..100c76e9051f3bc86eb94a7399a71309ab60137d 100755 (executable)
@@ -113,7 +113,6 @@ my @columns = qw(
     qa_contact
     classification
     version
-    votes
     keywords
     target_milestone
 );
index 036286454836380ecf33e5a6c2292e5839d2c9b6..4b6e524c23efbe9aeff126bd9545c7fbf0772745 100755 (executable)
@@ -102,7 +102,7 @@ unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
 # Users with 'editkeywords' privs only can only check keywords.
 ###########################################################################
 unless ($user->in_group('editcomponents')) {
-    check_votes_or_keywords('keywords');
+    check_keywords();
     Status('checks_completed');
 
     $template->process('global/footer.html.tmpl', $vars)
@@ -110,27 +110,6 @@ unless ($user->in_group('editcomponents')) {
     exit;
 }
 
-###########################################################################
-# Fix vote cache
-###########################################################################
-
-if ($cgi->param('rebuildvotecache')) {
-    Status('vote_cache_rebuild_start');
-    $dbh->bz_start_transaction();
-    $dbh->do(q{UPDATE bugs SET votes = 0});
-    my $sth_update = $dbh->prepare(q{UPDATE bugs 
-                                        SET votes = ? 
-                                      WHERE bug_id = ?});
-    my $sth = $dbh->prepare(q{SELECT bug_id, SUM(vote_count)
-                                FROM votes }. $dbh->sql_group_by('bug_id'));
-    $sth->execute();
-    while (my ($id, $v) = $sth->fetchrow_array) {
-        $sth_update->execute($v, $id);
-    }
-    $dbh->bz_commit_transaction();
-    Status('vote_cache_rebuild_end');
-}
-
 ###########################################################################
 # Create missing group_control_map entries
 ###########################################################################
@@ -310,7 +289,7 @@ if ($cgi->param('remove_invalid_bug_references')) {
                       'bugs_fulltext/', 'cc/',
                       'dependencies/blocked', 'dependencies/dependson',
                       'duplicates/dupe', 'duplicates/dupe_of',
-                      'flags/', 'keywords/', 'longdescs/', 'votes/') {
+                      'flags/', 'keywords/', 'longdescs/') {
 
         my ($table, $field) = split('/', $pair);
         $field ||= "bug_id";
@@ -489,7 +468,6 @@ CrossCheck("bugs", "bug_id",
            ["dependencies", "blocked"],
            ["dependencies", "dependson"],
            ['flags', 'bug_id'],
-           ["votes", "bug_id"],
            ["keywords", "bug_id"],
            ["duplicates", "dupe_of", "dupe"],
            ["duplicates", "dupe", "dupe_of"]);
@@ -524,7 +502,6 @@ CrossCheck("profiles", "userid",
            ["bugs_activity", "who", "bug_id"],
            ["cc", "who", "bug_id"],
            ['quips', 'userid'],
-           ["votes", "who", "bug_id"],
            ["longdescs", "who", "bug_id"],
            ["logincookies", "userid"],
            ["namedqueries", "userid"],
@@ -681,75 +658,19 @@ while (my ($id, $email) = $sth->fetchrow_array) {
 }
 
 ###########################################################################
-# Perform vote/keyword cache checks
+# Perform keyword cache checks
 ###########################################################################
 
-check_votes_or_keywords();
-
-sub check_votes_or_keywords {
-    my $check = shift || 'all';
-
+sub check_keywords {
     my $dbh = Bugzilla->dbh;
-    my $sth = $dbh->prepare(q{SELECT bug_id, votes, keywords
-                                FROM bugs
-                               WHERE votes != 0 OR keywords != ''});
-    $sth->execute;
-
-    my %votes;
-    my %keyword;
-
-    while (my ($id, $v, $k) = $sth->fetchrow_array) {
-        if ($v != 0) {
-            $votes{$id} = $v;
-        }
-        if ($k) {
-            $keyword{$id} = $k;
-        }
-    }
-
-    # If we only want to check keywords, skip checks about votes.
-    _check_votes(\%votes) unless ($check eq 'keywords');
-    # If we only want to check votes, skip checks about keywords.
-    _check_keywords(\%keyword) unless ($check eq 'votes');
-}
-
-sub _check_votes {
-    my $votes = shift;
-
-    Status('vote_count_start');
-    my $dbh = Bugzilla->dbh;
-    my $sth = $dbh->prepare(q{SELECT bug_id, SUM(vote_count)
-                                FROM votes }.
-                                $dbh->sql_group_by('bug_id'));
-    $sth->execute;
-
-    my $offer_votecache_rebuild = 0;
-
-    while (my ($id, $v) = $sth->fetchrow_array) {
-        if ($v <= 0) {
-            Status('vote_count_alert', {id => $id}, 'alert');
-        } else {
-            if (!defined $votes->{$id} || $votes->{$id} != $v) {
-                Status('vote_cache_alert', {id => $id}, 'alert');
-                $offer_votecache_rebuild = 1;
-            }
-            delete $votes->{$id};
-        }
-    }
-    foreach my $id (keys %$votes) {
-        Status('vote_cache_alert', {id => $id}, 'alert');
-        $offer_votecache_rebuild = 1;
-    }
+    my $cgi = Bugzilla->cgi;
 
-    Status('vote_cache_rebuild_fix') if $offer_votecache_rebuild;
-}
+    my %keyword = @{ $dbh->selectcol_arrayref(
+        q{SELECT bug_id, keywords FROM bugs WHERE keywords != ''},
+        {Columns=>[1,2]}) };
 
-sub _check_keywords {
-    my $keyword = shift;
 
     Status('keyword_check_start');
-    my $dbh = Bugzilla->dbh;
-    my $cgi = Bugzilla->cgi;
 
     my %keywordids;
     my $keywords = $dbh->selectall_arrayref(q{SELECT id, name
@@ -819,13 +740,13 @@ sub _check_keywords {
 
     my @badbugs = ();
 
-    foreach my $b (keys(%$keyword)) {
-        if (!exists $realk{$b} || $realk{$b} ne $keyword->{$b}) {
+    foreach my $b (keys(%keyword)) {
+        if (!exists $realk{$b} || $realk{$b} ne $keyword{$b}) {
             push(@badbugs, $b);
         }
     }
     foreach my $b (keys(%realk)) {
-        if (!exists $keyword->{$b}) {
+        if (!exists $keyword{$b}) {
             push(@badbugs, $b);
         }
     }
@@ -973,13 +894,6 @@ my $confirmed_open_states = join(', ', map {$dbh->quote($_)} @confirmed_open_sta
 BugCheck("bugs WHERE bug_status IN ($confirmed_open_states) AND everconfirmed = 0",
          'bug_check_status_everconfirmed_error_text2', 'repair_everconfirmed');
 
-Status('bug_check_votes_everconfirmed');
-
-BugCheck("bugs INNER JOIN products ON bugs.product_id = products.id " .
-         "WHERE everconfirmed = 0 AND votestoconfirm > 0
-                AND votestoconfirm <= votes",
-         'bug_check_votes_everconfirmed_error_text');
-
 ###########################################################################
 # Control Values
 ###########################################################################
index 3e330a169a4c0ebca37409ea676bc3fbc327e093..ec981d51c9b8081fd6d68dd7f93bef7cb3bb1ad3 100644 (file)
@@ -54,9 +54,8 @@ table#flags {
     height: 1em;
 }
 
-#duplicate_settings, #votes_container {
+#duplicate_settings {
     white-space: nowrap;
-    
 }
 
 #bz_big_form_parts td {
index a4d22db73297766edc9b6748b058f434093ee3df..4b76f734dc1d0c19a19dec793c702e29c864981e 100644 (file)
@@ -35,9 +35,6 @@
 
 [% PROCESS global/variables.none.tmpl %]
 
-[% useqacontact = Param('useqacontact') %]
-[% usevotes = Param('usevotes') %]
-
 <p>
   If you don't like getting a notification for "trivial"
   changes to [% terms.bugs %], you can use the settings below to
@@ -150,21 +147,28 @@ document.write('<input type="button" value="Disable All Mail" onclick="SetCheckb
 [% relationships = [
     { id = constants.REL_ASSIGNEE,
       description = "Assignee" },
-    { id = constants.REL_QA,
-      description = "QA Contact" },
     { id = constants.REL_REPORTER,
       description = "Reporter" },
     { id = constants.REL_CC,
       description = "CCed" },
-    { id = constants.REL_VOTER,
-      description = "Voter" },
 ] %]
 
+[% IF Param('useqacontact') %]
+  [% relationships.push({ id = constants.REL_QA, 
+                          description = "QA Contact" }) %]
+[% END %]
+
+
+[%# This is up here so that the "relationships" hook can modify it. %]
+[% no_added_removed = [constants.REL_REPORTER] %]
+
+[% Hook.process('relationships') %]
+
+[% num_columns = relationships.size %]
+
 <table class="bz_emailprefs" border="1">
   <tr>
-    <td colspan="[% (useqacontact AND usevotes) ? '5' : 
-                     ((useqacontact OR usevotes) ? '4' : '3') %]" 
-        align="center" width="50%">
+    <td colspan="[% num_columns FILTER html %]" align="center" width="50%">
       <b>When my relationship to this [% terms.bug %] is:</b>
     </td>
     <td rowspan="2" width="40%">
@@ -174,8 +178,6 @@ document.write('<input type="button" value="Disable All Mail" onclick="SetCheckb
 
   <tr>
     [% FOREACH relationship = relationships %]
-      [% NEXT IF (relationship.id == constants.REL_QA AND NOT useqacontact) OR
-                 (relationship.id == constants.REL_VOTER AND NOT usevotes) %]
       <th align="center" width="9%">
         [% relationship.description FILTER html %]
       </th>
@@ -186,16 +188,14 @@ document.write('<input type="button" value="Disable All Mail" onclick="SetCheckb
     [% count = loop.count() %]
     <tr class="bz_row_[% count % 2 == 1 ? "odd" : "even" %]">
       [% FOREACH relationship = relationships %]
-      [% NEXT IF (relationship.id == constants.REL_QA AND NOT useqacontact) OR
-                 (relationship.id == constants.REL_VOTER AND NOT usevotes) %]
         <td align="center">
           <input type="checkbox" 
             name="email-[% relationship.id %]-[% event.id %]"
             value="1"
             [%# The combinations don't always make sense; disable a couple %]
             [% IF event.id == constants.EVT_ADDED_REMOVED AND 
-                 (relationship.id == constants.REL_REPORTER OR
-                  relationship.id == constants.REL_VOTER) %]
+                  no_added_removed.contains(relationship.id) 
+            %]
                disabled
             [% ELSIF mail.${relationship.id}.${event.id} %]
                checked
@@ -209,8 +209,7 @@ document.write('<input type="button" value="Disable All Mail" onclick="SetCheckb
   [% END %]
   
   <tr>
-    <td colspan="[% (useqacontact AND usevotes) ? '5' : 
-                     ((useqacontact OR usevotes) ? '4' : '3') %]" 
+    <td colspan="[% num_columns FILTER html %]" 
         align="center" width="50%">
       &nbsp;
     </td>
@@ -223,8 +222,6 @@ document.write('<input type="button" value="Disable All Mail" onclick="SetCheckb
     [% count = loop.count() %]
     <tr class="bz_row_[% count % 2 == 1 ? "odd" : "even" %]">
       [% FOREACH relationship = relationships %]
-        [% NEXT IF (relationship.id == constants.REL_QA AND NOT useqacontact) OR
-                   (relationship.id == constants.REL_VOTER AND NOT usevotes) %]
         <td align="center">
           <input type="checkbox" 
             name="neg-email-[% relationship.id %]-[% event.id %]"
@@ -243,23 +240,17 @@ document.write('<input type="button" value="Disable All Mail" onclick="SetCheckb
 [%# Add hidden form fields for fields not used %]
 [% FOREACH event = events %]  
   [% FOREACH relationship = relationships %]
-    [% IF (relationship.id == constants.REL_QA AND NOT useqacontact) OR
-          (relationship.id == constants.REL_VOTER AND NOT usevotes) %]
-      <input type="hidden" 
-        name="email-[% relationship.id %]-[% event.id %]"
-        value="[% mail.${relationship.id}.${event.id} ? "1" : "0" %]">
-    [% END %]
+    <input type="hidden" 
+           name="email-[% relationship.id %]-[% event.id %]"
+           value="[% mail.${relationship.id}.${event.id} ? "1" : "0" %]">
   [% END %]
 [% END %]
 
 [% FOREACH event = neg_events %]  
   [% FOREACH relationship = relationships %]
-    [% IF (relationship.id == constants.REL_QA AND NOT useqacontact) OR
-          (relationship.id == constants.REL_VOTER AND NOT usevotes) %]
-      <input type="hidden" 
-        name="neg-email-[% relationship.id %]-[% event.id %]"
-        value="[% mail.${relationship.id}.${event.id} ? "0" : "1" %]">
-    [% END %]
+    <input type="hidden" 
+           name="neg-email-[% relationship.id %]-[% event.id %]"
+          value="[% mail.${relationship.id}.${event.id} ? "0" : "1" %]">
   [% END %]
 [% END %]
 
index 794f925b7853c93958ed13fc470e2d1cc7887fb8..58b08f6152f837aa5ff5a20c9dfc9c9a23889c6b 100644 (file)
 
   usestatuswhiteboard => "Do you wish to use the Status Whiteboard field?",
 
-  usevotes => "Do you wish to allow users to vote for ${terms.bugs}? Note that in order " _
-              "for this to be effective, you will have to change the maximum " _
-              "votes allowed in a product to be non-zero in " _
-              "<a href=\"editproducts.cgi\">the product edit page</a>.",
-
   usebugaliases => "Do you wish to use $terms.bug aliases, which allow you to assign " _
                    "$terms.bugs an easy-to-remember name by which you can refer to them?",
 
index f4a2161aad91e0d2133ea2cc847001fc0aab8ea4..045d3a34dc841e9bcf485cd2bf29f3aebb1d7215 100644 (file)
@@ -29,9 +29,6 @@
 %]
 
 [% DEFAULT
-  product.votesperuser = "0",
-  product.maxvotesperbug  = "10000",
-  product.votes_to_confirm = "0",
   product.is_active = 1,
   version = "unspecified",
   product.defaultmilestone = constants.DEFAULT_MILESTONE
index 2c94402d665bcf4fe17f1b1ae458829825269f3f..4812707cdbd9c05cf23a97e0c153403ca1f6d262 100644 (file)
       in this product:</label>
   </th>
   <td><input type="checkbox" id="allows_unconfirmed" name="allows_unconfirmed"
-             [% ' checked="checked"' IF product.allows_unconfirmed %]
-             [% IF Param('usevotes') %]
-               onchange="bz_toggleClass('votes_to_confirm_container', 
-                                        'bz_default_hidden')"
-             [% END %]>
-    [% IF Param('usevotes') %]
-      <span id="votes_to_confirm_container"
-            [% ' class="bz_default_hidden"' IF !product.allows_unconfirmed %]>
-        ...and automatically confirm [% terms.bugs %] if they get
-        <input size="3" maxlength="5" name="votestoconfirm" id="votestoconfirm"
-               value="[% product.votes_to_confirm FILTER html %]">
-         votes. (Setting this to 0 disables auto-confirming [% terms.bugs %]
-         by vote.)
-      </span>
-    [% END %]
+             [% ' checked="checked"' IF product.allows_unconfirmed %]>
   </td>
 </tr>
 
-[% IF Param('usevotes') %]
-  <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>
-[% END %]
+[% Hook.process('rows') %]
index 6fd5240afd8fcb219f0b0ee6d99cb7fc0821ab5e..fb026aaa43053620fbf0799fa8c0441cec6baec3 100644 (file)
        heading => "Open For New $terms.Bugs"
        yesno_field => 1
      },
-     { 
-       name => "votesperuser"
-       heading => "Votes Per User"
-       align => 'right'
-     },
-     { 
-       name => "maxvotesperbug"
-       heading => "Maximum Votes Per $terms.Bug"
-       align => 'right'
-     },
-     { 
-       name => "votestoconfirm"
-       heading => "Votes To Confirm"
-       align => 'right'
-     } ]
-%]
+] %]
 
 [% IF showbugcounts %]
 
index 6e484ff3418639966fc5642a97b16634f210ab4a..4140bab62820ef856c0d96f71f3408e1ada82e8e 100644 (file)
   '[% product.default_milestone FILTER html %]'.
   </p>
 [% END %]
-  
-[% IF changes.votesperuser.defined %]
-  <p>
-  Updated votes per user from
-  [%+ changes.votesperuser.0 FILTER html %] to
-  [%+ product.votes_per_user 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.max_votes_per_bug 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.votes_to_confirm FILTER html %].
-  </p>
-  [% checkvotes = 1 %]
-[% END %]
 
 [% IF changes.allows_unconfirmed.defined %]
   <p>
   </p>
 [% END %]
 
+[% Hook.process('changes') %]
+
 [% IF !changes.keys.size %]
   <p>Nothing changed for product '[% product.name FILTER html %]'.</p>
 [% 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 %]
-      &rarr;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 %]
-    &rarr;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 %]
-      &rarr;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 %]
-    &rarr;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 %]
-    &rarr;there were none.
-  [% END %]
-  </p>
-
-[% END %]
-
 [% PROCESS admin/products/footer.html.tmpl %]
 
 [% PROCESS global/footer.html.tmpl %]
index c3d5daacde234fad998e08a78f83a3cb41b6f9bc..39e2258d0d008c06c0c2f8d553bf2d38b082e316 100644 (file)
   [% ELSIF san_tag == "bug_check_status_everconfirmed_error_text2" %]
     [% terms.Bugs %] with confirmed status but don't have everconfirmed set
 
-  [% ELSIF san_tag == "bug_check_votes_everconfirmed" %]
-    Checking votes/everconfirmed
-
-  [% ELSIF san_tag == "bug_check_votes_everconfirmed_error_text" %]
-    [% terms.Bugs %] that have enough votes to be confirmed but haven't been
-
   [% ELSIF san_tag == "bug_check_control_values" %]
     Checking for bad values in group_control_map
 
   [% ELSIF san_tag == "unsent_bugmail_fix" %]
     <a href="sanitycheck.cgi?rescanallBugMail=1">Send these mails</a>.
 
-  [% ELSIF san_tag == "vote_cache_rebuild_start" %]
-    OK, now rebuilding vote cache.
-
-  [% ELSIF san_tag == "vote_cache_rebuild_end" %]
-    Vote cache has been rebuilt.
-
-  [% ELSIF san_tag == "vote_cache_rebuild_fix" %]
-    <a href="sanitycheck.cgi?rebuildvotecache=1">Click here to
-    rebuild the vote cache</a>
-
-  [% ELSIF san_tag == "vote_cache_alert" %]
-    Bad vote cache for [% PROCESS bug_link bug_id = id %]
-
-  [% ELSIF san_tag == "vote_count_start" %]
-    Checking cached vote counts.
-
-  [% ELSIF san_tag == "vote_count_alert" %]
-    Bad vote sum for [% terms.bug %] [%+ id FILTER html %].
-
   [% ELSIF san_tag == "whines_obsolete_target_deletion_start" %]
     OK, now removing non-existent users/groups from whines.
 
index b61a995411d281b72989e295b80c53c2a90237f6..4711376b0dd63d6ab2056e7135a0b43a5af763e1 100644 (file)
@@ -33,7 +33,6 @@
   # namedquery_group_map:     number of named queries the user has shared
   # profiles_activity:        number of changes made to other users' profiles
   # series:                   number of series the viewed user has created
-  # votes:                    number of bugs the viewed user has voted on
   # watch.watched:            number of users the viewed user is being watched
   #                           by
   # watch.watcher:            number of users the viewed user is watching
   [% END %]
 
   [% IF assignee_or_qa || cc || component_cc || email_setting || flags.requestee ||
-        namedqueries || profile_setting || quips || series || votes || watch.watched ||
-        watch.watcher || whine_events || whine_schedules %]
+        namedqueries || profile_setting || quips || series || watch.watched ||
+        watch.watcher || whine_events || whine_schedules || other_safe %]
     <div class="warningmessages">
       <p>The following deletions are <b>safe</b> and will not generate
       referential integrity inconsistencies.</p>
             will have no author anymore, but will remain available.
           </li>
         [% END %]
-        [% 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 %]
         [% IF watch.watched || watch.watcher %]
           <li>
             [% otheruser.login FILTER html %]
             but the whines themselves will be left unaltered.
           </li>
         [% END %]
+        [% Hook.process('warn_safe') %]
       </ul>
     </div>
 
index 95376bb7d31a841a426b495d6337869620e299f7..b84aa823830da0454b9b9c7ea768cb18eee7e044 100644 (file)
 [% BLOCK section_details2 %]
 
  [%###############################################################%]
- [%# Importance (priority, severity and votes) #%]
+ [%# Importance (priority and severity) #%]
  [%###############################################################%]
     <tr>
       <td class="field_label">
             bug = bug, field = bug_fields.bug_severity,
             no_tds = 1, value = bug.bug_severity
             editable = bug.check_can_change_field('bug_severity', 0, 1) %]
-        [% IF bug.use_votes %]
-          <span id="votes_container">
-          [% IF bug.votes %] 
-            with 
-            <a href="votes.cgi?action=show_bug&amp;bug_id=[% bug.bug_id %]">
-              [% bug.votes %] 
-              [% IF bug.votes == 1 %]
-                vote
-              [% ELSE %]
-                votes
-              [% END %]</a> 
-          [% END %]    
-          (<a href="votes.cgi?action=show_user&amp;bug_id=
-                  [% bug.bug_id %]#vote_[% bug.bug_id %]">vote</a>)
-          </span>  
-        [% END %]
+        [% Hook.process('after_importance', 'bug/edit.html.tmpl') %]
       </td>
     </tr>
 
index 27b72a918df4a297282481684bfa7e12fb656302..2d4a20303d2f18179102ce04514530311b534675 100644 (file)
@@ -39,8 +39,6 @@ X[% comment_body %]
 *** This [% terms.bug %] has been marked as a duplicate of [% terms.bug %] [%+ comment.extra_data %] ***
 [% ELSIF comment.type == constants.CMT_HAS_DUPE %]
 *** [% terms.Bug %] [%+ comment.extra_data %] has been marked as a duplicate of this [% terms.bug %]. ***
-[% ELSIF comment.type == constants.CMT_POPULAR_VOTES %]
-*** This [% terms.bug %] has been confirmed by popular vote. ***
 [% ELSIF comment.type == constants.CMT_MOVED_TO %]
 X[% comment_body %]
 
@@ -65,6 +63,8 @@ Comment on attachment [% comment.extra_data %]
 [%+ comment.attachment.description %]
 
 [%+ comment.body %]
+[% ELSIF comment.type %]
+  [% Hook.process('type') %]
 [% ELSE %]
 X[% comment_body %]
 [% END %]
index 79f0126d4f41385111a57b84bdab16fa930803f9..6b608b9ed2e49e2795dfb96e55d9efdce07dc20a 100644 (file)
@@ -39,8 +39,8 @@
   [% END %]
 [% ELSIF title_tag == "mid_air" %]
   [% title = "Mid-air collision!" %]
-[% ELSIF title_tag == "change_votes" %]
-  [% title = "Change Votes" %]
 [% END %]
 
+[% Hook.process('title') %]
+
 [% PROCESS global/header.html.tmpl %]
index d2adca8b8d668d137bd8230b583cc87e4f4410f2..7c1af42aff7d553fd5b824a9555e9cd85ed0cb4d 100644 (file)
     'bug' => "Changes submitted for $link" ,
     'dupe' => "Duplicate notation added to $link" ,
     'dep' => "Checking for dependency changes on $link" ,
-    'votes' => "$Link confirmed by number of votes" ,
     'created' => "$Link has been added to the database" ,
     'move' => "$Link has been moved to another database" ,
   }
 %]
 
+[% Hook.process('title') %]
+
 <dl>
   <dt>[% title.$type %]</dt>
   <dd>
index 1bcc2e40d1082a7e859d51e161588dae724965c4..7d30b890d2f24f980ac3353f6703d0903a58ee9b 100644 (file)
@@ -19,6 +19,8 @@
   #%]
 
 [% PROCESS "global/variables.none.tmpl" %]
+[% PROCESS "global/reason-descs.none.tmpl" %]
+
 From: [% Param('mailfrom') %]
 To: [% to_user.email %]
 Subject: [[% terms.Bug %] [%+ bugid %]] [% 'New: ' IF isnew %][%+ summary %]
@@ -56,33 +58,12 @@ X-Bugzilla-Changed-Fields: [% changedfields %]
 -- [%# Protect the trailing space of the signature marker %]
 Configure [% terms.bug %]mail: [% urlbase %]userprefs.cgi?tab=email
 ------- You are receiving this mail because: -------
-[% FOREACH relationship = reasons %]
-  [% SWITCH relationship %]
-    [% CASE constants.REL_ASSIGNEE %]
-You are the assignee for the [% terms.bug %].
-    [% CASE constants.REL_REPORTER %]
-You reported the [% terms.bug %].
-    [% CASE constants.REL_QA %]
-You are the QA contact for the [% terms.bug %].
-    [% CASE constants.REL_CC %]
-You are on the CC list for the [% terms.bug %].
-    [% CASE constants.REL_VOTER %]
-You are a voter for the [% terms.bug %].
-    [% CASE constants.REL_GLOBAL_WATCHER %]
-You are watching all [% terms.bug %] changes.
-  [% END %]
+[% SET reason_lines = [] %]
+[% FOREACH reason = reasons %]
+  [% reason_lines.push(reason_descs.$reason) IF reason_descs.$reason %]
 [% END %]
-[% FOREACH relationship = reasons_watch %]
-  [% SWITCH relationship %]
-    [% CASE constants.REL_ASSIGNEE %]
-You are watching the assignee of the [% terms.bug %].
-    [% CASE constants.REL_REPORTER %]
-You are watching the reporter.
-    [% CASE constants.REL_QA %]
-You are watching the QA contact of the [% terms.bug %].
-    [% CASE constants.REL_CC %]
-You are watching someone on the CC list of the [% terms.bug %].
-    [% CASE constants.REL_VOTER %]
-You are watching a voter for the [% terms.bug %].
-  [% END %]
+[% FOREACH reason = reasons_watch %]
+  [% reason_lines.push(watch_reason_descs.$reason)
+       IF watch_reason_descs.$reason %]
 [% END %]
+[%+ reason_lines.join("\n") %]
index a488f50cadec6071ab18a0cd4ecaf1ac5a634801..94604dc176368a5183cc12bad64253857daccb86 100644 (file)
 
 'global/site-navigation.html.tmpl' => [
   'bug.bug_id', 
-  'bug.votes', 
 ],
 
 'bug/comments.html.tmpl' => [
   'bug.remaining_time', 
   'bug.delta_ts', 
   'bug.bug_id', 
-  'bug.votes', 
   'group.bit', 
   'dep.title', 
   'dep.fieldname', 
        FILTER format("%d")', 
 ],
 
-'bug/votes/list-for-bug.html.tmpl' => [
-  'voter.vote_count', 
-  'total', 
-],
-
-'bug/votes/list-for-user.html.tmpl' => [
-  'product.maxperbug', 
-  'bug.id', 
-  'bug.count', 
-  'product.total', 
-  'product.maxvotes', 
-],
-
 'bug/process/results.html.tmpl' => [
   'title.$type', 
   '"$terms.Bug $id" FILTER bug_link(id)',
   'flags.setter',
   'longdescs',
   'quips',
-  'votes',
   'series',
   'watch.watched',
   'watch.watcher',
index 5012769ca0b74145ac6c35ae41370484beda95f0..2c93c3d8ac6772a2d9dff5f6ea95ee4d6bc27bb4 100644 (file)
@@ -84,7 +84,6 @@
                    "status_whiteboard"       => "Whiteboard",
                    "target_milestone"        => "Target Milestone",
                    "version"                 => "Version",
-                   "votes"                   => "Votes",
                    "work_time"               => "Hours Worked"} %]
 
 [%# Also include any custom fields or fields which don't have a
diff --git a/template/en/default/global/reason-descs.none.tmpl b/template/en/default/global/reason-descs.none.tmpl
new file mode 100644 (file)
index 0000000..4a39497
--- /dev/null
@@ -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>
+  #%]
+
+[% SET reason_descs = {
+  ${constants.REL_ASSIGNEE} => "You are the assignee for the ${terms.bug}.",
+  ${constants.REL_REPORTER} => "You reported the ${terms.bug}.",
+  ${constants.REL_QA}       => "You are the QA Contact for the ${terms.bug}.",
+  ${constants.REL_CC}       => "You are on the CC list for the ${terms.bug}.",
+  ${constants.REL_GLOBAL_WATCHER} => "You are watching all $terms.bug changes.",
+} %]
+
+[% SET watch_reason_descs => {
+  ${constants.REL_ASSIGNEE} =>
+    "You are the watching assignee of the ${terms.bug}.",
+  ${constants.REL_REPORTER} =>
+    "You watching the reporter of the ${terms.bug}.",
+  ${constants.REL_QA}       =>
+    "You are watching the QA Contact of the ${terms.bug}.",
+  ${constants.REL_CC}       =>
+    "You are watching someone on the CC list of the ${terms.bug}.",
+} %]
+
+[% Hook.process('end') %]
index bbf4f68624266071b1605e6c63be98c07125082c..60a8ddf96b499d11e2f39594b6ea3eb8b839e7b4 100644 (file)
@@ -37,7 +37,7 @@
   [% END %]
 
 
-  [%# *** Dependencies, Votes, Activity, Print-version *** %]
+  [%# *** Dependencies, Activity, Print-version *** %]
   [% IF bug %]
     <link rel="Show" title="Dependency Tree"
           href="showdependencytree.cgi?id=[% bug.bug_id %]&amp;hide_resolved=1">
             href="showdependencygraph.cgi?id=[% bug.bug_id %]">
     [% END %]
 
-    [% IF bug.use_votes %]
-      <link rel="Show" title="Votes ([% bug.votes %])"
-            href="votes.cgi?action=show_bug&amp;bug_id=[% bug.bug_id %]">
-    [% END %]
-
       <link rel="Show" title="[% terms.Bug %] Activity"
             href="show_activity.cgi?id=[% bug.bug_id %]">
       <link rel="Show" title="Printer-Friendly Version"
index bdedc41729c651c687b3a16fc29676a89afb5fe4..d9872f1b0bbd60d2234e05785c93c203b064995a 100644 (file)
     The group [% name FILTER html %] does not exist. Please specify
     a valid group name. Create it first if necessary!
 
-  [% ELSIF error == "illegal_at_least_x_votes" %]
-    [% title = "Your Search Makes No Sense" %]
-    The <em>At least ___ votes</em> field must be a simple number. 
-    You entered <tt>[% value FILTER html %]</tt>, which isn't.
-    
   [% ELSIF error == "illegal_attachment_edit" %]
     [% title = "Unauthorized Action" %]
     You are not authorized to edit attachment [% attach_id FILTER html %].
     [% group.name FILTER html %] is not an active [% terms.bug %] group
     and so you cannot edit group controls for it.
 
-  [% ELSIF error == "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 == "product_name_already_in_use" %]
     [% title = "Product name already exists" %]
     [% admindocslinks = {'products.html' => 'Administering products'} %]
     [% title = "User Protected" %]
     The user [% login FILTER html %] may not be impersonated by sudoers.
 
-  [% ELSIF error == "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 == "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.
-
   [% ELSIF error == "token_does_not_exist" %]
     [% title = "Token Does Not Exist" %]
     The token you submitted does not exist, has expired, or has
     Sorry, but you are not allowed to (un)mark comments or attachments
     as private.
 
-  [% ELSIF error == "votes_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 == "wrong_token_for_cancelling_email_change" %]
     [% title = "Wrong Token" %]
     That token cannot be used to cancel an email address change.
index 99c06c1ee44283fd65bd3e2256cadb020e0dd640..d7879a694d8ae25ac6586366561880fc06becc67 100644 (file)
@@ -38,7 +38,7 @@
           <bz:id nc:parseType="Integer">[% bug.bug_id %]</bz:id>
         
         [% FOREACH column = displaycolumns %]
-          <bz:[% column %][% ' nc:parseType="Integer"' IF column == "votes" %]>[% bug.$column FILTER html %]</bz:[% column %]>
+          <bz:[% column %]>[% bug.$column FILTER html %]</bz:[% column %]>
         [% END %]
         
         </bz:bug>
index 2e2ae73d6651f85a9692628a20fe5cc795c73866..63ca035654e255ae53037855636bb1dede0a80d4 100644 (file)
@@ -422,20 +422,14 @@ function doOnSelectProduct(selectmode) {
   </tr>
 </table>
 
-[%# *** Email Numbering Votes *** %]
+[%# *** Email Numbering *** %]
 
 <table>
   <tr>
     <td>
       <fieldset>
         <legend>
-          <strong>
-            [% IF Param('usevotes') %]
-              Email Addresses, [% terms.Bug %] Numbers, and Votes
-            [% ELSE %]
-              Email Addresses and [% terms.Bug %] Numbers
-            [% END %]
-          </strong>
+          <strong>Email Addresses and [% terms.Bug %] Numbers</strong>
         </legend>
 
 
@@ -550,18 +544,7 @@ function doOnSelectProduct(selectmode) {
     <td></td>
     <td>(comma-separated list)</td>
   </tr>
-  [% IF Param('usevotes') %]
-    <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
-      </td>
-    </tr>
-  [% END %]
+  [% Hook.process('email_numbering_end') %]
 </table>
 
 
index 12e82ba5e4bca5a77e6d8b5ce42f51af15e9b28f..4dbf6652a306b8f0c99fa936f02bb1a102c16cde 100644 (file)
@@ -82,9 +82,6 @@
            roles.<br>Here, you can search on what people are in what role." },
 { id => "bug_id", 
   html => "You can limit your search to a specific set of $terms.bugs ." },   
-{ id => "votes", 
-  html => "Some $terms.bugs can be voted for, and you can limit your search to 
-           $terms.bugs<br>with more than a certain number of votes." },   
 { id => "chfield", 
   html => "You can search for specific types of change - this field define <br>
            which field you are interested in changes for." },   
index de647871634d4d641e641f6208169628afa3c681..2ad7792487f36a176a15d9ab1c314f63af98da62 100644 (file)
@@ -29,7 +29,8 @@
   [% rep_fields = ["classification", "product", "component", "version", "rep_platform",  
                    "op_sys", "bug_status", "resolution", "bug_severity", 
                    "priority", "target_milestone", "assigned_to",
-                   "reporter", "qa_contact", "votes" ] %]
+                   "reporter", "qa_contact" ] %]
+  [% Hook.process('rep_fields', 'search/search-report-select.html.tmpl') %]
 
   <select name="[% name FILTER html %]">
     <option value="">&lt;none&gt;</option>
@@ -38,7 +39,6 @@
       [% NEXT IF field == "classification" AND !Param('useclassification') %]
       [% NEXT IF field == "target_milestone" AND !Param('usetargetmilestone') %]
       [% NEXT IF field == "qa_contact" AND !Param('useqacontact') %]
-      [% NEXT IF field == "votes" AND !Param('usevotes') %]      
       <option value="[% field FILTER html %]" 
         [% " selected" IF default.$name.0 == field %]>
         [% field_descs.$field || field FILTER html %]</option>
index 3df943e5cef783f114ffa8561dafcc2acb759d40..b2fa092ea5ec685cd80db73af7ff45c6e1f51cd5 100644 (file)
@@ -105,9 +105,6 @@ function normal_keypress_handler( aEvent ) {
       [% filtered_username = user.login FILTER url_quote %]
       <text class="text-link" onclick="load_relative_url('[% Param('mybugstemplate').replace('%userid%', filtered_username) FILTER js FILTER html %]')" value="my [% terms.bugs %]"/>
   [%- END %]
-  [%- IF Param('usevotes') %]
-      <text class="text-link" onclick="load_relative_url('votes.cgi?action=show_user')" value="my votes"/>
-  [%- END %]
 
   [%- FOREACH q = user.queries %]
       <text class="text-link" onclick="load_relative_url('buglist.cgi?cmdtype=runnamed&amp;namedcmd=[% q.name FILTER url_quote %]')" value="[% q.name FILTER html %]"/>
index 1c72431c45e7a8b3b8a0a853292a88ab60c1c2a6..b7622fbb04cfd47a094d72ba4e9c8c87e2c08969 100755 (executable)
--- a/votes.cgi
+++ b/votes.cgi
 #
 # 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.
+# 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): 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>
+# Contributor(s):
+#   Max Kanat-Alexander <mkanat@bugzilla.org>
+
+# This script remains as a backwards-compatibility URL for before
+# the time that Voting was an extension.
 
 use strict;
 use lib qw(. lib);
-
 use Bugzilla;
-use Bugzilla::Constants;
-use Bugzilla::Util;
-use Bugzilla::Error;
-use Bugzilla::Bug;
-use Bugzilla::User;
-use Bugzilla::Product;
-
-use List::Util qw(min);
 
 my $cgi = Bugzilla->cgi;
-local our $vars = {};
-
-# If the action is show_bug, you need a bug_id.
-# If the action is show_user, you can supply a userid to show the votes for
-# another user, otherwise you see your own.
-# If the action is vote, your votes are set to those encoded in the URL as 
-# <bug_id>=<votes>.
-#
-# If no action is defined, we default to show_bug if a bug_id is given,
-# otherwise to show_user.
-my $bug_id = $cgi->param('bug_id');
-my $action = $cgi->param('action') || ($bug_id ? "show_bug" : "show_user");
 
-if ($action eq "show_bug" ||
-    ($action eq "show_user" && defined $cgi->param('user_id')))
-{
-    Bugzilla->login();
-}
-else {
-    Bugzilla->login(LOGIN_REQUIRED);
-}
-
-################################################################################
-# Begin Data/Security Validation
-################################################################################
-
-# Make sure the bug ID is a positive integer representing an existing
-# bug that the user is authorized to access.
-
-if (defined $bug_id) {
-    my $bug = Bugzilla::Bug->check($bug_id);
-    $bug_id = $bug->id;
-}
-
-################################################################################
-# End Data/Security Validation
-################################################################################
+my $to_url;
+my $action = $cgi->param('action');
 
 if ($action eq "show_bug") {
-    show_bug($bug_id);
+    $cgi->delete('action');
+    $cgi->param('id', 'voting/bug.html');
 } 
-elsif ($action eq "show_user") {
-    show_user($bug_id);
-}
-elsif ($action eq "vote") {
-    record_votes() if Bugzilla->params->{'usevotes'};
-    show_user($bug_id);
+elsif ($action eq "show_user" or $action eq 'vote') {
+    $cgi->delete('action') unless $action eq 'vote';
+    $cgi->param('id', 'voting/user.html');
 }
 else {
     ThrowCodeError("unknown_action", {action => $action});
 }
 
+print $cgi->redirect('page.cgi?' . $cgi->query_string);
 exit;
-
-# Display the names of all the people voting for this one bug.
-sub show_bug {
-    my ($bug_id) = @_;
-    my $cgi = Bugzilla->cgi;
-    my $dbh = Bugzilla->dbh;
-    my $template = Bugzilla->template;
-
-    ThrowCodeError("missing_bug_id") unless defined $bug_id;
-
-    $vars->{'bug_id'} = $bug_id;
-    $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);
-
-    print $cgi->header();
-    $template->process("bug/votes/list-for-bug.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-}
-
-# Display all the votes for a particular user. If it's the user
-# doing the viewing, give them the option to edit them too.
-sub show_user {
-    my ($bug_id) = @_;
-    my $cgi = Bugzilla->cgi;
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-    my $template = Bugzilla->template;
-
-    # If a bug_id is given, and we're editing, we'll add it to the votes list.
-    $bug_id ||= "";
-
-    my $who_id = $cgi->param('user_id') || $user->id;
-    my $who = Bugzilla::User->check({ id => $who_id });
-
-    my $canedit = (Bugzilla->params->{'usevotes'} && $user->id == $who->id) 
-                  ? 1 : 0;
-
-    $dbh->bz_start_transaction();
-
-    if ($canedit && $bug_id) {
-        # 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 @all_bug_ids;
-    my @products;
-    my $products = $user->get_selectable_products;
-    # Read the votes data for this user for each product.
-    foreach my $product (@$products) {
-        next unless ($product->votes_per_user > 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->votes_per_user,
-                                 $product->max_votes_per_bug) == 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->votes_per_user,
-                               maxperbug => $product->max_votes_per_bug });
-        }
-    }
-
-    $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->{'bug_id'} = $bug_id;
-    $vars->{'all_bug_ids'} = \@all_bug_ids;
-
-    print $cgi->header();
-    $template->process("bug/votes/list-for-user.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-}
-
-# Update the user's votes in the database.
-sub record_votes {
-    ############################################################################
-    # Begin Data/Security Validation
-    ############################################################################
-
-    my $cgi = Bugzilla->cgi;
-    my $dbh = Bugzilla->dbh;
-    my $template = Bugzilla->template;
-
-    # 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 {/^[1-9][0-9]*$/} $cgi->param();
-
-    # 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("bug/votes/delete-all.html.tmpl", $vars)
-              || ThrowTemplateError($template->error());
-            exit();
-        }
-        elsif ($cgi->param('delete_all_votes') == 0) {
-            print $cgi->redirect("votes.cgi");
-            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;
-    foreach my $id (@buglist) {
-      my $bug = Bugzilla::Bug->check($id);
-      $id = $bug->id;
-      $votes{$id} = $cgi->param($id);
-      detaint_natural($votes{$id}) 
-        || ThrowUserError("votes_must_be_nonnegative");
-    }
-
-    ############################################################################
-    # End Data/Security Validation
-    ############################################################################
-    my $who = Bugzilla->user->id;
-
-    # If the user is voting for bugs, make sure they aren't overstuffing
-    # the ballot box.
-    if (scalar(@buglist)) {
-        my %prodcount;
-        my %products;
-        # XXX - We really need a $bug->product() method.
-        foreach my $bug_id (@buglist) {
-            my $bug = new Bugzilla::Bug($bug_id);
-            my $prod = $bug->product;
-            $products{$prod} ||= new Bugzilla::Product({name => $prod});
-            $prodcount{$prod} ||= 0;
-            $prodcount{$prod} += $votes{$bug_id};
-
-            # Make sure we haven't broken the votes-per-bug limit
-            ($votes{$bug_id} <= $products{$prod}->max_votes_per_bug)
-              || ThrowUserError("too_many_votes_for_bug",
-                                {max => $products{$prod}->max_votes_per_bug,
-                                 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}->votes_per_user)
-              || ThrowUserError("too_many_votes_for_product",
-                                {max => $products{$prod}->votes_per_user,
-                                 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 = CheckIfVotedConfirmed($id);
-        push (@updated_bugs, $id) if $confirmed;
-    }
-    $dbh->bz_commit_transaction();
-
-    $vars->{'type'} = "votes";
-    $vars->{'mailrecipients'} = { 'changer' => Bugzilla->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;
-}