summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbugreport%peshkin.net <>2002-10-25 12:59:26 +0200
committerbugreport%peshkin.net <>2002-10-25 12:59:26 +0200
commit7d1af605344e93b289e46c9d5520532b6f2cad15 (patch)
tree74e59342af2e8e550c79b379957de26d6a779cf7
parent80b04d87987037a62e3755beeca519246d99b319 (diff)
downloadbugzilla-7d1af605344e93b289e46c9d5520532b6f2cad15.tar.gz
bugzilla-7d1af605344e93b289e46c9d5520532b6f2cad15.tar.xz
Bug 162990 Shorthand/wildcard entry for login names in assign, cc, qa, fields
patch by not_erik@dasbistro.com r=joel, myk
-rw-r--r--Bugzilla/User.pm261
-rw-r--r--defparams.pl33
-rwxr-xr-xpost_bug.cgi8
-rwxr-xr-xprocess_bug.cgi10
-rw-r--r--template/en/default/global/code-error.html.tmpl4
-rw-r--r--template/en/default/global/field-descs.html.tmpl2
6 files changed, 299 insertions, 19 deletions
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index ae261e0d3..087dc1113 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -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.
diff --git a/defparams.pl b/defparams.pl
index bb5d43df7..a6abf0099 100644
--- a/defparams.pl
+++ b/defparams.pl
@@ -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;
diff --git a/post_bug.cgi b/post_bug.cgi
index 882bd3dd9..5bc94ca73 100755
--- a/post_bug.cgi
+++ b/post_bug.cgi
@@ -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.
diff --git a/process_bug.cgi b/process_bug.cgi
index 427e622c4..54ed0dc8f 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -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") {
diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl
index beca23562..1ec1c4626 100644
--- a/template/en/default/global/code-error.html.tmpl
+++ b/template/en/default/global/code-error.html.tmpl
@@ -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.
diff --git a/template/en/default/global/field-descs.html.tmpl b/template/en/default/global/field-descs.html.tmpl
index f8ffd4228..6349b6b1b 100644
--- a/template/en/default/global/field-descs.html.tmpl
+++ b/template/en/default/global/field-descs.html.tmpl
@@ -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",