Commit 91b171e7 authored by myk%mozilla.org's avatar myk%mozilla.org

Fix for bug 98801: Implementation of the request tracker, a set of enhancements…

Fix for bug 98801: Implementation of the request tracker, a set of enhancements to attachment statuses. r=gerv,bbaetz
parent 90975fe9
...@@ -31,10 +31,32 @@ package Attachment; ...@@ -31,10 +31,32 @@ package Attachment;
# This module requires that its caller have said "require CGI.pl" to import # This module requires that its caller have said "require CGI.pl" to import
# relevant functions from that script and its companion globals.pl. # relevant functions from that script and its companion globals.pl.
# Use the Flag module to handle flags.
use Bugzilla::Flag;
############################################################################ ############################################################################
# Functions # 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 sub query
{ {
# Retrieves and returns an array of attachment records for a given bug. # Retrieves and returns an array of attachment records for a given bug.
...@@ -65,22 +87,8 @@ sub query ...@@ -65,22 +87,8 @@ sub query
$a{'date'} = "$1-$2-$3 $4:$5"; $a{'date'} = "$1-$2-$3 $4:$5";
} }
# Retrieve a list of status flags that have been set on the attachment. # Retrieve a list of flags for this attachment.
&::PushGlobalSQLState(); $a{'flags'} = Bugzilla::Flag::match({ 'attach_id' => $a{'attachid'} });
&::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();
# We will display the edit link if the user can edit the attachment; # We will display the edit link if the user can edit the attachment;
# ie the are the submitter, or they have canedit. # ie the are the submitter, or they have canedit.
......
...@@ -31,10 +31,32 @@ package Attachment; ...@@ -31,10 +31,32 @@ package Attachment;
# This module requires that its caller have said "require CGI.pl" to import # This module requires that its caller have said "require CGI.pl" to import
# relevant functions from that script and its companion globals.pl. # relevant functions from that script and its companion globals.pl.
# Use the Flag module to handle flags.
use Bugzilla::Flag;
############################################################################ ############################################################################
# Functions # 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 sub query
{ {
# Retrieves and returns an array of attachment records for a given bug. # Retrieves and returns an array of attachment records for a given bug.
...@@ -65,22 +87,8 @@ sub query ...@@ -65,22 +87,8 @@ sub query
$a{'date'} = "$1-$2-$3 $4:$5"; $a{'date'} = "$1-$2-$3 $4:$5";
} }
# Retrieve a list of status flags that have been set on the attachment. # Retrieve a list of flags for this attachment.
&::PushGlobalSQLState(); $a{'flags'} = Bugzilla::Flag::match({ 'attach_id' => $a{'attachid'} });
&::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();
# We will display the edit link if the user can edit the attachment; # We will display the edit link if the user can edit the attachment;
# ie the are the submitter, or they have canedit. # ie the are the submitter, or they have canedit.
......
# -*- 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;
# -*- 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;
...@@ -62,6 +62,7 @@ sub init { ...@@ -62,6 +62,7 @@ sub init {
my @fields; my @fields;
my @supptables; my @supptables;
my @wherepart; my @wherepart;
my @having = ("(cntuseringroups = cntbugingroups OR canseeanyway)");
@fields = @$fieldsref if $fieldsref; @fields = @$fieldsref if $fieldsref;
my %F; my %F;
my %M; my %M;
...@@ -265,8 +266,8 @@ sub init { ...@@ -265,8 +266,8 @@ sub init {
} }
my $chartid; my $chartid;
# $statusid is used by the code that queries for attachment statuses. # $type_id is used by the code that queries for attachment flags.
my $statusid = 0; my $type_id = 0;
my $f; my $f;
my $ff; my $ff;
my $t; my $t;
...@@ -358,69 +359,61 @@ sub init { ...@@ -358,69 +359,61 @@ sub init {
} }
$f = "$table.$field"; $f = "$table.$field";
}, },
"^attachstatusdefs.name," => sub { "^flagtypes.name," => sub {
# The below has Fun with the names for attachment statuses. This # Matches bugs by flag name/status.
# isn't needed for changed* queries, so exclude those - the # Note that--for the purposes of querying--a flag comprises
# generic stuff will cope # its name plus its status (i.e. a flag named "review"
return if ($t =~ m/^changed/); # with a status of "+" can be found by searching for "review+").
# 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"}; # Don't do anything if this condition is about changes to flags,
&$ref; # as the generic change condition processors can handle those.
&::SendSQL("SELECT id FROM attachstatusdefs WHERE $term"); return if ($t =~ m/^changed/);
my @as_ids; # Add the flags and flagtypes tables to the query. We do
while (&::MoreSQLData()) { # a left join here so bugs without any flags still match
push @as_ids, &::FetchOneColumn(); # negative conditions (f.e. "flag isn't review+").
} my $flags = "flags_$chartid";
push(@supptables, "LEFT JOIN flags $flags " .
# When searching for multiple statuses within a single boolean chart, "ON bugs.bug_id = $flags.bug_id");
# we want to match each status record separately. In other words, my $flagtypes = "flagtypes_$chartid";
# "status = 'foo' AND status = 'bar'" should match attachments with push(@supptables, "LEFT JOIN flagtypes $flagtypes " .
# one status record equal to "foo" and another one equal to "bar", "ON $flags.type_id = $flagtypes.id");
# not attachments where the same status record equals both "foo" and
# "bar" (which is nonsensical). In order to do this we must add an # Generate the condition by running the operator-specific function.
# additional counter to the end of the "attachstatuses" table # Afterwards the condition resides in the global $term variable.
# reference. $ff = "CONCAT($flagtypes.name, $flags.status)";
++$statusid; &{$funcsbykey{",$t"}};
my $attachtable = "attachments_$chartid"; # If this is a negative condition (f.e. flag isn't "review+"),
my $statustable = "attachstatuses_${chartid}_$statusid"; # we only want bugs where all flags match the condition, not
# those where any flag matches, which needs special magic.
push(@supptables, "attachments $attachtable"); # Instead of adding the condition to the WHERE clause, we select
my $join = "LEFT JOIN attachstatuses $statustable ON ". # the number of flags matching the condition and the total number
"($attachtable.attach_id = $statustable.attach_id AND " . # of flags on each bug, then compare them in a HAVING clause.
"$statustable.statusid IN (" . join(",", @as_ids) . "))"; # If the numbers are the same, all flags match the condition,
push(@supptables, $join); # so this bug should be included.
push(@wherepart, "bugs.bug_id = $attachtable.bug_id"); if ($t =~ m/not/) {
if ($inverted) { push(@fields, "SUM($ff IS NOT NULL) AS allflags_$chartid");
$term = "$statustable.statusid IS NULL"; push(@fields, "SUM($term) AS matchingflags_$chartid");
} else { push(@having, "allflags_$chartid = matchingflags_$chartid");
$term = "$statustable.statusid IS NOT NULL"; $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 { "^changedin," => sub {
$f = "(to_days(now()) - to_days(bugs.delta_ts))"; $f = "(to_days(now()) - to_days(bugs.delta_ts))";
}, },
...@@ -817,8 +810,7 @@ sub init { ...@@ -817,8 +810,7 @@ sub init {
# Make sure we create a legal SQL query. # Make sure we create a legal SQL query.
@andlist = ("1 = 1") if !@andlist; @andlist = ("1 = 1") if !@andlist;
my $query = ("SELECT DISTINCT " . my $query = ("SELECT " . join(', ', @fields) .
join(', ', @fields) .
", COUNT(DISTINCT ugmap.group_id) AS cntuseringroups, " . ", COUNT(DISTINCT ugmap.group_id) AS cntuseringroups, " .
" COUNT(DISTINCT bgmap.group_id) AS cntbugingroups, " . " COUNT(DISTINCT bgmap.group_id) AS cntbugingroups, " .
" ((COUNT(DISTINCT ccmap.who) AND cclist_accessible) " . " ((COUNT(DISTINCT ccmap.who) AND cclist_accessible) " .
...@@ -834,10 +826,8 @@ sub init { ...@@ -834,10 +826,8 @@ sub init {
" LEFT JOIN cc AS ccmap " . " LEFT JOIN cc AS ccmap " .
" ON ccmap.who = $::userid AND ccmap.bug_id = bugs.bug_id " . " ON ccmap.who = $::userid AND ccmap.bug_id = bugs.bug_id " .
" WHERE " . join(' AND ', (@wherepart, @andlist)) . " WHERE " . join(' AND ', (@wherepart, @andlist)) .
" GROUP BY bugs.bug_id " . " GROUP BY bugs.bug_id" .
" HAVING cntuseringroups = cntbugingroups" . " HAVING " . join(" AND ", @having));
" OR canseeanyway"
);
if ($debug) { if ($debug) {
print "<p><code>" . value_quote($query) . "</code></p>\n"; print "<p><code>" . value_quote($query) . "</code></p>\n";
......
# -*- 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;
...@@ -46,6 +46,10 @@ if ($^O eq 'MSWin32') { ...@@ -46,6 +46,10 @@ if ($^O eq 'MSWin32') {
# Include the Bugzilla CGI and general utility library. # Include the Bugzilla CGI and general utility library.
require "CGI.pl"; require "CGI.pl";
# Use these modules to handle flags.
use Bugzilla::Flag;
use Bugzilla::FlagType;
# Establish a connection to the database backend. # Establish a connection to the database backend.
ConnectToDatabase(); ConnectToDatabase();
...@@ -110,7 +114,8 @@ elsif ($action eq "update") ...@@ -110,7 +114,8 @@ elsif ($action eq "update")
validateContentType() unless $::FORM{'ispatch'}; validateContentType() unless $::FORM{'ispatch'};
validateIsObsolete(); validateIsObsolete();
validatePrivate(); validatePrivate();
validateStatuses(); Bugzilla::Flag::validate(\%::FORM);
Bugzilla::FlagType::validate(\%::FORM);
update(); update();
} }
else else
...@@ -240,29 +245,6 @@ sub validatePrivate ...@@ -240,29 +245,6 @@ sub validatePrivate
$::FORM{'isprivate'} = $::FORM{'isprivate'} ? 1 : 0; $::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 sub validateData
{ {
$::FORM{'data'} $::FORM{'data'}
...@@ -380,18 +362,6 @@ sub viewall ...@@ -380,18 +362,6 @@ sub viewall
# !!! Yuck, what an ugly hack. Fix it! # !!! Yuck, what an ugly hack. Fix it!
$a{'isviewable'} = ( $a{'contenttype'} =~ /^(text|image|application\/vnd\.mozilla\.)/ ); $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. # Add the hash representing the attachment to the array of attachments.
push @attachments, \%a; push @attachments, \%a;
} }
...@@ -491,10 +461,14 @@ sub insert ...@@ -491,10 +461,14 @@ sub insert
# Make existing attachments obsolete. # Make existing attachments obsolete.
my $fieldid = GetFieldID('attachments.isobsolete'); my $fieldid = GetFieldID('attachments.isobsolete');
foreach my $attachid (@{$::MFORM{'obsolete'}}) { foreach my $obsolete_id (@{$::MFORM{'obsolete'}}) {
SendSQL("UPDATE attachments SET isobsolete = 1 WHERE attach_id = $attachid"); 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) 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 # Send mail to let people know the attachment has been created. Uses a
...@@ -544,32 +518,6 @@ sub edit ...@@ -544,32 +518,6 @@ sub edit
# !!! Yuck, what an ugly hack. Fix it! # !!! Yuck, what an ugly hack. Fix it!
my $isviewable = ( $contenttype =~ /^(text|image|application\/vnd\.mozilla\.)/ ); 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 # 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. # 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"); SendSQL("SELECT attach_id FROM attachments WHERE bug_id = $bugid ORDER BY attach_id");
...@@ -578,6 +526,19 @@ sub edit ...@@ -578,6 +526,19 @@ sub edit
SendSQL("SELECT short_desc FROM bugs WHERE bug_id = $bugid"); SendSQL("SELECT short_desc FROM bugs WHERE bug_id = $bugid");
my ($bugsummary) = FetchSQLData(); 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. # Define the variables and functions that will be passed to the UI template.
$vars->{'attachid'} = $::FORM{'id'}; $vars->{'attachid'} = $::FORM{'id'};
$vars->{'description'} = $description; $vars->{'description'} = $description;
...@@ -589,8 +550,6 @@ sub edit ...@@ -589,8 +550,6 @@ sub edit
$vars->{'isobsolete'} = $isobsolete; $vars->{'isobsolete'} = $isobsolete;
$vars->{'isprivate'} = $isprivate; $vars->{'isprivate'} = $isprivate;
$vars->{'isviewable'} = $isviewable; $vars->{'isviewable'} = $isviewable;
$vars->{'statuses'} = \%statuses;
$vars->{'statusdefs'} = \@statusdefs;
$vars->{'attachments'} = \@bugattachments; $vars->{'attachments'} = \@bugattachments;
# Return the appropriate HTTP response headers. # Return the appropriate HTTP response headers.
...@@ -604,7 +563,7 @@ sub edit ...@@ -604,7 +563,7 @@ sub edit
sub update sub update
{ {
# Update an attachment record. # Updates an attachment record.
# Get the bug ID for the bug to which this attachment is attached. # Get the bug ID for the bug to which this attachment is attached.
SendSQL("SELECT bug_id FROM attachments WHERE attach_id = $::FORM{'id'}"); SendSQL("SELECT bug_id FROM attachments WHERE attach_id = $::FORM{'id'}");
...@@ -616,8 +575,11 @@ sub update ...@@ -616,8 +575,11 @@ sub update
} }
# Lock database tables in preparation for updating the attachment. # Lock database tables in preparation for updating the attachment.
SendSQL("LOCK TABLES attachments WRITE , attachstatuses WRITE , SendSQL("LOCK TABLES attachments WRITE , flags WRITE , " .
attachstatusdefs READ , fielddefs READ , bugs_activity 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 # Get a copy of the attachment record before we make changes
# so we can record those changes in the activity table. # so we can record those changes in the activity table.
SendSQL("SELECT description, mimetype, filename, ispatch, isobsolete, isprivate SendSQL("SELECT description, mimetype, filename, ispatch, isobsolete, isprivate
...@@ -625,41 +587,6 @@ sub update ...@@ -625,41 +587,6 @@ sub update
my ($olddescription, $oldcontenttype, $oldfilename, $oldispatch, my ($olddescription, $oldcontenttype, $oldfilename, $oldispatch,
$oldisobsolete, $oldisprivate) = FetchSQLData(); $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. # Quote the description and content type for use in the SQL UPDATE statement.
my $quoteddescription = SqlQuote($::FORM{'description'}); my $quoteddescription = SqlQuote($::FORM{'description'});
my $quotedcontenttype = SqlQuote($::FORM{'contenttype'}); my $quotedcontenttype = SqlQuote($::FORM{'contenttype'});
...@@ -677,18 +604,23 @@ sub update ...@@ -677,18 +604,23 @@ sub update
WHERE attach_id = $::FORM{'id'} WHERE attach_id = $::FORM{'id'}
"); ");
# Figure out when the changes were made.
SendSQL("SELECT NOW()");
my $timestamp = FetchOneColumn();
# Record changes in the activity table. # Record changes in the activity table.
my $sql_timestamp = SqlQuote($timestamp);
if ($olddescription ne $::FORM{'description'}) { if ($olddescription ne $::FORM{'description'}) {
my $quotedolddescription = SqlQuote($olddescription); my $quotedolddescription = SqlQuote($olddescription);
my $fieldid = GetFieldID('attachments.description'); my $fieldid = GetFieldID('attachments.description');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 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'}) { if ($oldcontenttype ne $::FORM{'contenttype'}) {
my $quotedoldcontenttype = SqlQuote($oldcontenttype); my $quotedoldcontenttype = SqlQuote($oldcontenttype);
my $fieldid = GetFieldID('attachments.mimetype'); my $fieldid = GetFieldID('attachments.mimetype');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 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'}) { if ($oldfilename ne $::FORM{'filename'}) {
my $quotedoldfilename = SqlQuote($oldfilename); my $quotedoldfilename = SqlQuote($oldfilename);
...@@ -699,48 +631,26 @@ sub update ...@@ -699,48 +631,26 @@ sub update
if ($oldispatch ne $::FORM{'ispatch'}) { if ($oldispatch ne $::FORM{'ispatch'}) {
my $fieldid = GetFieldID('attachments.ispatch'); my $fieldid = GetFieldID('attachments.ispatch');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 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'}) { if ($oldisobsolete ne $::FORM{'isobsolete'}) {
my $fieldid = GetFieldID('attachments.isobsolete'); my $fieldid = GetFieldID('attachments.isobsolete');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 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'}) { if ($oldisprivate ne $::FORM{'isprivate'}) {
my $fieldid = GetFieldID('attachments.isprivate'); my $fieldid = GetFieldID('attachments.isprivate');
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 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'})"); VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $oldisprivate, $::FORM{'isprivate'})");
} }
if ($oldstatuslist ne $newstatuslist) {
my ($removed, $added) = DiffStrings($oldstatuslist, $newstatuslist); # Update flags.
my $quotedremoved = SqlQuote($removed); my $target = Bugzilla::Flag::GetTarget(undef, $::FORM{'id'});
my $quotedadded = SqlQuote($added); Bugzilla::Flag::process($target, $timestamp, \%::FORM);
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)");
}
# Unlock all database tables now that we are finished updating the database. # Unlock all database tables now that we are finished updating the database.
SendSQL("UNLOCK TABLES"); 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, # If the user submitted a comment while editing the attachment,
# add the comment to the bug. # add the comment to the bug.
if ( $::FORM{'comment'} ) if ( $::FORM{'comment'} )
...@@ -772,7 +682,7 @@ sub update ...@@ -772,7 +682,7 @@ sub update
my $neverused = $::userid; my $neverused = $::userid;
# Append the comment to the list of comments in the database. # Append the comment to the list of comments in the database.
AppendComment($bugid, $who, $wrappedcomment, $::FORM{'isprivate'}); AppendComment($bugid, $who, $wrappedcomment, $::FORM{'isprivate'}, $timestamp);
} }
......
...@@ -28,6 +28,10 @@ use RelationSet; ...@@ -28,6 +28,10 @@ use RelationSet;
# Use the Attachment module to display attachments for the bug. # Use the Attachment module to display attachments for the bug.
use Attachment; use Attachment;
# Use the Flag modules to display flags on the bug.
use Bugzilla::Flag;
use Bugzilla::FlagType;
sub show_bug { sub show_bug {
# Shut up misguided -w warnings about "used only once". For some reason, # Shut up misguided -w warnings about "used only once". For some reason,
# "use vars" chokes on me when I try it here. # "use vars" chokes on me when I try it here.
...@@ -76,10 +80,10 @@ sub show_bug { ...@@ -76,10 +80,10 @@ sub show_bug {
# Populate the bug hash with the info we get directly from the DB. # Populate the bug hash with the info we get directly from the DB.
my $query = " my $query = "
SELECT bugs.bug_id, alias, products.name, version, rep_platform, SELECT bugs.bug_id, alias, bugs.product_id, products.name, version,
op_sys, bug_status, resolution, priority, rep_platform, op_sys, bug_status, resolution, priority,
bug_severity, components.name, assigned_to, reporter, bug_severity, bugs.component_id, components.name, assigned_to,
bug_file_loc, short_desc, target_milestone, reporter, bug_file_loc, short_desc, target_milestone,
qa_contact, status_whiteboard, qa_contact, status_whiteboard,
date_format(creation_ts,'%Y-%m-%d %H:%i'), date_format(creation_ts,'%Y-%m-%d %H:%i'),
delta_ts, sum(votes.count), delta_ts calc_disp_date delta_ts, sum(votes.count), delta_ts calc_disp_date
...@@ -101,12 +105,12 @@ sub show_bug { ...@@ -101,12 +105,12 @@ sub show_bug {
my $value; my $value;
my $disp_date; my $disp_date;
my @row = FetchSQLData(); my @row = FetchSQLData();
foreach my $field ("bug_id", "alias", "product", "version", "rep_platform", foreach my $field ("bug_id", "alias", "product_id", "product", "version",
"op_sys", "bug_status", "resolution", "priority", "rep_platform", "op_sys", "bug_status", "resolution",
"bug_severity", "component", "assigned_to", "reporter", "priority", "bug_severity", "component_id", "component",
"bug_file_loc", "short_desc", "target_milestone", "assigned_to", "reporter", "bug_file_loc", "short_desc",
"qa_contact", "status_whiteboard", "creation_ts", "target_milestone", "qa_contact", "status_whiteboard",
"delta_ts", "votes", "calc_disp_date") "creation_ts", "delta_ts", "votes", "calc_disp_date")
{ {
$value = shift(@row); $value = shift(@row);
if ($field eq "calc_disp_date") { if ($field eq "calc_disp_date") {
...@@ -198,6 +202,28 @@ sub show_bug { ...@@ -198,6 +202,28 @@ sub show_bug {
# Attachments # Attachments
$bug{'attachments'} = Attachment::query($id); $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 # Dependencies
my @list; my @list;
SendSQL("SELECT dependson FROM dependencies WHERE SendSQL("SELECT dependson FROM dependencies WHERE
......
...@@ -1336,24 +1336,65 @@ $table{attachments} = ...@@ -1336,24 +1336,65 @@ $table{attachments} =
index(bug_id), index(bug_id),
index(creation_ts)'; index(creation_ts)';
# 2001-05-05 myk@mozilla.org: Tables to support attachment statuses. # September 2002 myk@mozilla.org: Tables to support status flags,
# "attachstatuses" stores one record for each status on each attachment. # which replace attachment statuses and allow users to flag bugs
# "attachstatusdefs" defines the statuses that can be set on attachments. # or attachments with statuses (review+, approval-, etc.).
#
$table{attachstatuses} = # "flags" stores one record for each flag on each bug/attachment.
' # "flagtypes" defines the types of flags that can be set.
attach_id MEDIUMINT NOT NULL , # "flaginclusions" and "flagexclusions" specify the products/components
statusid SMALLINT NOT NULL , # a bug/attachment must belong to in order for flags of a given type
PRIMARY KEY(attach_id, statusid) # 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} = $table{flagtypes} =
' 'id SMALLINT NOT NULL PRIMARY KEY ,
id SMALLINT NOT NULL PRIMARY KEY ,
name VARCHAR(50) NOT NULL , name VARCHAR(50) NOT NULL ,
description MEDIUMTEXT NULL , description TEXT NULL ,
sortkey SMALLINT NOT NULL DEFAULT 0 , cc_list VARCHAR(200) NULL ,
product_id SMALLINT NOT 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); ...@@ -1792,7 +1833,7 @@ AddFDef("attachments.mimetype", "Attachment mime type", 0);
AddFDef("attachments.ispatch", "Attachment is patch", 0); AddFDef("attachments.ispatch", "Attachment is patch", 0);
AddFDef("attachments.isobsolete", "Attachment is obsolete", 0); AddFDef("attachments.isobsolete", "Attachment is obsolete", 0);
AddFDef("attachments.isprivate", "Attachment is private", 0); AddFDef("attachments.isprivate", "Attachment is private", 0);
AddFDef("attachstatusdefs.name", "Attachment Status", 0);
AddFDef("target_milestone", "Target Milestone", 0); AddFDef("target_milestone", "Target Milestone", 0);
AddFDef("delta_ts", "Last changed date", 0); AddFDef("delta_ts", "Last changed date", 0);
AddFDef("(to_days(now()) - to_days(bugs.delta_ts))", "Days since bug changed", AddFDef("(to_days(now()) - to_days(bugs.delta_ts))", "Days since bug changed",
...@@ -1807,6 +1848,10 @@ AddFDef("bug_group", "Group", 0); ...@@ -1807,6 +1848,10 @@ AddFDef("bug_group", "Group", 0);
# Oops. Bug 163299 # Oops. Bug 163299
$dbh->do("DELETE FROM fielddefs WHERE name='cc_accessible'"); $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 # Detect changed local settings
########################################################################### ###########################################################################
...@@ -3246,6 +3291,133 @@ if (GetFieldDef("profiles", "groupset")) { ...@@ -3246,6 +3291,133 @@ if (GetFieldDef("profiles", "groupset")) {
$dbh->do("DELETE FROM fielddefs WHERE name = " . $dbh->quote('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 # If you had to change the --TABLE-- definition in any way, then add your
# differential change code *** A B O V E *** this comment. # differential change code *** A B O V E *** this comment.
# #
......
#!/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.");
}
...@@ -581,7 +581,9 @@ if ($action eq 'delete') { ...@@ -581,7 +581,9 @@ if ($action eq 'delete') {
bugs WRITE, bugs WRITE,
bugs_activity WRITE, bugs_activity WRITE,
components 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, # 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 # so I have to iterate over bugs and delete all the indivial entries
...@@ -610,6 +612,12 @@ if ($action eq 'delete') { ...@@ -610,6 +612,12 @@ if ($action eq 'delete') {
print "Bugs deleted.<BR>\n"; 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 SendSQL("DELETE FROM components
WHERE id=$component_id"); WHERE id=$component_id");
print "Components deleted.<P>\n"; print "Components deleted.<P>\n";
......
#!/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;
}
...@@ -539,7 +539,9 @@ if ($action eq 'delete') { ...@@ -539,7 +539,9 @@ if ($action eq 'delete') {
products WRITE, products WRITE,
groups WRITE, groups WRITE,
profiles 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, # 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 # so I have to iterate over bugs and delete all the indivial entries
...@@ -581,6 +583,12 @@ if ($action eq 'delete') { ...@@ -581,6 +583,12 @@ if ($action eq 'delete') {
WHERE product_id=$product_id"); WHERE product_id=$product_id");
print "Milestones deleted.<BR>\n"; 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 SendSQL("DELETE FROM products
WHERE id=$product_id"); WHERE id=$product_id");
print "Product '$product' deleted.<BR>\n"; print "Product '$product' deleted.<BR>\n";
......
...@@ -300,7 +300,12 @@ sub FetchOneColumn { ...@@ -300,7 +300,12 @@ sub FetchOneColumn {
"status", "resolution", "summary"); "status", "resolution", "summary");
sub AppendComment { 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/\n/g; # Get rid of windows-style line endings.
$comment =~ s/\r/\n/g; # Get rid of mac-style line endings. $comment =~ s/\r/\n/g; # Get rid of mac-style line endings.
if ($comment =~ /^\s*$/) { # Nothin' but whitespace. if ($comment =~ /^\s*$/) { # Nothin' but whitespace.
...@@ -310,7 +315,7 @@ sub AppendComment { ...@@ -310,7 +315,7 @@ sub AppendComment {
my $whoid = DBNameToIdAndCheck($who); my $whoid = DBNameToIdAndCheck($who);
my $privacyval = $isprivate ? 1 : 0 ; my $privacyval = $isprivate ? 1 : 0 ;
SendSQL("INSERT INTO longdescs (bug_id, who, bug_when, thetext, isprivate) " . SendSQL("INSERT INTO longdescs (bug_id, who, bug_when, thetext, isprivate) " .
"VALUES($bugid, $whoid, now(), " . SqlQuote($comment) . ", " . "VALUES($bugid, $whoid, $timestamp, " . SqlQuote($comment) . ", " .
$privacyval . ")"); $privacyval . ")");
SendSQL("UPDATE bugs SET delta_ts = now() WHERE bug_id = $bugid"); SendSQL("UPDATE bugs SET delta_ts = now() WHERE bug_id = $bugid");
...@@ -902,8 +907,7 @@ sub get_product_name { ...@@ -902,8 +907,7 @@ sub get_product_name {
sub get_component_id { sub get_component_id {
my ($prod_id, $comp) = @_; my ($prod_id, $comp) = @_;
die "non-numeric prod_id '$prod_id' passed to get_component_id" return undef unless ($prod_id =~ /^\d+$/);
unless ($prod_id =~ /^\d+$/);
PushGlobalSQLState(); PushGlobalSQLState();
SendSQL("SELECT id FROM components " . SendSQL("SELECT id FROM components " .
"WHERE product_id = $prod_id AND name = " . SqlQuote($comp)); "WHERE product_id = $prod_id AND name = " . SqlQuote($comp));
......
...@@ -36,6 +36,9 @@ require "bug_form.pl"; ...@@ -36,6 +36,9 @@ require "bug_form.pl";
use RelationSet; 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": # Shut up misguided -w warnings about "used only once":
use vars qw(%versions use vars qw(%versions
...@@ -1052,8 +1055,9 @@ foreach my $id (@idlist) { ...@@ -1052,8 +1055,9 @@ foreach my $id (@idlist) {
"profiles $write, dependencies $write, votes $write, " . "profiles $write, dependencies $write, votes $write, " .
"products READ, components READ, " . "products READ, components READ, " .
"keywords $write, longdescs $write, fielddefs $write, " . "keywords $write, longdescs $write, fielddefs $write, " .
"bug_group_map $write, " . "bug_group_map $write, flags $write, " .
"user_group_map READ, " . "user_group_map READ, flagtypes READ, " .
"flaginclusions AS i READ, flagexclusions AS e READ, " .
"keyworddefs READ, groups READ, attachments READ"); "keyworddefs READ, groups READ, attachments READ");
my @oldvalues = SnapShotBug($id); my @oldvalues = SnapShotBug($id);
my %oldhash; my %oldhash;
...@@ -1238,7 +1242,7 @@ foreach my $id (@idlist) { ...@@ -1238,7 +1242,7 @@ foreach my $id (@idlist) {
LogActivityEntry($id, "bug_group", $groupDelNames, $groupAddNames); LogActivityEntry($id, "bug_group", $groupDelNames, $groupAddNames);
if (defined $::FORM{'comment'}) { if (defined $::FORM{'comment'}) {
AppendComment($id, $::COOKIE{'Bugzilla_login'}, $::FORM{'comment'}, AppendComment($id, $::COOKIE{'Bugzilla_login'}, $::FORM{'comment'},
$::FORM{'commentprivacy'}); $::FORM{'commentprivacy'}, $timestamp);
} }
my $removedCcString = ""; my $removedCcString = "";
...@@ -1399,6 +1403,14 @@ foreach my $id (@idlist) { ...@@ -1399,6 +1403,14 @@ foreach my $id (@idlist) {
# what has changed since before we wrote out the new values. # what has changed since before we wrote out the new values.
# #
my @newvalues = SnapShotBug($id); 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 # for passing to processmail to ensure that when someone is removed
# from one of these fields, they get notified of that fact (if desired) # from one of these fields, they get notified of that fact (if desired)
...@@ -1411,12 +1423,6 @@ foreach my $id (@idlist) { ...@@ -1411,12 +1423,6 @@ foreach my $id (@idlist) {
# values in place. # values in place.
my $old = shift @oldvalues; my $old = shift @oldvalues;
my $new = shift @newvalues; my $new = shift @newvalues;
if (!defined $old) {
$old = "";
}
if (!defined $new) {
$new = "";
}
if ($old ne $new) { if ($old ne $new) {
# Products and components are now stored in the DB using ID's # Products and components are now stored in the DB using ID's
...@@ -1461,6 +1467,11 @@ foreach my $id (@idlist) { ...@@ -1461,6 +1467,11 @@ foreach my $id (@idlist) {
LogActivityEntry($id,$col,$old,$new); 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) { if ($bug_changed) {
SendSQL("UPDATE bugs SET delta_ts = " . SqlQuote($timestamp) . " WHERE bug_id = $id"); SendSQL("UPDATE bugs SET delta_ts = " . SqlQuote($timestamp) . " WHERE bug_id = $id");
} }
......
// 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 );
}
#!/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'} });
}
...@@ -232,11 +232,11 @@ CrossCheck("fielddefs", "fieldid", ...@@ -232,11 +232,11 @@ CrossCheck("fielddefs", "fieldid",
["bugs_activity", "fieldid"]); ["bugs_activity", "fieldid"]);
CrossCheck("attachments", "attach_id", CrossCheck("attachments", "attach_id",
["attachstatuses", "attach_id"], ["flags", "attach_id"],
["bugs_activity", "attach_id"]); ["bugs_activity", "attach_id"]);
CrossCheck("attachstatusdefs", "id", CrossCheck("flagtypes", "id",
["attachstatuses", "statusid"]); ["flags", "type_id"]);
CrossCheck("bugs", "bug_id", CrossCheck("bugs", "bug_id",
["bugs_activity", "bug_id"], ["bugs_activity", "bug_id"],
...@@ -280,7 +280,7 @@ CrossCheck("products", "id", ...@@ -280,7 +280,7 @@ CrossCheck("products", "id",
["components", "product_id", "name"], ["components", "product_id", "name"],
["milestones", "product_id", "value"], ["milestones", "product_id", "value"],
["versions", "product_id", "value"], ["versions", "product_id", "value"],
["attachstatusdefs", "product_id", "name"]); ["flagtypes", "product_id", "name"]);
DateCheck("groups", "last_changed"); DateCheck("groups", "last_changed");
DateCheck("profiles", "refreshed_when"); DateCheck("profiles", "refreshed_when");
......
...@@ -83,9 +83,27 @@ ...@@ -83,9 +83,27 @@
<tr> <tr>
<td width="150"></td> <td width="150"></td>
<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" <input type="checkbox" name="ExcludeSelf" id="ExcludeSelf" value="on"
[% " checked" IF excludeself %]> [% " 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> <br>
</td> </td>
</tr> </tr>
......
<!-- 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 %]
[%# 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 %]
[%# 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 %]
...@@ -32,6 +32,8 @@ ...@@ -32,6 +32,8 @@
table.attachment_info th { text-align: right; vertical-align: top; } table.attachment_info th { text-align: right; vertical-align: top; }
table.attachment_info td { text-align: left; vertical-align: top; } table.attachment_info td { text-align: left; vertical-align: top; }
#noview { text-align: left; vertical-align: center; } #noview { text-align: left; vertical-align: center; }
table#flags th, table#flags td { font-size: small; vertical-align: baseline; }
" "
%] %]
...@@ -159,7 +161,6 @@ ...@@ -159,7 +161,6 @@
<b>MIME Type:</b><br> <b>MIME Type:</b><br>
<input type="text" size="20" name="contenttypeentry" value="[% contenttype FILTER html %]"><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" <input type="checkbox" id="ispatch" name="ispatch" value="1"
[% 'checked="checked"' IF ispatch %]> [% 'checked="checked"' IF ispatch %]>
<label for="ispatch">patch</label> <label for="ispatch">patch</label>
...@@ -168,18 +169,12 @@ ...@@ -168,18 +169,12 @@
<label for="isobsolete">obsolete</label><br> <label for="isobsolete">obsolete</label><br>
[% IF (Param("insidergroup") && UserInGroup(Param("insidergroup"))) %] [% IF (Param("insidergroup") && UserInGroup(Param("insidergroup"))) %]
<input type="checkbox" name="isprivate" value="1"[% " checked" IF isprivate %]> private<br><br> <input type="checkbox" name="isprivate" value="1"[% " checked" IF isprivate %]> private<br><br>
[% ELSE %]<br>
[% END %] [% END %]
[% IF statusdefs.size %] [% IF flag_types.size > 0 %]
<b>Status:</b><br> <b>Flags:</b><br>
[% FOREACH def = statusdefs %] [% PROCESS "flag/list.html.tmpl" bug_id=bugid attach_id=attachid %]<br>
<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 %]
[% END %] [% END %]
<div id="smallCommentFrame"> <div id="smallCommentFrame">
......
...@@ -19,13 +19,18 @@ ...@@ -19,13 +19,18 @@
# Contributor(s): Myk Melez <myk@mozilla.org> # Contributor(s): Myk Melez <myk@mozilla.org>
#%] #%]
[%# Whether or not to include flags. %]
[% display_flags = num_attachment_flag_types > 0 %]
<br> <br>
<table cellspacing="0" cellpadding="4" border="1"> <table cellspacing="0" cellpadding="4" border="1">
<tr> <tr>
<th bgcolor="#cccccc" align="left">Attachment</th> <th bgcolor="#cccccc" align="left">Attachment</th>
<th bgcolor="#cccccc" align="left">Type</th> <th bgcolor="#cccccc" align="left">Type</th>
<th bgcolor="#cccccc" align="left">Created</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> <th bgcolor="#cccccc" align="left">Actions</th>
</tr> </tr>
[% canseeprivate = !Param("insidergroup") || UserInGroup(Param("insidergroup")) %] [% canseeprivate = !Param("insidergroup") || UserInGroup(Param("insidergroup")) %]
...@@ -50,15 +55,23 @@ ...@@ -50,15 +55,23 @@
<td valign="top">[% attachment.date %]</td> <td valign="top">[% attachment.date %]</td>
[% IF display_flags %]
<td valign="top"> <td valign="top">
[% IF attachment.statuses.size == 0 %] [% IF attachment.flags.size == 0 %]
<i>none</i> <i>none</i>
[% ELSE %] [% ELSE %]
[% FOREACH s = attachment.statuses %] [% FOR flag = attachment.flags %]
[% s FILTER html FILTER replace('\s', '&nbsp;') %]<br> [% 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 %] [% END %]
</td> </td>
[% END %]
<td valign="top"> <td valign="top">
[% IF attachment.canedit %] [% IF attachment.canedit %]
...@@ -72,7 +85,7 @@ ...@@ -72,7 +85,7 @@
[% END %] [% END %]
<tr> <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.) <a href="attachment.cgi?bugid=[% bugid %]&amp;action=enter">Create a New Attachment</a> (proposed patch, testcase, etc.)
</td> </td>
<td colspan="1"> <td colspan="1">
......
...@@ -194,7 +194,7 @@ ...@@ -194,7 +194,7 @@
[% END %] [% END %]
</tr> </tr>
[%# *** QAContact URL Summary Whiteboard Keywords *** %] [%# *** QAContact URL Requests Summary Whiteboard Keywords *** %]
[% IF Param('useqacontact') %] [% IF Param('useqacontact') %]
<tr> <tr>
...@@ -218,17 +218,23 @@ ...@@ -218,17 +218,23 @@
[% END %] [% END %]
</b> </b>
</td> </td>
<td colspan="7"> <td colspan="5">
<input name="bug_file_loc" accesskey="u" <input name="bug_file_loc" accesskey="u"
value="[% bug.bug_file_loc FILTER html %]" size="60"> value="[% bug.bug_file_loc FILTER html %]" size="60">
</td> </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>
<tr> <tr>
<td align="right"> <td align="right">
<b><u>S</u>ummary:</b> <b><u>S</u>ummary:</b>
</td> </td>
<td colspan="7"> <td colspan="5">
<input name="short_desc" accesskey="s" <input name="short_desc" accesskey="s"
value="[% bug.short_desc FILTER html %]" size="60"> value="[% bug.short_desc FILTER html %]" size="60">
</td> </td>
...@@ -239,7 +245,7 @@ ...@@ -239,7 +245,7 @@
<td align="right"> <td align="right">
<b>Status <u>W</u>hiteboard:</b> <b>Status <u>W</u>hiteboard:</b>
</td> </td>
<td colspan="7"> <td colspan="5">
<input name="status_whiteboard" accesskey="w" <input name="status_whiteboard" accesskey="w"
value="[% bug.status_whiteboard FILTER html %]" size="60"> value="[% bug.status_whiteboard FILTER html %]" size="60">
</td> </td>
...@@ -252,7 +258,7 @@ ...@@ -252,7 +258,7 @@
<b> <b>
<a href="describekeywords.cgi"><u>K</u>eywords</a>: <a href="describekeywords.cgi"><u>K</u>eywords</a>:
</b> </b>
<td colspan="7"> <td colspan="5">
<input name="keywords" accesskey="k" <input name="keywords" accesskey="k"
value="[% bug.keywords.join(', ') FILTER html %]" size="60"> value="[% bug.keywords.join(', ') FILTER html %]" size="60">
</td> </td>
......
[%# 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>
...@@ -40,6 +40,10 @@ ...@@ -40,6 +40,10 @@
to any [% parameters %] which you may have set before calling to any [% parameters %] which you may have set before calling
ThrowCodeError. 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" %] [% ELSIF error == "attachment_already_obsolete" %]
Attachment #[% attachid FILTER html %] ([% description FILTER html %]) Attachment #[% attachid FILTER html %] ([% description FILTER html %])
is already obsolete. is already obsolete.
...@@ -78,10 +82,40 @@ ...@@ -78,10 +82,40 @@
[% ELSIF error == "no_bug_data" %] [% ELSIF error == "no_bug_data" %]
No data when fetching bug [% bug_id %]. 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" %] [% ELSIF error == "no_y_axis_defined" %]
No Y axis was defined when creating report. The X axis is optional, No Y axis was defined when creating report. The X axis is optional,
but the Y axis is compulsory. 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" %] [% ELSIF error == "template_error" %]
[% template_error_msg %] [% template_error_msg %]
...@@ -91,6 +125,14 @@ ...@@ -91,6 +125,14 @@
[% ELSIF error == "unknown_action" %] [% ELSIF error == "unknown_action" %]
Unknown action [% action FILTER html %]! 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 %] [% ELSE %]
[%# Give sensible error if error functions are used incorrectly. [%# Give sensible error if error functions are used incorrectly.
#%] #%]
......
...@@ -81,6 +81,34 @@ ...@@ -81,6 +81,34 @@
[% title = "Password Changed" %] [% title = "Password Changed" %]
Your password has been 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" %] [% ELSIF message_tag == "shutdown" %]
[% title = "Bugzilla is Down" %] [% title = "Bugzilla is Down" %]
[% Param("shutdownhtml") %] [% Param("shutdownhtml") %]
......
...@@ -22,12 +22,18 @@ ...@@ -22,12 +22,18 @@
[%# INTERFACE: [%# INTERFACE:
# name: string; the name of the menu. # 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. # options: array or hash; the items with which to populate the array.
# If a hash is passed, the hash keys become the names displayed # 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. # to the user while the hash values become the value of the item.
# #
# default: string; the item selected in the menu by default. # 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, [%# Get the scalar representation of the options reference,
...@@ -37,7 +43,9 @@ ...@@ -37,7 +43,9 @@
#%] #%]
[% options_type = BLOCK %][% options %][% END %] [% 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") %] [% IF options_type.search("ARRAY") %]
[% FOREACH value = options %] [% FOREACH value = options %]
<option value="[% value FILTER html %]" <option value="[% value FILTER html %]"
...@@ -45,7 +53,7 @@ ...@@ -45,7 +53,7 @@
[% value FILTER html %] [% value FILTER html %]
</option> </option>
[% END %] [% END %]
[% ELSIF values_type.search("HASH") %] [% ELSIF options_type.search("HASH") %]
[% FOREACH option = options %] [% FOREACH option = options %]
<option value="[% option.value FILTER html %]" <option value="[% option.value FILTER html %]"
[% " selected" IF option.value == default %]> [% " selected" IF option.value == default %]>
......
...@@ -51,6 +51,8 @@ ...@@ -51,6 +51,8 @@
<a href="reports.cgi">Reports</a> <a href="reports.cgi">Reports</a>
| <a href="request.cgi">Requests</a>
[% IF user.login && Param('usevotes') %] [% IF user.login && Param('usevotes') %]
| <a href="votes.cgi?action=show_user">My Votes</a> | <a href="votes.cgi?action=show_user">My Votes</a>
[% END %] [% END %]
...@@ -68,7 +70,7 @@ ...@@ -68,7 +70,7 @@
|| user.canblessany %] || user.canblessany %]
[% ', <a href="editproducts.cgi">products</a>' [% ', <a href="editproducts.cgi">products</a>'
IF user.groups.editcomponents %] IF user.groups.editcomponents %]
[% ', <a href="editattachstatuses.cgi"> attachment&nbsp;statuses</a>' [% ', <a href="editflagtypes.cgi">flags</a>'
IF user.groups.editcomponents %] IF user.groups.editcomponents %]
[% ', <a href="editgroups.cgi">groups</a>' [% ', <a href="editgroups.cgi">groups</a>'
IF user.groups.creategroups %] IF user.groups.creategroups %]
......
...@@ -75,6 +75,10 @@ ...@@ -75,6 +75,10 @@
Bug aliases cannot be longer than 20 characters. Bug aliases cannot be longer than 20 characters.
Please choose a shorter alias. Please choose a shorter alias.
[% ELSIF error == "authorization_failure" %]
[% title = "Authorization Failed" %]
You are not allowed to [% action %].
[% ELSIF error == "attachment_access_denied" %] [% ELSIF error == "attachment_access_denied" %]
[% title = "Access Denied" %] [% title = "Access Denied" %]
You are not permitted access to this attachment. You are not permitted access to this attachment.
...@@ -129,6 +133,23 @@ ...@@ -129,6 +133,23 @@
format like JPG or PNG, or put it elsewhere on the web and 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. 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" %] [% ELSIF error == "illegal_at_least_x_votes" %]
[% title = "Your Query Makes No Sense" %] [% title = "Your Query Makes No Sense" %]
The <em>At least ___ votes</em> field must be a simple number. The <em>At least ___ votes</em> field must be a simple number.
...@@ -176,10 +197,6 @@ ...@@ -176,10 +197,6 @@
[% title = "Invalid Attachment ID" %] [% title = "Invalid Attachment ID" %]
The attachment id [% attach_id FILTER html %] is invalid. 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" %] [% ELSIF error == "invalid_content_type" %]
[% title = "Invalid Content-Type" %] [% title = "Invalid Content-Type" %]
The content type <em>[% contenttype FILTER html %]</em> is invalid. The content type <em>[% contenttype FILTER html %]</em> is invalid.
...@@ -281,6 +298,18 @@ ...@@ -281,6 +298,18 @@
intentionally cleared out the "Reassign bug to" intentionally cleared out the "Reassign bug to"
field, [% Param("browserbugmessage") %] 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" %] [% ELSIF error == "unknown_keyword" %]
[% title = "Unknown Keyword" %] [% title = "Unknown Keyword" %]
<code>[% keyword FILTER html %]</code> is not a known keyword. <code>[% keyword FILTER html %]</code> is not a known keyword.
......
[%# 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 %]
[%# 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 %]
[%# 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 %]
<!-- 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 %]
...@@ -207,6 +207,11 @@ sub DoEmail { ...@@ -207,6 +207,11 @@ sub DoEmail {
$vars->{'excludeself'} = 0; $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, # 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). # 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. # Preferences not existing in the user's list are assumed to be on.
...@@ -234,6 +239,10 @@ sub SaveEmail { ...@@ -234,6 +239,10 @@ sub SaveEmail {
$updateString .= 'ExcludeSelf~'; $updateString .= 'ExcludeSelf~';
} }
foreach my $flag qw(FlagRequestee FlagRequester) {
$updateString .= "~$flag~" . (defined($::FORM{$flag}) ? "on" : "");
}
foreach my $role (@roles) { foreach my $role (@roles) {
foreach my $reason (@reasons) { foreach my $reason (@reasons) {
# Add this preference to the list without giving it a value, # Add this preference to the list without giving it a value,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment