2008-11-04 Anders Carlsson <andersca@apple.com>
[webkit/qt.git] / BugsSite / editproducts.cgi
blobe8bca53504fdd16f671a7feaf5f979c66ddb93a9
1 #!/usr/bin/perl -wT
2 # -*- Mode: perl; indent-tabs-mode: nil -*-
4 # The contents of this file are subject to the Mozilla Public
5 # License Version 1.1 (the "License"); you may not use this file
6 # except in compliance with the License. You may obtain a copy of
7 # the License at http://www.mozilla.org/MPL/
9 # Software distributed under the License is distributed on an "AS
10 # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
11 # implied. See the License for the specific language governing
12 # rights and limitations under the License.
14 # The Original Code is mozilla.org code.
16 # The Initial Developer of the Original Code is Holger
17 # Schurig. Portions created by Holger Schurig are
18 # Copyright (C) 1999 Holger Schurig. All
19 # Rights Reserved.
21 # Contributor(s): Holger Schurig <holgerschurig@nikocity.de>
22 # Terry Weissman <terry@mozilla.org>
23 # Dawn Endico <endico@mozilla.org>
24 # Joe Robins <jmrobins@tgix.com>
25 # Gavin Shelley <bugzilla@chimpychompy.org>
26 # Frédéric Buclin <LpSolit@gmail.com>
27 # Greg Hendricks <ghendricks@novell.com>
29 # Direct any questions on this source code to
31 # Holger Schurig <holgerschurig@nikocity.de>
33 use strict;
34 use lib ".";
35 use vars qw ($template $vars);
36 use Bugzilla::Constants;
37 require "CGI.pl";
38 require "globals.pl";
39 use Bugzilla::Bug;
40 use Bugzilla::Series;
41 use Bugzilla::User;
42 use Bugzilla::Config qw(:DEFAULT $datadir);
44 # Shut up misguided -w warnings about "used only once". "use vars" just
45 # doesn't work for me.
46 use vars qw(@legal_bug_status @legal_resolution);
48 my %ctl = (
49 &::CONTROLMAPNA => 'NA',
50 &::CONTROLMAPSHOWN => 'Shown',
51 &::CONTROLMAPDEFAULT => 'Default',
52 &::CONTROLMAPMANDATORY => 'Mandatory'
55 # TestProduct: just returns if the specified product does exists
56 # CheckProduct: same check, optionally emit an error text
58 sub TestProduct ($)
60 my $prod = shift;
62 # does the product exist?
63 SendSQL("SELECT name
64 FROM products
65 WHERE name=" . SqlQuote($prod));
66 return FetchOneColumn();
69 sub CheckProduct ($)
71 my $prod = shift;
73 # do we have a product?
74 unless ($prod) {
75 print "Sorry, you haven't specified a product.";
76 PutTrailer();
77 exit;
80 unless (TestProduct $prod) {
81 print "Sorry, product '$prod' does not exist.";
82 PutTrailer();
83 exit;
87 # TestClassification: just returns if the specified classification does exists
88 # CheckClassification: same check, optionally emit an error text
90 sub TestClassification ($)
92 my $cl = shift;
94 # does the classification exist?
95 SendSQL("SELECT name
96 FROM classifications
97 WHERE name=" . SqlQuote($cl));
98 return FetchOneColumn();
101 sub CheckClassification ($)
103 my $cl = shift;
105 # do we have a classification?
106 unless ($cl) {
107 print "Sorry, you haven't specified a classification.";
108 PutTrailer();
109 exit;
112 unless (TestClassification $cl) {
113 print "Sorry, classification '$cl' does not exist.";
114 PutTrailer();
115 exit;
119 # For the transition period, as this file is templatised bit by bit,
120 # we need this routine, which does things properly, and will
121 # eventually be the only version. (The older versions assume a
122 # PutHeader() call has been made)
123 sub CheckClassificationNew ($)
125 my $cl = shift;
127 # do we have a classification?
128 unless ($cl) {
129 ThrowUserError('classification_not_specified');
132 unless (TestClassification $cl) {
133 ThrowUserError('classification_doesnt_exist',
134 {'name' => $cl});
139 sub CheckClassificationProduct ($$)
141 my $cl = shift;
142 my $prod = shift;
143 my $dbh = Bugzilla->dbh;
145 CheckClassification($cl);
146 CheckProduct($prod);
148 trick_taint($prod);
149 trick_taint($cl);
151 my $query = q{SELECT products.name
152 FROM products
153 INNER JOIN classifications
154 ON products.classification_id = classifications.id
155 WHERE products.name = ?
156 AND classifications.name = ?};
157 my $res = $dbh->selectrow_array($query, undef, ($prod, $cl));
159 unless ($res) {
160 print "Sorry, classification->product '$cl'->'$prod' does not exist.";
161 PutTrailer();
162 exit;
166 sub CheckClassificationProductNew ($$)
168 my ($cl, $prod) = @_;
169 my $dbh = Bugzilla->dbh;
171 CheckClassificationNew($cl);
173 trick_taint($prod);
174 trick_taint($cl);
176 my ($res) = $dbh->selectrow_array(q{
177 SELECT products.name
178 FROM products
179 INNER JOIN classifications
180 ON products.classification_id = classifications.id
181 WHERE products.name = ? AND classifications.name = ?},
182 undef, ($prod, $cl));
184 unless ($res) {
185 ThrowUserError('classification_doesnt_exist_for_product',
186 { product => $prod, classification => $cl });
191 # Displays the form to edit a products parameters
194 sub EmitFormElements ($$$$$$$$$)
196 my ($classification, $product, $description, $milestoneurl, $disallownew,
197 $votesperuser, $maxvotesperbug, $votestoconfirm, $defaultmilestone)
198 = @_;
200 $product = value_quote($product);
201 $description = value_quote($description);
203 if (Param('useclassification')) {
204 print " <TH ALIGN=\"right\">Classification:</TH>\n";
205 print " <TD><b>",html_quote($classification),"</b></TD>\n";
206 print "</TR><TR>\n";
209 print " <TH ALIGN=\"right\">Product:</TH>\n";
210 print " <TD><INPUT SIZE=64 MAXLENGTH=64 NAME=\"product\" VALUE=\"$product\"></TD>\n";
211 print "</TR><TR>\n";
213 print " <TH ALIGN=\"right\">Description:</TH>\n";
214 print " <TD><TEXTAREA ROWS=4 COLS=64 WRAP=VIRTUAL NAME=\"description\">$description</TEXTAREA></TD>\n";
216 $defaultmilestone = value_quote($defaultmilestone);
217 if (Param('usetargetmilestone')) {
218 $milestoneurl = value_quote($milestoneurl);
219 print "</TR><TR>\n";
220 print " <TH ALIGN=\"right\">URL describing milestones for this product:</TH>\n";
221 print " <TD><INPUT TYPE=TEXT SIZE=64 MAXLENGTH=255 NAME=\"milestoneurl\" VALUE=\"$milestoneurl\"></TD>\n";
223 print "</TR><TR>\n";
224 print " <TH ALIGN=\"right\">Default milestone:</TH>\n";
226 print " <TD><INPUT TYPE=TEXT SIZE=20 MAXLENGTH=20 NAME=\"defaultmilestone\" VALUE=\"$defaultmilestone\"></TD>\n";
227 } else {
228 print qq{<INPUT TYPE=HIDDEN NAME="defaultmilestone" VALUE="$defaultmilestone">\n};
232 print "</TR><TR>\n";
233 print " <TH ALIGN=\"right\">Closed for bug entry:</TH>\n";
234 my $closed = $disallownew ? "CHECKED" : "";
235 print " <TD><INPUT TYPE=CHECKBOX NAME=\"disallownew\" $closed VALUE=\"1\"></TD>\n";
237 print "</TR><TR>\n";
238 print " <TH ALIGN=\"right\">Maximum votes per person:</TH>\n";
239 print " <TD><INPUT SIZE=5 MAXLENGTH=5 NAME=\"votesperuser\" VALUE=\"$votesperuser\"></TD>\n";
241 print "</TR><TR>\n";
242 print " <TH ALIGN=\"right\">Maximum votes a person can put on a single bug:</TH>\n";
243 print " <TD><INPUT SIZE=5 MAXLENGTH=5 NAME=\"maxvotesperbug\" VALUE=\"$maxvotesperbug\"></TD>\n";
245 print "</TR><TR>\n";
246 print " <TH ALIGN=\"right\">Number of votes a bug in this product needs to automatically get out of the <A HREF=\"page.cgi?id=fields.html#status\">UNCONFIRMED</A> state:</TH>\n";
247 print " <TD><INPUT SIZE=5 MAXLENGTH=5 NAME=\"votestoconfirm\" VALUE=\"$votestoconfirm\"></TD>\n";
252 # Displays a text like "a.", "a or b.", "a, b or c.", "a, b, c or d."
255 sub PutTrailer (@)
257 my (@links) = ("Back to the <A HREF=\"query.cgi\">query page</A>", @_);
259 my $count = $#links;
260 my $num = 0;
261 print "<P>\n";
262 foreach (@links) {
263 print $_;
264 if ($num == $count) {
265 print ".\n";
267 elsif ($num == $count-1) {
268 print " or ";
270 else {
271 print ", ";
273 $num++;
275 PutFooter();
285 # Preliminary checks:
288 my $user = Bugzilla->login(LOGIN_REQUIRED);
289 my $whoid = $user->id;
291 my $cgi = Bugzilla->cgi;
292 print $cgi->header();
294 UserInGroup("editcomponents")
295 || ThrowUserError("auth_failure", {group => "editcomponents",
296 action => "edit",
297 object => "products"});
300 # often used variables
302 my $classification = trim($cgi->param('classification') || '');
303 my $product = trim($cgi->param('product') || '');
304 my $action = trim($cgi->param('action') || '');
305 my $headerdone = 0;
306 my $localtrailer = "<A HREF=\"editproducts.cgi\">edit</A> more products";
307 my $classhtmlvarstart = "";
308 my $classhtmlvar = "";
309 my $dbh = Bugzilla->dbh;
312 # product = '' -> Show nice list of classifications (if
313 # classifications enabled)
316 if (Param('useclassification')) {
317 if ($classification) {
318 $classhtmlvar = "&classification=" . url_quote($classification);
319 $classhtmlvarstart = "?classification=" . url_quote($classification);
320 $localtrailer .= ", <A HREF=\"editproducts.cgi" . $classhtmlvarstart . "\">edit</A> in this classification";
322 elsif (!$product) {
323 my $query =
324 "SELECT classifications.name, classifications.description,
325 COUNT(classification_id) AS product_count
326 FROM classifications
327 LEFT JOIN products
328 ON classifications.id = products.classification_id " .
329 $dbh->sql_group_by('classifications.id',
330 'classifications.name,
331 classifications.description') . "
332 ORDER BY name";
334 $vars->{'classifications'} = $dbh->selectall_arrayref($query,
335 {'Slice' => {}});
337 $template->process("admin/products/list-classifications.html.tmpl",
338 $vars)
339 || ThrowTemplateError($template->error());
341 exit;
347 # action = '' -> Show a nice list of products, unless a product
348 # is already specified (then edit it)
351 if (!$action && !$product) {
353 if (Param('useclassification')) {
354 CheckClassificationNew($classification);
357 my @execute_params = ();
358 my @products = ();
360 my $query = "SELECT products.name,
361 COALESCE(products.description,'') AS description,
362 disallownew = 0 AS status,
363 votesperuser, maxvotesperbug, votestoconfirm,
364 COUNT(bug_id) AS bug_count
365 FROM products";
367 if (Param('useclassification')) {
368 $query .= " INNER JOIN classifications " .
369 "ON classifications.id = products.classification_id";
372 $query .= " LEFT JOIN bugs ON products.id = bugs.product_id";
374 if (Param('useclassification')) {
375 $query .= " WHERE classifications.name = ? ";
377 # trick_taint is OK because we use this in a placeholder in a SELECT
378 trick_taint($classification);
380 push(@execute_params,
381 $classification);
384 $query .= " " . $dbh->sql_group_by('products.name',
385 'products.description, disallownew,
386 votesperuser, maxvotesperbug,
387 votestoconfirm');
388 $query .= " ORDER BY products.name";
390 $vars->{'products'} = $dbh->selectall_arrayref($query,
391 {'Slice' => {}},
392 @execute_params);
394 $vars->{'classification'} = $classification;
395 $template->process("admin/products/list.html.tmpl",
396 $vars)
397 || ThrowTemplateError($template->error());
399 exit;
406 # action='add' -> present form for parameters for new product
408 # (next action will be 'new')
411 if ($action eq 'add') {
412 PutHeader("Add Product");
414 if (Param('useclassification')) {
415 CheckClassification($classification);
417 #print "This page lets you add a new product to bugzilla.\n";
419 print "<FORM METHOD=POST ACTION=editproducts.cgi>\n";
420 print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n";
422 EmitFormElements($classification,'', '', '', 0, 0, 10000, 0, "---");
424 print "</TR><TR>\n";
425 print " <TH ALIGN=\"right\">Version:</TH>\n";
426 print " <TD><INPUT SIZE=64 MAXLENGTH=255 NAME=\"version\" VALUE=\"unspecified\"></TD>\n";
427 print "</TR><TR>\n";
428 print " <TH ALIGN=\"right\">Create chart datasets for this product:</TH>\n";
429 print " <TD><INPUT TYPE=CHECKBOX NAME=\"createseries\" VALUE=1></TD>";
430 print "</TR>\n";
432 print "</TABLE>\n<HR>\n";
433 print "<INPUT TYPE=SUBMIT VALUE=\"Add\">\n";
434 print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"new\">\n";
435 print "<INPUT TYPE=HIDDEN NAME='subcategory' VALUE='-All-'>\n";
436 print "<INPUT TYPE=HIDDEN NAME='open_name' VALUE='All Open'>\n";
437 print "<INPUT TYPE=HIDDEN NAME='classification' VALUE='",html_quote($classification),"'>\n";
438 print "</FORM>";
440 my $other = $localtrailer;
441 $other =~ s/more/other/;
442 PutTrailer($other);
443 exit;
449 # action='new' -> add product entered in the 'action=add' screen
452 if ($action eq 'new') {
453 PutHeader("Adding new product");
455 # Cleanups and validity checks
457 my $classification_id = 1;
458 if (Param('useclassification')) {
459 CheckClassification($classification);
460 $classification_id = get_classification_id($classification);
463 unless ($product) {
464 print "You must enter a name for the new product. Please press\n";
465 print "<b>Back</b> and try again.\n";
466 PutTrailer($localtrailer);
467 exit;
470 my $existing_product = TestProduct($product);
472 if ($existing_product) {
474 # Check for exact case sensitive match:
475 if ($existing_product eq $product) {
476 print "The product '$product' already exists. Please press\n";
477 print "<b>Back</b> and try again.\n";
478 PutTrailer($localtrailer);
479 exit;
482 # Next check for a case-insensitive match:
483 if (lc($existing_product) eq lc($product)) {
484 print "The new product '$product' differs from existing product ";
485 print "'$existing_product' only in case. Please press\n";
486 print "<b>Back</b> and try again.\n";
487 PutTrailer($localtrailer);
488 exit;
492 my $version = trim($cgi->param('version') || '');
494 if ($version eq '') {
495 print "You must enter a version for product '$product'. Please press\n";
496 print "<b>Back</b> and try again.\n";
497 PutTrailer($localtrailer);
498 exit;
501 my $description = trim($cgi->param('description') || '');
503 if ($description eq '') {
504 print "You must enter a description for product '$product'. Please press\n";
505 print "<b>Back</b> and try again.\n";
506 PutTrailer($localtrailer);
507 exit;
510 my $milestoneurl = trim($cgi->param('milestoneurl') || '');
511 my $disallownew = 0;
512 $disallownew = 1 if $cgi->param('disallownew');
513 my $votesperuser = $cgi->param('votesperuser');
514 $votesperuser ||= 0;
515 my $maxvotesperbug = $cgi->param('maxvotesperbug');
516 $maxvotesperbug = 10000 if !defined $maxvotesperbug;
517 my $votestoconfirm = $cgi->param('votestoconfirm');
518 $votestoconfirm ||= 0;
519 my $defaultmilestone = $cgi->param('defaultmilestone') || "---";
521 # Add the new product.
522 SendSQL("INSERT INTO products ( " .
523 "name, description, milestoneurl, disallownew, votesperuser, " .
524 "maxvotesperbug, votestoconfirm, defaultmilestone, classification_id" .
525 " ) VALUES ( " .
526 SqlQuote($product) . "," .
527 SqlQuote($description) . "," .
528 SqlQuote($milestoneurl) . "," .
529 # had tainting issues under cygwin, IIS 5.0, perl -T %s %s
530 # see bug 208647. http://bugzilla.mozilla.org/show_bug.cgi?id=208647
531 # had to de-taint $disallownew, $votesperuser, $maxvotesperbug,
532 # and $votestoconfirm w/ SqlQuote()
533 # - jpyeron@pyerotechnics.com
534 SqlQuote($disallownew) . "," .
535 SqlQuote($votesperuser) . "," .
536 SqlQuote($maxvotesperbug) . "," .
537 SqlQuote($votestoconfirm) . "," .
538 SqlQuote($defaultmilestone) . "," .
539 SqlQuote($classification_id) . ")");
540 my $product_id = $dbh->bz_last_key('products', 'id');
542 SendSQL("INSERT INTO versions ( " .
543 "value, product_id" .
544 " ) VALUES ( " .
545 SqlQuote($version) . "," .
546 $product_id . ")" );
548 SendSQL("INSERT INTO milestones (product_id, value) VALUES (" .
549 $product_id . ", " . SqlQuote($defaultmilestone) . ")");
551 # If we're using bug groups, then we need to create a group for this
552 # product as well. -JMR, 2/16/00
553 if (Param("makeproductgroups")) {
554 # Next we insert into the groups table
555 my $productgroup = $product;
556 while (GroupExists($productgroup)) {
557 $productgroup .= '_';
559 SendSQL("INSERT INTO groups " .
560 "(name, description, isbuggroup, last_changed) " .
561 "VALUES (" .
562 SqlQuote($productgroup) . ", " .
563 SqlQuote("Access to bugs in the $product product") . ", 1, NOW())");
564 my $gid = $dbh->bz_last_key('groups', 'id');
565 my $admin = GroupNameToId('admin');
566 # If we created a new group, give the "admin" group priviledges
567 # initially.
568 SendSQL("INSERT INTO group_group_map (member_id, grantor_id, grant_type)
569 VALUES ($admin, $gid," . GROUP_MEMBERSHIP .")");
570 SendSQL("INSERT INTO group_group_map (member_id, grantor_id, grant_type)
571 VALUES ($admin, $gid," . GROUP_BLESS .")");
572 SendSQL("INSERT INTO group_group_map (member_id, grantor_id, grant_type)
573 VALUES ($admin, $gid," . GROUP_VISIBLE .")");
575 # Associate the new group and new product.
576 SendSQL("INSERT INTO group_control_map " .
577 "(group_id, product_id, entry, " .
578 "membercontrol, othercontrol, canedit) VALUES " .
579 "($gid, $product_id, " . Param("useentrygroupdefault") .
580 ", " . CONTROLMAPDEFAULT . ", " .
581 CONTROLMAPNA . ", 0)");
584 if ($cgi->param('createseries')) {
585 # Insert default charting queries for this product.
586 # If they aren't using charting, this won't do any harm.
587 GetVersionTable();
589 # $open_name and $product are sqlquoted by the series code
590 # and never used again here, so we can trick_taint them.
591 my $open_name = $cgi->param('open_name');
592 trick_taint($open_name);
593 trick_taint($product);
595 my @series;
597 # We do every status, every resolution, and an "opened" one as well.
598 foreach my $bug_status (@::legal_bug_status) {
599 push(@series, [$bug_status,
600 "bug_status=" . url_quote($bug_status)]);
603 foreach my $resolution (@::legal_resolution) {
604 next if !$resolution;
605 push(@series, [$resolution, "resolution=" .url_quote($resolution)]);
608 # For localisation reasons, we get the name of the "global" subcategory
609 # and the title of the "open" query from the submitted form.
610 my @openedstatuses = OpenStates();
611 my $query =
612 join("&", map { "bug_status=" . url_quote($_) } @openedstatuses);
613 push(@series, [$open_name, $query]);
615 foreach my $sdata (@series) {
616 my $series = new Bugzilla::Series(undef, $product,
617 scalar $cgi->param('subcategory'),
618 $sdata->[0], $::userid, 1,
619 $sdata->[1] . "&product=" . url_quote($product), 1);
620 $series->writeToDatabase();
623 # Make versioncache flush
624 unlink "$datadir/versioncache";
626 print "OK, done.<p>\n";
627 print "<div style='border: 1px red solid; padding: 1ex;'><b>You will need to
628 <a href=\"editcomponents.cgi?action=add&product=" .
629 url_quote($product) . "\">add at least one
630 component</a> before you can enter bugs against this product.</b></div>";
631 PutTrailer($localtrailer,
632 "<a href=\"editproducts.cgi?action=add\">add</a> a new product",
633 "<a href=\"editcomponents.cgi?action=add&product=" .
634 url_quote($product) . $classhtmlvar .
635 "\">add</a> components to this new product");
636 exit;
642 # action='del' -> ask if user really wants to delete
644 # (next action would be 'delete')
647 if ($action eq 'del') {
649 if (!$product) {
650 ThrowUserError('product_not_specified');
653 my $product_id = get_product_id($product);
654 $product_id || ThrowUserError('product_doesnt_exist',
655 {product => $product});
657 my $classification_id = 1;
659 if (Param('useclassification')) {
660 CheckClassificationProductNew($classification, $product);
661 $classification_id = get_classification_id($classification);
662 $vars->{'classification'} = $classification;
665 # Extract some data about the product
666 my $query = q{SELECT classifications.description,
667 products.description,
668 products.milestoneurl,
669 products.disallownew
670 FROM products
671 INNER JOIN classifications
672 ON products.classification_id = classifications.id
673 WHERE products.id = ?};
675 my ($class_description,
676 $prod_description,
677 $milestoneurl,
678 $disallownew) = $dbh->selectrow_array($query, undef,
679 $product_id);
681 $vars->{'class_description'} = $class_description;
682 $vars->{'product_id'} = $product_id;
683 $vars->{'prod_description'} = $prod_description;
684 $vars->{'milestoneurl'} = $milestoneurl;
685 $vars->{'disallownew'} = $disallownew;
686 $vars->{'product_name'} = $product;
688 $vars->{'components'} = $dbh->selectall_arrayref(q{
689 SELECT name, description FROM components
690 WHERE product_id = ? ORDER BY name}, {'Slice' => {}},
691 $product_id);
693 $vars->{'versions'} = $dbh->selectcol_arrayref(q{
694 SELECT value FROM versions
695 WHERE product_id = ? ORDER BY value}, undef,
696 $product_id);
698 # Adding listing for associated target milestones -
699 # matthew@zeroknowledge.com
700 if (Param('usetargetmilestone')) {
701 $vars->{'milestones'} = $dbh->selectcol_arrayref(q{
702 SELECT value FROM milestones
703 WHERE product_id = ?
704 ORDER BY sortkey, value}, undef, $product_id);
707 ($vars->{'bug_count'}) = $dbh->selectrow_array(q{
708 SELECT COUNT(*) FROM bugs WHERE product_id = ?},
709 undef, $product_id) || 0;
711 $template->process("admin/products/confirm-delete.html.tmpl", $vars)
712 || ThrowTemplateError($template->error());
713 exit;
717 # action='delete' -> really delete the product
720 if ($action eq 'delete') {
722 if (!$product) {
723 ThrowUserError('product_not_specified');
726 my $product_id = get_product_id($product);
727 $product_id || ThrowUserError('product_doesnt_exist',
728 {product => $product});
730 $vars->{'product'} = $product;
731 $vars->{'classification'} = $classification;
733 my $bug_ids = $dbh->selectcol_arrayref(q{
734 SELECT bug_id FROM bugs
735 WHERE product_id = ?}, undef, $product_id);
737 my $nb_bugs = scalar(@$bug_ids);
738 if ($nb_bugs) {
739 if (Param("allowbugdeletion")) {
740 foreach my $bug_id (@$bug_ids) {
741 my $bug = new Bugzilla::Bug($bug_id, $whoid);
742 $bug->remove_from_db();
745 else {
746 ThrowUserError("product_has_bugs", { nb => $nb_bugs });
748 $vars->{'nb_bugs'} = $nb_bugs;
751 $dbh->bz_lock_tables('products WRITE', 'components WRITE',
752 'versions WRITE', 'milestones WRITE',
753 'group_control_map WRITE',
754 'flaginclusions WRITE', 'flagexclusions WRITE');
756 $dbh->do("DELETE FROM components WHERE product_id = ?",
757 undef, $product_id);
759 $dbh->do("DELETE FROM versions WHERE product_id = ?",
760 undef, $product_id);
762 $dbh->do("DELETE FROM milestones WHERE product_id = ?",
763 undef, $product_id);
765 $dbh->do("DELETE FROM group_control_map WHERE product_id = ?",
766 undef, $product_id);
768 $dbh->do("DELETE FROM flaginclusions WHERE product_id = ?",
769 undef, $product_id);
771 $dbh->do("DELETE FROM flagexclusions WHERE product_id = ?",
772 undef, $product_id);
774 $dbh->do("DELETE FROM products WHERE id = ?",
775 undef, $product_id);
777 $dbh->bz_unlock_tables();
779 unlink "$datadir/versioncache";
781 $template->process("admin/products/deleted.html.tmpl", $vars)
782 || ThrowTemplateError($template->error());
783 exit;
787 # action='edit' -> present the 'edit product' form
788 # If a product is given with no action associated with it, then edit it.
790 # (next action would be 'update')
793 if ($action eq 'edit' || (!$action && $product)) {
794 PutHeader("Edit Product");
795 CheckProduct($product);
796 my $classification_id=1;
797 if (Param('useclassification')) {
798 # If a product has been given with no classification associated
799 # with it, take this information from the DB
800 if ($classification) {
801 CheckClassificationProduct($classification, $product);
802 } else {
803 trick_taint($product);
804 $classification =
805 $dbh->selectrow_array("SELECT classifications.name
806 FROM products, classifications
807 WHERE products.name = ?
808 AND classifications.id = products.classification_id",
809 undef, $product);
811 $classification_id = get_classification_id($classification);
814 # get data of product
815 SendSQL("SELECT classifications.description,
816 products.id,products.description,milestoneurl,disallownew,
817 votesperuser,maxvotesperbug,votestoconfirm,defaultmilestone
818 FROM products,classifications
819 WHERE products.name=" . SqlQuote($product) .
820 " AND classifications.id=" . SqlQuote($classification_id));
821 my ($class_description, $product_id,$prod_description, $milestoneurl, $disallownew,
822 $votesperuser, $maxvotesperbug, $votestoconfirm, $defaultmilestone) =
823 FetchSQLData();
825 print "<FORM METHOD=POST ACTION=editproducts.cgi>\n";
826 print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n";
828 EmitFormElements($classification, $product, $prod_description, $milestoneurl,
829 $disallownew, $votesperuser, $maxvotesperbug,
830 $votestoconfirm, $defaultmilestone);
832 print "</TR><TR VALIGN=top>\n";
833 print " <TH ALIGN=\"right\"><A HREF=\"editcomponents.cgi?product=", url_quote($product), $classhtmlvar, "\">Edit components:</A></TH>\n";
834 print " <TD>";
835 SendSQL("SELECT name,description
836 FROM components
837 WHERE product_id=$product_id");
838 if (MoreSQLData()) {
839 print "<table>";
840 while ( MoreSQLData() ) {
841 my ($component, $description) = FetchSQLData();
842 $description ||= "<FONT COLOR=\"red\">description missing</FONT>";
843 print "<tr><th align=right valign=top>$component:</th>";
844 print "<td valign=top>$description</td></tr>\n";
846 print "</table>\n";
847 } else {
848 print "<FONT COLOR=\"red\">missing</FONT>";
852 print "</TD>\n</TR><TR>\n";
853 print " <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editversions.cgi?product=", url_quote($product), $classhtmlvar, "\">Edit versions:</A></TH>\n";
854 print " <TD>";
855 SendSQL("SELECT value
856 FROM versions
857 WHERE product_id=$product_id
858 ORDER BY value");
859 if (MoreSQLData()) {
860 my $br = 0;
861 while ( MoreSQLData() ) {
862 my ($version) = FetchSQLData();
863 print "<BR>" if $br;
864 print $version;
865 $br = 1;
867 } else {
868 print "<FONT COLOR=\"red\">missing</FONT>";
872 # Adding listing for associated target milestones - matthew@zeroknowledge.com
874 if (Param('usetargetmilestone')) {
875 print "</TD>\n</TR><TR>\n";
876 print " <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editmilestones.cgi?product=", url_quote($product), $classhtmlvar, "\">Edit milestones:</A></TH>\n";
877 print " <TD>";
878 SendSQL("SELECT value
879 FROM milestones
880 WHERE product_id=$product_id
881 ORDER BY sortkey,value");
882 if(MoreSQLData()) {
883 my $br = 0;
884 while ( MoreSQLData() ) {
885 my ($milestone) = FetchSQLData();
886 print "<BR>" if $br;
887 print $milestone;
888 $br = 1;
890 } else {
891 print "<FONT COLOR=\"red\">missing</FONT>";
895 print "</TD>\n</TR><TR>\n";
896 print " <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editproducts.cgi?action=editgroupcontrols&product=", url_quote($product), $classhtmlvar,"\">Edit Group Access Controls:</A></TH>\n";
897 print "<TD>\n";
898 SendSQL("SELECT id, name, isactive, entry, membercontrol, othercontrol, canedit " .
899 "FROM groups, " .
900 "group_control_map " .
901 "WHERE group_control_map.group_id = id AND product_id = $product_id " .
902 "AND isbuggroup != 0 ORDER BY name");
903 while (MoreSQLData()) {
904 my ($id, $name, $isactive, $entry, $membercontrol, $othercontrol, $canedit)
905 = FetchSQLData();
906 print "<B>" . html_quote($name) . ":</B> ";
907 if ($isactive) {
908 print $ctl{$membercontrol} . "/" . $ctl{$othercontrol};
909 print ", ENTRY" if $entry;
910 print ", CANEDIT" if $canedit;
911 } else {
912 print "DISABLED";
914 print "<BR>\n";
916 print "</TD>\n</TR><TR>\n";
917 print " <TH ALIGN=\"right\">Bugs:</TH>\n";
918 print " <TD>";
919 SendSQL("SELECT count(bug_id), product_id
920 FROM bugs " .
921 $dbh->sql_group_by('product_id') . "
922 HAVING product_id = $product_id");
923 my $bugs = '';
924 $bugs = FetchOneColumn() if MoreSQLData();
925 print $bugs || 'none';
927 print "</TD>\n</TR></TABLE>\n";
929 print "<INPUT TYPE=HIDDEN NAME=\"classification\" VALUE=\"" .
930 html_quote($classification) . "\">\n";
931 print "<INPUT TYPE=HIDDEN NAME=\"productold\" VALUE=\"" .
932 html_quote($product) . "\">\n";
933 print "<INPUT TYPE=HIDDEN NAME=\"descriptionold\" VALUE=\"" .
934 html_quote($prod_description) . "\">\n";
935 print "<INPUT TYPE=HIDDEN NAME=\"milestoneurlold\" VALUE=\"" .
936 html_quote($milestoneurl) . "\">\n";
937 print "<INPUT TYPE=HIDDEN NAME=\"disallownewold\" VALUE=\"$disallownew\">\n";
938 print "<INPUT TYPE=HIDDEN NAME=\"votesperuserold\" VALUE=\"$votesperuser\">\n";
939 print "<INPUT TYPE=HIDDEN NAME=\"maxvotesperbugold\" VALUE=\"$maxvotesperbug\">\n";
940 print "<INPUT TYPE=HIDDEN NAME=\"votestoconfirmold\" VALUE=\"$votestoconfirm\">\n";
941 $defaultmilestone = value_quote($defaultmilestone);
942 print "<INPUT TYPE=HIDDEN NAME=\"defaultmilestoneold\" VALUE=\"$defaultmilestone\">\n";
943 print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"update\">\n";
944 print "<INPUT TYPE=SUBMIT VALUE=\"Update\">\n";
946 print "</FORM>";
948 my $x = $localtrailer;
949 $x =~ s/more/other/;
950 PutTrailer($x);
951 exit;
955 # action='updategroupcontrols' -> update the product
958 if ($action eq 'updategroupcontrols') {
959 my $product_id = get_product_id($product);
960 my @now_na = ();
961 my @now_mandatory = ();
962 foreach my $f ($cgi->param()) {
963 if ($f =~ /^membercontrol_(\d+)$/) {
964 my $id = $1;
965 if ($cgi->param($f) == CONTROLMAPNA) {
966 push @now_na,$id;
967 } elsif ($cgi->param($f) == CONTROLMAPMANDATORY) {
968 push @now_mandatory,$id;
972 if (!defined $cgi->param('confirmed')) {
973 my @na_groups = ();
974 if (@now_na) {
975 SendSQL("SELECT groups.name, COUNT(bugs.bug_id)
976 FROM bugs, bug_group_map, groups
977 WHERE groups.id IN(" . join(', ', @now_na) . ")
978 AND bug_group_map.group_id = groups.id
979 AND bug_group_map.bug_id = bugs.bug_id
980 AND bugs.product_id = $product_id " .
981 $dbh->sql_group_by('groups.name'));
982 while (MoreSQLData()) {
983 my ($groupname, $bugcount) = FetchSQLData();
984 my %g = ();
985 $g{'name'} = $groupname;
986 $g{'count'} = $bugcount;
987 push @na_groups,\%g;
991 my @mandatory_groups = ();
992 if (@now_mandatory) {
993 SendSQL("SELECT groups.name, COUNT(bugs.bug_id)
994 FROM bugs
995 LEFT JOIN bug_group_map
996 ON bug_group_map.bug_id = bugs.bug_id
997 INNER JOIN groups
998 ON bug_group_map.group_id = groups.id
999 WHERE groups.id IN(" . join(', ', @now_mandatory) . ")
1000 AND bugs.product_id = $product_id
1001 AND bug_group_map.bug_id IS NULL " .
1002 $dbh->sql_group_by('groups.name'));
1003 while (MoreSQLData()) {
1004 my ($groupname, $bugcount) = FetchSQLData();
1005 my %g = ();
1006 $g{'name'} = $groupname;
1007 $g{'count'} = $bugcount;
1008 push @mandatory_groups,\%g;
1011 if ((@na_groups) || (@mandatory_groups)) {
1012 $vars->{'product'} = $product;
1013 $vars->{'na_groups'} = \@na_groups;
1014 $vars->{'mandatory_groups'} = \@mandatory_groups;
1015 $template->process("admin/products/groupcontrol/confirm-edit.html.tmpl", $vars)
1016 || ThrowTemplateError($template->error());
1017 exit;
1020 PutHeader("Update group access controls for $product");
1021 $headerdone = 1;
1022 SendSQL("SELECT id, name FROM groups " .
1023 "WHERE isbuggroup != 0 AND isactive != 0");
1024 while (MoreSQLData()){
1025 my ($groupid, $groupname) = FetchSQLData();
1026 my $newmembercontrol = $cgi->param("membercontrol_$groupid") || 0;
1027 my $newothercontrol = $cgi->param("othercontrol_$groupid") || 0;
1028 # Legality of control combination is a function of
1029 # membercontrol\othercontrol
1030 # NA SH DE MA
1031 # NA + - - -
1032 # SH + + + +
1033 # DE + - + +
1034 # MA - - - +
1035 unless (($newmembercontrol == $newothercontrol)
1036 || ($newmembercontrol == CONTROLMAPSHOWN)
1037 || (($newmembercontrol == CONTROLMAPDEFAULT)
1038 && ($newothercontrol != CONTROLMAPSHOWN))) {
1039 ThrowUserError('illegal_group_control_combination',
1040 {groupname => $groupname,
1041 header_done => 1});
1044 $dbh->bz_lock_tables('groups READ',
1045 'group_control_map WRITE',
1046 'bugs WRITE',
1047 'bugs_activity WRITE',
1048 'bug_group_map WRITE',
1049 'fielddefs READ');
1050 SendSQL("SELECT id, name, entry, membercontrol, othercontrol, canedit " .
1051 "FROM groups " .
1052 "LEFT JOIN group_control_map " .
1053 "ON group_control_map.group_id = id AND product_id = $product_id " .
1054 "WHERE isbuggroup != 0 AND isactive != 0");
1055 while (MoreSQLData()) {
1056 my ($groupid, $groupname, $entry, $membercontrol,
1057 $othercontrol, $canedit) = FetchSQLData();
1058 my $newentry = $cgi->param("entry_$groupid") || 0;
1059 my $newmembercontrol = $cgi->param("membercontrol_$groupid") || 0;
1060 my $newothercontrol = $cgi->param("othercontrol_$groupid") || 0;
1061 my $newcanedit = $cgi->param("canedit_$groupid") || 0;
1062 my $oldentry = $entry;
1063 $entry = $entry || 0;
1064 $membercontrol = $membercontrol || 0;
1065 $othercontrol = $othercontrol || 0;
1066 $canedit = $canedit || 0;
1067 detaint_natural($newentry);
1068 detaint_natural($newothercontrol);
1069 detaint_natural($newmembercontrol);
1070 detaint_natural($newcanedit);
1071 if ((!defined($oldentry)) &&
1072 (($newentry) || ($newmembercontrol) || ($newcanedit))) {
1073 PushGlobalSQLState();
1074 SendSQL("INSERT INTO group_control_map " .
1075 "(group_id, product_id, entry, " .
1076 "membercontrol, othercontrol, canedit) " .
1077 "VALUES " .
1078 "($groupid, $product_id, $newentry, " .
1079 "$newmembercontrol, $newothercontrol, $newcanedit)");
1080 PopGlobalSQLState();
1081 } elsif (($newentry != $entry)
1082 || ($newmembercontrol != $membercontrol)
1083 || ($newothercontrol != $othercontrol)
1084 || ($newcanedit != $canedit)) {
1085 PushGlobalSQLState();
1086 SendSQL("UPDATE group_control_map " .
1087 "SET entry = $newentry, " .
1088 "membercontrol = $newmembercontrol, " .
1089 "othercontrol = $newothercontrol, " .
1090 "canedit = $newcanedit " .
1091 "WHERE group_id = $groupid " .
1092 "AND product_id = $product_id");
1093 PopGlobalSQLState();
1096 if (($newentry == 0) && ($newmembercontrol == 0)
1097 && ($newothercontrol == 0) && ($newcanedit == 0)) {
1098 PushGlobalSQLState();
1099 SendSQL("DELETE FROM group_control_map " .
1100 "WHERE group_id = $groupid " .
1101 "AND product_id = $product_id");
1102 PopGlobalSQLState();
1106 foreach my $groupid (@now_na) {
1107 print "Removing bugs from NA group "
1108 . html_quote(GroupIdToName($groupid)) . "<P>\n";
1109 my $count = 0;
1110 SendSQL("SELECT bugs.bug_id,
1111 (lastdiffed >= delta_ts)
1112 FROM bugs, bug_group_map
1113 WHERE group_id = $groupid
1114 AND bug_group_map.bug_id = bugs.bug_id
1115 AND bugs.product_id = $product_id
1116 ORDER BY bugs.bug_id");
1117 while (MoreSQLData()) {
1118 my ($bugid, $mailiscurrent) = FetchSQLData();
1119 PushGlobalSQLState();
1120 SendSQL("DELETE FROM bug_group_map WHERE
1121 bug_id = $bugid AND group_id = $groupid");
1122 SendSQL("SELECT name, NOW() FROM groups WHERE id = $groupid");
1123 my ($removed, $timestamp) = FetchSQLData();
1124 LogActivityEntry($bugid, "bug_group", $removed, "",
1125 $::userid, $timestamp);
1126 my $diffed = "";
1127 if ($mailiscurrent) {
1128 $diffed = ", lastdiffed = " . SqlQuote($timestamp);
1130 SendSQL("UPDATE bugs SET delta_ts = " . SqlQuote($timestamp) .
1131 $diffed . " WHERE bug_id = $bugid");
1132 PopGlobalSQLState();
1133 $count++;
1135 print "dropped $count bugs<p>\n";
1138 foreach my $groupid (@now_mandatory) {
1139 print "Adding bugs to Mandatory group "
1140 . html_quote(GroupIdToName($groupid)) . "<P>\n";
1141 my $count = 0;
1142 SendSQL("SELECT bugs.bug_id,
1143 (lastdiffed >= delta_ts)
1144 FROM bugs
1145 LEFT JOIN bug_group_map
1146 ON bug_group_map.bug_id = bugs.bug_id
1147 AND group_id = $groupid
1148 WHERE bugs.product_id = $product_id
1149 AND bug_group_map.bug_id IS NULL
1150 ORDER BY bugs.bug_id");
1151 while (MoreSQLData()) {
1152 my ($bugid, $mailiscurrent) = FetchSQLData();
1153 PushGlobalSQLState();
1154 SendSQL("INSERT INTO bug_group_map (bug_id, group_id)
1155 VALUES ($bugid, $groupid)");
1156 SendSQL("SELECT name, NOW() FROM groups WHERE id = $groupid");
1157 my ($added, $timestamp) = FetchSQLData();
1158 LogActivityEntry($bugid, "bug_group", "", $added,
1159 $::userid, $timestamp);
1160 my $diffed = "";
1161 if ($mailiscurrent) {
1162 $diffed = ", lastdiffed = " . SqlQuote($timestamp);
1164 SendSQL("UPDATE bugs SET delta_ts = " . SqlQuote($timestamp) .
1165 $diffed . " WHERE bug_id = $bugid");
1166 PopGlobalSQLState();
1167 $count++;
1169 print "added $count bugs<p>\n";
1171 $dbh->bz_unlock_tables();
1173 print "Group control updates done<P>\n";
1175 PutTrailer($localtrailer);
1176 exit;
1180 # action='update' -> update the product
1183 if ($action eq 'update') {
1184 PutHeader("Update product");
1186 my $productold = trim($cgi->param('productold') || '');
1187 my $description = trim($cgi->param('description') || '');
1188 my $descriptionold = trim($cgi->param('descriptionold') || '');
1189 my $disallownew = trim($cgi->param('disallownew') || '');
1190 my $disallownewold = trim($cgi->param('disallownewold') || '');
1191 my $milestoneurl = trim($cgi->param('milestoneurl') || '');
1192 my $milestoneurlold = trim($cgi->param('milestoneurlold') || '');
1193 my $votesperuser = trim($cgi->param('votesperuser') || 0);
1194 my $votesperuserold = trim($cgi->param('votesperuserold') || 0);
1195 my $maxvotesperbug = trim($cgi->param('maxvotesperbug') || 0);
1196 my $maxvotesperbugold = trim($cgi->param('maxvotesperbugold') || 0);
1197 my $votestoconfirm = trim($cgi->param('votestoconfirm') || 0);
1198 my $votestoconfirmold = trim($cgi->param('votestoconfirmold') || 0);
1199 my $defaultmilestone = trim($cgi->param('defaultmilestone') || '---');
1200 my $defaultmilestoneold = trim($cgi->param('defaultmilestoneold') || '---');
1202 my $checkvotes = 0;
1204 CheckProduct($productold);
1205 my $product_id = get_product_id($productold);
1207 if (!detaint_natural($maxvotesperbug)) {
1208 print "Sorry, the max votes per bug must be an integer >= 0.";
1209 PutTrailer($localtrailer);
1210 exit;
1213 if (!detaint_natural($votesperuser)) {
1214 print "Sorry, the votes per user must be an integer >= 0.";
1215 PutTrailer($localtrailer);
1216 exit;
1219 if (!detaint_natural($votestoconfirm)) {
1220 print "Sorry, the votes to confirm must be an integer >= 0.";
1221 PutTrailer($localtrailer);
1222 exit;
1225 # Note that we got the $product_id using $productold above so it will
1226 # remain static even after we rename the product in the database.
1228 $dbh->bz_lock_tables('products WRITE',
1229 'versions READ',
1230 'groups WRITE',
1231 'group_control_map WRITE',
1232 'profiles WRITE',
1233 'milestones READ');
1235 if ($disallownew ne $disallownewold) {
1236 $disallownew = $disallownew ? 1 : 0;
1237 SendSQL("UPDATE products
1238 SET disallownew=$disallownew
1239 WHERE id=$product_id");
1240 print "Updated bug submit status.<BR>\n";
1243 if ($description ne $descriptionold) {
1244 unless ($description) {
1245 print "Sorry, I can't delete the description.";
1246 $dbh->bz_unlock_tables(UNLOCK_ABORT);
1247 PutTrailer($localtrailer);
1248 exit;
1250 SendSQL("UPDATE products
1251 SET description=" . SqlQuote($description) . "
1252 WHERE id=$product_id");
1253 print "Updated description.<BR>\n";
1256 if (Param('usetargetmilestone') && $milestoneurl ne $milestoneurlold) {
1257 SendSQL("UPDATE products
1258 SET milestoneurl=" . SqlQuote($milestoneurl) . "
1259 WHERE id=$product_id");
1260 print "Updated milestone URL.<BR>\n";
1264 if ($votesperuser ne $votesperuserold) {
1265 SendSQL("UPDATE products
1266 SET votesperuser=$votesperuser
1267 WHERE id=$product_id");
1268 print "Updated votes per user.<BR>\n";
1269 $checkvotes = 1;
1273 if ($maxvotesperbug ne $maxvotesperbugold) {
1274 SendSQL("UPDATE products
1275 SET maxvotesperbug=$maxvotesperbug
1276 WHERE id=$product_id");
1277 print "Updated max votes per bug.<BR>\n";
1278 $checkvotes = 1;
1282 if ($votestoconfirm ne $votestoconfirmold) {
1283 SendSQL("UPDATE products
1284 SET votestoconfirm=$votestoconfirm
1285 WHERE id=$product_id");
1286 print "Updated votes to confirm.<BR>\n";
1287 $checkvotes = 1;
1291 if ($defaultmilestone ne $defaultmilestoneold) {
1292 SendSQL("SELECT value FROM milestones " .
1293 "WHERE value = " . SqlQuote($defaultmilestone) .
1294 " AND product_id = $product_id");
1295 if (!FetchOneColumn()) {
1296 print "Sorry, the milestone $defaultmilestone must be defined first.";
1297 $dbh->bz_unlock_tables(UNLOCK_ABORT);
1298 PutTrailer($localtrailer);
1299 exit;
1301 SendSQL("UPDATE products " .
1302 "SET defaultmilestone = " . SqlQuote($defaultmilestone) .
1303 "WHERE id=$product_id");
1304 print "Updated default milestone.<BR>\n";
1307 my $qp = SqlQuote($product);
1308 my $qpold = SqlQuote($productold);
1310 if ($product ne $productold) {
1311 unless ($product) {
1312 print "Sorry, I can't delete the product name.";
1313 $dbh->bz_unlock_tables(UNLOCK_ABORT);
1314 PutTrailer($localtrailer);
1315 exit;
1318 if (lc($product) ne lc($productold) &&
1319 TestProduct($product)) {
1320 print "Sorry, product name '$product' is already in use.";
1321 $dbh->bz_unlock_tables(UNLOCK_ABORT);
1322 PutTrailer($localtrailer);
1323 exit;
1326 SendSQL("UPDATE products SET name=$qp WHERE id=$product_id");
1327 print "Updated product name.<BR>\n";
1329 $dbh->bz_unlock_tables();
1330 unlink "$datadir/versioncache";
1332 if ($checkvotes) {
1333 # 1. too many votes for a single user on a single bug.
1334 if ($maxvotesperbug < $votesperuser) {
1335 print "<br>Checking existing votes in this product for anybody who now has too many votes for a single bug.";
1336 SendSQL("SELECT votes.who, votes.bug_id " .
1337 "FROM votes, bugs " .
1338 "WHERE bugs.bug_id = votes.bug_id " .
1339 " AND bugs.product_id = $product_id " .
1340 " AND votes.vote_count > $maxvotesperbug");
1341 my @list;
1342 while (MoreSQLData()) {
1343 my ($who, $id) = (FetchSQLData());
1344 push(@list, [$who, $id]);
1346 foreach my $ref (@list) {
1347 my ($who, $id) = (@$ref);
1348 RemoveVotes($id, $who, "The rules for voting on this product has changed;\nyou had too many votes for a single bug.");
1349 my $name = DBID_to_name($who);
1350 print qq{<br>Removed votes for bug <A HREF="show_bug.cgi?id=$id">$id</A> from $name\n};
1354 # 2. too many total votes for a single user.
1355 # This part doesn't work in the general case because RemoveVotes
1356 # doesn't enforce votesperuser (except per-bug when it's less
1357 # than maxvotesperbug). See RemoveVotes in globals.pl.
1358 print "<br>Checking existing votes in this product for anybody who now has too many total votes.";
1359 SendSQL("SELECT votes.who, votes.vote_count FROM votes, bugs " .
1360 "WHERE bugs.bug_id = votes.bug_id " .
1361 " AND bugs.product_id = $product_id");
1362 my %counts;
1363 while (MoreSQLData()) {
1364 my ($who, $count) = (FetchSQLData());
1365 if (!defined $counts{$who}) {
1366 $counts{$who} = $count;
1367 } else {
1368 $counts{$who} += $count;
1371 foreach my $who (keys(%counts)) {
1372 if ($counts{$who} > $votesperuser) {
1373 SendSQL("SELECT votes.bug_id FROM votes, bugs " .
1374 "WHERE bugs.bug_id = votes.bug_id " .
1375 " AND bugs.product_id = $product_id " .
1376 " AND votes.who = $who");
1377 while (MoreSQLData()) {
1378 my ($id) = FetchSQLData();
1379 RemoveVotes($id, $who,
1380 "The rules for voting on this product has changed; you had too many\ntotal votes, so all votes have been removed.");
1381 my $name = DBID_to_name($who);
1382 print qq{<br>Removed votes for bug <A HREF="show_bug.cgi?id=$id">$id</A> from $name\n};
1386 # 3. enough votes to confirm
1387 my $bug_list = $dbh->selectcol_arrayref("SELECT bug_id FROM bugs
1388 WHERE product_id = ?
1389 AND bug_status = 'UNCONFIRMED'
1390 AND votes >= ?",
1391 undef, ($product_id, $votestoconfirm));
1392 if (scalar(@$bug_list)) {
1393 print "<br>Checking unconfirmed bugs in this product for any which now have sufficient votes.";
1395 my @updated_bugs = ();
1396 foreach my $bug_id (@$bug_list) {
1397 my $confirmed = CheckIfVotedConfirmed($bug_id, $whoid);
1398 push (@updated_bugs, $bug_id) if $confirmed;
1401 $vars->{'type'} = "votes";
1402 $vars->{'mailrecipients'} = { 'changer' => $whoid };
1403 $vars->{'header_done'} = 1;
1405 foreach my $bug_id (@updated_bugs) {
1406 $vars->{'id'} = $bug_id;
1407 $template->process("bug/process/results.html.tmpl", $vars)
1408 || ThrowTemplateError($template->error());
1412 PutTrailer($localtrailer);
1413 exit;
1417 # action='editgroupcontrols' -> update product group controls
1420 if ($action eq 'editgroupcontrols') {
1421 my $product_id = get_product_id($product);
1422 $product_id
1423 || ThrowUserError("invalid_product_name", { product => $product });
1424 # Display a group if it is either enabled or has bugs for this product.
1425 SendSQL("SELECT id, name, entry, membercontrol, othercontrol, canedit, " .
1426 "isactive, COUNT(bugs.bug_id) " .
1427 "FROM groups " .
1428 "LEFT JOIN group_control_map " .
1429 "ON group_control_map.group_id = id " .
1430 "AND group_control_map.product_id = $product_id " .
1431 "LEFT JOIN bug_group_map " .
1432 "ON bug_group_map.group_id = groups.id " .
1433 "LEFT JOIN bugs " .
1434 "ON bugs.bug_id = bug_group_map.bug_id " .
1435 "AND bugs.product_id = $product_id " .
1436 "WHERE isbuggroup != 0 " .
1437 "AND (isactive != 0 OR entry IS NOT NULL " .
1438 "OR bugs.bug_id IS NOT NULL) " .
1439 $dbh->sql_group_by('name', 'id, entry, membercontrol,
1440 othercontrol, canedit, isactive'));
1441 my @groups = ();
1442 while (MoreSQLData()) {
1443 my %group = ();
1444 my ($groupid, $groupname, $entry, $membercontrol, $othercontrol,
1445 $canedit, $isactive, $bugcount) = FetchSQLData();
1446 $group{'id'} = $groupid;
1447 $group{'name'} = $groupname;
1448 $group{'entry'} = $entry;
1449 $group{'membercontrol'} = $membercontrol;
1450 $group{'othercontrol'} = $othercontrol;
1451 $group{'canedit'} = $canedit;
1452 $group{'isactive'} = $isactive;
1453 $group{'bugcount'} = $bugcount;
1454 push @groups,\%group;
1456 $vars->{'header_done'} = $headerdone;
1457 $vars->{'product'} = $product;
1458 $vars->{'classification'} = $classification;
1459 $vars->{'groups'} = \@groups;
1460 $vars->{'const'} = {
1461 'CONTROLMAPNA' => CONTROLMAPNA,
1462 'CONTROLMAPSHOWN' => CONTROLMAPSHOWN,
1463 'CONTROLMAPDEFAULT' => CONTROLMAPDEFAULT,
1464 'CONTROLMAPMANDATORY' => CONTROLMAPMANDATORY,
1467 $template->process("admin/products/groupcontrol/edit.html.tmpl", $vars)
1468 || ThrowTemplateError($template->error());
1469 exit;
1474 # No valid action found
1477 PutHeader("Error");
1478 print "I don't have a clue what you want.<BR>\n";