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