]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Fix for bug 98801: Implementation of the request tracker, a set of enhancements to...
authormyk%mozilla.org <>
Sun, 29 Sep 2002 01:42:23 +0000 (01:42 +0000)
committermyk%mozilla.org <>
Sun, 29 Sep 2002 01:42:23 +0000 (01:42 +0000)
r=gerv,bbaetz

36 files changed:
Attachment.pm
Bugzilla/Attachment.pm
Bugzilla/Flag.pm [new file with mode: 0644]
Bugzilla/FlagType.pm [new file with mode: 0644]
Bugzilla/Search.pm
Bugzilla/User.pm [new file with mode: 0644]
attachment.cgi
bug_form.pl
checksetup.pl
editattachstatuses.cgi [deleted file]
editcomponents.cgi
editflagtypes.cgi [new file with mode: 0755]
editproducts.cgi
globals.pl
process_bug.cgi
productmenu.js [new file with mode: 0644]
request.cgi [new file with mode: 0755]
sanitycheck.cgi
template/en/default/account/prefs/email.html.tmpl
template/en/default/admin/flag-type/confirm-delete.html.tmpl [new file with mode: 0644]
template/en/default/admin/flag-type/edit.html.tmpl [new file with mode: 0644]
template/en/default/admin/flag-type/list.html.tmpl [new file with mode: 0644]
template/en/default/attachment/edit.html.tmpl
template/en/default/attachment/list.html.tmpl
template/en/default/bug/edit.html.tmpl
template/en/default/flag/list.html.tmpl [new file with mode: 0644]
template/en/default/global/code-error.html.tmpl
template/en/default/global/messages.html.tmpl
template/en/default/global/select-menu.html.tmpl
template/en/default/global/useful-links.html.tmpl
template/en/default/global/user-error.html.tmpl
template/en/default/request/created-email.txt.tmpl [new file with mode: 0644]
template/en/default/request/fulfilled-email.txt.tmpl [new file with mode: 0644]
template/en/default/request/queue.html.tmpl [new file with mode: 0644]
template/en/default/request/verify.html.tmpl [new file with mode: 0644]
userprefs.cgi

index 3a6248cf452d4efe97eb687f87540b51b6bafec0..53690170e202106d23db569284e91f92504b85d5 100644 (file)
@@ -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,
index 3a6248cf452d4efe97eb687f87540b51b6bafec0..53690170e202106d23db569284e91f92504b85d5 100644 (file)
@@ -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 (file)
index 0000000..3feaae4
--- /dev/null
@@ -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 (file)
index 0000000..2e272f6
--- /dev/null
@@ -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;
index 642965eb2c4d33f7ec7a7c0f918c2ce9e59ec281..6d11c073950c07138b7c3af9f4fa1a2b221e5e5c 100644 (file)
@@ -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 (file)
index 0000000..72870d5
--- /dev/null
@@ -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;
index 4d5fea475b8bf3447f7344b01bc33716f27c2dbe..b185312c6ec4b381c388dcbce379695bd7e81bf3 100755 (executable)
@@ -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,
index dfffca9b8ecc7b0742b996e920f197b090d9c123..d087b4db2aafa2819f423be877ce077719e282c5 100644 (file)
@@ -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;
index 27bcf26f9ca4a0a19d4ddd283db20c217381bb17..737a629e8088477dda1dcc3d407c8bd0bcbcd6e7 100755 (executable)
@@ -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 (executable)
index eedf2ad..0000000
+++ /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.");
-}
index 7ad81ddfa51dc72eec8472e4be962aaffd321a09..fc45b52c8ba2138eb4eb46fefeb55e9f804ddf41 100755 (executable)
@@ -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 (executable)
index 0000000..aed73f2
--- /dev/null
@@ -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;
+}
+
index 18ad4216db0be33155aa1dc63453aa60c6eff245..3db7c6f84fb79207f35fa818465532f68223a9e5 100755 (executable)
@@ -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";
index a6a751562847a961e1357b8e1cb3f3db89ec3ec6..9a625a84219d468ee13d4281dbbb10303ed4cb63 100644 (file)
@@ -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));
index 4ddfcca2cf8af56e3efb090735ed0c89394098b6..076e014fcb5fa4dfd52f91ca4a78f98f5018d4ef 100755 (executable)
@@ -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 (file)
index 0000000..d917d32
--- /dev/null
@@ -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 (executable)
index 0000000..eb36555
--- /dev/null
@@ -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'} });
+}
+
index da71163cc56c7d39e55ba6bbfcc641c2515b7b7f..2798dfd2f9af37f8876421fd9c3fdfcecb29d8cb 100755 (executable)
@@ -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");
index e14ea9910c68e598a18dab72ced0f5ae5c6e3d08..5d73a357b6d4fb0e8c1461ee26fef2d85ec7137b 100644 (file)
   <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 (file)
