Commit 7d1af605 authored by bugreport%peshkin.net's avatar bugreport%peshkin.net

Bug 162990 Shorthand/wildcard entry for login names in assign, cc, qa, fields

patch by not_erik@dasbistro.com r=joel, myk
parent 80b04d87
......@@ -18,6 +18,7 @@
# Rights Reserved.
#
# Contributor(s): Myk Melez <myk@mozilla.org>
# Erik Stambaugh <not_erik@dasbistro.com>
################################################################################
# Module Initialization
......@@ -79,33 +80,255 @@ sub new {
sub match {
# Generates a list of users whose login name (email address) or real name
# matches a substring.
# matches a substring or wildcard.
# $str contains the string to match against, while $limit contains the
# $str contains the string to match, 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();
my @users = ();
return \@users if $str =~ /^\s*$/;
# The search order is wildcards, then exact match, then INSTR search.
# Wildcard matching is skipped if there is no '*', and exact matches will
# not (?) have a '*' in them. If any search comes up with something, the
# ones following it will not execute.
# first try wildcards
my $wildstr = $str;
if ($wildstr =~ s/\*/\%/g) { # don't do wildcards if no '*' in the string
# Build the query.
my $sqlstr = &::SqlQuote($wildstr);
my $query = "SELECT userid, realname, login_name " .
"FROM profiles " .
"WHERE (login_name LIKE $sqlstr " .
"OR realname LIKE $sqlstr) ";
$query .= "AND disabledtext = '' " if $exclude_disabled;
$query .= "ORDER BY length(login_name) ";
$query .= "LIMIT $limit " if $limit;
# Execute the query, retrieve the results, and make them into
# User objects.
&::PushGlobalSQLState();
&::SendSQL($query);
push(@users, new Bugzilla::User(&::FetchSQLData())) while &::MoreSQLData();
&::PopGlobalSQLState();
}
else { # try an exact match
my $sqlstr = &::SqlQuote($str);
my $query = "SELECT userid, realname, login_name " .
"FROM profiles " .
"WHERE login_name = $sqlstr ";
$query .= "AND disabledtext = '' " if $exclude_disabled;
&::PushGlobalSQLState();
&::SendSQL($query);
push(@users, new Bugzilla::User(&::FetchSQLData())) if &::MoreSQLData();
&::PopGlobalSQLState();
}
# then try instr
if ((scalar(@users) == 0)
&& (&::Param('usermatchmode') eq 'search')
&& (length($str) >= 3))
{
my $sqlstr = &::SqlQuote($str);
my $query = "SELECT userid, realname, login_name " .
"FROM profiles " .
"WHERE (INSTR(login_name, $sqlstr) " .
"OR INSTR(realname, $sqlstr)) ";
$query .= "AND disabledtext = '' " if $exclude_disabled;
$query .= "ORDER BY length(login_name) ";
$query .= "LIMIT $limit " if $limit;
&::PushGlobalSQLState();
&::SendSQL($query);
push(@users, new Bugzilla::User(&::FetchSQLData())) while &::MoreSQLData();
&::PopGlobalSQLState();
}
# order @users by alpha
@users = sort { uc($a->{'email'}) cmp uc($b->{'email'}) } @users;
return \@users;
}
# match_field() is a CGI wrapper for the match() function.
#
# Here's what it does:
#
# 1. Accepts a list of fields along with whether they may take multiple values
# 2. Takes the values of those fields from $::FORM and passes them to match()
# 3. Checks the results of the match and displays confirmation or failure
# messages as appropriate.
#
# The confirmation screen functions the same way as verify-new-product and
# confirm-duplicate, by rolling all of the state information into a
# form which is passed back, but in this case the searched fields are
# replaced with the search results.
#
# The act of displaying the confirmation or failure messages means it must
# throw a template and terminate. When confirmation is sent, all of the
# searchable fields have been replaced by exact fields and the calling script
# is executed as normal.
#
# match_field must be called early in a script, before anything external is
# done with the form data.
#
# In order to do a simple match without dealing with templates, confirmation,
# or globals, simply calling Bugzilla::User::match instead will be
# sufficient.
# How to call it:
#
# Bugzilla::User::match_field ({
# 'field_name' => { 'type' => fieldtype },
# 'field_name2' => { 'type' => fieldtype },
# [...]
# });
#
# fieldtype can be either 'single' or 'multi'.
#
sub match_field {
my $fields = shift; # arguments as a hash
my $matches = {}; # the values sent to the template
my $matchsuccess = 1; # did the match fail?
my $need_confirm = 0; # whether to display confirmation screen
# prepare default form values
my $vars = $::vars;
$vars->{'form'} = \%::FORM;
$vars->{'mform'} = \%::MFORM;
# Skip all of this if the option has been turned off
return 1 if (&::Param('usermatchmode') eq 'off');
for my $field (keys %{$fields}) {
# Tolerate fields that do not exist.
#
# This is so that fields like qa_contact can be specified in the code
# and it won't break if $::MFORM does not define them.
#
# It has the side-effect that if a bad field name is passed it will be
# quietly ignored rather than raising a code error.
next if !defined($vars->{'mform'}->{$field});
# We need to move the query to $raw_field, where it will be split up,
# modified by the search, and put back into $::FORM and $::MFORM
# incrementally.
my $raw_field = join(" ", @{$vars->{'mform'}->{$field}});
$vars->{'form'}->{$field} = '';
$vars->{'mform'}->{$field} = [];
my @queries = ();
# Now we either split $raw_field by spaces/commas and put the list
# into @queries, or in the case of fields which only accept single
# entries, we simply use the verbatim text.
$raw_field =~ s/^\s+|\s+$//sg; # trim leading/trailing space
# single field
if ($fields->{$field}->{'type'} eq 'single') {
@queries = ($raw_field) unless $raw_field =~ /^\s*$/;
# multi-field
}
elsif ($fields->{$field}->{'type'} eq 'multi') {
@queries = split(/[\s,]+/, $raw_field);
}
else {
# bad argument
$vars->{'argument'} = $fields->{$field}->{'type'};
$vars->{'function'} = 'Bugzilla::User::match_field';
&::ThrowCodeError('bad_arg');
}
for my $query (@queries) {
my $users = match(
$query, # match string
(&::Param('maxusermatches') || 0) + 1, # match limit
1 # exclude_disabled
);
# skip confirmation for exact matches
if ((scalar(@{$users}) == 1)
&& (@{$users}[0]->{'email'} eq $query))
{
$vars->{'form'}->{$field} .= @{$users}[0]->{'email'} . " ";
push @{$vars->{'mform'}->{$field}}, @{$users}[0]->{'email'} . " ";
next;
}
$matches->{$field}->{$query}->{'users'} = $users;
$matches->{$field}->{$query}->{'status'} = 'success';
$matches->{$field}->{$query}->{'selecttype'} =
$fields->{$field}->{'type'};
# here is where it checks for multiple matches
if (scalar(@{$users}) == 1) {
# exactly one match
$vars->{'form'}->{$field} .= @{$users}[0]->{'email'} . " ";
push @{$vars->{'mform'}->{$field}}, @{$users}[0]->{'email'} . " ";
$need_confirm = 1 if &::Param('confirmuniqueusermatch');
}
elsif ((scalar(@{$users}) > 1)
&& (&::Param('maxusermatches') != 1)) {
$need_confirm = 1;
if ((&::Param('maxusermatches'))
&& (scalar(@{$users}) > &::Param('maxusermatches')))
{
$matches->{$field}->{$query}->{'status'} = 'trunc';
pop @{$users}; # take the last one out
}
}
else {
# everything else fails
$matchsuccess = 0; # fail
$matches->{$field}->{$query}->{'status'} = 'fail';
$need_confirm = 1; # confirmation screen shows failures
}
}
}
return 1 unless $need_confirm; # skip confirmation if not needed.
$vars->{'script'} = $ENV{'SCRIPT_NAME'}; # for self-referencing URLs
$vars->{'matches'} = $matches; # matches that were made
$vars->{'matchsuccess'} = $matchsuccess; # continue or fail
print "Content-type: text/html\n\n";
$::template->process("global/confirm-user-match.html.tmpl", $vars)
|| &::ThrowTemplateError($::template->error());
exit;
}
sub email_prefs {
# Get or set (not implemented) the user's email notification preferences.
......
......@@ -879,6 +879,39 @@ Reason: %reason%
default => '32',
checker => \&check_netmask
},
{
name => 'usermatchmode',
desc => 'Allow match strings to be entered for user names when entering ' .
'and editing bugs. <p>' .
'"off" disables matching,<br> ' .
'"wildcard" allows only wildcards,<br> ' .
'and "search" allows both wildcards and substring (freetext) ' .
'matches.',
type => 's',
choices => ['off', 'wildcard', 'search'],
default => 'off'
},
{
name => 'maxusermatches',
desc => 'Search for no more than this many matches. <br>'.
'If set to "1", no users will be displayed on ambiguous matches. '.
'This is useful for user privacy purposes. <br>'.
'A value of zero means no limit.',
type => 't',
default => '1000',
checker => \&check_numeric
},
{
name => 'confirmuniqueusermatch',
desc => 'Whether a confirmation screen should be displayed when only ' .
'one user matches a search entry',
type => 'b',
default => 1,
},
);
1;
......
......@@ -29,6 +29,8 @@ use lib qw(.);
require "CGI.pl";
require "bug_form.pl";
use Bugzilla::User;
# Shut up misguided -w warnings about "used only once". For some reason,
# "use vars" chokes on me when I try it here.
sub sillyness {
......@@ -51,6 +53,12 @@ use vars qw($vars $template);
ConnectToDatabase();
my $whoid = confirm_login();
# do a match on the fields if applicable
&Bugzilla::User::match_field ({
'cc' => { 'type' => 'multi' },
'assigned_to' => { 'type' => 'single' },
});
# The format of the initial comment can be structured by adding fields to the
# enter_bug template and then referencing them in the comment template.
......
......@@ -34,6 +34,8 @@ use lib qw(.);
require "CGI.pl";
require "bug_form.pl";
use Bugzilla::User;
use RelationSet;
# Use the Flag module to modify flag data if the user set flags.
......@@ -86,6 +88,14 @@ if (defined $::FORM{'id'}) {
# Make sure there are bugs to process.
scalar(@idlist) || ThrowUserError("no_bugs_chosen");
# do a match on the fields if applicable
&Bugzilla::User::match_field({
'qa_contact' => { 'type' => 'single' },
'newcc' => { 'type' => 'multi' },
'assigned_to' => { 'type' => 'single' },
});
# If we are duping bugs, let's also make sure that we can change
# the original. This takes care of issue A on bug 96085.
if (defined $::FORM{'dup_id'} && $::FORM{'knob'} eq "duplicate") {
......
......@@ -90,6 +90,10 @@
Attempted to add bug to an inactive group, identified by the bit
'[% bit FILTER html %]'.
[% ELSIF error == "bad_arg" %]
Bad argument <code>[% argument %]</code> sent to
<code>[% function %]</code> function.
[% ELSIF error == "invalid_attach_id_to_obsolete" %]
The attachment number of one of the attachments you wanted to obsolete,
[% attach_id FILTER html %], is invalid.
......
......@@ -28,6 +28,7 @@
"bug_id" => "Bug ID",
"bug_severity" => "Severity",
"bug_status" => "Status",
"cc" => "CC",
"cclist_accessible" => "CC list accessible?",
"component_id" => "Component ID",
"component" => "Component",
......@@ -37,6 +38,7 @@
"everconfirmed" => "Ever confirmed?",
"groupset" => "Groupset",
"keywords" => "Keywords",
"newcc" => "CC",
"op_sys" => "OS",
"percentage_complete" => "%Complete",
"priority" => "Priority",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment