summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormyk%mozilla.org <>2002-09-29 03:42:23 +0200
committermyk%mozilla.org <>2002-09-29 03:42:23 +0200
commit91b171e7584920d03abb9c45e779c84f3dee975c (patch)
treefc59becfe02d1a4dc84e5f3501f0139effcf1c7a
parent90975fe914d066726d06f53abe8696399b13a61a (diff)
downloadbugzilla-91b171e7584920d03abb9c45e779c84f3dee975c.tar.gz
bugzilla-91b171e7584920d03abb9c45e779c84f3dee975c.tar.xz
Fix for bug 98801: Implementation of the request tracker, a set of enhancements to attachment statuses.
r=gerv,bbaetz
-rw-r--r--Attachment.pm42
-rw-r--r--Bugzilla/Attachment.pm42
-rw-r--r--Bugzilla/Flag.pm591
-rw-r--r--Bugzilla/FlagType.pm325
-rw-r--r--Bugzilla/Search.pm128
-rw-r--r--Bugzilla/User.pm176
-rwxr-xr-xattachment.cgi188
-rw-r--r--bug_form.pl46
-rwxr-xr-xchecksetup.pl206
-rwxr-xr-xeditattachstatuses.cgi347
-rwxr-xr-xeditcomponents.cgi10
-rwxr-xr-xeditflagtypes.cgi494
-rwxr-xr-xeditproducts.cgi10
-rw-r--r--globals.pl12
-rwxr-xr-xprocess_bug.cgi29
-rw-r--r--productmenu.js242
-rwxr-xr-xrequest.cgi279
-rwxr-xr-xsanitycheck.cgi8
-rw-r--r--template/en/default/account/prefs/email.html.tmpl20
-rw-r--r--template/en/default/admin/flag-type/confirm-delete.html.tmpl58
-rw-r--r--template/en/default/admin/flag-type/edit.html.tmpl189
-rw-r--r--template/en/default/admin/flag-type/list.html.tmpl107
-rw-r--r--template/en/default/attachment/edit.html.tmpl21
-rw-r--r--template/en/default/attachment/list.html.tmpl35
-rw-r--r--template/en/default/bug/edit.html.tmpl20
-rw-r--r--template/en/default/flag/list.html.tmpl94
-rw-r--r--template/en/default/global/code-error.html.tmpl42
-rw-r--r--template/en/default/global/messages.html.tmpl28
-rw-r--r--template/en/default/global/select-menu.html.tmpl12
-rw-r--r--template/en/default/global/useful-links.html.tmpl4
-rw-r--r--template/en/default/global/user-error.html.tmpl39
-rw-r--r--template/en/default/request/created-email.txt.tmpl41
-rw-r--r--template/en/default/request/fulfilled-email.txt.tmpl42
-rw-r--r--template/en/default/request/queue.html.tmpl193
-rw-r--r--template/en/default/request/verify.html.tmpl108
-rwxr-xr-xuserprefs.cgi9
36 files changed, 3562 insertions, 675 deletions
diff --git a/Attachment.pm b/Attachment.pm
index 3a6248cf4..53690170e 100644
--- a/Attachment.pm
+++ b/Attachment.pm
@@ -31,10 +31,32 @@ package Attachment;
# This module requires that its caller have said "require CGI.pl" to import
# relevant functions from that script and its companion globals.pl.
+# Use the Flag module to handle flags.
+use Bugzilla::Flag;
+
############################################################################
# Functions
############################################################################
+sub new {
+ # Returns a hash of information about the attachment with the given ID.
+
+ my ($invocant, $id) = @_;
+ return undef if !$id;
+ my $self = { 'id' => $id };
+ my $class = ref($invocant) || $invocant;
+ bless($self, $class);
+
+ &::PushGlobalSQLState();
+ &::SendSQL("SELECT 1, description, bug_id FROM attachments " .
+ "WHERE attach_id = $id");
+ ($self->{'exists'}, $self->{'summary'}, $self->{'bug_id'}) =
+ &::FetchSQLData();
+ &::PopGlobalSQLState();
+
+ return $self;
+}
+
sub query
{
# Retrieves and returns an array of attachment records for a given bug.
@@ -65,23 +87,9 @@ sub query
$a{'date'} = "$1-$2-$3 $4:$5";
}
- # Retrieve a list of status flags that have been set on the attachment.
- &::PushGlobalSQLState();
- &::SendSQL("
- SELECT name
- FROM attachstatuses, attachstatusdefs
- WHERE attach_id = $a{'attachid'}
- AND attachstatuses.statusid = attachstatusdefs.id
- ORDER BY sortkey
- ");
- my @statuses = ();
- while (&::MoreSQLData()) {
- my ($status) = &::FetchSQLData();
- push @statuses , $status;
- }
- $a{'statuses'} = \@statuses;
- &::PopGlobalSQLState();
-
+ # Retrieve a list of flags for this attachment.
+ $a{'flags'} = Bugzilla::Flag::match({ 'attach_id' => $a{'attachid'} });
+
# We will display the edit link if the user can edit the attachment;
# ie the are the submitter, or they have canedit.
# Also show the link if the user is not logged in - in that cae,
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
index 3a6248cf4..53690170e 100644
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -31,10 +31,32 @@ package Attachment;
# This module requires that its caller have said "require CGI.pl" to import
# relevant functions from that script and its companion globals.pl.
+# Use the Flag module to handle flags.
+use Bugzilla::Flag;
+
############################################################################
# Functions
############################################################################
+sub new {
+ # Returns a hash of information about the attachment with the given ID.
+
+ my ($invocant, $id) = @_;
+ return undef if !$id;
+ my $self = { 'id' => $id };
+ my $class = ref($invocant) || $invocant;
+ bless($self, $class);
+
+ &::PushGlobalSQLState();
+ &::SendSQL("SELECT 1, description, bug_id FROM attachments " .
+ "WHERE attach_id = $id");
+ ($self->{'exists'}, $self->{'summary'}, $self->{'bug_id'}) =
+ &::FetchSQLData();
+ &::PopGlobalSQLState();
+
+ return $self;
+}
+
sub query
{
# Retrieves and returns an array of attachment records for a given bug.
@@ -65,23 +87,9 @@ sub query
$a{'date'} = "$1-$2-$3 $4:$5";
}
- # Retrieve a list of status flags that have been set on the attachment.
- &::PushGlobalSQLState();
- &::SendSQL("
- SELECT name
- FROM attachstatuses, attachstatusdefs
- WHERE attach_id = $a{'attachid'}
- AND attachstatuses.statusid = attachstatusdefs.id
- ORDER BY sortkey
- ");
- my @statuses = ();
- while (&::MoreSQLData()) {
- my ($status) = &::FetchSQLData();
- push @statuses , $status;
- }
- $a{'statuses'} = \@statuses;
- &::PopGlobalSQLState();
-
+ # Retrieve a list of flags for this attachment.
+ $a{'flags'} = Bugzilla::Flag::match({ 'attach_id' => $a{'attachid'} });
+
# We will display the edit link if the user can edit the attachment;
# ie the are the submitter, or they have canedit.
# Also show the link if the user is not logged in - in that cae,
diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm
new file mode 100644
index 000000000..3feaae4cd
--- /dev/null
+++ b/Bugzilla/Flag.pm
@@ -0,0 +1,591 @@
+# -*- 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): Myk Melez <myk@mozilla.org>
+
+################################################################################
+# Module Initialization
+################################################################################
+
+# Make it harder for us to do dangerous things in Perl.
+use strict;
+
+# This module implements bug and attachment flags.
+package Bugzilla::Flag;
+
+use Bugzilla::FlagType;
+use Bugzilla::User;
+use Attachment;
+
+use vars qw($template $vars);
+
+# Note! This module requires that its caller have said "require CGI.pl"
+# to import relevant functions from that script and its companion globals.pl.
+
+################################################################################
+# Global Variables
+################################################################################
+
+# basic sets of columns and tables for getting flags from the database
+
+my @base_columns =
+ ("1", "id", "type_id", "bug_id", "attach_id", "requestee_id", "setter_id",
+ "status");
+
+# Note: when adding tables to @base_tables, make sure to include the separator
+# (i.e. a comma or words like "LEFT OUTER JOIN") before the table name,
+# since tables take multiple separators based on the join type, and therefore
+# it is not possible to join them later using a single known separator.
+
+my @base_tables = ("flags");
+
+################################################################################
+# Searching/Retrieving Flags
+################################################################################
+
+# !!! Implement a cache for this function!
+sub get {
+ # Retrieves and returns a flag from the database.
+
+ my ($id) = @_;
+
+ my $select_clause = "SELECT " . join(", ", @base_columns);
+ my $from_clause = "FROM " . join(" ", @base_tables);
+
+ # Execute the query, retrieve the result, and write it into a record.
+ &::PushGlobalSQLState();
+ &::SendSQL("$select_clause $from_clause WHERE flags.id = $id");
+ my $flag = perlify_record(&::FetchSQLData());
+ &::PopGlobalSQLState();
+
+ return $flag;
+}
+
+sub match {
+ # Queries the database for flags matching the given criteria
+ # (specified as a hash of field names and their matching values)
+ # and returns an array of matching records.
+
+ my ($criteria) = @_;
+
+ my $select_clause = "SELECT " . join(", ", @base_columns);
+ my $from_clause = "FROM " . join(" ", @base_tables);
+
+ my @criteria = sqlify_criteria($criteria);
+
+ my $where_clause = "WHERE " . join(" AND ", @criteria);
+
+ # Execute the query, retrieve the results, and write them into records.
+ &::PushGlobalSQLState();
+ &::SendSQL("$select_clause $from_clause $where_clause");
+ my @flags;
+ while (&::MoreSQLData()) {
+ my $flag = perlify_record(&::FetchSQLData());
+ push(@flags, $flag);
+ }
+ &::PopGlobalSQLState();
+
+ return \@flags;
+}
+
+sub count {
+ # Queries the database for flags matching the given criteria
+ # (specified as a hash of field names and their matching values)
+ # and returns an array of matching records.
+
+ my ($criteria) = @_;
+
+ my @criteria = sqlify_criteria($criteria);
+
+ my $where_clause = "WHERE " . join(" AND ", @criteria);
+
+ # Execute the query, retrieve the result, and write it into a record.
+ &::PushGlobalSQLState();
+ &::SendSQL("SELECT COUNT(id) FROM flags $where_clause");
+ my $count = &::FetchOneColumn();
+ &::PopGlobalSQLState();
+
+ return $count;
+}
+
+################################################################################
+# Creating and Modifying
+################################################################################
+
+sub validate {
+ # Validates fields containing flag modifications.
+
+ my ($data) = @_;
+
+ # Get a list of flags to validate. Uses the "map" function
+ # to extract flag IDs from form field names by matching fields
+ # whose name looks like "flag-nnn", where "nnn" is the ID,
+ # and returning just the ID portion of matching field names.
+ my @ids = map(/^flag-(\d+)$/ ? $1 : (), keys %$data);
+
+ foreach my $id (@ids)
+ {
+ my $status = $data->{"flag-$id"};
+
+ # Make sure the flag exists.
+ my $flag = get($id);
+ $flag || &::ThrowCodeError("flag_nonexistent", { id => $id });
+
+ # Don't bother validating flags the user didn't change.
+ next if $status eq $flag->{'status'};
+
+ # Make sure the user chose a valid status.
+ grep($status eq $_, qw(X + - ?))
+ || &::ThrowCodeError("flag_status_invalid",
+ { id => $id , status => $status });
+ }
+}
+
+sub process {
+ # Processes changes to flags.
+
+ # The target is the bug or attachment this flag is about, the timestamp
+ # is the date/time the bug was last touched (so that changes to the flag
+ # can be stamped with the same date/time), the data is the form data
+ # with flag fields that the user submitted, the old bug is the bug record
+ # before the user made changes to it, and the new bug is the bug record
+ # after the user made changes to it.
+
+ my ($target, $timestamp, $data, $oldbug, $newbug) = @_;
+
+ # Use the date/time we were given if possible (allowing calling code
+ # to synchronize the comment's timestamp with those of other records).
+ $timestamp = ($timestamp ? &::SqlQuote($timestamp) : "NOW()");
+
+ # Take a snapshot of flags before any changes.
+ my $flags = match({ 'bug_id' => $target->{'bug'}->{'id'} ,
+ 'attach_id' => $target->{'attachment'}->{'id'} });
+ my @old_summaries;
+ foreach my $flag (@$flags) {
+ my $summary = $flag->{'type'}->{'name'} . $flag->{'status'};
+ push(@old_summaries, $summary);
+ }
+
+ # Create new flags and update existing flags.
+ my $new_flags = FormToNewFlags($target, $data);
+ foreach my $flag (@$new_flags) { create($flag, $timestamp) }
+ modify($data, $timestamp);
+
+ # In case the bug's product/component has changed, clear flags that are
+ # no longer valid.
+ &::SendSQL("
+ SELECT flags.id
+ FROM flags, bugs LEFT OUTER JOIN flaginclusions i
+ ON (flags.type_id = i.type_id
+ AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
+ AND (bugs.component_id = i.component_id OR i.component_id IS NULL))
+ WHERE flags.type_id = $target->{'bug'}->{'id'}
+ AND flags.bug_id = bugs.bug_id
+ AND i.type_id IS NULL
+ ");
+ clear(&::FetchOneColumn()) while &::MoreSQLData();
+ &::SendSQL("
+ SELECT flags.id
+ FROM flags, bugs, flagexclusions e
+ WHERE flags.type_id = $target->{'bug'}->{'id'}
+ AND flags.bug_id = bugs.bug_id
+ AND flags.type_id = e.type_id
+ AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
+ AND (bugs.component_id = e.component_id OR e.component_id IS NULL)
+ ");
+ clear(&::FetchOneColumn()) while &::MoreSQLData();
+
+ # Take a snapshot of flags after changes.
+ $flags = match({ 'bug_id' => $target->{'bug'}->{'id'} ,
+ 'attach_id' => $target->{'attachment'}->{'id'} });
+ my @new_summaries;
+ foreach my $flag (@$flags) {
+ my $summary = $flag->{'type'}->{'name'} . $flag->{'status'};
+ push(@new_summaries, $summary);
+ }
+
+ my $old_summaries = join(", ", @old_summaries);
+ my $new_summaries = join(", ", @new_summaries);
+ my ($removed, $added) = &::DiffStrings($old_summaries, $new_summaries);
+ if ($removed ne $added) {
+ my $sql_removed = &::SqlQuote($removed);
+ my $sql_added = &::SqlQuote($added);
+ my $field_id = &::GetFieldID('flagtypes.name');
+ my $attach_id = $target->{'attachment'}->{'id'} || 'NULL';
+ &::SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, " .
+ "bug_when, fieldid, removed, added) VALUES " .
+ "($target->{'bug'}->{'id'}, $attach_id, $::userid, " .
+ "$timestamp, $field_id, $sql_removed, $sql_added)");
+ }
+}
+
+
+sub create {
+ # Creates a flag record in the database.
+
+ my ($flag, $timestamp) = @_;
+
+ # Determine the ID for the flag record by retrieving the last ID used
+ # and incrementing it.
+ &::SendSQL("SELECT MAX(id) FROM flags");
+ $flag->{'id'} = (&::FetchOneColumn() || 0) + 1;
+
+ # Insert a record for the flag into the flags table.
+ my $attach_id = $flag->{'target'}->{'attachment'}->{'id'} || "NULL";
+ my $requestee_id = $flag->{'requestee'} ? $flag->{'requestee'}->{'id'} : "NULL";
+ &::SendSQL("INSERT INTO flags (id, type_id,
+ bug_id, attach_id,
+ requestee_id, setter_id, status,
+ creation_date, modification_date)
+ VALUES ($flag->{'id'},
+ $flag->{'type'}->{'id'},
+ $flag->{'target'}->{'bug'}->{'id'},
+ $attach_id,
+ $requestee_id,
+ $flag->{'setter'}->{'id'},
+ '$flag->{'status'}',
+ $timestamp,
+ $timestamp)");
+
+ # Send an email notifying the relevant parties about the flag creation.
+ if ($flag->{'requestee'} && $flag->{'requestee'}->email_prefs->{'FlagRequestee'}
+ || $flag->{'type'}->{'cc_list'}) {
+ notify($flag, "request/created-email.txt.tmpl");
+ }
+}
+
+sub migrate {
+ # Moves a flag from one attachment to another. Useful for migrating
+ # a flag from an obsolete attachment to the attachment that obsoleted it.
+
+ my ($old_attach_id, $new_attach_id) = @_;
+
+ # Update the record in the flags table to point to the new attachment.
+ &::SendSQL("UPDATE flags " .
+ "SET attach_id = $new_attach_id , " .
+ " modification_date = NOW() " .
+ "WHERE attach_id = $old_attach_id");
+}
+
+sub modify {
+ # Modifies flags in the database when a user changes them.
+
+ my ($data, $timestamp) = @_;
+
+ # Use the date/time we were given if possible (allowing calling code
+ # to synchronize the comment's timestamp with those of other records).
+ $timestamp = ($timestamp ? &::SqlQuote($timestamp) : "NOW()");
+
+ # Extract a list of flags from the form data.
+ my @ids = map(/^flag-(\d+)$/ ? $1 : (), keys %$data);
+
+ # Loop over flags and update their record in the database.
+ my @flags;
+ foreach my $id (@ids) {
+ my $flag = get($id);
+ my $status = $data->{"flag-$id"};
+
+ # Ignore flags the user didn't change.
+ next if $status eq $flag->{'status'};
+
+ # Since the status is validated, we know it's safe, but it's still
+ # tainted, so we have to detaint it before using it in a query.
+ &::trick_taint($status);
+
+ if ($status eq '+' || $status eq '-') {
+ &::SendSQL("UPDATE flags
+ SET setter_id = $::userid ,
+ status = '$status' ,
+ modification_date = $timestamp
+ WHERE id = $flag->{'id'}");
+
+ # Send an email notifying the relevant parties about the fulfillment.
+ if ($flag->{'setter'}->email_prefs->{'FlagRequester'}
+ || $flag->{'type'}->{'cc_list'})
+ {
+ $flag->{'status'} = $status;
+ notify($flag, "request/fulfilled-email.txt.tmpl");
+ }
+ }
+ elsif ($status eq '?') {
+ &::SendSQL("UPDATE flags
+ SET status = '$status' ,
+ modification_date = $timestamp
+ WHERE id = $flag->{'id'}");
+ }
+ # The user unset the flag, so delete it from the database.
+ elsif ($status eq 'X') {
+ clear($flag->{'id'});
+ }
+
+ push(@flags, $flag);
+ }
+
+ return \@flags;
+}
+
+sub clear {
+ my ($id) = @_;
+
+ my $flag = get($id);
+
+ &::PushGlobalSQLState();
+ &::SendSQL("DELETE FROM flags WHERE id = $id");
+ &::PopGlobalSQLState();
+
+ # Set the flag's status to "cleared" so the email template
+ # knows why email is being sent about the request.
+ $flag->{'status'} = "X";
+
+ notify($flag, "request/fulfilled-email.txt.tmpl") if $flag->{'requestee'};
+}
+
+
+################################################################################
+# Utility Functions
+################################################################################
+
+sub FormToNewFlags {
+ my ($target, $data) = @_;
+
+ # Flag for whether or not we must get verification of the requestees
+ # (if the user did not uniquely identify them).
+ my $verify_requestees = 0;
+
+ # Get information about the setter to add to each flag.
+ # Uses a conditional to suppress Perl's "used only once" warnings.
+ my $setter = new Bugzilla::User($::userid);
+
+ # Extract a list of flag type IDs from field names.
+ my @type_ids = map(/^flag_type-(\d+)$/ ? $1 : (), keys %$data);
+ @type_ids = grep($data->{"flag_type-$_"} ne 'X', @type_ids);
+
+ # Process the form data and create an array of flag objects.
+ my @flags;
+ foreach my $type_id (@type_ids) {
+ my $status = $data->{"flag_type-$type_id"};
+ &::trick_taint($status);
+
+ # Create the flag record and populate it with data from the form.
+ my $flag = {
+ type => Bugzilla::FlagType::get($type_id) ,
+ target => $target ,
+ setter => $setter ,
+ status => $status
+ };
+
+ my $requestee_str = $data->{"requestee-$type_id"} || $data->{'requestee'};
+ if ($requestee_str) {
+ $flag->{'requestee_str'} = $requestee_str;
+ MatchRequestees($flag);
+ $verify_requestees = 1 if scalar(@{$flag->{'requestees'}}) != 1;
+ }
+
+ # Add the flag to the array of flags.
+ push(@flags, $flag);
+ }
+
+ if ($verify_requestees) {
+ $vars->{'target'} = $target;
+ $vars->{'flags'} = \@flags;
+ $vars->{'form'} = $data;
+ $vars->{'mform'} = \%::MFORM || \%::MFORM;
+
+ print "Content-Type: text/html\n\n" unless $vars->{'header_done'};
+ $::template->process("request/verify.html.tmpl", $vars)
+ || &::ThrowTemplateError($template->error());
+ exit;
+ }
+
+ # Return the list of flags.
+ return \@flags;
+}
+
+sub MatchRequestees {
+ my ($flag) = @_;
+
+ my $requestee_str = $flag->{'requestee_str'};
+
+ # To reduce the size of queries, require the user to enter at least
+ # three characters of each requestee's name unless this installation
+ # automatically appends an email suffix to each user's login name,
+ # in which case we can't guarantee their names are at least three
+ # characters long.
+ if (!&Param('emailsuffix') && length($requestee_str) < 3) {
+ &::ThrowUserError("requestee_too_short");
+ }
+
+ # Get a list of potential requestees whose email address or real name
+ # matches the substring entered by the user. Try an exact match first,
+ # then fall back to a substring search. Limit search to 100 matches,
+ # since at that point there are too many to make the user wade through,
+ # and we need to get the user to enter a more constrictive match string.
+ my $user_id = &::DBname_to_id($requestee_str);
+ if ($user_id) { $flag->{'requestees'} = [ new Bugzilla::User($user_id) ] }
+ else { $flag->{'requestees'} = Bugzilla::User::match($requestee_str, 101, 1) }
+
+ # If there is only one requestee match, make them the requestee.
+ if (scalar(@{$flag->{'requestees'}}) == 1) {
+ $flag->{'requestee'} = $flag->{'requestees'}[0];
+ }
+
+ # If there are too many requestee matches, throw an error.
+ elsif (scalar(@{$flag->{'requestees'}}) == 101) {
+ &::ThrowUserError("requestee_too_many_matches",
+ { requestee => $requestee_str });
+ }
+}
+
+
+# Ideally, we'd use Bug.pm, but it's way too heavyweight, and it can't be
+# made lighter without totally rewriting it, so we'll use this function
+# until that one gets rewritten.
+sub GetBug {
+ # Returns a hash of information about a target bug.
+ my ($id) = @_;
+
+ # Save the currently running query (if any) so we do not overwrite it.
+ &::PushGlobalSQLState();
+
+ &::SendSQL("SELECT 1, short_desc, product_id, component_id
+ FROM bugs
+ WHERE bug_id = $id");
+
+ my $bug = { 'id' => $id };
+
+ ($bug->{'exists'}, $bug->{'summary'}, $bug->{'product_id'},
+ $bug->{'component_id'}) = &::FetchSQLData();
+
+ # Restore the previously running query (if any).
+ &::PopGlobalSQLState();
+
+ return $bug;
+}
+
+sub GetTarget {
+ my ($bug_id, $attach_id) = @_;
+
+ # Create an object representing the target bug/attachment.
+ my $target = { 'exists' => 0 };
+
+ if ($attach_id) {
+ $target->{'attachment'} = new Attachment($attach_id);
+ if ($bug_id) {
+ # Make sure the bug and attachment IDs correspond to each other
+ # (i.e. this is the bug to which this attachment is attached).
+ $bug_id == $target->{'attachment'}->{'bug_id'}
+ || return { 'exists' => 0 };
+ }
+ $target->{'bug'} = GetBug($target->{'attachment'}->{'bug_id'});
+ $target->{'exists'} = $target->{'attachment'}->{'exists'};
+ $target->{'type'} = "attachment";
+ }
+ elsif ($bug_id) {
+ $target->{'bug'} = GetBug($bug_id);
+ $target->{'exists'} = $target->{'bug'}->{'exists'};
+ $target->{'type'} = "bug";
+ }
+
+ return $target;
+}
+
+sub notify {
+ # Sends an email notification about a flag being created or fulfilled.
+
+ my ($flag, $template_file) = @_;
+
+ # Work around the intricacies of globals.pl not being templatized
+ # by defining local variables for the $::template and $::vars globals.
+ my $template = $::template;
+ my $vars = $::vars;
+
+ $vars->{'flag'} = $flag;
+
+ my $message;
+ my $rv =
+ $template->process($template_file, $vars, \$message);
+ if (!$rv) {
+ print "Content-Type: text/html\n\n" unless $vars->{'header_done'};
+ &::ThrowTemplateError($template->error());
+ }
+
+ my $delivery_mode = &::Param("sendmailnow") ? "" : "-ODeliveryMode=deferred";
+ open(SENDMAIL, "|/usr/lib/sendmail $delivery_mode -t -i")
+ || die "Can't open sendmail";
+ print SENDMAIL $message;
+ close(SENDMAIL);
+}
+
+################################################################################
+# Private Functions
+################################################################################
+
+sub sqlify_criteria {
+ # Converts a hash of criteria into a list of SQL criteria.
+
+ # a reference to a hash containing the criteria (field => value)
+ my ($criteria) = @_;
+
+ # the generated list of SQL criteria; "1=1" is a clever way of making sure
+ # there's something in the list so calling code doesn't have to check list
+ # size before building a WHERE clause out of it
+ my @criteria = ("1=1");
+
+ # If the caller specified only bug or attachment flags,
+ # limit the query to those kinds of flags.
+ if (defined($criteria->{'target_type'})) {
+ if ($criteria->{'target_type'} eq 'bug') { push(@criteria, "attach_id IS NULL") }
+ elsif ($criteria->{'target_type'} eq 'attachment') { push(@criteria, "attach_id IS NOT NULL") }
+ }
+
+ # Go through each criterion from the calling code and add it to the query.
+ foreach my $field (keys %$criteria) {
+ my $value = $criteria->{$field};
+ next unless defined($value);
+ if ($field eq 'type_id') { push(@criteria, "type_id = $value") }
+ elsif ($field eq 'bug_id') { push(@criteria, "bug_id = $value") }
+ elsif ($field eq 'attach_id') { push(@criteria, "attach_id = $value") }
+ elsif ($field eq 'requestee_id') { push(@criteria, "requestee_id = $value") }
+ elsif ($field eq 'setter_id') { push(@criteria, "setter_id = $value") }
+ elsif ($field eq 'status') { push(@criteria, "status = '$value'") }
+ }
+
+ return @criteria;
+}
+
+sub perlify_record {
+ # Converts a row from the database into a Perl record.
+ my ($exists, $id, $type_id, $bug_id, $attach_id,
+ $requestee_id, $setter_id, $status) = @_;
+
+ my $flag =
+ {
+ exists => $exists ,
+ id => $id ,
+ type => Bugzilla::FlagType::get($type_id) ,
+ target => GetTarget($bug_id, $attach_id) ,
+ requestee => new Bugzilla::User($requestee_id) ,
+ setter => new Bugzilla::User($setter_id) ,
+ status => $status ,
+ };
+
+ return $flag;
+}
+
+1;
diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm
new file mode 100644
index 000000000..2e272f67c
--- /dev/null
+++ b/Bugzilla/FlagType.pm
@@ -0,0 +1,325 @@
+# -*- 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): Myk Melez <myk@mozilla.org>
+
+################################################################################
+# Module Initialization
+################################################################################
+
+# Make it harder for us to do dangerous things in Perl.
+use strict;
+
+# This module implements flag types for the flag tracker.
+package Bugzilla::FlagType;
+
+# Use Bugzilla's User module which contains utilities for handling users.
+use Bugzilla::User;
+
+# Note! This module requires that its caller have said "require CGI.pl"
+# to import relevant functions from that script and its companion globals.pl.
+
+################################################################################
+# Global Variables
+################################################################################
+
+# basic sets of columns and tables for getting flag types from the database
+
+my @base_columns =
+ ("1", "flagtypes.id", "flagtypes.name", "flagtypes.description",
+ "flagtypes.cc_list", "flagtypes.target_type", "flagtypes.sortkey",
+ "flagtypes.is_active", "flagtypes.is_requestable",
+ "flagtypes.is_requesteeble", "flagtypes.is_multiplicable");
+
+# Note: when adding tables to @base_tables, make sure to include the separator
+# (i.e. a comma or words like "LEFT OUTER JOIN") before the table name,
+# since tables take multiple separators based on the join type, and therefore
+# it is not possible to join them later using a single known separator.
+
+my @base_tables = ("flagtypes");
+
+################################################################################
+# Public Functions
+################################################################################
+
+sub get {
+ # Returns a hash of information about a flag type.
+
+ my ($id) = @_;
+
+ my $select_clause = "SELECT " . join(", ", @base_columns);
+ my $from_clause = "FROM " . join(" ", @base_tables);
+
+ &::PushGlobalSQLState();
+ &::SendSQL("$select_clause $from_clause WHERE flagtypes.id = $id");
+ my @data = &::FetchSQLData();
+ my $type = perlify_record(@data);
+ &::PopGlobalSQLState();
+
+ return $type;
+}
+
+sub get_inclusions {
+ my ($id) = @_;
+ return get_clusions($id, "in");
+}
+
+sub get_exclusions {
+ my ($id) = @_;
+ return get_clusions($id, "ex");
+}
+
+sub get_clusions {
+ my ($id, $type) = @_;
+
+ &::PushGlobalSQLState();
+ &::SendSQL("SELECT products.name, components.name " .
+ "FROM flagtypes, flag${type}clusions " .
+ "LEFT OUTER JOIN products ON flag${type}clusions.product_id = products.id " .
+ "LEFT OUTER JOIN components ON flag${type}clusions.component_id = components.id " .
+ "WHERE flagtypes.id = $id AND flag${type}clusions.type_id = flagtypes.id");
+ my @clusions = ();
+ while (&::MoreSQLData()) {
+ my ($product, $component) = &::FetchSQLData();
+ $product ||= "Any";
+ $component ||= "Any";
+ push(@clusions, "$product:$component");
+ }
+ &::PopGlobalSQLState();
+
+ return \@clusions;
+}
+
+sub match {
+ # Queries the database for flag types matching the given criteria
+ # and returns the set of matching types.
+
+ my ($criteria, $include_count) = @_;
+
+ my @tables = @base_tables;
+ my @columns = @base_columns;
+ my $having = "";
+
+ # Include a count of the number of flags per type if requested.
+ if ($include_count) {
+ push(@columns, "COUNT(flags.id)");
+ push(@tables, "LEFT OUTER JOIN flags ON flagtypes.id = flags.type_id");
+ }
+
+ # Generate the SQL WHERE criteria.
+ my @criteria = sqlify_criteria($criteria, \@tables, \@columns, \$having);
+
+ # Build the query, grouping the types if we are counting flags.
+ my $select_clause = "SELECT " . join(", ", @columns);
+ my $from_clause = "FROM " . join(" ", @tables);
+ my $where_clause = "WHERE " . join(" AND ", @criteria);
+
+ my $query = "$select_clause $from_clause $where_clause";
+ $query .= " GROUP BY flagtypes.id " if ($include_count || $having ne "");
+ $query .= " HAVING $having " if $having ne "";
+ $query .= " ORDER BY flagtypes.sortkey, flagtypes.name";
+
+ # Execute the query and retrieve the results.
+ &::PushGlobalSQLState();
+ &::SendSQL($query);
+ my @types;
+ while (&::MoreSQLData()) {
+ my @data = &::FetchSQLData();
+ my $type = perlify_record(@data);
+ push(@types, $type);
+ }
+ &::PopGlobalSQLState();
+
+ return \@types;
+}
+
+sub count {
+ # Returns the total number of flag types matching the given criteria.
+
+ my ($criteria) = @_;
+
+ # Generate query components.
+ my @tables = @base_tables;
+ my @columns = ("COUNT(flagtypes.id)");
+ my $having = "";
+ my @criteria = sqlify_criteria($criteria, \@tables, \@columns, \$having);
+
+ # Build the query.
+ my $select_clause = "SELECT " . join(", ", @columns);
+ my $from_clause = "FROM " . join(" ", @tables);
+ my $where_clause = "WHERE " . join(" AND ", @criteria);
+ my $query = "$select_clause $from_clause $where_clause";
+ $query .= " GROUP BY flagtypes.id HAVING $having " if $having ne "";
+
+ # Execute the query and get the results.
+ &::PushGlobalSQLState();
+ &::SendSQL($query);
+ my $count = &::FetchOneColumn();
+ &::PopGlobalSQLState();
+
+ return $count;
+}
+
+sub validate {
+ my ($data) = @_;
+
+ # Get a list of flags types to validate. Uses the "map" function
+ # to extract flag type IDs from form field names by matching columns
+ # whose name looks like "flag_type-nnn", where "nnn" is the ID,
+ # and returning just the ID portion of matching field names.
+ my @ids = map(/^flag_type-(\d+)$/ ? $1 : (), keys %$data);
+
+ foreach my $id (@ids)
+ {
+ my $status = $data->{"flag_type-$id"};
+
+ # Don't bother validating types the user didn't touch.
+ next if $status eq "X";
+
+ # Make sure the flag exists.
+ get($id)
+ || &::ThrowCodeError("flag_type_nonexistent", { id => $id });
+
+ # Make sure the value of the field is a valid status.
+ grep($status eq $_, qw(X + - ?))
+ || &::ThrowCodeError("flag_status_invalid",
+ { id => $id , status => $status });
+ }
+}
+
+sub normalize {
+ # Given a list of flag types, checks its flags to make sure they should
+ # still exist after a change to the inclusions/exclusions lists.
+
+ # A list of IDs of flag types to normalize.
+ my (@ids) = @_;
+
+ my $ids = join(", ", @ids);
+
+ # Check for flags whose product/component is no longer included.
+ &::SendSQL("
+ SELECT flags.id
+ FROM flags, bugs LEFT OUTER JOIN flaginclusions AS i
+ ON (flags.type_id = i.type_id
+ AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
+ AND (bugs.component_id = i.component_id OR i.component_id IS NULL))
+ WHERE flags.type_id IN ($ids)
+ AND flags.bug_id = bugs.bug_id
+ AND i.type_id IS NULL
+ ");
+ Bugzilla::Flag::clear(&::FetchOneColumn()) while &::MoreSQLData();
+
+ &::SendSQL("
+ SELECT flags.id
+ FROM flags, bugs, flagexclusions AS e
+ WHERE flags.type_id IN ($ids)
+ AND flags.bug_id = bugs.bug_id
+ AND flags.type_id = e.type_id
+ AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
+ AND (bugs.component_id = e.component_id OR e.component_id IS NULL)
+ ");
+ Bugzilla::Flag::clear(&::FetchOneColumn()) while &::MoreSQLData();
+}
+
+################################################################################
+# Private Functions
+################################################################################
+
+sub sqlify_criteria {
+ # Converts a hash of criteria into a list of SQL criteria.
+ # $criteria is a reference to the criteria (field => value),
+ # $tables is a reference to an array of tables being accessed
+ # by the query, $columns is a reference to an array of columns
+ # being returned by the query, and $having is a reference to
+ # a criterion to put into the HAVING clause.
+ my ($criteria, $tables, $columns, $having) = @_;
+
+ # the generated list of SQL criteria; "1=1" is a clever way of making sure
+ # there's something in the list so calling code doesn't have to check list
+ # size before building a WHERE clause out of it
+ my @criteria = ("1=1");
+
+ if ($criteria->{name}) {
+ push(@criteria, "flagtypes.name = " . &::SqlQuote($criteria->{name}));
+ }
+ if ($criteria->{target_type}) {
+ # The target type is stored in the database as a one-character string
+ # ("a" for attachment and "b" for bug), but this function takes complete
+ # names ("attachment" and "bug") for clarity, so we must convert them.
+ my $target_type = &::SqlQuote(substr($criteria->{target_type}, 0, 1));
+ push(@criteria, "flagtypes.target_type = $target_type");
+ }
+ if (exists($criteria->{is_active})) {
+ my $is_active = $criteria->{is_active} ? "1" : "0";
+ push(@criteria, "flagtypes.is_active = $is_active");
+ }
+ if ($criteria->{product_id} && $criteria->{'component_id'}) {
+ my $product_id = $criteria->{product_id};
+ my $component_id = $criteria->{component_id};
+
+ # Add inclusions to the query, which simply involves joining the table
+ # by flag type ID and target product/component.
+ push(@$tables, ", flaginclusions");
+ push(@criteria, "flagtypes.id = flaginclusions.type_id");
+ push(@criteria, "(flaginclusions.product_id = $product_id " .
+ " OR flaginclusions.product_id IS NULL)");
+ push(@criteria, "(flaginclusions.component_id = $component_id " .
+ " OR flaginclusions.component_id IS NULL)");
+
+ # Add exclusions to the query, which is more complicated. First of all,
+ # we do a LEFT JOIN so we don't miss flag types with no exclusions.
+ # Then, as with inclusions, we join on flag type ID and target product/
+ # component. However, since we want flag types that *aren't* on the
+ # exclusions list, we count the number of exclusions records returned
+ # and use a HAVING clause to weed out types with one or more exclusions.
+ my $join_clause = "flagtypes.id = flagexclusions.type_id " .
+ "AND (flagexclusions.product_id = $product_id " .
+ "OR flagexclusions.product_id IS NULL) " .
+ "AND (flagexclusions.component_id = $component_id " .
+ "OR flagexclusions.component_id IS NULL)";
+ push(@$tables, "LEFT JOIN flagexclusions ON ($join_clause)");
+ push(@$columns, "COUNT(flagexclusions.type_id) AS num_exclusions");
+ $$having = "num_exclusions = 0";
+ }
+
+ return @criteria;
+}
+
+sub perlify_record {
+ # Converts data retrieved from the database into a Perl record.
+
+ my $type = {};
+
+ $type->{'exists'} = $_[0];
+ $type->{'id'} = $_[1];
+ $type->{'name'} = $_[2];
+ $type->{'description'} = $_[3];
+ $type->{'cc_list'} = $_[4];
+ $type->{'target_type'} = $_[5] eq "b" ? "bug" : "attachment";
+ $type->{'sortkey'} = $_[6];
+ $type->{'is_active'} = $_[7];
+ $type->{'is_requestable'} = $_[8];
+ $type->{'is_requesteeble'} = $_[9];
+ $type->{'is_multiplicable'} = $_[10];
+ $type->{'flag_count'} = $_[11];
+
+ return $type;
+}
+
+1;
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index 642965eb2..6d11c0739 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -62,6 +62,7 @@ sub init {
my @fields;
my @supptables;
my @wherepart;
+ my @having = ("(cntuseringroups = cntbugingroups OR canseeanyway)");
@fields = @$fieldsref if $fieldsref;
my %F;
my %M;
@@ -265,8 +266,8 @@ sub init {
}
my $chartid;
- # $statusid is used by the code that queries for attachment statuses.
- my $statusid = 0;
+ # $type_id is used by the code that queries for attachment flags.
+ my $type_id = 0;
my $f;
my $ff;
my $t;
@@ -358,69 +359,61 @@ sub init {
}
$f = "$table.$field";
},
- "^attachstatusdefs.name," => sub {
- # The below has Fun with the names for attachment statuses. This
- # isn't needed for changed* queries, so exclude those - the
- # generic stuff will cope
+ "^flagtypes.name," => sub {
+ # Matches bugs by flag name/status.
+ # Note that--for the purposes of querying--a flag comprises
+ # its name plus its status (i.e. a flag named "review"
+ # with a status of "+" can be found by searching for "review+").
+
+ # Don't do anything if this condition is about changes to flags,
+ # as the generic change condition processors can handle those.
return if ($t =~ m/^changed/);
-
- # Searching for "status != 'bar'" wants us to look for an
- # attachment without the 'bar' status, not for an attachment with
- # a status not equal to 'bar' (Which would pick up an attachment
- # with more than one status). We do this by LEFT JOINS, after
- # grabbing the matching attachment status ids.
- # Note that this still won't find bugs with no attachments, since
- # that isn't really what people would expect.
-
- # First, get the attachment status ids, using the other funcs
- # to match the WHERE term.
- # Note that we need to reverse the negated bits for this to work
- # This somewhat abuses the definitions of the various terms -
- # eg, does 'contains all' mean that the status has to contain all
- # those words, or that all those words must be exact matches to
- # statuses, which must all be on a single attachment, or should
- # the match on the status descriptions be a contains match, too?
-
- my $inverted = 0;
- if ($t =~ m/not(.*)/) {
- $t = $1;
- $inverted = 1;
- }
-
- $ref = $funcsbykey{",$t"};
- &$ref;
- &::SendSQL("SELECT id FROM attachstatusdefs WHERE $term");
-
- my @as_ids;
- while (&::MoreSQLData()) {
- push @as_ids, &::FetchOneColumn();
- }
-
- # When searching for multiple statuses within a single boolean chart,
- # we want to match each status record separately. In other words,
- # "status = 'foo' AND status = 'bar'" should match attachments with
- # one status record equal to "foo" and another one equal to "bar",
- # not attachments where the same status record equals both "foo" and
- # "bar" (which is nonsensical). In order to do this we must add an
- # additional counter to the end of the "attachstatuses" table
- # reference.
- ++$statusid;
-
- my $attachtable = "attachments_$chartid";
- my $statustable = "attachstatuses_${chartid}_$statusid";
-
- push(@supptables, "attachments $attachtable");
- my $join = "LEFT JOIN attachstatuses $statustable ON ".
- "($attachtable.attach_id = $statustable.attach_id AND " .
- "$statustable.statusid IN (" . join(",", @as_ids) . "))";
- push(@supptables, $join);
- push(@wherepart, "bugs.bug_id = $attachtable.bug_id");
- if ($inverted) {
- $term = "$statustable.statusid IS NULL";
- } else {
- $term = "$statustable.statusid IS NOT NULL";
+
+ # Add the flags and flagtypes tables to the query. We do
+ # a left join here so bugs without any flags still match
+ # negative conditions (f.e. "flag isn't review+").
+ my $flags = "flags_$chartid";
+ push(@supptables, "LEFT JOIN flags $flags " .
+ "ON bugs.bug_id = $flags.bug_id");
+ my $flagtypes = "flagtypes_$chartid";
+ push(@supptables, "LEFT JOIN flagtypes $flagtypes " .
+ "ON $flags.type_id = $flagtypes.id");
+
+ # Generate the condition by running the operator-specific function.
+ # Afterwards the condition resides in the global $term variable.
+ $ff = "CONCAT($flagtypes.name, $flags.status)";
+ &{$funcsbykey{",$t"}};
+
+ # If this is a negative condition (f.e. flag isn't "review+"),
+ # we only want bugs where all flags match the condition, not
+ # those where any flag matches, which needs special magic.
+ # Instead of adding the condition to the WHERE clause, we select
+ # the number of flags matching the condition and the total number
+ # of flags on each bug, then compare them in a HAVING clause.
+ # If the numbers are the same, all flags match the condition,
+ # so this bug should be included.
+ if ($t =~ m/not/) {
+ push(@fields, "SUM($ff IS NOT NULL) AS allflags_$chartid");
+ push(@fields, "SUM($term) AS matchingflags_$chartid");
+ push(@having, "allflags_$chartid = matchingflags_$chartid");
+ $term = "0=0";
}
},
+ "^requesters.login_name," => sub {
+ push(@supptables, "flags flags_$chartid");
+ push(@wherepart, "bugs.bug_id = flags_$chartid.bug_id");
+ push(@supptables, "profiles requesters_$chartid");
+ push(@wherepart, "flags_$chartid.requester_id = requesters_$chartid.userid");
+ $f = "requesters_$chartid.login_name";
+ },
+ "^setters.login_name," => sub {
+ push(@supptables, "flags flags_$chartid");
+ push(@wherepart, "bugs.bug_id = flags_$chartid.bug_id");
+ push(@supptables, "profiles setters_$chartid");
+ push(@wherepart, "flags_$chartid.setter_id = setters_$chartid.userid");
+ $f = "setters_$chartid.login_name";
+ },
+
"^changedin," => sub {
$f = "(to_days(now()) - to_days(bugs.delta_ts))";
},
@@ -817,8 +810,7 @@ sub init {
# Make sure we create a legal SQL query.
@andlist = ("1 = 1") if !@andlist;
- my $query = ("SELECT DISTINCT " .
- join(', ', @fields) .
+ my $query = ("SELECT " . join(', ', @fields) .
", COUNT(DISTINCT ugmap.group_id) AS cntuseringroups, " .
" COUNT(DISTINCT bgmap.group_id) AS cntbugingroups, " .
" ((COUNT(DISTINCT ccmap.who) AND cclist_accessible) " .
@@ -834,11 +826,9 @@ sub init {
" LEFT JOIN cc AS ccmap " .
" ON ccmap.who = $::userid AND ccmap.bug_id = bugs.bug_id " .
" WHERE " . join(' AND ', (@wherepart, @andlist)) .
- " GROUP BY bugs.bug_id " .
- " HAVING cntuseringroups = cntbugingroups" .
- " OR canseeanyway"
- );
-
+ " GROUP BY bugs.bug_id" .
+ " HAVING " . join(" AND ", @having));
+
if ($debug) {
print "<p><code>" . value_quote($query) . "</code></p>\n";
exit;
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
new file mode 100644
index 000000000..72870d544
--- /dev/null
+++ b/Bugzilla/User.pm
@@ -0,0 +1,176 @@
+# -*- 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): Myk Melez <myk@mozilla.org>
+
+################################################################################
+# Module Initialization
+################################################################################
+
+# Make it harder for us to do dangerous things in Perl.
+use strict;
+
+# This module implements utilities for dealing with Bugzilla users.
+package Bugzilla::User;
+
+################################################################################
+# Functions
+################################################################################
+
+my $user_cache = {};
+sub new {
+ # Returns a hash of information about a particular user.
+
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+
+ my $exists = 1;
+ my ($id, $name, $email) = @_;
+
+ return undef if !$id;
+ return $user_cache->{$id} if exists($user_cache->{$id});
+
+ my $self = { 'id' => $id };
+
+ bless($self, $class);
+
+ if (!$name && !$email) {
+ &::PushGlobalSQLState();
+ &::SendSQL("SELECT 1, realname, login_name FROM profiles WHERE userid = $id");
+ ($exists, $name, $email) = &::FetchSQLData();
+ &::PopGlobalSQLState();
+ }
+
+ $self->{'name'} = $name;
+ $self->{'email'} = $email;
+ $self->{'exists'} = $exists;
+
+ # Generate a string to identify the user by name + email if the user
+ # has a name or by email only if she doesn't.
+ $self->{'identity'} = $name ? "$name <$email>" : $email;
+
+ # Generate a user "nickname" -- i.e. a shorter, not-necessarily-unique name
+ # by which to identify the user. Currently the part of the user's email
+ # address before the at sign (@), but that could change, especially if we
+ # implement usernames not dependent on email address.
+ my @email_components = split("@", $email);
+ $self->{'nick'} = $email_components[0];
+
+ $user_cache->{$id} = $self;
+
+ return $self;
+}
+
+sub match {
+ # Generates a list of users whose login name (email address) or real name
+ # matches a substring.
+
+ # $str contains the string to match against, while $limit contains the
+ # maximum number of records to retrieve.
+ my ($str, $limit, $exclude_disabled) = @_;
+
+ # Build the query.
+ my $sqlstr = &::SqlQuote($str);
+ my $qry = "
+ SELECT userid, realname, login_name
+ FROM profiles
+ WHERE (INSTR(login_name, $sqlstr) OR INSTR(realname, $sqlstr))
+ ";
+ $qry .= "AND disabledtext = '' " if $exclude_disabled;
+ $qry .= "ORDER BY realname, login_name ";
+ $qry .= "LIMIT $limit " if $limit;
+
+ # Execute the query, retrieve the results, and make them into User objects.
+ my @users;
+ &::PushGlobalSQLState();
+ &::SendSQL($qry);
+ push(@users, new Bugzilla::User(&::FetchSQLData())) while &::MoreSQLData();
+ &::PopGlobalSQLState();
+
+ return \@users;
+}
+
+sub email_prefs {
+ # Get or set (not implemented) the user's email notification preferences.
+
+ my $self = shift;
+
+ # If the calling code is setting the email preferences, update the object
+ # but don't do anything else. This needs to write email preferences back
+ # to the database.
+ if (@_) { $self->{email_prefs} = shift; return; }
+
+ # If we already got them from the database, return the existing values.
+ return $self->{email_prefs} if $self->{email_prefs};
+
+ # Retrieve the values from the database.
+ &::SendSQL("SELECT emailflags FROM profiles WHERE userid = $self->{id}");
+ my ($flags) = &::FetchSQLData();
+
+ my @roles = qw(Owner Reporter QAcontact CClist Voter);
+ my @reasons = qw(Removeme Comments Attachments Status Resolved Keywords
+ CC Other Unconfirmed);
+
+ # If the prefs are empty, this user hasn't visited the email pane
+ # of userprefs.cgi since before the change to use the "emailflags"
+ # column, so initialize that field with the default prefs.
+ if (!$flags) {
+ # Create a default prefs string that causes the user to get all email.
+ $flags = "ExcludeSelf~on~FlagRequestee~on~FlagRequester~on~";
+ foreach my $role (@roles) {
+ foreach my $reason (@reasons) {
+ $flags .= "email$role$reason~on~";
+ }
+ }
+ chop $flags;
+ }
+
+ # Convert the prefs from the flags string from the database into
+ # a Perl record. The 255 param is here because split will trim
+ # any trailing null fields without a third param, which causes Perl
+ # to eject lots of warnings. Any suitably large number would do.
+ my $prefs = { split(/~/, $flags, 255) };
+
+ # Determine the value of the "excludeself" global email preference.
+ # Note that the value of "excludeself" is assumed to be off if the
+ # preference does not exist in the user's list, unlike other
+ # preferences whose value is assumed to be on if they do not exist.
+ $prefs->{ExcludeSelf} =
+ exists($prefs->{ExcludeSelf}) && $prefs->{ExcludeSelf} eq "on";
+
+ # Determine the value of the global request preferences.
+ foreach my $pref qw(FlagRequestee FlagRequester) {
+ $prefs->{$pref} = !exists($prefs->{$pref}) || $prefs->{$pref} eq "on";
+ }
+
+ # Determine the value of the rest of the preferences by looping over
+ # all roles and reasons and converting their values to Perl booleans.
+ foreach my $role (@roles) {
+ foreach my $reason (@reasons) {
+ my $key = "email$role$reason";
+ $prefs->{$key} = !exists($prefs->{$key}) || $prefs->{$key} eq "on";
+ }
+ }
+
+ $self->{email_prefs} = $prefs;
+
+ return $self->{email_prefs};
+}
+
+1;
diff --git a/attachment.cgi b/attachment.cgi
index 4d5fea475..b185312c6 100755
--- a/attachment.cgi
+++ b/attachment.cgi
@@ -46,6 +46,10 @@ if ($^O eq 'MSWin32') {
# Include the Bugzilla CGI and general utility library.
require "CGI.pl";
+# Use these modules to handle flags.
+use Bugzilla::Flag;
+use Bugzilla::FlagType;
+
# Establish a connection to the database backend.
ConnectToDatabase();
@@ -110,7 +114,8 @@ elsif ($action eq "update")
validateContentType() unless $::FORM{'ispatch'};
validateIsObsolete();
validatePrivate();
- validateStatuses();
+ Bugzilla::Flag::validate(\%::FORM);
+ Bugzilla::FlagType::validate(\%::FORM);
update();
}
else
@@ -240,29 +245,6 @@ sub validatePrivate
$::FORM{'isprivate'} = $::FORM{'isprivate'} ? 1 : 0;
}
-sub validateStatuses
-{
- # Get a list of attachment statuses that are valid for this attachment.
- PushGlobalSQLState();
- SendSQL("SELECT attachstatusdefs.id
- FROM attachments, bugs, attachstatusdefs
- WHERE attachments.attach_id = $::FORM{'id'}
- AND attachments.bug_id = bugs.bug_id
- AND attachstatusdefs.product_id = bugs.product_id");
- my @statusdefs;
- push(@statusdefs, FetchSQLData()) while MoreSQLData();
- PopGlobalSQLState();
-
- foreach my $status (@{$::MFORM{'status'}})
- {
- grep($_ == $status, @statusdefs)
- || ThrowUserError("invalid_attach_status");
-
- # We have tested that the status is valid, so it can be detainted
- detaint_natural($status);
- }
-}
-
sub validateData
{
$::FORM{'data'}
@@ -380,18 +362,6 @@ sub viewall
# !!! Yuck, what an ugly hack. Fix it!
$a{'isviewable'} = ( $a{'contenttype'} =~ /^(text|image|application\/vnd\.mozilla\.)/ );
- # Retrieve a list of status flags that have been set on the attachment.
- PushGlobalSQLState();
- SendSQL("SELECT name
- FROM attachstatuses, attachstatusdefs
- WHERE attach_id = $a{'attachid'}
- AND attachstatuses.statusid = attachstatusdefs.id
- ORDER BY sortkey");
- my @statuses;
- push(@statuses, FetchSQLData()) while MoreSQLData();
- $a{'statuses'} = \@statuses;
- PopGlobalSQLState();
-
# Add the hash representing the attachment to the array of attachments.
push @attachments, \%a;
}
@@ -491,10 +461,14 @@ sub insert
# Make existing attachments obsolete.
my $fieldid = GetFieldID('attachments.isobsolete');
- foreach my $attachid (@{$::MFORM{'obsolete'}}) {
- SendSQL("UPDATE attachments SET isobsolete = 1 WHERE attach_id = $attachid");
+ foreach my $obsolete_id (@{$::MFORM{'obsolete'}}) {
+ SendSQL("UPDATE attachments SET isobsolete = 1 WHERE attach_id = $obsolete_id");
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
- VALUES ($::FORM{'bugid'}, $attachid, $::userid, NOW(), $fieldid, '0', '1')");
+ VALUES ($::FORM{'bugid'}, $obsolete_id, $::userid, NOW(), $fieldid, '0', '1')");
+ # If the obsolete attachment has pending flags, migrate them to the new attachment.
+ if (Bugzilla::Flag::count({ 'attach_id' => $obsolete_id , 'status' => 'pending' })) {
+ Bugzilla::Flag::migrate($obsolete_id, $attachid);
+ }
}
# Send mail to let people know the attachment has been created. Uses a
@@ -544,32 +518,6 @@ sub edit
# !!! Yuck, what an ugly hack. Fix it!
my $isviewable = ( $contenttype =~ /^(text|image|application\/vnd\.mozilla\.)/ );
- # Retrieve a list of status flags that have been set on the attachment.
- my %statuses;
- SendSQL("SELECT id, name
- FROM attachstatuses JOIN attachstatusdefs
- WHERE attachstatuses.statusid = attachstatusdefs.id
- AND attach_id = $::FORM{'id'}");
- while ( my ($id, $name) = FetchSQLData() )
- {
- $statuses{$id} = $name;
- }
-
- # Retrieve a list of statuses for this bug's product, and build an array
- # of hashes in which each hash is a status flag record.
- # ???: Move this into versioncache or its own routine?
- my @statusdefs;
- SendSQL("SELECT id, name
- FROM attachstatusdefs, bugs
- WHERE bug_id = $bugid
- AND attachstatusdefs.product_id = bugs.product_id
- ORDER BY sortkey");
- while ( MoreSQLData() )
- {
- my ($id, $name) = FetchSQLData();
- push @statusdefs, { 'id' => $id , 'name' => $name };
- }
-
# Retrieve a list of attachments for this bug as well as a summary of the bug
# to use in a navigation bar across the top of the screen.
SendSQL("SELECT attach_id FROM attachments WHERE bug_id = $bugid ORDER BY attach_id");
@@ -577,7 +525,20 @@ sub edit
push(@bugattachments, FetchSQLData()) while (MoreSQLData());
SendSQL("SELECT short_desc FROM bugs WHERE bug_id = $bugid");
my ($bugsummary) = FetchSQLData();
-
+
+ # Get a list of flag types that can be set for this attachment.
+ SendSQL("SELECT product_id, component_id FROM bugs WHERE bug_id = $bugid");
+ my ($product_id, $component_id) = FetchSQLData();
+ my $flag_types = Bugzilla::FlagType::match({ 'target_type' => 'attachment' ,
+ 'product_id' => $product_id ,
+ 'component_id' => $component_id ,
+ 'is_active' => 1});
+ foreach my $flag_type (@$flag_types) {
+ $flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id' => $flag_type->{'id'},
+ 'attach_id' => $::FORM{'id'} });
+ }
+ $vars->{'flag_types'} = $flag_types;
+
# Define the variables and functions that will be passed to the UI template.
$vars->{'attachid'} = $::FORM{'id'};
$vars->{'description'} = $description;
@@ -589,8 +550,6 @@ sub edit
$vars->{'isobsolete'} = $isobsolete;
$vars->{'isprivate'} = $isprivate;
$vars->{'isviewable'} = $isviewable;
- $vars->{'statuses'} = \%statuses;
- $vars->{'statusdefs'} = \@statusdefs;
$vars->{'attachments'} = \@bugattachments;
# Return the appropriate HTTP response headers.
@@ -604,7 +563,7 @@ sub edit
sub update
{
- # Update an attachment record.
+ # Updates an attachment record.
# Get the bug ID for the bug to which this attachment is attached.
SendSQL("SELECT bug_id FROM attachments WHERE attach_id = $::FORM{'id'}");
@@ -616,8 +575,11 @@ sub update
}
# Lock database tables in preparation for updating the attachment.
- SendSQL("LOCK TABLES attachments WRITE , attachstatuses WRITE ,
- attachstatusdefs READ , fielddefs READ , bugs_activity WRITE");
+ SendSQL("LOCK TABLES attachments WRITE , flags WRITE , " .
+ "flagtypes READ , fielddefs READ , bugs_activity WRITE, " .
+ "flaginclusions AS i READ, flagexclusions AS e READ, " .
+ "bugs READ, profiles READ");
+
# Get a copy of the attachment record before we make changes
# so we can record those changes in the activity table.
SendSQL("SELECT description, mimetype, filename, ispatch, isobsolete, isprivate
@@ -625,41 +587,6 @@ sub update
my ($olddescription, $oldcontenttype, $oldfilename, $oldispatch,
$oldisobsolete, $oldisprivate) = FetchSQLData();
- # Get the list of old status flags.
- SendSQL("SELECT attachstatusdefs.name
- FROM attachments, attachstatuses, attachstatusdefs
- WHERE attachments.attach_id = $::FORM{'id'}
- AND attachments.attach_id = attachstatuses.attach_id
- AND attachstatuses.statusid = attachstatusdefs.id
- ORDER BY attachstatusdefs.sortkey
- ");
- my @oldstatuses;
- while (MoreSQLData()) {
- push(@oldstatuses, FetchSQLData());
- }
- my $oldstatuslist = join(', ', @oldstatuses);
-
- # Update the database with the new status flags.
- SendSQL("DELETE FROM attachstatuses WHERE attach_id = $::FORM{'id'}");
- foreach my $statusid (@{$::MFORM{'status'}})
- {
- SendSQL("INSERT INTO attachstatuses (attach_id, statusid) VALUES ($::FORM{'id'}, $statusid)");
- }
-
- # Get the list of new status flags.
- SendSQL("SELECT attachstatusdefs.name
- FROM attachments, attachstatuses, attachstatusdefs
- WHERE attachments.attach_id = $::FORM{'id'}
- AND attachments.attach_id = attachstatuses.attach_id
- AND attachstatuses.statusid = attachstatusdefs.id
- ORDER BY attachstatusdefs.sortkey
- ");
- my @newstatuses;
- while (MoreSQLData()) {
- push(@newstatuses, FetchSQLData());
- }
- my $newstatuslist = join(', ', @newstatuses);
-
# Quote the description and content type for use in the SQL UPDATE statement.
my $quoteddescription = SqlQuote($::FORM{'description'});
my $quotedcontenttype = SqlQuote($::FORM{'contenttype'});
@@ -677,18 +604,23 @@ sub update
WHERE attach_id = $::FORM{'id'}
");
+ # Figure out when the changes were made.
+ SendSQL("SELECT NOW()");
+ my $timestamp = FetchOneColumn();
+
# Record changes in the activity table.
+ my $sql_timestamp = SqlQuote($timestamp);
if ($olddescription ne $::FORM{'description'}) {
my $quotedolddescription = SqlQuote($olddescription);
my $fieldid = GetFieldID('attachments.description');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
- VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $quotedolddescription, $quoteddescription)");
+ VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $quotedolddescription, $quoteddescription)");
}
if ($oldcontenttype ne $::FORM{'contenttype'}) {
my $quotedoldcontenttype = SqlQuote($oldcontenttype);
my $fieldid = GetFieldID('attachments.mimetype');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
- VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $quotedoldcontenttype, $quotedcontenttype)");
+ VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $quotedoldcontenttype, $quotedcontenttype)");
}
if ($oldfilename ne $::FORM{'filename'}) {
my $quotedoldfilename = SqlQuote($oldfilename);
@@ -699,48 +631,26 @@ sub update
if ($oldispatch ne $::FORM{'ispatch'}) {
my $fieldid = GetFieldID('attachments.ispatch');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
- VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $oldispatch, $::FORM{'ispatch'})");
+ VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $oldispatch, $::FORM{'ispatch'})");
}
if ($oldisobsolete ne $::FORM{'isobsolete'}) {
my $fieldid = GetFieldID('attachments.isobsolete');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
- VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $oldisobsolete, $::FORM{'isobsolete'})");
+ VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $oldisobsolete, $::FORM{'isobsolete'})");
}
if ($oldisprivate ne $::FORM{'isprivate'}) {
my $fieldid = GetFieldID('attachments.isprivate');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $oldisprivate, $::FORM{'isprivate'})");
}
- if ($oldstatuslist ne $newstatuslist) {
- my ($removed, $added) = DiffStrings($oldstatuslist, $newstatuslist);
- my $quotedremoved = SqlQuote($removed);
- my $quotedadded = SqlQuote($added);
- my $fieldid = GetFieldID('attachstatusdefs.name');
- SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
- VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $quotedremoved, $quotedadded)");
- }
-
+
+ # Update flags.
+ my $target = Bugzilla::Flag::GetTarget(undef, $::FORM{'id'});
+ Bugzilla::Flag::process($target, $timestamp, \%::FORM);
+
# Unlock all database tables now that we are finished updating the database.
SendSQL("UNLOCK TABLES");
- # If this installation has enabled the request manager, let the manager know
- # an attachment was updated so it can check for requests on that attachment
- # and fulfill them. The request manager allows users to request database
- # changes of other users and tracks the fulfillment of those requests. When
- # an attachment record is updated and the request manager is called, it will
- # fulfill those requests that were requested of the user performing the update
- # which are requests for the attachment being updated.
- #my $requests;
- #if (Param('userequestmanager'))
- #{
- # use Request;
- # # Specify the fieldnames that have been updated.
- # my @fieldnames = ('description', 'mimetype', 'status', 'ispatch', 'isobsolete');
- # # Fulfill pending requests.
- # $requests = Request::fulfillRequest('attachment', $::FORM{'id'}, @fieldnames);
- # $vars->{'requests'} = $requests;
- #}
-
# If the user submitted a comment while editing the attachment,
# add the comment to the bug.
if ( $::FORM{'comment'} )
@@ -772,10 +682,10 @@ sub update
my $neverused = $::userid;
# Append the comment to the list of comments in the database.
- AppendComment($bugid, $who, $wrappedcomment, $::FORM{'isprivate'});
+ AppendComment($bugid, $who, $wrappedcomment, $::FORM{'isprivate'}, $timestamp);
}
-
+
# Send mail to let people know the bug has changed. Uses a special syntax
# of the "open" and "exec" commands to capture the output of "processmail",
# which "system" doesn't allow, without running the command through a shell,
diff --git a/bug_form.pl b/bug_form.pl
index dfffca9b8..d087b4db2 100644
--- a/bug_form.pl
+++ b/bug_form.pl
@@ -28,6 +28,10 @@ use RelationSet;
# Use the Attachment module to display attachments for the bug.
use Attachment;
+# Use the Flag modules to display flags on the bug.
+use Bugzilla::Flag;
+use Bugzilla::FlagType;
+
sub show_bug {
# Shut up misguided -w warnings about "used only once". For some reason,
# "use vars" chokes on me when I try it here.
@@ -76,10 +80,10 @@ sub show_bug {
# Populate the bug hash with the info we get directly from the DB.
my $query = "
- SELECT bugs.bug_id, alias, products.name, version, rep_platform,
- op_sys, bug_status, resolution, priority,
- bug_severity, components.name, assigned_to, reporter,
- bug_file_loc, short_desc, target_milestone,
+ SELECT bugs.bug_id, alias, bugs.product_id, products.name, version,
+ rep_platform, op_sys, bug_status, resolution, priority,
+ bug_severity, bugs.component_id, components.name, assigned_to,
+ reporter, bug_file_loc, short_desc, target_milestone,
qa_contact, status_whiteboard,
date_format(creation_ts,'%Y-%m-%d %H:%i'),
delta_ts, sum(votes.count), delta_ts calc_disp_date
@@ -101,12 +105,12 @@ sub show_bug {
my $value;
my $disp_date;
my @row = FetchSQLData();
- foreach my $field ("bug_id", "alias", "product", "version", "rep_platform",
- "op_sys", "bug_status", "resolution", "priority",
- "bug_severity", "component", "assigned_to", "reporter",
- "bug_file_loc", "short_desc", "target_milestone",
- "qa_contact", "status_whiteboard", "creation_ts",
- "delta_ts", "votes", "calc_disp_date")
+ foreach my $field ("bug_id", "alias", "product_id", "product", "version",
+ "rep_platform", "op_sys", "bug_status", "resolution",
+ "priority", "bug_severity", "component_id", "component",
+ "assigned_to", "reporter", "bug_file_loc", "short_desc",
+ "target_milestone", "qa_contact", "status_whiteboard",
+ "creation_ts", "delta_ts", "votes", "calc_disp_date")
{
$value = shift(@row);
if ($field eq "calc_disp_date") {
@@ -197,6 +201,28 @@ sub show_bug {
# Attachments
$bug{'attachments'} = Attachment::query($id);
+
+ # The types of flags that can be set on this bug.
+ # If none, no UI for setting flags will be displayed.
+ my $flag_types =
+ Bugzilla::FlagType::match({ 'target_type' => 'bug',
+ 'product_id' => $bug{'product_id'},
+ 'component_id' => $bug{'component_id'},
+ 'is_active' => 1 });
+ foreach my $flag_type (@$flag_types) {
+ $flag_type->{'flags'} =
+ Bugzilla::Flag::match({ 'bug_id' => $id ,
+ 'target_type' => 'bug' });
+ }
+ $vars->{'flag_types'} = $flag_types;
+
+ # The number of types of flags that can be set on attachments
+ # to this bug. If none, flags won't be shown in the list of attachments.
+ $vars->{'num_attachment_flag_types'} =
+ Bugzilla::FlagType::count({ 'target_type' => 'a',
+ 'product_id' => $bug{'product_id'},
+ 'component_id' => $bug{'component_id'},
+ 'is_active' => 1 });
# Dependencies
my @list;
diff --git a/checksetup.pl b/checksetup.pl
index 27bcf26f9..737a629e8 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -1336,24 +1336,65 @@ $table{attachments} =
index(bug_id),
index(creation_ts)';
-# 2001-05-05 myk@mozilla.org: Tables to support attachment statuses.
-# "attachstatuses" stores one record for each status on each attachment.
-# "attachstatusdefs" defines the statuses that can be set on attachments.
-
-$table{attachstatuses} =
- '
- attach_id MEDIUMINT NOT NULL ,
- statusid SMALLINT NOT NULL ,
- PRIMARY KEY(attach_id, statusid)
+# September 2002 myk@mozilla.org: Tables to support status flags,
+# which replace attachment statuses and allow users to flag bugs
+# or attachments with statuses (review+, approval-, etc.).
+#
+# "flags" stores one record for each flag on each bug/attachment.
+# "flagtypes" defines the types of flags that can be set.
+# "flaginclusions" and "flagexclusions" specify the products/components
+# a bug/attachment must belong to in order for flags of a given type
+# to be set for them.
+
+$table{flags} =
+ 'id MEDIUMINT NOT NULL PRIMARY KEY ,
+ type_id SMALLINT NOT NULL ,
+ status CHAR(1) NOT NULL ,
+
+ bug_id MEDIUMINT NOT NULL ,
+ attach_id MEDIUMINT NULL ,
+
+ creation_date DATETIME NOT NULL ,
+ modification_date DATETIME NULL ,
+
+ setter_id MEDIUMINT NULL ,
+ requestee_id MEDIUMINT NULL ,
+
+ INDEX(bug_id, attach_id) ,
+ INDEX(setter_id) ,
+ INDEX(requestee_id)
';
-$table{attachstatusdefs} =
- '
- id SMALLINT NOT NULL PRIMARY KEY ,
- name VARCHAR(50) NOT NULL ,
- description MEDIUMTEXT NULL ,
- sortkey SMALLINT NOT NULL DEFAULT 0 ,
- product_id SMALLINT NOT NULL
+$table{flagtypes} =
+ 'id SMALLINT NOT NULL PRIMARY KEY ,
+ name VARCHAR(50) NOT NULL ,
+ description TEXT NULL ,
+ cc_list VARCHAR(200) NULL ,
+
+ target_type CHAR(1) NOT NULL DEFAULT \'b\' ,
+
+ is_active TINYINT NOT NULL DEFAULT 1 ,
+ is_requestable TINYINT NOT NULL DEFAULT 0 ,
+ is_requesteeble TINYINT NOT NULL DEFAULT 0 ,
+ is_multiplicable TINYINT NOT NULL DEFAULT 0 ,
+
+ sortkey SMALLINT NOT NULL DEFAULT 0
+ ';
+
+$table{flaginclusions} =
+ 'type_id SMALLINT NOT NULL ,
+ product_id SMALLINT NULL ,
+ component_id SMALLINT NULL ,
+
+ INDEX(type_id, product_id, component_id)
+ ';
+
+$table{flagexclusions} =
+ 'type_id SMALLINT NOT NULL ,
+ product_id SMALLINT NULL ,
+ component_id SMALLINT NULL ,
+
+ INDEX(type_id, product_id, component_id)
';
#
@@ -1792,7 +1833,7 @@ AddFDef("attachments.mimetype", "Attachment mime type", 0);
AddFDef("attachments.ispatch", "Attachment is patch", 0);
AddFDef("attachments.isobsolete", "Attachment is obsolete", 0);
AddFDef("attachments.isprivate", "Attachment is private", 0);
-AddFDef("attachstatusdefs.name", "Attachment Status", 0);
+
AddFDef("target_milestone", "Target Milestone", 0);
AddFDef("delta_ts", "Last changed date", 0);
AddFDef("(to_days(now()) - to_days(bugs.delta_ts))", "Days since bug changed",
@@ -1807,6 +1848,10 @@ AddFDef("bug_group", "Group", 0);
# Oops. Bug 163299
$dbh->do("DELETE FROM fielddefs WHERE name='cc_accessible'");
+AddFDef("flagtypes.name", "Flag", 0);
+AddFDef("requesters.login_name", "Flag Requester", 0);
+AddFDef("setters.login_name", "Flag Setter", 0);
+
###########################################################################
# Detect changed local settings
###########################################################################
@@ -3246,6 +3291,133 @@ if (GetFieldDef("profiles", "groupset")) {
$dbh->do("DELETE FROM fielddefs WHERE name = " . $dbh->quote('groupset'));
}
+# September 2002 myk@mozilla.org bug 98801
+# Convert the attachment statuses tables into flags tables.
+if (TableExists("attachstatuses") && TableExists("attachstatusdefs")) {
+ print "Converting attachment statuses to flags...\n";
+
+ # Get IDs for the old attachment status and new flag fields.
+ $sth = $dbh->prepare("SELECT fieldid FROM fielddefs " .
+ "WHERE name='attachstatusdefs.name'");
+ $sth->execute();
+ my $old_field_id = $sth->fetchrow_arrayref()->[0] || 0;
+
+ $sth = $dbh->prepare("SELECT fieldid FROM fielddefs " .
+ "WHERE name='flagtypes.name'");
+ $sth->execute();
+ my $new_field_id = $sth->fetchrow_arrayref()->[0];
+
+ # Convert attachment status definitions to flag types. If more than one
+ # status has the same name and description, it is merged into a single
+ # status with multiple inclusion records.
+ $sth = $dbh->prepare("SELECT id, name, description, sortkey, product_id " .
+ "FROM attachstatusdefs");
+
+ # status definition IDs indexed by name/description
+ my $def_ids = {};
+
+ # merged IDs and the IDs they were merged into. The key is the old ID,
+ # the value is the new one. This allows us to give statuses the right
+ # ID when we convert them over to flags. This map includes IDs that
+ # weren't merged (in this case the old and new IDs are the same), since
+ # it makes the code simpler.
+ my $def_id_map = {};
+
+ $sth->execute();
+ while (my ($id, $name, $desc, $sortkey, $prod_id) = $sth->fetchrow_array()) {
+ my $key = $name . $desc;
+ if (!$def_ids->{$key}) {
+ $def_ids->{$key} = $id;
+ my $quoted_name = $dbh->quote($name);
+ my $quoted_desc = $dbh->quote($desc);
+ $dbh->do("INSERT INTO flagtypes (id, name, description, sortkey, " .
+ "target_type) VALUES ($id, $quoted_name, $quoted_desc, " .
+ "$sortkey, 'a')");
+ }
+ $def_id_map->{$id} = $def_ids->{$key};
+ $dbh->do("INSERT INTO flaginclusions (type_id, product_id) " .
+ "VALUES ($def_id_map->{$id}, $prod_id)");
+ }
+
+ # Note: even though we've converted status definitions, we still can't drop
+ # the table because we need it to convert the statuses themselves.
+
+ # Convert attachment statuses to flags. To do this we select the statuses
+ # from the status table and then, for each one, figure out who set it
+ # and when they set it from the bugs activity table.
+ my $id = 0;
+ $sth = $dbh->prepare("SELECT attachstatuses.attach_id, attachstatusdefs.id, " .
+ "attachstatusdefs.name, attachments.bug_id " .
+ "FROM attachstatuses, attachstatusdefs, attachments " .
+ "WHERE attachstatuses.statusid = attachstatusdefs.id " .
+ "AND attachstatuses.attach_id = attachments.attach_id");
+
+ # a query to determine when the attachment status was set and who set it
+ my $sth2 = $dbh->prepare("SELECT added, who, bug_when " .
+ "FROM bugs_activity " .
+ "WHERE bug_id = ? AND attach_id = ? " .
+ "AND fieldid = $old_field_id " .
+ "ORDER BY bug_when DESC");
+
+ $sth->execute();
+ while (my ($attach_id, $def_id, $status, $bug_id) = $sth->fetchrow_array()) {
+ ++$id;
+
+ # Determine when the attachment status was set and who set it.
+ # We should always be able to find out this info from the bug activity,
+ # but we fall back to default values just in case.
+ $sth2->execute($bug_id, $attach_id);
+ my ($added, $who, $when);
+ while (($added, $who, $when) = $sth2->fetchrow_array()) {
+ last if $added =~ /(^|[, ]+)\Q$status\E([, ]+|$)/;
+ }
+ $who = $dbh->quote($who); # "NULL" by default if $who is undefined
+ $when = $when ? $dbh->quote($when) : "NOW()";
+
+
+ $dbh->do("INSERT INTO flags (id, type_id, status, bug_id, attach_id, " .
+ "creation_date, modification_date, requestee_id, setter_id) " .
+ "VALUES ($id, $def_id_map->{$def_id}, '+', $bug_id, " .
+ "$attach_id, $when, $when, NULL, $who)");
+ }
+
+ # Now that we've converted both tables we can drop them.
+ $dbh->do("DROP TABLE attachstatuses");
+ $dbh->do("DROP TABLE attachstatusdefs");
+
+ # Convert activity records for attachment statuses into records for flags.
+ my $sth = $dbh->prepare("SELECT attach_id, who, bug_when, added, removed " .
+ "FROM bugs_activity WHERE fieldid = $old_field_id");
+ $sth->execute();
+ while (my ($attach_id, $who, $when, $old_added, $old_removed) =
+ $sth->fetchrow_array())
+ {
+ my @additions = split(/[, ]+/, $old_added);
+ @additions = map("$_+", @additions);
+ my $new_added = $dbh->quote(join(", ", @additions));
+
+ my @removals = split(/[, ]+/, $old_removed);
+ @removals = map("$_+", @removals);
+ my $new_removed = $dbh->quote(join(", ", @removals));
+
+ $old_added = $dbh->quote($old_added);
+ $old_removed = $dbh->quote($old_removed);
+ $who = $dbh->quote($who);
+ $when = $dbh->quote($when);
+
+ $dbh->do("UPDATE bugs_activity SET fieldid = $new_field_id, " .
+ "added = $new_added, removed = $new_removed " .
+ "WHERE attach_id = $attach_id AND who = $who " .
+ "AND bug_when = $when AND fieldid = $old_field_id " .
+ "AND added = $old_added AND removed = $old_removed");
+ }
+
+ # Remove the attachment status field from the field definitions.
+ $dbh->do("DELETE FROM fielddefs WHERE name='attachstatusdefs.name'");
+
+ print "done.\n";
+}
+
# If you had to change the --TABLE-- definition in any way, then add your
# differential change code *** A B O V E *** this comment.
#
diff --git a/editattachstatuses.cgi b/editattachstatuses.cgi
deleted file mode 100755
index eedf2add4..000000000
--- a/editattachstatuses.cgi
+++ /dev/null
@@ -1,347 +0,0 @@
-#!/usr/bonsaitools/bin/perl -w
-# -*- 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>
-# Myk Melez <myk@mozilla.org>
-
-################################################################################
-# Script Initialization
-################################################################################
-
-# Make it harder for us to do dangerous things in Perl.
-use strict;
-use lib ".";
-
-use vars qw(
- $template
- $vars
-);
-
-# Include the Bugzilla CGI and general utility library.
-require "CGI.pl";
-
-# Establish a connection to the database backend.
-ConnectToDatabase();
-
-# Make sure the user is logged in and is allowed to edit products
-# (i.e. the user has "editcomponents" privileges), since attachment
-# statuses are product-specific.
-confirm_login();
-UserInGroup("editcomponents")
- || DisplayError("You are not authorized to administer attachment statuses.")
- && exit;
-
-################################################################################
-# Main Body Execution
-################################################################################
-
-# All calls to this script should contain an "action" variable whose value
-# determines what the user wants to do. The code below checks the value of
-# that variable and runs the appropriate code.
-
-# Determine whether to use the action specified by the user or the default.
-my $action = $::FORM{'action'} || 'list';
-
-if ($action eq "list")
-{
- list();
-}
-elsif ($action eq "create")
-{
- create();
-}
-elsif ($action eq "insert")
-{
- validateName();
- validateDescription();
- validateSortKey();
- validateProduct();
- insert();
-}
-elsif ($action eq "edit")
-{
- edit();
-}
-elsif ($action eq "update")
-{
- validateID();
- validateName();
- validateDescription();
- validateSortKey();
- update();
-}
-elsif ($action eq "confirmdelete")
-{
- validateID();
- confirmDelete();
-}
-elsif ($action eq "delete")
-{
- validateID();
- deleteStatus();
-}
-else
-{
- DisplayError("I could not figure out what you wanted to do.")
-}
-
-exit;
-
-################################################################################
-# Data Validation
-################################################################################
-
-sub validateID
-{
- $::FORM{'id'} =~ /^[1-9][0-9]*$/
- || DisplayError("The status ID is not a positive integer.")
- && exit;
-
- SendSQL("SELECT 1 FROM attachstatusdefs WHERE id = $::FORM{'id'}");
- my ($defexists) = FetchSQLData();
- $defexists
- || DisplayError("The status with ID #$::FORM{'id'} does not exist.")
- && exit;
-}
-
-sub validateName
-{
- $::FORM{'name'}
- || DisplayError("You must enter a name for the status.")
- && exit;
-
- $::FORM{'name'} !~ /[\s,]/
- || DisplayError("The status name cannot contain commas or whitespace.")
- && exit;
-
- length($::FORM{'name'}) <= 50
- || DisplayError("The status name cannot be more than 50 characters long.")
- && exit;
-}
-
-sub validateDescription
-{
- $::FORM{'desc'}
- || DisplayError("You must enter a description of the status.")
- && exit;
-}
-
-sub validateSortKey
-{
- $::FORM{'sortkey'} =~ /^\d+$/
- && $::FORM{'sortkey'} < 32768
- || DisplayError("The sort key must be an integer between 0 and 32767 inclusive.")
- && exit;
-}
-
-sub validateProduct
-{
- # Retrieve a list of products.
- SendSQL("SELECT name FROM products");
- my @products;
- push(@products, FetchSQLData()) while MoreSQLData();
-
- grep($_ eq $::FORM{'product'}, @products)
- || DisplayError("You must select an existing product for the status.")
- && exit;
-}
-
-################################################################################
-# Functions
-################################################################################
-
-sub list
-{
- # Administer attachment status flags, which is the set of status flags
- # that can be applied to an attachment.
-
- # If the user is seeing this screen as a result of doing something to
- # an attachment status flag, display a message about what happened
- # to that flag (i.e. "The attachment status flag was updated.").
- my ($message) = (@_);
-
- # Retrieve a list of attachment status flags and create an array of hashes
- # in which each hash contains the data for one flag.
- SendSQL("SELECT attachstatusdefs.id, attachstatusdefs.name, " .
- "attachstatusdefs.description, attachstatusdefs.sortkey, products.name, " .
- "count(attachstatusdefs.id) " .
- "FROM attachstatusdefs, products " .
- "WHERE products.id = attachstatusdefs.product_id " .
- "GROUP BY id " .
- "ORDER BY attachstatusdefs.sortkey");
- my @statusdefs;
- while ( MoreSQLData() )
- {
- my ($id, $name, $description, $sortkey, $product, $attachcount) = FetchSQLData();
- push @statusdefs, { 'id' => $id , 'name' => $name , 'description' => $description ,
- 'sortkey' => $sortkey , 'product' => $product,
- 'attachcount' => $attachcount };
- }
-
- # Define the variables and functions that will be passed to the UI template.
- $vars->{'message'} = $message;
- $vars->{'statusdefs'} = \@statusdefs;
-
- # Return the appropriate HTTP response headers.
- print "Content-type: text/html\n\n";
-
- # Generate and return the UI (HTML page) from the appropriate template.
- $template->process("admin/attachstatus/list.html.tmpl", $vars)
- || ThrowTemplateError($template->error());
-}
-
-
-sub create
-{
- # Display a form for creating a new attachment status flag.
-
- # Retrieve a list of products to which the attachment status may apply.
- SendSQL("SELECT name FROM products");
- my @products;
- push(@products, FetchSQLData()) while MoreSQLData();
-
- # Define the variables and functions that will be passed to the UI template.
- $vars->{'products'} = \@products;
-
- # Return the appropriate HTTP response headers.
- print "Content-type: text/html\n\n";
-
- # Generate and return the UI (HTML page) from the appropriate template.
- $template->process("admin/attachstatus/create.html.tmpl", $vars)
- || ThrowTemplateError($template->error());
-}
-
-
-sub insert
-{
- # Insert a new attachment status flag into the database.
-
- # Quote the flag's name and description as appropriate for inclusion
- # in a SQL statement.
- my $name = SqlQuote($::FORM{'name'});
- my $desc = SqlQuote($::FORM{'desc'});
- my $product_id = get_product_id($::FORM{'product'});
-
- SendSQL("LOCK TABLES attachstatusdefs WRITE");
- SendSQL("SELECT MAX(id) FROM attachstatusdefs");
- my $id = FetchSQLData() + 1;
- SendSQL("INSERT INTO attachstatusdefs (id, name, description, sortkey, product_id)
- VALUES ($id, $name, $desc, $::FORM{'sortkey'}, $product_id)");
- SendSQL("UNLOCK TABLES");
-
- # Display the "administer attachment status flags" page
- # along with a message that the flag has been created.
- list("The attachment status has been created.");
-}
-
-
-sub edit
-{
- # Display a form for editing an existing attachment status flag.
-
- # Retrieve the definition from the database.
- SendSQL("SELECT attachstatusdefs.name, attachstatusdefs.description, " .
- " attachstatusdefs.sortkey, products.name " .
- "FROM attachstatusdefs, products " .
- "WHERE attachstatusdefs.product_id = products.id " .
- " AND attachstatusdefs.id = $::FORM{'id'}");
- my ($name, $desc, $sortkey, $product) = FetchSQLData();
-
- # Define the variables and functions that will be passed to the UI template.
- $vars->{'id'} = $::FORM{'id'};
- $vars->{'name'} = $name;
- $vars->{'desc'} = $desc;
- $vars->{'sortkey'} = $sortkey;
- $vars->{'product'} = $product;
-
- # Return the appropriate HTTP response headers.
- print "Content-type: text/html\n\n";
-
- # Generate and return the UI (HTML page) from the appropriate template.
- $template->process("admin/attachstatus/edit.html.tmpl", $vars)
- || ThrowTemplateError($template->error());
-}
-
-
-sub update
-{
- # Update an attachment status flag in the database.
-
- # Quote the flag's name and description as appropriate for inclusion
- # in a SQL statement.
- my $name = SqlQuote($::FORM{'name'});
- my $desc = SqlQuote($::FORM{'desc'});
-
- SendSQL("LOCK TABLES attachstatusdefs WRITE");
- SendSQL("
- UPDATE attachstatusdefs
- SET name = $name ,
- description = $desc ,
- sortkey = $::FORM{'sortkey'}
- WHERE id = $::FORM{'id'}
- ");
- SendSQL("UNLOCK TABLES");
-
- # Display the "administer attachment status flags" page
- # along with a message that the flag has been updated.
- list("The attachment status has been updated.");
-}
-
-sub confirmDelete
-{
- # check if we need confirmation to delete:
-
- SendSQL("SELECT COUNT(attach_id), name
- FROM attachstatusdefs LEFT JOIN attachstatuses
- ON attachstatuses.statusid=attachstatusdefs.id
- WHERE statusid = $::FORM{'id'}
- GROUP BY attachstatuses.statusid;");
-
- my ($attachcount, $name) = FetchSQLData();
-
- if ($attachcount > 0) {
-
- $vars->{'id'} = $::FORM{'id'};
- $vars->{'attachcount'} = $attachcount;
- $vars->{'name'} = $name;
-
- print "Content-type: text/html\n\n";
-
- $template->process("admin/attachstatus/delete.html.tmpl", $vars)
- || ThrowTemplateError($template->error());
- }
- else {
- deleteStatus();
- }
-}
-
-sub deleteStatus
-{
- # Delete an attachment status flag from the database.
-
- SendSQL("LOCK TABLES attachstatusdefs WRITE, attachstatuses WRITE");
- SendSQL("DELETE FROM attachstatuses WHERE statusid = $::FORM{'id'}");
- SendSQL("DELETE FROM attachstatusdefs WHERE id = $::FORM{'id'}");
- SendSQL("UNLOCK TABLES");
-
- # Display the "administer attachment status flags" page
- # along with a message that the flag has been deleted.
- list("The attachment status has been deleted.");
-}
diff --git a/editcomponents.cgi b/editcomponents.cgi
index 7ad81ddfa..fc45b52c8 100755
--- a/editcomponents.cgi
+++ b/editcomponents.cgi
@@ -581,7 +581,9 @@ if ($action eq 'delete') {
bugs WRITE,
bugs_activity WRITE,
components WRITE,
- dependencies WRITE");
+ dependencies WRITE,
+ flaginclusions WRITE,
+ flagexclusions WRITE");
# According to MySQL doc I cannot do a DELETE x.* FROM x JOIN Y,
# so I have to iterate over bugs and delete all the indivial entries
@@ -610,6 +612,12 @@ if ($action eq 'delete') {
print "Bugs deleted.<BR>\n";
}
+ SendSQL("DELETE FROM flaginclusions
+ WHERE component_id=$component_id");
+ SendSQL("DELETE FROM flagexclusions
+ WHERE component_id=$component_id");
+ print "Flag inclusions and exclusions deleted.<BR>\n";
+
SendSQL("DELETE FROM components
WHERE id=$component_id");
print "Components deleted.<P>\n";
diff --git a/editflagtypes.cgi b/editflagtypes.cgi
new file mode 100755
index 000000000..aed73f284
--- /dev/null
+++ b/editflagtypes.cgi
@@ -0,0 +1,494 @@
+#!/usr/bonsaitools/bin/perl -wT
+# -*- 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): Myk Melez <myk@mozilla.org>
+
+################################################################################
+# Script Initialization
+################################################################################
+
+# Make it harder for us to do dangerous things in Perl.
+use strict;
+use lib ".";
+
+# Include the Bugzilla CGI and general utility library.
+require "CGI.pl";
+
+# Establish a connection to the database backend.
+ConnectToDatabase();
+
+# Use Bugzilla's flag modules for handling flag types.
+use Bugzilla::Flag;
+use Bugzilla::FlagType;
+
+use vars qw( $template $vars );
+
+# Make sure the user is logged in and is an administrator.
+confirm_login();
+UserInGroup("editcomponents")
+ || ThrowUserError("authorization_failure",
+ { action => "administer flag types" });
+
+# Suppress "used only once" warnings.
+use vars qw(@legal_product @legal_components %components);
+
+my $product_id;
+my $component_id;
+
+################################################################################
+# Main Body Execution
+################################################################################
+
+# All calls to this script should contain an "action" variable whose value
+# determines what the user wants to do. The code below checks the value of
+# that variable and runs the appropriate code.
+
+# Determine whether to use the action specified by the user or the default.
+my $action = $::FORM{'action'} || 'list';
+
+if ($::FORM{'categoryAction'}) {
+ processCategoryChange();
+ exit;
+}
+
+if ($action eq 'list') { list(); }
+elsif ($action eq 'enter') { edit(); }
+elsif ($action eq 'copy') { edit(); }
+elsif ($action eq 'edit') { edit(); }
+elsif ($action eq 'insert') { insert(); }
+elsif ($action eq 'update') { update(); }
+elsif ($action eq 'confirmdelete') { confirmDelete(); }
+elsif ($action eq 'delete') { &delete(); }
+elsif ($action eq 'deactivate') { deactivate(); }
+else {
+ ThrowCodeError("action_unrecognized", { action => $action });
+}
+
+exit;
+
+################################################################################
+# Functions
+################################################################################
+
+sub list {
+ # Define the variables and functions that will be passed to the UI template.
+ $vars->{'bug_types'} = Bugzilla::FlagType::match({ 'target_type' => 'bug' }, 1);
+ $vars->{'attachment_types'} =
+ Bugzilla::FlagType::match({ 'target_type' => 'attachment' }, 1);
+
+ # Return the appropriate HTTP response headers.
+ print "Content-type: text/html\n\n";
+
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $template->process("admin/flag-type/list.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+
+
+sub edit {
+ $action eq 'enter' ? validateTargetType() : validateID();
+
+ # Get this installation's products and components.
+ GetVersionTable();
+
+ # products and components and the function used to modify the components
+ # menu when the products menu changes; used by the template to populate
+ # the menus and keep the components menu consistent with the products menu
+ $vars->{'products'} = \@::legal_product;
+ $vars->{'components'} = \@::legal_components;
+ $vars->{'components_by_product'} = \%::components;
+
+ $vars->{'last_action'} = $::FORM{'action'};
+ if ($::FORM{'action'} eq 'enter' || $::FORM{'action'} eq 'copy') {
+ $vars->{'action'} = "insert";
+ }
+ else {
+ $vars->{'action'} = "update";
+ }
+
+ # If copying or editing an existing flag type, retrieve it.
+ if ($::FORM{'action'} eq 'copy' || $::FORM{'action'} eq 'edit') {
+ $vars->{'type'} = Bugzilla::FlagType::get($::FORM{'id'});
+ $vars->{'type'}->{'inclusions'} = Bugzilla::FlagType::get_inclusions($::FORM{'id'});
+ $vars->{'type'}->{'exclusions'} = Bugzilla::FlagType::get_exclusions($::FORM{'id'});
+ }
+ # Otherwise set the target type (the minimal information about the type
+ # that the template needs to know) from the URL parameter.
+ else {
+ $vars->{'type'} = { 'target_type' => $::FORM{'target_type'} };
+ }
+
+ # Return the appropriate HTTP response headers.
+ print "Content-type: text/html\n\n";
+
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $template->process("admin/flag-type/edit.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+
+sub processCategoryChange {
+ validateIsActive();
+ validateIsRequestable();
+ validateIsRequesteeble();
+ validateAllowMultiple();
+
+ my @inclusions = $::MFORM{'inclusions'} ? @{$::MFORM{'inclusions'}} : ();
+ my @exclusions = $::MFORM{'exclusions'} ? @{$::MFORM{'exclusions'}} : ();
+ if ($::FORM{'categoryAction'} eq "Include") {
+ validateProduct();
+ validateComponent();
+ my $category = ($::FORM{'product'} || "__Any__") . ":" . ($::FORM{'component'} || "__Any__");
+ push(@inclusions, $category) unless grep($_ eq $category, @inclusions);
+ }
+ elsif ($::FORM{'categoryAction'} eq "Exclude") {
+ validateProduct();
+ validateComponent();
+ my $category = ($::FORM{'product'} || "__Any__") . ":" . ($::FORM{'component'} || "__Any__");
+ push(@exclusions, $category) unless grep($_ eq $category, @exclusions);
+ }
+ elsif ($::FORM{'categoryAction'} eq "Remove Inclusion") {
+ @inclusions = map(($_ eq $::FORM{'inclusion_to_remove'} ? () : $_), @inclusions);
+ }
+ elsif ($::FORM{'categoryAction'} eq "Remove Exclusion") {
+ @exclusions = map(($_ eq $::FORM{'exclusion_to_remove'} ? () : $_), @exclusions);
+ }
+
+ # Get this installation's products and components.
+ GetVersionTable();
+
+ # products and components; used by the template to populate the menus
+ # and keep the components menu consistent with the products menu
+ $vars->{'products'} = \@::legal_product;
+ $vars->{'components'} = \@::legal_components;
+ $vars->{'components_by_product'} = \%::components;
+
+ $vars->{'action'} = $::FORM{'action'};
+ my $type = {};
+ foreach my $key (keys %::FORM) { $type->{$key} = $::FORM{$key} }
+ $type->{'inclusions'} = \@inclusions;
+ $type->{'exclusions'} = \@exclusions;
+ $vars->{'type'} = $type;
+
+ # Return the appropriate HTTP response headers.
+ print "Content-type: text/html\n\n";
+
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $template->process("admin/flag-type/edit.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+
+sub insert {
+ validateName();
+ validateDescription();
+ validateCCList();
+ validateTargetType();
+ validateSortKey();
+ validateIsActive();
+ validateIsRequestable();
+ validateIsRequesteeble();
+ validateAllowMultiple();
+
+ my $name = SqlQuote($::FORM{'name'});
+ my $description = SqlQuote($::FORM{'description'});
+ my $cc_list = SqlQuote($::FORM{'cc_list'});
+ my $target_type = $::FORM{'target_type'} eq "bug" ? "b" : "a";
+
+ SendSQL("LOCK TABLES flagtypes WRITE, products READ, components READ, " .
+ "flaginclusions WRITE, flagexclusions WRITE");
+
+ # Determine the new flag type's unique identifier.
+ SendSQL("SELECT MAX(id) FROM flagtypes");
+ my $id = FetchSQLData() + 1;
+
+ # Insert a record for the new flag type into the database.
+ SendSQL("INSERT INTO flagtypes (id, name, description, cc_list,
+ target_type, sortkey, is_active, is_requestable,
+ is_requesteeble, is_multiplicable)
+ VALUES ($id, $name, $description, $cc_list, '$target_type',
+ $::FORM{'sortkey'}, $::FORM{'is_active'},
+ $::FORM{'is_requestable'}, $::FORM{'is_requesteeble'},
+ $::FORM{'is_multiplicable'})");
+
+ # Populate the list of inclusions/exclusions for this flag type.
+ foreach my $category_type ("inclusions", "exclusions") {
+ foreach my $category (@{$::MFORM{$category_type}}) {
+ my ($product, $component) = split(/:/, $category);
+ my $product_id = get_product_id($product) || "NULL";
+ my $component_id =
+ get_component_id($product_id, $component) || "NULL";
+ SendSQL("INSERT INTO flag$category_type (type_id, product_id, " .
+ "component_id) VALUES ($id, $product_id, $component_id)");
+ }
+ }
+
+ SendSQL("UNLOCK TABLES");
+
+ $vars->{'name'} = $::FORM{'name'};
+ $vars->{'message'} = "flag_type_created";
+
+ # Return the appropriate HTTP response headers.
+ print "Content-type: text/html\n\n";
+
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $template->process("global/message.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+
+
+sub update {
+ validateID();
+ validateName();
+ validateDescription();
+ validateCCList();
+ validateTargetType();
+ validateSortKey();
+ validateIsActive();
+ validateIsRequestable();
+ validateIsRequesteeble();
+ validateAllowMultiple();
+
+ my $name = SqlQuote($::FORM{'name'});
+ my $description = SqlQuote($::FORM{'description'});
+ my $cc_list = SqlQuote($::FORM{'cc_list'});
+
+ SendSQL("LOCK TABLES flagtypes WRITE, products READ, components READ, " .
+ "flaginclusions WRITE, flagexclusions WRITE");
+ SendSQL("UPDATE flagtypes
+ SET name = $name ,
+ description = $description ,
+ cc_list = $cc_list ,
+ sortkey = $::FORM{'sortkey'} ,
+ is_active = $::FORM{'is_active'} ,
+ is_requestable = $::FORM{'is_requestable'} ,
+ is_requesteeble = $::FORM{'is_requesteeble'} ,
+ is_multiplicable = $::FORM{'is_multiplicable'}
+ WHERE id = $::FORM{'id'}");
+
+ # Update the list of inclusions/exclusions for this flag type.
+ foreach my $category_type ("inclusions", "exclusions") {
+ SendSQL("DELETE FROM flag$category_type WHERE type_id = $::FORM{'id'}");
+ foreach my $category (@{$::MFORM{$category_type}}) {
+ my ($product, $component) = split(/:/, $category);
+ my $product_id = get_product_id($product) || "NULL";
+ my $component_id =
+ get_component_id($product_id, $component) || "NULL";
+ SendSQL("INSERT INTO flag$category_type (type_id, product_id, " .
+ "component_id) VALUES ($::FORM{'id'}, $product_id, " .
+ "$component_id)");
+ }
+ }
+
+ SendSQL("UNLOCK TABLES");
+
+ # Clear existing flags for bugs/attachments in categories no longer on
+ # the list of inclusions or that have been added to the list of exclusions.
+ SendSQL("
+ SELECT flags.id
+ FROM flags, bugs LEFT OUTER JOIN flaginclusions AS i
+ ON (flags.type_id = i.type_id
+ AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
+ AND (bugs.component_id = i.component_id OR i.component_id IS NULL))
+ WHERE flags.type_id = $::FORM{'id'}
+ AND flags.bug_id = bugs.bug_id
+ AND i.type_id IS NULL
+ ");
+ Bugzilla::Flag::clear(FetchOneColumn()) while MoreSQLData();
+
+ SendSQL("
+ SELECT flags.id
+ FROM flags, bugs, flagexclusions AS e
+ WHERE flags.type_id = $::FORM{'id'}
+ AND flags.bug_id = bugs.bug_id
+ AND flags.type_id = e.type_id
+ AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
+ AND (bugs.component_id = e.component_id OR e.component_id IS NULL)
+ ");
+ Bugzilla::Flag::clear(FetchOneColumn()) while MoreSQLData();
+
+ $vars->{'name'} = $::FORM{'name'};
+ $vars->{'message'} = "flag_type_changes_saved";
+
+ # Return the appropriate HTTP response headers.
+ print "Content-type: text/html\n\n";
+
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $template->process("global/message.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+
+
+sub confirmDelete
+{
+ validateID();
+ # check if we need confirmation to delete:
+
+ my $count = Bugzilla::Flag::count({ 'type_id' => $::FORM{'id'} });
+
+ if ($count > 0) {
+ $vars->{'flag_type'} = Bugzilla::FlagType::get($::FORM{'id'});
+ $vars->{'flag_count'} = scalar($count);
+
+ # Return the appropriate HTTP response headers.
+ print "Content-type: text/html\n\n";
+
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $template->process("admin/flag-type/confirm-delete.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+ }
+ else {
+ deleteType();
+ }
+}
+
+
+sub delete {
+ validateID();
+
+ SendSQL("LOCK TABLES flagtypes WRITE, flags WRITE, " .
+ "flaginclusions WRITE, flagexclusions WRITE");
+
+ # Get the name of the flag type so we can tell users
+ # what was deleted.
+ SendSQL("SELECT name FROM flagtypes WHERE id = $::FORM{'id'}");
+ $vars->{'name'} = FetchOneColumn();
+
+ SendSQL("DELETE FROM flags WHERE type_id = $::FORM{'id'}");
+ SendSQL("DELETE FROM flaginclusions WHERE type_id = $::FORM{'id'}");
+ SendSQL("DELETE FROM flagexclusions WHERE type_id = $::FORM{'id'}");
+ SendSQL("DELETE FROM flagtypes WHERE id = $::FORM{'id'}");
+ SendSQL("UNLOCK TABLES");
+
+ $vars->{'message'} = "flag_type_deleted";
+
+ # Return the appropriate HTTP response headers.
+ print "Content-type: text/html\n\n";
+
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $template->process("global/message.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+
+
+sub deactivate {
+ validateID();
+ validateIsActive();
+
+ SendSQL("LOCK TABLES flagtypes WRITE");
+ SendSQL("UPDATE flagtypes SET is_active = 0 WHERE id = $::FORM{'id'}");
+ SendSQL("UNLOCK TABLES");
+
+ $vars->{'message'} = "flag_type_deactivated";
+ $vars->{'flag_type'} = Bugzilla::FlagType::get($::FORM{'id'});
+
+ # Return the appropriate HTTP response headers.
+ print "Content-type: text/html\n\n";
+
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $template->process("global/message.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+
+
+################################################################################
+# Data Validation / Security Authorization
+################################################################################
+
+sub validateID {
+ detaint_natural($::FORM{'id'})
+ || ThrowCodeError("flag_type_id_invalid", { id => $::FORM{'id'} });
+
+ SendSQL("SELECT 1 FROM flagtypes WHERE id = $::FORM{'id'}");
+ FetchOneColumn()
+ || ThrowCodeError("flag_type_nonexistent", { id => $::FORM{'id'} });
+}
+
+sub validateName {
+ $::FORM{'name'}
+ && length($::FORM{'name'}) <= 50
+ || ThrowUserError("flag_type_name_invalid", { name => $::FORM{'name'} });
+}
+
+sub validateDescription {
+ length($::FORM{'description'}) < 2^16-1
+ || ThrowUserError("flag_type_description_invalid");
+}
+
+sub validateCCList {
+ length($::FORM{'cc_list'}) <= 200
+ || ThrowUserError("flag_type_cc_list_invalid",
+ { cc_list => $::FORM{'cc_list'} });
+
+ my @addresses = split(/[, ]+/, $::FORM{'cc_list'});
+ foreach my $address (@addresses) { CheckEmailSyntax($address) }
+}
+
+sub validateProduct {
+ return if !$::FORM{'product'};
+
+ $product_id = get_product_id($::FORM{'product'});
+
+ defined($product_id)
+ || ThrowCodeError("flag_type_product_nonexistent",
+ { product => $::FORM{'product'} });
+}
+
+sub validateComponent {
+ return if !$::FORM{'component'};
+
+ $product_id
+ || ThrowCodeError("flag_type_component_without_product");
+
+ $component_id = get_component_id($product_id, $::FORM{'component'});
+
+ defined($component_id)
+ || ThrowCodeError("flag_type_component_nonexistent",
+ { product => $::FORM{'product'},
+ component => $::FORM{'component'} });
+}
+
+sub validateSortKey {
+ detaint_natural($::FORM{'sortkey'})
+ && $::FORM{'sortkey'} < 32768
+ || ThrowUserError("flag_type_sortkey_invalid",
+ { sortkey => $::FORM{'sortkey'} });
+}
+
+sub validateTargetType {
+ grep($::FORM{'target_type'} eq $_, ("bug", "attachment"))
+ || ThrowCodeError("flag_type_target_type_invalid",
+ { target_type => $::FORM{'target_type'} });
+}
+
+sub validateIsActive {
+ $::FORM{'is_active'} = $::FORM{'is_active'} ? 1 : 0;
+}
+
+sub validateIsRequestable {
+ $::FORM{'is_requestable'} = $::FORM{'is_requestable'} ? 1 : 0;
+}
+
+sub validateIsRequesteeble {
+ $::FORM{'is_requesteeble'} = $::FORM{'is_requesteeble'} ? 1 : 0;
+}
+
+sub validateAllowMultiple {
+ $::FORM{'is_multiplicable'} = $::FORM{'is_multiplicable'} ? 1 : 0;
+}
+
diff --git a/editproducts.cgi b/editproducts.cgi
index 18ad4216d..3db7c6f84 100755
--- a/editproducts.cgi
+++ b/editproducts.cgi
@@ -539,7 +539,9 @@ if ($action eq 'delete') {
products WRITE,
groups WRITE,
profiles WRITE,
- milestones WRITE");
+ milestones WRITE,
+ flaginclusions WRITE,
+ flagexclusions WRITE);
# According to MySQL doc I cannot do a DELETE x.* FROM x JOIN Y,
# so I have to iterate over bugs and delete all the indivial entries
@@ -581,6 +583,12 @@ if ($action eq 'delete') {
WHERE product_id=$product_id");
print "Milestones deleted.<BR>\n";
+ SendSQL("DELETE FROM flaginclusions
+ WHERE product_id=$product_id");
+ SendSQL("DELETE FROM flagexclusions
+ WHERE product_id=$product_id");
+ print "Flag inclusions and exclusions deleted.<BR>\n";
+
SendSQL("DELETE FROM products
WHERE id=$product_id");
print "Product '$product' deleted.<BR>\n";
diff --git a/globals.pl b/globals.pl
index a6a751562..9a625a842 100644
--- a/globals.pl
+++ b/globals.pl
@@ -300,7 +300,12 @@ sub FetchOneColumn {
"status", "resolution", "summary");
sub AppendComment {
- my ($bugid,$who,$comment,$isprivate) = (@_);
+ my ($bugid, $who, $comment, $isprivate, $timestamp) = @_;
+
+ # Use the date/time we were given if possible (allowing calling code
+ # to synchronize the comment's timestamp with those of other records).
+ $timestamp = ($timestamp ? SqlQuote($timestamp) : "NOW()");
+
$comment =~ s/\r\n/\n/g; # Get rid of windows-style line endings.
$comment =~ s/\r/\n/g; # Get rid of mac-style line endings.
if ($comment =~ /^\s*$/) { # Nothin' but whitespace.
@@ -310,7 +315,7 @@ sub AppendComment {
my $whoid = DBNameToIdAndCheck($who);
my $privacyval = $isprivate ? 1 : 0 ;
SendSQL("INSERT INTO longdescs (bug_id, who, bug_when, thetext, isprivate) " .
- "VALUES($bugid, $whoid, now(), " . SqlQuote($comment) . ", " .
+ "VALUES($bugid, $whoid, $timestamp, " . SqlQuote($comment) . ", " .
$privacyval . ")");
SendSQL("UPDATE bugs SET delta_ts = now() WHERE bug_id = $bugid");
@@ -902,8 +907,7 @@ sub get_product_name {
sub get_component_id {
my ($prod_id, $comp) = @_;
- die "non-numeric prod_id '$prod_id' passed to get_component_id"
- unless ($prod_id =~ /^\d+$/);
+ return undef unless ($prod_id =~ /^\d+$/);
PushGlobalSQLState();
SendSQL("SELECT id FROM components " .
"WHERE product_id = $prod_id AND name = " . SqlQuote($comp));
diff --git a/process_bug.cgi b/process_bug.cgi
index 4ddfcca2c..076e014fc 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -36,6 +36,9 @@ require "bug_form.pl";
use RelationSet;
+# Use the Flag module to modify flag data if the user set flags.
+use Bugzilla::Flag;
+
# Shut up misguided -w warnings about "used only once":
use vars qw(%versions
@@ -1052,8 +1055,9 @@ foreach my $id (@idlist) {
"profiles $write, dependencies $write, votes $write, " .
"products READ, components READ, " .
"keywords $write, longdescs $write, fielddefs $write, " .
- "bug_group_map $write, " .
- "user_group_map READ, " .
+ "bug_group_map $write, flags $write, " .
+ "user_group_map READ, flagtypes READ, " .
+ "flaginclusions AS i READ, flagexclusions AS e READ, " .
"keyworddefs READ, groups READ, attachments READ");
my @oldvalues = SnapShotBug($id);
my %oldhash;
@@ -1238,7 +1242,7 @@ foreach my $id (@idlist) {
LogActivityEntry($id, "bug_group", $groupDelNames, $groupAddNames);
if (defined $::FORM{'comment'}) {
AppendComment($id, $::COOKIE{'Bugzilla_login'}, $::FORM{'comment'},
- $::FORM{'commentprivacy'});
+ $::FORM{'commentprivacy'}, $timestamp);
}
my $removedCcString = "";
@@ -1399,6 +1403,14 @@ foreach my $id (@idlist) {
# what has changed since before we wrote out the new values.
#
my @newvalues = SnapShotBug($id);
+ my %newhash;
+ $i = 0;
+ foreach my $col (@::log_columns) {
+ # Consider NULL db entries to be equivalent to the empty string
+ $newvalues[$i] ||= '';
+ $newhash{$col} = $newvalues[$i];
+ $i++;
+ }
# for passing to processmail to ensure that when someone is removed
# from one of these fields, they get notified of that fact (if desired)
@@ -1411,12 +1423,6 @@ foreach my $id (@idlist) {
# values in place.
my $old = shift @oldvalues;
my $new = shift @newvalues;
- if (!defined $old) {
- $old = "";
- }
- if (!defined $new) {
- $new = "";
- }
if ($old ne $new) {
# Products and components are now stored in the DB using ID's
@@ -1461,6 +1467,11 @@ foreach my $id (@idlist) {
LogActivityEntry($id,$col,$old,$new);
}
}
+ # Set and update flags.
+ if ($UserInEditGroupSet) {
+ my $target = Bugzilla::Flag::GetTarget($id);
+ Bugzilla::Flag::process($target, $timestamp, \%::FORM);
+ }
if ($bug_changed) {
SendSQL("UPDATE bugs SET delta_ts = " . SqlQuote($timestamp) . " WHERE bug_id = $id");
}
diff --git a/productmenu.js b/productmenu.js
new file mode 100644
index 000000000..d917d325c
--- /dev/null
+++ b/productmenu.js
@@ -0,0 +1,242 @@
+// Adds to the target select object all elements in array that
+// correspond to the elements selected in source.
+// - array should be a array of arrays, indexed by product name. the
+// array should contain the elements that correspont to that
+// product. Example:
+// var array = Array();
+// array['ProductOne'] = [ 'ComponentA', 'ComponentB' ];
+// updateSelect(array, source, target);
+// - sel is a list of selected items, either whole or a diff
+// depending on sel_is_diff.
+// - sel_is_diff determines if we are sending in just a diff or the
+// whole selection. a diff is used to optimize adding selections.
+// - target should be the target select object.
+// - single specifies if we selected a single item. if we did, no
+// need to merge.
+
+function updateSelect( array, sel, target, sel_is_diff, single, blank ) {
+
+ var i, j, comp;
+
+ // if single, even if it's a diff (happens when you have nothing
+ // selected and select one item alone), skip this.
+ if ( ! single ) {
+
+ // array merging/sorting in the case of multiple selections
+ if ( sel_is_diff ) {
+
+ // merge in the current options with the first selection
+ comp = merge_arrays( array[sel[0]], target.options, 1 );
+
+ // merge the rest of the selection with the results
+ for ( i = 1 ; i < sel.length ; i++ ) {
+ comp = merge_arrays( array[sel[i]], comp, 0 );
+ }
+ } else {
+ // here we micro-optimize for two arrays to avoid merging with a
+ // null array
+ comp = merge_arrays( array[sel[0]],array[sel[1]], 0 );
+
+ // merge the arrays. not very good for multiple selections.
+ for ( i = 2; i < sel.length; i++ ) {
+ comp = merge_arrays( comp, array[sel[i]], 0 );
+ }
+ }
+ } else {
+ // single item in selection, just get me the list
+ comp = array[sel[0]];
+ }
+
+ // save the selection in the target select so we can restore it later
+ var selections = new Array();
+ for ( i = 0; i < target.options.length; i++ )
+ if (target.options[i].selected) selections.push(target.options[i].value);
+
+ // clear select
+ target.options.length = 0;
+
+ // add empty "Any" value back to the list
+ if (blank) target.options[0] = new Option( blank, "" );
+
+ // load elements of list into select
+ for ( i = 0; i < comp.length; i++ ) {
+ target.options[target.options.length] = new Option( comp[i], comp[i] );
+ }
+
+ // restore the selection
+ for ( i=0 ; i<selections.length ; i++ )
+ for ( j=0 ; j<target.options.length ; j++ )
+ if (target.options[j].value == selections[i]) target.options[j].selected = true;
+
+}
+
+// Returns elements in a that are not in b.
+// NOT A REAL DIFF: does not check the reverse.
+// - a,b: arrays of values to be compare.
+
+function fake_diff_array( a, b ) {
+ var newsel = new Array();
+
+ // do a boring array diff to see who's new
+ for ( var ia in a ) {
+ var found = 0;
+ for ( var ib in b ) {
+ if ( a[ia] == b[ib] ) {
+ found = 1;
+ }
+ }
+ if ( ! found ) {
+ newsel[newsel.length] = a[ia];
+ }
+ found = 0;
+ }
+ return newsel;
+ }
+
+// takes two arrays and sorts them by string, returning a new, sorted
+// array. the merge removes dupes, too.
+// - a, b: arrays to be merge.
+// - b_is_select: if true, then b is actually an optionitem and as
+// such we need to use item.value on it.
+
+ function merge_arrays( a, b, b_is_select ) {
+ var pos_a = 0;
+ var pos_b = 0;
+ var ret = new Array();
+ var bitem, aitem;
+
+ // iterate through both arrays and add the larger item to the return
+ // list. remove dupes, too. Use toLowerCase to provide
+ // case-insensitivity.
+
+ while ( ( pos_a < a.length ) && ( pos_b < b.length ) ) {
+
+ if ( b_is_select ) {
+ bitem = b[pos_b].value;
+ } else {
+ bitem = b[pos_b];
+ }
+ aitem = a[pos_a];
+
+ // smaller item in list a
+ if ( aitem.toLowerCase() < bitem.toLowerCase() ) {
+ ret[ret.length] = aitem;
+ pos_a++;
+ } else {
+ // smaller item in list b
+ if ( aitem.toLowerCase() > bitem.toLowerCase() ) {
+ ret[ret.length] = bitem;
+ pos_b++;
+ } else {
+ // list contents are equal, inc both counters.
+ ret[ret.length] = aitem;
+ pos_a++;
+ pos_b++;
+ }
+ }
+ }
+
+ // catch leftovers here. these sections are ugly code-copying.
+ if ( pos_a < a.length ) {
+ for ( ; pos_a < a.length ; pos_a++ ) {
+ ret[ret.length] = a[pos_a];
+ }
+ }
+
+ if ( pos_b < b.length ) {
+ for ( ; pos_b < b.length; pos_b++ ) {
+ if ( b_is_select ) {
+ bitem = b[pos_b].value;
+ } else {
+ bitem = b[pos_b];
+ }
+ ret[ret.length] = bitem;
+ }
+ }
+ return ret;
+ }
+
+// selectProduct reads the selection from f[productfield] and updates
+// f.version, component and target_milestone accordingly.
+// - f: a form containing product, component, varsion and
+// target_milestone select boxes.
+// globals (3vil!):
+// - cpts, vers, tms: array of arrays, indexed by product name. the
+// subarrays contain a list of names to be fed to the respective
+// selectboxes. For bugzilla, these are generated with perl code
+// at page start.
+// - usetms: this is a global boolean that is defined if the
+// bugzilla installation has it turned on. generated in perl too.
+// - first_load: boolean, specifying if it's the first time we load
+// the query page.
+// - last_sel: saves our last selection list so we know what has
+// changed, and optimize for additions.
+
+function selectProduct( f , productfield, componentfield, blank ) {
+
+ // this is to avoid handling events that occur before the form
+ // itself is ready, which happens in buggy browsers.
+
+ if ( ( !f ) || ( ! f[productfield] ) ) {
+ return;
+ }
+
+ // if this is the first load and nothing is selected, no need to
+ // merge and sort all components; perl gives it to us sorted.
+
+ if ( ( first_load ) && ( f[productfield].selectedIndex == -1 ) ) {
+ first_load = 0;
+ return;
+ }
+
+ // turn first_load off. this is tricky, since it seems to be
+ // redundant with the above clause. It's not: if when we first load
+ // the page there is _one_ element selected, it won't fall into that
+ // clause, and first_load will remain 1. Then, if we unselect that
+ // item, selectProduct will be called but the clause will be valid
+ // (since selectedIndex == -1), and we will return - incorrectly -
+ // without merge/sorting.
+
+ first_load = 0;
+
+ // - sel keeps the array of products we are selected.
+ // - is_diff says if it's a full list or just a list of products that
+ // were added to the current selection.
+ // - single indicates if a single item was selected
+ var sel = Array();
+ var is_diff = 0;
+ var single;
+
+ // if nothing selected, pick all
+ if ( f[productfield].selectedIndex == -1 ) {
+ for ( var i = 0 ; i < f[productfield].length ; i++ ) {
+ sel[sel.length] = f[productfield].options[i].value;
+ }
+ single = 0;
+ } else {
+
+ for ( i = 0 ; i < f[productfield].length ; i++ ) {
+ if ( f[productfield].options[i].selected ) {
+ sel[sel.length] = f[productfield].options[i].value;
+ }
+ }
+
+ single = ( sel.length == 1 );
+
+ // save last_sel before we kill it
+ var tmp = last_sel;
+ last_sel = sel;
+
+ // this is an optimization: if we've added components, no need
+ // to remerge them; just merge the new ones with the existing
+ // options.
+
+ if ( ( tmp ) && ( tmp.length < sel.length ) ) {
+ sel = fake_diff_array(sel, tmp);
+ is_diff = 1;
+ }
+ }
+
+ // do the actual fill/update
+ updateSelect( cpts, sel, f[componentfield], is_diff, single, blank );
+}
diff --git a/request.cgi b/request.cgi
new file mode 100755
index 000000000..eb365559e
--- /dev/null
+++ b/request.cgi
@@ -0,0 +1,279 @@
+#!/usr/bonsaitools/bin/perl -wT
+# -*- 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): Myk Melez <myk@mozilla.org>
+
+################################################################################
+# Script Initialization
+################################################################################
+
+# Make it harder for us to do dangerous things in Perl.
+use diagnostics;
+use strict;
+
+# Include the Bugzilla CGI and general utility library.
+use lib qw(.);
+require "CGI.pl";
+
+# Establish a connection to the database backend.
+ConnectToDatabase();
+
+# Use Bugzilla's Request module which contains utilities for handling requests.
+use Bugzilla::Flag;
+use Bugzilla::FlagType;
+
+# use Bugzilla's User module which contains utilities for handling users.
+use Bugzilla::User;
+
+use vars qw($template $vars @legal_product @legal_components %components);
+
+# Make sure the user is logged in.
+quietly_check_login();
+
+################################################################################
+# Main Body Execution
+################################################################################
+
+queue();
+exit;
+
+################################################################################
+# Functions
+################################################################################
+
+sub queue {
+ validateStatus();
+ validateGroup();
+
+ my $attach_join_clause = "flags.attach_id = attachments.attach_id";
+ if (Param("insidergroup") && !UserInGroup(Param("insidergroup"))) {
+ $attach_join_clause .= " AND attachment.isprivate < 1";
+ }
+
+ my $query =
+ # Select columns describing each flag, the bug/attachment on which
+ # it has been set, who set it, and of whom they are requesting it.
+ " SELECT flags.id, flagtypes.name,
+ flags.status,
+ flags.bug_id, bugs.short_desc,
+ products.name, components.name,
+ flags.attach_id, attachments.description,
+ requesters.realname, requesters.login_name,
+ requestees.realname, requestees.login_name,
+ flags.creation_date,
+ " .
+ # Select columns that help us weed out secure bugs to which the user
+ # should not have access.
+ " COUNT(DISTINCT ugmap.group_id) AS cntuseringroups,
+ COUNT(DISTINCT bgmap.group_id) AS cntbugingroups,
+ ((COUNT(DISTINCT ccmap.who) AND cclist_accessible)
+ OR ((bugs.reporter = $::userid) AND bugs.reporter_accessible)
+ OR bugs.assigned_to = $::userid ) AS canseeanyway
+ " .
+ # Use the flags and flagtypes tables for information about the flags,
+ # the bugs and attachments tables for target info, the profiles tables
+ # for setter and requestee info, the products/components tables
+ # so we can display product and component names, and the bug_group_map
+ # and user_group_map tables to help us weed out secure bugs to which
+ # the user should not have access.
+ " FROM flags
+ LEFT JOIN attachments ON ($attach_join_clause),
+ flagtypes,
+ profiles AS requesters
+ LEFT JOIN profiles AS requestees
+ ON flags.requestee_id = requestees.userid,
+ bugs
+ LEFT JOIN products ON bugs.product_id = products.id
+ LEFT JOIN components ON bugs.component_id = components.id
+ LEFT JOIN bug_group_map AS bgmap
+ ON bgmap.bug_id = bugs.bug_id
+ LEFT JOIN user_group_map AS ugmap
+ ON bgmap.group_id = ugmap.group_id
+ AND ugmap.user_id = $::userid
+ AND ugmap.isbless = 0
+ LEFT JOIN cc AS ccmap
+ ON ccmap.who = $::userid AND ccmap.bug_id = bugs.bug_id
+ " .
+ # All of these are inner join clauses. Actual match criteria are added
+ # in the code below.
+ " WHERE flags.type_id = flagtypes.id
+ AND flags.setter_id = requesters.userid
+ AND flags.bug_id = bugs.bug_id
+ ";
+
+ # A list of columns to exclude from the report because the report conditions
+ # limit the data being displayed to exact matches for those columns.
+ # In other words, if we are only displaying "pending" , we don't
+ # need to display a "status" column in the report because the value for that
+ # column will always be the same.
+ my @excluded_columns = ();
+
+ # Filter requests by status: "pending", "granted", "denied", "all"
+ # (which means any), or "fulfilled" (which means "granted" or "denied").
+ $::FORM{'status'} ||= "?";
+ if ($::FORM{'status'} eq "+-") {
+ $query .= " AND flags.status IN ('+', '-')";
+ }
+ elsif ($::FORM{'status'} ne "all") {
+ $query .= " AND flags.status = '$::FORM{'status'}'";
+ push(@excluded_columns, 'status');
+ }
+
+ # Filter results by exact email address of requester or requestee.
+ if (defined($::FORM{'requester'}) && $::FORM{'requester'} ne "") {
+ $query .= " AND requesters.login_name = " . SqlQuote($::FORM{'requester'});
+ push(@excluded_columns, 'requester');
+ }
+ if (defined($::FORM{'requestee'}) && $::FORM{'requestee'} ne "") {
+ $query .= " AND requestees.login_name = " . SqlQuote($::FORM{'requestee'});
+ push(@excluded_columns, 'requestee');
+ }
+
+ # Filter results by exact product or component.
+ if (defined($::FORM{'product'}) && $::FORM{'product'} ne "") {
+ my $product_id = get_product_id($::FORM{'product'});
+ if ($product_id) {
+ $query .= " AND bugs.product_id = $product_id";
+ push(@excluded_columns, 'product');
+ if (defined($::FORM{'component'}) && $::FORM{'component'} ne "") {
+ my $component_id = get_component_id($product_id, $::FORM{'component'});
+ if ($component_id) {
+ $query .= " AND bugs.component_id = $component_id";
+ push(@excluded_columns, 'component');
+ }
+ else { ThrowCodeError("unknown_component", { %::FORM }) }
+ }
+ }
+ else { ThrowCodeError("unknown_product", { %::FORM }) }
+ }
+
+ # Filter results by flag types.
+ if (defined($::FORM{'type'}) && !grep($::FORM{'type'} eq $_, ("", "all"))) {
+ # Check if any matching types are for attachments. If not, don't show
+ # the attachment column in the report.
+ my $types = Bugzilla::FlagType::match({ 'name' => $::FORM{'type'} });
+ my $has_attachment_type = 0;
+ foreach my $type (@$types) {
+ if ($type->{'target_type'} eq "attachment") {
+ $has_attachment_type = 1;
+ last;
+ }
+ }
+ if (!$has_attachment_type) { push(@excluded_columns, 'attachment') }
+
+ $query .= " AND flagtypes.name = " . SqlQuote($::FORM{'type'});
+ push(@excluded_columns, 'type');
+ }
+
+ # Group the records by flag ID so we don't get multiple rows of data
+ # for each flag. This is only necessary because of the code that
+ # removes flags on bugs the user is unauthorized to access.
+ $query .= " GROUP BY flags.id " .
+ "HAVING cntuseringroups = cntbugingroups OR canseeanyway ";
+
+ # Group the records, in other words order them by the group column
+ # so the loop in the display template can break them up into separate
+ # tables every time the value in the group column changes.
+ $::FORM{'group'} ||= "requestee";
+ if ($::FORM{'group'} eq "requester") {
+ $query .= " ORDER BY requesters.realname, requesters.login_name";
+ }
+ elsif ($::FORM{'group'} eq "requestee") {
+ $query .= " ORDER BY requestees.realname, requestees.login_name";
+ }
+ elsif ($::FORM{'group'} eq "category") {
+ $query .= " ORDER BY products.name, components.name";
+ }
+ elsif ($::FORM{'group'} eq "type") {
+ $query .= " ORDER BY flagtypes.name";
+ }
+
+ # Order the records (within each group).
+ $query .= " , flags.creation_date";
+
+ # Pass the query to the template for use when debugging this script.
+ $vars->{'query'} = $query;
+
+ SendSQL($query);
+ my @requests = ();
+ while (MoreSQLData()) {
+ my @data = FetchSQLData();
+ my $request = {
+ 'id' => $data[0] ,
+ 'type' => $data[1] ,
+ 'status' => $data[2] ,
+ 'bug_id' => $data[3] ,
+ 'bug_summary' => $data[4] ,
+ 'category' => "$data[5]: $data[6]" ,
+ 'attach_id' => $data[7] ,
+ 'attach_summary' => $data[8] ,
+ 'requester' => ($data[9] ? "$data[9] <$data[10]>" : $data[10]) ,
+ 'requestee' => ($data[11] ? "$data[11] <$data[12]>" : $data[12]) ,
+ 'created' => $data[13]
+ };
+ push(@requests, $request);
+ }
+
+ # Get a list of request type names to use in the filter form.
+ my @types = ("all");
+ SendSQL("SELECT DISTINCT(name) FROM flagtypes ORDER BY name");
+ push(@types, FetchOneColumn()) while MoreSQLData();
+
+ # products and components and the function used to modify the components
+ # menu when the products menu changes; used by the template to populate
+ # the menus and keep the components menu consistent with the products menu
+ GetVersionTable();
+ $vars->{'products'} = \@::legal_product;
+ $vars->{'components'} = \@::legal_components;
+ $vars->{'components_by_product'} = \%::components;
+
+ $vars->{'excluded_columns'} = \@excluded_columns;
+ $vars->{'group_field'} = $::FORM{'group'};
+ $vars->{'requests'} = \@requests;
+ $vars->{'form'} = \%::FORM;
+ $vars->{'types'} = \@types;
+
+ # Return the appropriate HTTP response headers.
+ print "Content-type: text/html\n\n";
+
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $template->process("request/queue.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+
+################################################################################
+# Data Validation / Security Authorization
+################################################################################
+
+sub validateStatus {
+ return if !defined($::FORM{'status'});
+
+ grep($::FORM{'status'} eq $_, qw(? +- + - all))
+ || ThrowCodeError("flag_status_invalid", { status => $::FORM{'status'} });
+}
+
+sub validateGroup {
+ return if !defined($::FORM{'group'});
+
+ grep($::FORM{'group'} eq $_, qw(requester requestee category type))
+ || ThrowCodeError("request_queue_group_invalid",
+ { group => $::FORM{'group'} });
+}
+
diff --git a/sanitycheck.cgi b/sanitycheck.cgi
index da71163cc..2798dfd2f 100755
--- a/sanitycheck.cgi
+++ b/sanitycheck.cgi
@@ -232,11 +232,11 @@ CrossCheck("fielddefs", "fieldid",
["bugs_activity", "fieldid"]);
CrossCheck("attachments", "attach_id",
- ["attachstatuses", "attach_id"],
+ ["flags", "attach_id"],
["bugs_activity", "attach_id"]);
-CrossCheck("attachstatusdefs", "id",
- ["attachstatuses", "statusid"]);
+CrossCheck("flagtypes", "id",
+ ["flags", "type_id"]);
CrossCheck("bugs", "bug_id",
["bugs_activity", "bug_id"],
@@ -280,7 +280,7 @@ CrossCheck("products", "id",
["components", "product_id", "name"],
["milestones", "product_id", "value"],
["versions", "product_id", "value"],
- ["attachstatusdefs", "product_id", "name"]);
+ ["flagtypes", "product_id", "name"]);
DateCheck("groups", "last_changed");
DateCheck("profiles", "refreshed_when");
diff --git a/template/en/default/account/prefs/email.html.tmpl b/template/en/default/account/prefs/email.html.tmpl
index e14ea9910..5d73a357b 100644
--- a/template/en/default/account/prefs/email.html.tmpl
+++ b/template/en/default/account/prefs/email.html.tmpl
@@ -83,9 +83,27 @@
<tr>
<td width="150"></td>
<td>
- <label for="ExcludeSelf">Only email me reports of changes made by other people</label>
<input type="checkbox" name="ExcludeSelf" id="ExcludeSelf" value="on"
[% " checked" IF excludeself %]>
+ <label for="ExcludeSelf">Only email me reports of changes made by other people</label>
+ <br>
+ </td>
+ </tr>
+ <tr>
+ <td width="150"></td>
+ <td>
+ <input type="checkbox" name="FlagRequestee" id="FlagRequestee" value="on"
+ [% " checked" IF FlagRequestee %]>
+ <label for="FlagRequestee">Email me when someone asks me to set a flag</label>
+ <br>
+ </td>
+ </tr>
+ <tr>
+ <td width="150"></td>
+ <td>
+ <input type="checkbox" name="FlagRequester" id="FlagRequester" value="on"
+ [% " checked" IF FlagRequester %]>
+ <label for="FlagRequester">Email me when someone sets a flag I asked for</label>
<br>
</td>
</tr>
diff --git a/template/en/default/admin/flag-type/confirm-delete.html.tmpl b/template/en/default/admin/flag-type/confirm-delete.html.tmpl
new file mode 100644
index 000000000..b022e621e
--- /dev/null
+++ b/template/en/default/admin/flag-type/confirm-delete.html.tmpl
@@ -0,0 +1,58 @@
+<!-- 1.0@bugzilla.org -->
+[%# 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): Myk Melez <myk@mozilla.org>
+ #%]
+
+[%# Filter off the name here to be used multiple times below %]
+[% name = BLOCK %][% flag_type.name FILTER html %][% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Confirm Deletion of Flag Type '$name'"
+%]
+
+<p>
+ There are [% flag_count %] flags of type [% name %].
+ If you delete this type, those flags will also be deleted. Note that
+ instead of deleting the type you can
+ <a href="editflagtypes.cgi?action=deactivate&id=[% flag_type.id %]">deactivate it</a>,
+ in which case the type and its flags will remain in the database
+ but will not appear in the Bugzilla UI.
+</p>
+
+<table>
+ <tr>
+ <td colspan=2>
+ Do you really want to delete this type?
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <a href="editflagtypes.cgi?action=delete&id=[% flag_type.id %]">
+ Yes, delete
+ </a>
+ </td>
+ <td align="right">
+ <a href="editflagtypes.cgi">
+ No, don't delete
+ </a>
+ </td>
+ </tr>
+</table>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/admin/flag-type/edit.html.tmpl b/template/en/default/admin/flag-type/edit.html.tmpl
new file mode 100644
index 000000000..ca01f6365
--- /dev/null
+++ b/template/en/default/admin/flag-type/edit.html.tmpl
@@ -0,0 +1,189 @@
+[%# 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): Myk Melez <myk@mozilla.org>
+ #%]
+
+[%# The javascript and header_html blocks get used in header.html.tmpl. %]
+[% javascript = BLOCK %]
+ var usetms = 0; // do we have target milestone?
+ var first_load = 1; // is this the first time we load the page?
+ var last_sel = []; // caches last selection
+ var cpts = new Array();
+ [% FOREACH p = products %]
+ cpts['[% p FILTER js %]'] = [
+ [%- FOREACH item = components_by_product.$p %]'[% item FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+ [% END %]
+[% END %]
+
+[% header_html = BLOCK %]
+ <script language="JavaScript" type="text/javascript" src="productmenu.js"></script>
+[% END %]
+
+[% IF type.target_type == "bug" %]
+ [% title = "Create Flag Type for Bugs" %]
+[% ELSE %]
+ [% title = "Create Flag Type for Attachments" %]
+[% END %]
+
+[% IF last_action == "copy" %]
+ [% title = "Create Flag Type Based on $type.name" %]
+[% ELSIF last_action == "edit" %]
+ [% title = "Edit Flag Type $type.name" %]
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = title
+ style = "
+ table#form th { text-align: right; vertical-align: baseline; white-space: nowrap; }
+ table#form td { text-align: left; vertical-align: baseline; }
+ "
+ onload="selectProduct(forms[0], 'product', 'component', '__Any__');"
+%]
+
+<form method="post" action="editflagtypes.cgi">
+ <input type="hidden" name="action" value="[% action %]">
+ <input type="hidden" name="id" value="[% type.id %]">
+ <input type="hidden" name="target_type" value="[% type.target_type %]">
+ [% FOREACH category = type.inclusions %]
+ <input type="hidden" name="inclusions" value="[% category %]">
+ [% END %]
+ [% FOREACH category = type.exclusions %]
+ <input type="hidden" name="exclusions" value="[% category %]">
+ [% END %]
+
+ <table id="form" cellspacing="0" cellpadding="4" border="0">
+ <tr>
+ <th>Name:</th>
+ <td>
+ a short name identifying this type<br>
+ <input type="text" name="name" value="[% type.name FILTER html %]"
+ size="50" maxlength="50">
+ </td>
+ </tr>
+
+ <tr>
+ <th>Description:</th>
+ <td>
+ a comprehensive description of this type<br>
+ <textarea name="description" rows="4" cols="80">[% type.description FILTER html %]</textarea>
+ </td>
+ </tr>
+
+ <tr>
+ <th>Category:</th>
+ <td>
+ the products/components to which [% type.target_type %]s must
+ (inclusions) or must not (exclusions) belong in order for users
+ to be able to set flags of this type for them
+ <table>
+ <tr>
+ <td style="vertical-align: top;">
+ <b>Product/Component:</b><br>
+ <select name="product" onChange="selectProduct(this.form, 'product', 'component', '__Any__');">
+ <option value="">__Any__</option>
+ [% FOREACH item = products %]
+ <option value="[% item %]" [% "selected" IF type.product.name == item %]>[% item %]</option>
+ [% END %]
+ </select><br>
+ <select name="component">
+ <option value="">__Any__</option>
+ [% FOREACH item = components %]
+ <option value="[% item %]" [% "selected" IF type.component.name == item %]>[% item %]</option>
+ [% END %]
+ </select><br>
+ <input type="submit" name="categoryAction" value="Include">
+ <input type="submit" name="categoryAction" value="Exclude">
+ </td>
+ <td style="vertical-align: top;">
+ <b>Inclusions:</b><br>
+ [% PROCESS "global/select-menu.html.tmpl" name="inclusion_to_remove" multiple=1 size=4 options=type.inclusions %]<br>
+ <input type="submit" name="categoryAction" value="Remove Inclusion">
+ </td>
+ <td style="vertical-align: top;">
+ <b>Exclusions:</b><br>
+ [% PROCESS "global/select-menu.html.tmpl" name="exclusion_to_remove" multiple=1 size=4 options=type.exclusions %]<br>
+ <input type="submit" name="categoryAction" value="Remove Exclusion">
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+
+ <tr>
+ <th>Sort Key:</th>
+ <td>
+ a number between 1 and 32767 by which this type will be sorted
+ when displayed to users in a list; ignore if you don't care
+ what order the types appear in or if you want them to appear
+ in alphabetical order<br>
+ <input type="text" name="sortkey" value="[% type.sortkey || 1 %]" size="5" maxlength="5">
+ </td>
+ </tr>
+
+ <tr>
+ <th>&nbsp;</th>
+ <td>
+ <input type="checkbox" name="is_active" [% "checked" IF type.is_active || !type.is_active.defined %]>
+ active (flags of this type appear in the UI and can be set)
+ </td>
+ </tr>
+
+ <tr>
+ <th>&nbsp;</th>
+ <td>
+ <input type="checkbox" name="is_requestable" [% "checked" IF type.is_requestable || !type.is_requestable.defined %]>
+ requestable (users can ask for flags of this type to be set)
+ </td>
+ </tr>
+
+ <tr>
+ <th>CC List:</th>
+ <td>
+ if requestable, who should get carbon copied on email notification of requests<br>
+ <input type="text" name="cc_list" value="[% type.cc_list FILTER html %]" size="80" maxlength="200">
+ </td>
+ </tr>
+
+ <tr>
+ <th>&nbsp;</th>
+ <td>
+ <input type="checkbox" name="is_requesteeble" [% "checked" IF type.is_requesteeble || !type.is_requesteeble.defined %]>
+ specifically requestable (users can ask specific other users to set flags of this type as opposed to just asking the wind)
+ </td>
+ </tr>
+
+ <tr>
+ <th>&nbsp;</th>
+ <td>
+ <input type="checkbox" name="is_multiplicable" [% "checked" IF type.is_multiplicable || !type.is_multiplicable.defined %]>
+ multiplicable (multiple flags of this type can be set on the same [% type.target_type %])
+ </td>
+ </tr>
+
+ <tr>
+ <th></th>
+ <td>
+ <input type="submit" value="[% (last_action == "enter" || last_action == "copy") ? "Create" : "Save Changes" %]">
+ </td>
+ </tr>
+
+ </table>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/admin/flag-type/list.html.tmpl b/template/en/default/admin/flag-type/list.html.tmpl
new file mode 100644
index 000000000..76a835639
--- /dev/null
+++ b/template/en/default/admin/flag-type/list.html.tmpl
@@ -0,0 +1,107 @@
+[%# 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): Myk Melez <myk@mozilla.org>
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = 'Administer Flag Types'
+ style = "
+ table#flag_types tr th { text-align: left; }
+ .inactive { color: #787878; }
+ "
+%]
+
+<p>
+ Flags are markers that identify whether a bug or attachment has been granted
+ or denied some status. Flags appear in the UI as a name and a status symbol
+ ("+" for granted, "-" for denied, and "?" for statuses requested by users).
+</p>
+
+<p>
+ For example, you might define a "review" status for users to request review
+ for their patches. When a patch writer requests review, the string "review?"
+ will appear in the attachment. When a patch reviewer reviews the patch,
+ either the string "review+" or the string "review-" will appear in the patch,
+ depending on whether the patch passed or failed review.
+</p>
+
+<h3>Flag Types for Bugs</h3>
+
+[% PROCESS display_flag_types types=bug_types %]
+
+<p>
+ <a href="editflagtypes.cgi?action=enter&target_type=bug">Create Flag Type for Bugs</a>
+</p>
+
+<h3>Flag Types for Attachments</h3>
+
+[% PROCESS display_flag_types types=attachment_types %]
+
+<p>
+ <a href="editflagtypes.cgi?action=enter&target_type=attachment">Create Flag Type For Attachments</a>
+</p>
+
+<script language="JavaScript">
+ <!--
+ function confirmDelete(id, name, count)
+ {
+ if (count > 0) {
+ var msg = 'There are ' + count + ' flags of type ' + name + '. ' +
+ 'If you delete this type, those flags will also be ' +
+ 'deleted.\n\nNote: to deactivate the type instead ' +
+ 'of deleting it, edit it and uncheck its "is active" ' +
+ 'flag.\n\nDo you really want to delete this flag type?';
+ if (!confirm(msg)) return false;
+ }
+ location.href = "editflagtypes.cgi?action=delete&id=" + id;
+ return false; // prevent strict JavaScript warning that this function
+ // does not always return a value
+ }
+ //-->
+</script>
+
+[% PROCESS global/footer.html.tmpl %]
+
+
+[% BLOCK display_flag_types %]
+ <table id="flag_types" cellspacing="0" cellpadding="4" border="1">
+
+ <tr>
+ <th>Name</th>
+ <th>Description</th>
+ <th>Actions</th>
+ </tr>
+
+ [% FOREACH type = types %]
+
+ <tr class="[% type.is_active ? "active" : "inactive" %]">
+ <td>[% type.name FILTER html %]</td>
+ <td>[% type.description FILTER html %]</td>
+ <td>
+ <a href="editflagtypes.cgi?action=edit&id=[% type.id %]">Edit</a>
+ | <a href="editflagtypes.cgi?action=copy&id=[% type.id %]">Copy</a>
+ | <a href="editflagtypes.cgi?action=confirmdelete&id=[% type.id %]"
+ onclick="return confirmDelete([% type.id %], '[% type.name FILTER js %]',
+ [% type.flag_count %]);">Delete</a>
+ </td>
+ </tr>
+
+ [% END %]
+
+ </table>
+[% END %]
diff --git a/template/en/default/attachment/edit.html.tmpl b/template/en/default/attachment/edit.html.tmpl
index ec2616bf9..32449f041 100644
--- a/template/en/default/attachment/edit.html.tmpl
+++ b/template/en/default/attachment/edit.html.tmpl
@@ -32,6 +32,8 @@
table.attachment_info th { text-align: right; vertical-align: top; }
table.attachment_info td { text-align: left; vertical-align: top; }
#noview { text-align: left; vertical-align: center; }
+
+ table#flags th, table#flags td { font-size: small; vertical-align: baseline; }
"
%]
@@ -158,8 +160,7 @@
<b>MIME Type:</b><br>
<input type="text" size="20" name="contenttypeentry" value="[% contenttype FILTER html %]"><br>
-
- <b>Flags:</b><br>
+
<input type="checkbox" id="ispatch" name="ispatch" value="1"
[% 'checked="checked"' IF ispatch %]>
<label for="ispatch">patch</label>
@@ -168,20 +169,14 @@
<label for="isobsolete">obsolete</label><br>
[% IF (Param("insidergroup") && UserInGroup(Param("insidergroup"))) %]
<input type="checkbox" name="isprivate" value="1"[% " checked" IF isprivate %]> private<br><br>
+ [% ELSE %]<br>
[% END %]
- [% IF statusdefs.size %]
- <b>Status:</b><br>
- [% FOREACH def = statusdefs %]
- <input type="checkbox" id="status-[% def.id %]" name="status"
- value="[% def.id %]"
- [% 'checked="checked"' IF statuses.${def.id} %]>
- <label for="status-[% def.id %]">
- [% def.name FILTER html %]
- </label><br>
- [% END %]
+ [% IF flag_types.size > 0 %]
+ <b>Flags:</b><br>
+ [% PROCESS "flag/list.html.tmpl" bug_id=bugid attach_id=attachid %]<br>
[% END %]
-
+
<div id="smallCommentFrame">
<b>Comment (on the bug):</b><br>
<textarea name="comment" rows="5" cols="25" wrap="soft"></textarea><br>
diff --git a/template/en/default/attachment/list.html.tmpl b/template/en/default/attachment/list.html.tmpl
index e7aa8b0ef..59f749695 100644
--- a/template/en/default/attachment/list.html.tmpl
+++ b/template/en/default/attachment/list.html.tmpl
@@ -19,13 +19,18 @@
# Contributor(s): Myk Melez <myk@mozilla.org>
#%]
+[%# Whether or not to include flags. %]
+[% display_flags = num_attachment_flag_types > 0 %]
+
<br>
<table cellspacing="0" cellpadding="4" border="1">
<tr>
<th bgcolor="#cccccc" align="left">Attachment</th>
<th bgcolor="#cccccc" align="left">Type</th>
<th bgcolor="#cccccc" align="left">Created</th>
- <th bgcolor="#cccccc" align="left">Status</th>
+ [% IF display_flags %]
+ <th bgcolor="#cccccc" align="left">Flags</th>
+ [% END %]
<th bgcolor="#cccccc" align="left">Actions</th>
</tr>
[% canseeprivate = !Param("insidergroup") || UserInGroup(Param("insidergroup")) %]
@@ -50,16 +55,24 @@
<td valign="top">[% attachment.date %]</td>
- <td valign="top">
- [% IF attachment.statuses.size == 0 %]
- <i>none</i>
- [% ELSE %]
- [% FOREACH s = attachment.statuses %]
- [% s FILTER html FILTER replace('\s', '&nbsp;') %]<br>
+ [% IF display_flags %]
+ <td valign="top">
+ [% IF attachment.flags.size == 0 %]
+ <i>none</i>
+ [% ELSE %]
+ [% FOR flag = attachment.flags %]
+ [% IF flag.setter %]
+ [% flag.setter.nick FILTER html %]:
+ [% END %]
+ [%+ flag.type.name %][% flag.status %]
+ [%+ IF flag.status == "?" && flag.requestee %]
+ ([% flag.requestee.nick %])
+ [% END %]<br>
+ [% END %]
[% END %]
- [% END %]
- </td>
-
+ </td>
+ [% END %]
+
<td valign="top">
[% IF attachment.canedit %]
<a href="attachment.cgi?id=[% attachment.attachid %]&amp;action=edit">Edit</a>
@@ -72,7 +85,7 @@
[% END %]
<tr>
- <td colspan="4">
+ <td colspan="[% display_flags ? 4 : 3 %]">
<a href="attachment.cgi?bugid=[% bugid %]&amp;action=enter">Create a New Attachment</a> (proposed patch, testcase, etc.)
</td>
<td colspan="1">
diff --git a/template/en/default/bug/edit.html.tmpl b/template/en/default/bug/edit.html.tmpl
index 9cf33b8b5..ef9ec2d7f 100644
--- a/template/en/default/bug/edit.html.tmpl
+++ b/template/en/default/bug/edit.html.tmpl
@@ -194,7 +194,7 @@
[% END %]
</tr>
-[%# *** QAContact URL Summary Whiteboard Keywords *** %]
+[%# *** QAContact URL Requests Summary Whiteboard Keywords *** %]
[% IF Param('useqacontact') %]
<tr>
@@ -218,17 +218,23 @@
[% END %]
</b>
</td>
- <td colspan="7">
+ <td colspan="5">
<input name="bug_file_loc" accesskey="u"
value="[% bug.bug_file_loc FILTER html %]" size="60">
</td>
+ <td rowspan="4" colspan="2" valign="top">
+ [% IF flag_types.size > 0 %]
+ <b>Flags:</b><br>
+ [% PROCESS "flag/list.html.tmpl" %]
+ [% END %]
+ </td>
</tr>
<tr>
<td align="right">
<b><u>S</u>ummary:</b>
</td>
- <td colspan="7">
+ <td colspan="5">
<input name="short_desc" accesskey="s"
value="[% bug.short_desc FILTER html %]" size="60">
</td>
@@ -239,7 +245,7 @@
<td align="right">
<b>Status <u>W</u>hiteboard:</b>
</td>
- <td colspan="7">
+ <td colspan="5">
<input name="status_whiteboard" accesskey="w"
value="[% bug.status_whiteboard FILTER html %]" size="60">
</td>
@@ -252,7 +258,7 @@
<b>
<a href="describekeywords.cgi"><u>K</u>eywords</a>:
</b>
- <td colspan="7">
+ <td colspan="5">
<input name="keywords" accesskey="k"
value="[% bug.keywords.join(', ') FILTER html %]" size="60">
</td>
@@ -263,8 +269,8 @@
[%# *** Attachments *** %]
[% PROCESS attachment/list.html.tmpl
- attachments = bug.attachments
- bugid = bug.bug_id %]
+ attachments = bug.attachments
+ bugid = bug.bug_id %]
[%# *** Dependencies Votes *** %]
diff --git a/template/en/default/flag/list.html.tmpl b/template/en/default/flag/list.html.tmpl
new file mode 100644
index 000000000..951f248db
--- /dev/null
+++ b/template/en/default/flag/list.html.tmpl
@@ -0,0 +1,94 @@
+[%# 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): Myk Melez <myk@mozilla.org>
+ #%]
+
+<table id="flags">
+
+ [% FOREACH type = flag_types %]
+ [% FOREACH flag = type.flags %]
+ <tr>
+ <td>
+ [% flag.setter.nick FILTER html %]:
+ </td>
+ <td>
+ [% type.name FILTER html %]
+ </td>
+ <td>
+ <select name="flag-[% flag.id %]">
+ <option value="X"></option>
+ <option value="+" [% "selected" IF flag.status == "+" %]>+</option>
+ <option value="-" [% "selected" IF flag.status == "-" %]>-</option>
+ <option value="?" [% "selected" IF flag.status == "?" %]>?</option>
+ </select>
+ </td>
+ <td>
+ [% IF flag.status == "?" && flag.requestee %]([% flag.requestee.nick FILTER html %])[% END %]
+ </td>
+ </tr>
+ [% END %]
+ [% IF !type.flags || type.flags.size == 0 %]
+ <tr>
+ <td>&nbsp;</td>
+ <td>[% type.name %]</td>
+ <td>
+ <select name="flag_type-[% type.id %]">
+ <option value="X"></option>
+ <option value="+">+</option>
+ <option value="-">-</option>
+ [% IF type.is_requestable %]
+ <option value="?">?</option>
+ [% END %]
+ </select>
+ </td>
+ <td>
+ [% IF type.is_requestable && type.is_requesteeble %]
+ (<input type="text" name="requestee-[% type.id %]" size="8" maxlength="255">)
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ [% END %]
+
+ [% FOREACH type = flag_types %]
+ [% NEXT UNLESS type.flags.size > 0 && type.is_multiplicable %]
+ [% IF !separator_displayed %]
+ <tr><td colspan="3"><hr></td></tr>
+ [% separator_displayed = 1 %]
+ [% END %]
+ <tr>
+ <td colspan="2">addl. [% type.name %]</td>
+ <td>
+ <select name="flag_type-[% type.id %]">
+ <option value="X"></option>
+ <option value="+">+</option>
+ <option value="-">-</option>
+ [% IF type.is_requestable %]
+ <option value="?">?</option>
+ [% END %]
+ </select>
+ </td>
+ <td>
+ [% IF type.is_requestable && type.is_requesteeble %]
+ (<input type="text" name="requestee-[% type.id %]" size="8" maxlength="255">)
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+
+</table>
diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl
index bf93977ad..1981364f1 100644
--- a/template/en/default/global/code-error.html.tmpl
+++ b/template/en/default/global/code-error.html.tmpl
@@ -40,6 +40,10 @@
to any [% parameters %] which you may have set before calling
ThrowCodeError.
+ [% ELSIF error == "action_unrecognized" %]
+ I don't recognize the value (<em>[% variables.action FILTER html %]</em>)
+ of the <em>action</em> variable.
+
[% ELSIF error == "attachment_already_obsolete" %]
Attachment #[% attachid FILTER html %] ([% description FILTER html %])
is already obsolete.
@@ -78,10 +82,40 @@
[% ELSIF error == "no_bug_data" %]
No data when fetching bug [% bug_id %].
+ [% ELSIF error == "flag_nonexistent" %]
+ There is no flag with ID #[% variables.id %].
+
+ [% ELSIF error == "flag_status_invalid" %]
+ The flag status <em>[% variables.status FILTER html %]</em> is invalid.
+
+ [% ELSIF error == "flag_type_component_nonexistent" %]
+ The component <em>[% variables.component FILTER html %] does not exist
+ in the product <em>[% variables.product FILTER html %]</em>.
+
+ [% ELSIF error == "flag_type_component_without_product" %]
+ A component was selected without a product being selected.
+
+ [% ELSIF error == "flag_type_id_invalid" %]
+ The flag type ID <em>[% variables.id FILTER html %]</em> is not
+ a positive integer.
+
+ [% ELSIF error == "flag_type_nonexistent" %]
+ There is no flag type with the ID <em>[% variables.id %]</em>.
+
+ [% ELSIF error == "flag_type_product_nonexistent" %]
+ The product <em>[% variables.product FILTER html %]</em> does not exist.
+
+ [% ELSIF error == "flag_type_target_type_invalid" %]
+ The target type was neither <em>bug</em> nor <em>attachment</em>
+ but rather <em>[% variables.target_type FILTER html %]</em>.
+
[% ELSIF error == "no_y_axis_defined" %]
No Y axis was defined when creating report. The X axis is optional,
but the Y axis is compulsory.
+ [% ELSIF error == "request_queue_group_invalid" %]
+ The group field <em>[% group FILTER html %]</em> is invalid.
+
[% ELSIF error == "template_error" %]
[% template_error_msg %]
@@ -91,6 +125,14 @@
[% ELSIF error == "unknown_action" %]
Unknown action [% action FILTER html %]!
+ [% ELSIF error == "unknown_component" %]
+ [% title = "Unknown Component" %]
+ There is no component named <em>[% variables.component FILTER html %]</em>.
+
+ [% ELSIF error == "unknown_product" %]
+ [% title = "Unknown Product" %]
+ There is no product named <em>[% variables.product FILTER html %]</em>.
+
[% ELSE %]
[%# Give sensible error if error functions are used incorrectly.
#%]
diff --git a/template/en/default/global/messages.html.tmpl b/template/en/default/global/messages.html.tmpl
index 584c4a93e..85c678fdc 100644
--- a/template/en/default/global/messages.html.tmpl
+++ b/template/en/default/global/messages.html.tmpl
@@ -81,6 +81,34 @@
[% title = "Password Changed" %]
Your password has been changed.
+ [% ELSIF message_tag == "flag_type_created" %]
+ [% title = "Flag Type Created" %]
+ The flag type <em>[% name FILTER html %]</em> has been created.
+ <a href="editflagtypes.cgi">Back to flag types.</a>
+
+ [% ELSIF message_tag == "flag_type_changes_saved" %]
+ [% title = "Flag Type Changes Saved" %]
+ <p>
+ Your changes to the flag type <em>[% name FILTER html %]</em>
+ have been saved.
+ <a href="editflagtypes.cgi">Back to flag types.</a>
+ </p>
+
+ [% ELSIF message_tag == "flag_type_deleted" %]
+ [% title = "Flag Type Deleted" %]
+ <p>
+ The flag type <em>[% name FILTER html %]</em> has been deleted.
+ <a href="editflagtypes.cgi">Back to flag types.</a>
+ </p>
+
+ [% ELSIF message_tag == "flag_type_deactivated" %]
+ [% title = "Flag Type Deactivated" %]
+ <p>
+ The flag type <em>[% flag_type.name FILTER html %]</em>
+ has been deactivated.
+ <a href="editflagtypes.cgi">Back to flag types.</a>
+ </p>
+
[% ELSIF message_tag == "shutdown" %]
[% title = "Bugzilla is Down" %]
[% Param("shutdownhtml") %]
diff --git a/template/en/default/global/select-menu.html.tmpl b/template/en/default/global/select-menu.html.tmpl
index c27f60e8b..7b7fddb29 100644
--- a/template/en/default/global/select-menu.html.tmpl
+++ b/template/en/default/global/select-menu.html.tmpl
@@ -22,12 +22,18 @@
[%# INTERFACE:
# name: string; the name of the menu.
#
+ # multiple: boolean; whether or not the menu is multi-select
+ #
+ # size: integer; if multi-select, the number of items to display at once
+ #
# options: array or hash; the items with which to populate the array.
# If a hash is passed, the hash keys become the names displayed
# to the user while the hash values become the value of the item.
#
# default: string; the item selected in the menu by default.
#
+ # onchange: code; JavaScript to be run when the user changes the value
+ # selected in the menu.
#%]
[%# Get the scalar representation of the options reference,
@@ -37,7 +43,9 @@
#%]
[% options_type = BLOCK %][% options %][% END %]
-<select name="[% name FILTER html %]">
+<select name="[% name FILTER html %]"
+ [% IF onchange %]onchange="[% onchange %]"[% END %]
+ [% IF multiple %] multiple [% IF size %] size="[% size %]" [% END %] [% END %]>
[% IF options_type.search("ARRAY") %]
[% FOREACH value = options %]
<option value="[% value FILTER html %]"
@@ -45,7 +53,7 @@
[% value FILTER html %]
</option>
[% END %]
- [% ELSIF values_type.search("HASH") %]
+ [% ELSIF options_type.search("HASH") %]
[% FOREACH option = options %]
<option value="[% option.value FILTER html %]"
[% " selected" IF option.value == default %]>
diff --git a/template/en/default/global/useful-links.html.tmpl b/template/en/default/global/useful-links.html.tmpl
index 1e5b09df1..785a9d75e 100644
--- a/template/en/default/global/useful-links.html.tmpl
+++ b/template/en/default/global/useful-links.html.tmpl
@@ -50,6 +50,8 @@
<input name="id" size="6"> |
<a href="reports.cgi">Reports</a>
+
+ | <a href="request.cgi">Requests</a>
[% IF user.login && Param('usevotes') %]
| <a href="votes.cgi?action=show_user">My Votes</a>
@@ -68,7 +70,7 @@
|| user.canblessany %]
[% ', <a href="editproducts.cgi">products</a>'
IF user.groups.editcomponents %]
- [% ', <a href="editattachstatuses.cgi"> attachment&nbsp;statuses</a>'
+ [% ', <a href="editflagtypes.cgi">flags</a>'
IF user.groups.editcomponents %]
[% ', <a href="editgroups.cgi">groups</a>'
IF user.groups.creategroups %]
diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index c6f970df3..c9dca30d4 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -30,7 +30,7 @@
#%]
[% DEFAULT title = "Error" %]
-
+
[% error_message = BLOCK %]
[% IF error == "aaa_example_error_tag" %]
[% title = "Example Error" %]
@@ -75,6 +75,10 @@
Bug aliases cannot be longer than 20 characters.
Please choose a shorter alias.
+ [% ELSIF error == "authorization_failure" %]
+ [% title = "Authorization Failed" %]
+ You are not allowed to [% action %].
+
[% ELSIF error == "attachment_access_denied" %]
[% title = "Access Denied" %]
You are not permitted access to this attachment.
@@ -129,6 +133,23 @@
format like JPG or PNG, or put it elsewhere on the web and
link to it from the bug's URL field or in a comment on the bug.
+ [% ELSIF error == "flag_type_cc_list_invalid" %]
+ [% title = "Flag Type CC List Invalid" %]
+ The CC list [% cc_list FILTER html %] must be less than 200 characters long.
+
+ [% ELSIF error == "flag_type_description_invalid" %]
+ [% title = "Flag Type Description Invalid" %]
+ The description must be less than 32K.
+
+ [% ELSIF error == "flag_type_name_invalid" %]
+ [% title = "Flag Type Name Invalid" %]
+ The name <em>[% name FILTER html %]</em> must be 1-50 characters long.
+
+ [% ELSIF error == "flag_type_sortkey_invalid" %]
+ [% title = "Flag Type Sort Key Invalid" %]
+ The sort key must be an integer between 0 and 32767 inclusive.
+ It cannot be <em>[% variables.sortkey %]</em>.
+
[% ELSIF error == "illegal_at_least_x_votes" %]
[% title = "Your Query Makes No Sense" %]
The <em>At least ___ votes</em> field must be a simple number.
@@ -176,10 +197,6 @@
[% title = "Invalid Attachment ID" %]
The attachment id [% attach_id FILTER html %] is invalid.
- [% ELSIF error == "invalid_attach_status" %]
- [% title = "Invalid Attachment Status" %]
- One of the statuses you entered is not a valid status for this attachment.
-
[% ELSIF error == "invalid_content_type" %]
[% title = "Invalid Content-Type" %]
The content type <em>[% contenttype FILTER html %]</em> is invalid.
@@ -281,6 +298,18 @@
intentionally cleared out the "Reassign bug to"
field, [% Param("browserbugmessage") %]
+ [% ELSIF error == "requestee_too_short" %]
+ [% title = "Requestee Name Too Short" %]
+ One or two characters match too many users, so please enter at least
+ three characters of the name/email address of the user you want to set
+ the flag.
+
+ [% ELSIF error == "requestee_too_many_matches" %]
+ [% title = "Requestee String Matched Too Many Times" %]
+ The string <em>[% requestee FILTER html %]</em> matched more than
+ 100 users. Enter more of the name to bring the number of matches
+ down to a reasonable amount.
+
[% ELSIF error == "unknown_keyword" %]
[% title = "Unknown Keyword" %]
<code>[% keyword FILTER html %]</code> is not a known keyword.
diff --git a/template/en/default/request/created-email.txt.tmpl b/template/en/default/request/created-email.txt.tmpl
new file mode 100644
index 000000000..3edf10786
--- /dev/null
+++ b/template/en/default/request/created-email.txt.tmpl
@@ -0,0 +1,41 @@
+[%# 1.0@bugzilla.org %]
+[%# 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): Myk Melez <myk@mozilla.org>
+ #%]
+From: bugzilla-request-daemon
+To: [% flag.requestee.email IF flag.requestee.email_prefs.FlagRequestee %]
+CC: [% flag.type.cc_list %]
+Subject: [% flag.type.name %]: [Bug [% flag.target.bug.id %]] [% flag.target.bug.summary %]
+[%- IF flag.target.attachment.exists %] :
+ [Attachment [% flag.target.attachment.id %]] [% flag.target.attachment.summary %][% END %]
+
+[%+ USE wrap -%]
+[%- FILTER bullet = wrap(80) -%]
+[% flag.setter.identity %] has asked you for [% flag.type.name %] on bug #
+ [%- flag.target.bug.id %] ([% flag.target.bug.summary %])
+[%- IF flag.target.attachment.exists %], attachment #
+ [%- flag.target.attachment.id %] ([% flag.target.attachment.summary %])[% END %].
+
+[%+ IF flag.target.type == 'bug' -%]
+ [% Param('urlbase') %]show_bug.cgi?id=[% flag.target.bug.id %]
+[%- ELSIF flag.target.type == 'attachment' -%]
+ [% Param('urlbase') %]attachment.cgi?id=[% flag.target.attachment.id %]&action=edit
+[%- END %]
+
+[%- END %]
diff --git a/template/en/default/request/fulfilled-email.txt.tmpl b/template/en/default/request/fulfilled-email.txt.tmpl
new file mode 100644
index 000000000..84608c546
--- /dev/null
+++ b/template/en/default/request/fulfilled-email.txt.tmpl
@@ -0,0 +1,42 @@
+[%# 1.0@bugzilla.org %]
+[%# 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): Myk Melez <myk@mozilla.org>
+ #%]
+[% statuses = { '+' => "approved" , '-' => 'denied' , 'X' => "cancelled" } %]
+From: bugzilla-request-daemon
+To: [% flag.setter.email IF flag.setter.email_prefs.FlagRequester %]
+CC: [% flag.type.cc_list %]
+Subject: [% flag.type.name %]: [Bug [% flag.target.bug.id %]] [% flag.target.bug.summary %]
+[%- IF flag.target.attachment.exists %] :
+ [Attachment [% flag.target.attachment.id %]] [% flag.target.attachment.summary %][% END %]
+
+[%+ USE wrap -%]
+[%- FILTER bullet = wrap(80) -%]
+[% user.realname %] <[% user.login %]> has [% statuses.${flag.status} %] your request for [% flag.type.name %] on bug #
+ [%- flag.target.bug.id %] ([% flag.target.bug.summary %])
+[%- IF flag.target.attachment.exists %], attachment #
+ [%- flag.target.attachment.id %] ([% flag.target.attachment.summary %])[% END %].
+
+[%+ IF flag.target.type == 'bug' -%]
+ [% Param('urlbase') %]show_bug.cgi?id=[% flag.target.bug.id %]
+[%- ELSIF flag.target.type == 'attachment' -%]
+ [% Param('urlbase') %]attachment.cgi?id=[% flag.target.attachment.id %]&action=edit
+[%- END %]
+
+[%- END %]
diff --git a/template/en/default/request/queue.html.tmpl b/template/en/default/request/queue.html.tmpl
new file mode 100644
index 000000000..14f244ab3
--- /dev/null
+++ b/template/en/default/request/queue.html.tmpl
@@ -0,0 +1,193 @@
+[%# 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): Myk Melez <myk@mozilla.org>
+ #%]
+
+[%# The javascript and header_html blocks get used in header.html.tmpl. %]
+[% javascript = BLOCK %]
+ var usetms = 0; // do we have target milestone?
+ var first_load = 1; // is this the first time we load the page?
+ var last_sel = []; // caches last selection
+ var cpts = new Array();
+ [% FOREACH p = products %]
+ cpts['[% p FILTER js %]'] = [
+ [%- FOREACH item = components_by_product.$p %]'[% item FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+ [% END %]
+[% END %]
+
+[% header_html = BLOCK %]
+ <script language="JavaScript" type="text/javascript" src="productmenu.js"></script>
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title="Request Queue"
+ style = "
+ table.requests th { text-align: left; }
+ table#filter th { text-align: right; }
+ "
+%]
+
+[% column_headers = {
+ "type" => "Flag" ,
+ "status" => "Status" ,
+ "bug" => "Bug" ,
+ "attachment" => "Attachment" ,
+ "requester" => "Requester" ,
+ "requestee" => "Requestee" ,
+ "created" => "Created" ,
+ "category" => "Product/Component" } %]
+
+[% DEFAULT display_columns = ["requester", "requestee", "type", "bug", "attachment", "created"]
+ group_field = "Requestee"
+ group_value = ""
+%]
+
+[% IF requests.size == 0 %]
+ <p>
+ No requests.
+ </p>
+[% ELSE %]
+ [% FOREACH request = requests %]
+ [% PROCESS start_new_table IF request.$group_field != group_value %]
+ <tr>
+ [% FOREACH column = display_columns %]
+ [% NEXT IF column == group_field || excluded_columns.contains(column) %]
+ <td>[% PROCESS "display_$column" %]</td>
+ [% END %]
+ </tr>
+ [% END %]
+ </table>
+[% END %]
+
+<h3>Filter the Queue</h3>
+
+<form action="request.cgi" method="get">
+ <input type="hidden" name="action" value="queue">
+
+ <table id="filter">
+ <tr>
+ <th>Requester:</th>
+ <td><input type="text" name="requester" value="[% form.requester FILTER html %]" size="20"></td>
+ <th>Product:</th>
+ <td>
+ <select name="product" onChange="selectProduct(this.form, 'product', 'component', 'Any');">
+ <option value="">Any</option>
+ [% FOREACH item = products %]
+ <option value="[% item FILTER html %]"
+ [% "selected" IF form.product == item %]>[% item FILTER html %]</option>
+ [% END %]
+ </select>
+ </td>
+ <th>Flag:</th>
+ <td>
+ [% PROCESS "global/select-menu.html.tmpl"
+ name="type"
+ options=types
+ default=form.type %]
+ </td>
+
+ [%# We could let people see a "queue" of non-pending requests. %]
+ <!--
+ <th>Status:</th>
+ <td>
+ [%# PROCESS "global/select-menu.html.tmpl"
+ name="status"
+ options=["all", "?", "+-", "+", "-"]
+ default=form.status %]
+ </td>
+ -->
+
+ </tr>
+ <tr>
+ <th>Requestee:</th>
+ <td><input type="text" name="requestee" value="[% form.requestee FILTER html %]" size="20"></td>
+ <th>Component:</th>
+ <td>
+ <select name="component">
+ <option value="">Any</option>
+ [% FOREACH item = components %]
+ <option value="[% item FILTER html %]" [% "selected" IF form.component == item %]>
+ [% item FILTER html %]</option>
+ [% END %]
+ </select>
+ </td>
+ <th>Group By:</th>
+ <td>
+ [% groups = {
+ "Requester" => 'requester' ,
+ "Requestee" => 'requestee',
+ "Flag" => 'type' ,
+ "Product/Component" => 'category'
+ } %]
+ [% PROCESS "global/select-menu.html.tmpl" name="group" options=groups default=form.group %]
+ </td>
+ <td><input type="submit" value="Filter"></td>
+ </tr>
+ </table>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
+
+[% BLOCK start_new_table %]
+ [% "</table>" UNLESS group_value == "" %]
+ <h3>[% column_headers.$group_field %]: [% request.$group_field FILTER html %]</h3>
+ <table class="requests" cellspacing="0" cellpadding="4" border="1">
+ <tr>
+ [% FOREACH column = display_columns %]
+ [% NEXT IF column == group_field || excluded_columns.contains(column) %]
+ <th>[% column_headers.$column %]</th>
+ [% END %]
+ </tr>
+ [% group_value = request.$group_field %]
+[% END %]
+
+[% BLOCK display_type %]
+ [% request.type FILTER html %]
+[% END %]
+
+[% BLOCK display_status %]
+ [% request.status %]
+[% END %]
+
+[% BLOCK display_bug %]
+ <a href="show_bug.cgi?id=[% request.bug_id %]">
+ [% request.bug_id %]: [%+ request.bug_summary FILTER html %]</a>
+[% END %]
+
+[% BLOCK display_attachment %]
+ [% IF request.attach_id %]
+ <a href="attachment.cgi?id=[% request.attach_id %]&action=edit">
+ [% request.attach_id %]: [%+ request.attach_summary FILTER html %]</a>
+ [% ELSE %]
+ N/A
+ [% END %]
+[% END %]
+
+[% BLOCK display_requestee %]
+ [% request.requestee FILTER html %]
+[% END %]
+
+[% BLOCK display_requester %]
+ [% request.requester FILTER html %]
+[% END %]
+
+[% BLOCK display_created %]
+ [% request.created FILTER html %]
+[% END %]
+
diff --git a/template/en/default/request/verify.html.tmpl b/template/en/default/request/verify.html.tmpl
new file mode 100644
index 000000000..ad4c07d2c
--- /dev/null
+++ b/template/en/default/request/verify.html.tmpl
@@ -0,0 +1,108 @@
+<!-- 1.0@bugzilla.org -->
+[%# 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): Myk Melez <myk@mozilla.org>
+ #%]
+
+[%# INTERFACE:
+ # form, mform: hashes; the form values submitted to the script, used by
+ # hidden-fields to generate hidden form fields replicating
+ # the original form
+ # flags: array; the flags the user made, including information about
+ # potential requestees for those flags (based on
+ # the string the user typed into the requestee fields)
+ # target: record; the bug/attachment for which the flags are being made
+ #%]
+
+[% UNLESS header_done %]
+ [% title = BLOCK %]
+ Verify Requests for Bug #[% target.bug.id %]
+ [% IF target.attachment %], Attachment #[% target.attachment.id %][% END %]
+ [% END %]
+
+ [% h1 = BLOCK %]
+ Verify Requests for <a href="show_bug.cgi?id=[% target.bug.id %]">Bug #[% target.bug.id %]</a>
+ [% IF target.attachment.exists %],
+ <a href="attachment.cgi?id=[% target.attachment.id %]&action=edit">Attachment #[% target.attachment.id %]</a>
+ [% END %]
+ [% END %]
+
+ [% h2 = BLOCK %]
+ [% target.bug.summary FILTER html %]
+ [% IF target.attachment.exists %]
+ : [% target.attachment.summary FILTER html %]
+ [% END %]
+ [% END %]
+
+ [% PROCESS global/header.html.tmpl %]
+[% END %]
+
+<form method="post">
+
+[% PROCESS "global/hidden-fields.html.tmpl"
+ exclude=("^(flag_type|requestee)-") %]
+
+[% FOREACH flag = flags %]
+ [% IF flag.requestees.size == 0 %]
+ <p>
+ Sorry, I can't find a user whose name or email address contains
+ the string <em>[% flag.requestee_str FILTER html %]</em>.
+ Double-check that the user's name or email address contains that
+ string, or try entering a shorter string.
+ </p>
+ <p>
+ Ask <input type="text" size="20" maxlength="255"
+ name="requestee-[% flag.type.id %]"
+ value="[% flag.requestee_str FILTER html %]">
+ for [% flag.type.name FILTER html %]
+ <input type="hidden" name="flag_type-[% flag.type.id %]" value="?">
+ </p>
+
+ [% ELSIF flag.requestees.size == 1 %]
+ <input type="hidden"
+ name="requestee-[% flag.type.id %]"
+ value="[% flag.requestee.email FILTER html %]">
+ <input type="hidden" name="flag_type-[% flag.type.id %]" value="?">
+
+ [% ELSE %]
+ <p>
+ More than one user's name or email address contains the string
+ <em>[% flag.requestee_str FILTER html %]</em>. Choose the user
+ you meant from the following menu or click the back button and try
+ again with a more specific string.
+ </p>
+ <p>
+ Ask <select name="requestee-[% flag.type.id %]">
+ [% FOREACH requestee = flag.requestees %]
+ <option value="[% requestee.email FILTER html %]">
+ [% requestee.identity FILTER html%]</option>
+ [% END %]
+ </select>
+ for [% flag.type.name %]
+ <input type="hidden" name="flag_type-[% flag.type.id %]" value="?">
+ </p>
+
+ [% END %]
+[% END %]
+
+<input type="submit" value="Commit">
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
+
diff --git a/userprefs.cgi b/userprefs.cgi
index 369c681ca..1d4be2a78 100755
--- a/userprefs.cgi
+++ b/userprefs.cgi
@@ -207,6 +207,11 @@ sub DoEmail {
$vars->{'excludeself'} = 0;
}
+ foreach my $flag qw(FlagRequestee FlagRequester) {
+ $vars->{$flag} =
+ !exists($emailflags{$flag}) || $emailflags{$flag} eq 'on';
+ }
+
# Parse the info into a hash of hashes; the first hash keyed by role,
# the second by reason, and the value being 1 or 0 for (on or off).
# Preferences not existing in the user's list are assumed to be on.
@@ -234,6 +239,10 @@ sub SaveEmail {
$updateString .= 'ExcludeSelf~';
}
+ foreach my $flag qw(FlagRequestee FlagRequester) {
+ $updateString .= "~$flag~" . (defined($::FORM{$flag}) ? "on" : "");
+ }
+
foreach my $role (@roles) {
foreach my $reason (@reasons) {
# Add this preference to the list without giving it a value,