From 7d1af605344e93b289e46c9d5520532b6f2cad15 Mon Sep 17 00:00:00 2001 From: "bugreport%peshkin.net" <> Date: Fri, 25 Oct 2002 10:59:26 +0000 Subject: Bug 162990 Shorthand/wildcard entry for login names in assign, cc, qa, fields patch by not_erik@dasbistro.com r=joel, myk --- Bugzilla/User.pm | 261 +++++++++++++++++++++-- defparams.pl | 33 +++ post_bug.cgi | 8 + process_bug.cgi | 10 + template/en/default/global/code-error.html.tmpl | 4 + template/en/default/global/field-descs.html.tmpl | 2 + 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 +# Erik Stambaugh ################################################################################ # 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.

' . + '"off" disables matching,
' . + '"wildcard" allows only wildcards,
' . + '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.
'. + 'If set to "1", no users will be displayed on ambiguous matches. '. + 'This is useful for user privacy purposes.
'. + '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 [% argument %] sent to + [% function %] 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", -- cgit v1.2.3-24-g4f1b