index 0000000..b022e62
--- /dev/null
@@ -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 (file)
index 0000000..ca01f63
--- /dev/null
@@ -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 (file)
index 0000000..76a8356
--- /dev/null
@@ -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 %]
index ec2616bf95c2ffcfa712fd79ca36341ba284e540..32449f041520f277caf9977c2e776160e3bdd679 100644 (file)
@@ -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; }
   "
 %]
 
 
         <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>
           <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>
index e7aa8b0ef6df4e417516832627fad9c3927f9a93..59f749695e28636f48da4a762d203de12bc0ca86 100644 (file)
   # 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")) %]
 
       <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">
index 9cf33b8b521ff640b5d76892d2bb76fef6a1226f..ef9ec2d7f89d77d80228110cfb1666a15ca9da1b 100644 (file)
       [% END %]
     </tr>
 
-[%# *** QAContact URL Summary Whiteboard Keywords *** %]
+[%# *** QAContact URL Requests Summary Whiteboard Keywords *** %]
 
    [% IF Param('useqacontact') %]
      <tr>
         [% 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>
       <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>
         <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>
 [%# *** 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 (file)
index 0000000..951f248
--- /dev/null
@@ -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>
index bf93977adc4b600a23c8010463d23f8ad6be14c6..1981364f114cb0c32e1796ceaed9df3b0eb98331 100644 (file)
     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.
   [% 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 %]
 
   [% 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.
       #%]        
index 584c4a93e78710a6cac9a67effb439a6da1a7ba1..85c678fdc3ed72f0605a7a86cd3aad8d29def186 100644 (file)
     [% 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") %]
index c27f60e8b5eecb40dea8669167a8b3c2343e9c52..7b7fddb29adb0bc2ce6d93a07195f6a51c92f389 100644 (file)
 [%# 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 %]>
index 1e5b09df1a649c2907d02fb675a917003f9bfed6..785a9d75eda63895d1cc0b4be144572a2ab91654 100644 (file)
@@ -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 %]
index c6f970df36e7d21430450b829f079024f744964d..c9dca30d4c4dc1abc286f1580aee7bbc222bd812 100644 (file)
@@ -30,7 +30,7 @@
   #%]  
   
 [% DEFAULT title = "Error" %]
-  
+
 [% error_message = BLOCK %]
   [% IF    error == "aaa_example_error_tag" %]
     [% title = "Example Error" %]    
     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.
     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. 
     [% 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.
     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 (file)
index 0000000..3edf107
--- /dev/null
@@ -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 (file)
index 0000000..84608c5
--- /dev/null
@@ -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 (file)
index 0000000..14f244a
--- /dev/null
@@ -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 (file)
index 0000000..ad4c07d
--- /dev/null
@@ -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 %]
+
index 369c681ca8d834c0ae0be894fdf62276e9614d1d..1d4be2a780a2a467a23907bfe7f79c400a369213 100755 (executable)
@@ -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,