|
Lines 22-27
use List::MoreUtils qw( any );
Link Here
|
| 22 |
use Scalar::Util qw( reftype ); |
22 |
use Scalar::Util qw( reftype ); |
| 23 |
use JSON qw( decode_json ); |
23 |
use JSON qw( decode_json ); |
| 24 |
|
24 |
|
|
|
25 |
use C4::Context; |
| 25 |
use Koha::Exceptions; |
26 |
use Koha::Exceptions; |
| 26 |
use Koha::Exceptions::REST; |
27 |
use Koha::Exceptions::REST; |
| 27 |
|
28 |
|
|
Lines 354-367
sub _reserved_words {
Link Here
|
| 354 |
|
355 |
|
| 355 |
=head3 _build_order_atom |
356 |
=head3 _build_order_atom |
| 356 |
|
357 |
|
| 357 |
my $order_atom = _build_order_atom( $string ); |
358 |
my $order_atom = _build_order_atom({ |
|
|
359 |
string => $string, |
| 360 |
result_set => $result_set |
| 361 |
}); |
| 358 |
|
362 |
|
| 359 |
Parses I<$string> and outputs data valid for using in SQL::Abstract order_by attribute |
363 |
Parses I<$string> and outputs data valid for using in SQL::Abstract order_by attribute |
| 360 |
according to the following rules: |
364 |
according to the following rules: |
| 361 |
|
365 |
|
| 362 |
string -> I<string> |
366 |
string -> I<string> |
| 363 |
+string -> I<{ -asc => string }> |
367 |
+string -> I<{ -asc => string }> |
| 364 |
-string -> I<{ -desc => string }> |
368 |
-string -> I<{ -desc => string }> |
|
|
369 |
+string/ignore-articles -> I<{ -asc => \[ REGEXP_REPLACE(...) ] }> |
| 370 |
-string/ignore-articles -> I<{ -desc => \[ REGEXP_REPLACE(...) ] }> |
| 371 |
|
| 372 |
The C</ignore-articles> modifier strips leading articles (based on the |
| 373 |
ArticleSortStopwords system preference) when sorting. Articles are matched |
| 374 |
case-insensitively and only when followed by a space, preventing false matches |
| 375 |
like "Theater" or "Another". |
| 365 |
|
376 |
|
| 366 |
=cut |
377 |
=cut |
| 367 |
|
378 |
|
|
Lines 370-398
sub _build_order_atom {
Link Here
|
| 370 |
my $string = $args->{string}; |
381 |
my $string = $args->{string}; |
| 371 |
my $result_set = $args->{result_set}; |
382 |
my $result_set = $args->{result_set}; |
| 372 |
|
383 |
|
|
|
384 |
# Step 1: Extract direction operator |
| 385 |
my $direction = ''; |
| 386 |
if ( $string =~ s/^(\+)// ) { |
| 387 |
$direction = 'asc'; |
| 388 |
} elsif ( $string =~ s/^(\-)// ) { |
| 389 |
$direction = 'desc'; |
| 390 |
} elsif ( $string =~ s/^(\s)// ) { |
| 391 |
$direction = 'asc'; |
| 392 |
} |
| 393 |
|
| 394 |
# Step 2: Check for /ignore-articles modifier |
| 395 |
my $ignore_articles = 0; |
| 396 |
if ( $string =~ s/\/ignore-articles$// ) { |
| 397 |
$ignore_articles = 1; |
| 398 |
} |
| 399 |
|
| 400 |
# Step 3: Map API field name to database column |
| 373 |
my $param = $string; |
401 |
my $param = $string; |
| 374 |
$param =~ s/^(\+|\-|\s)//; |
|
|
| 375 |
if ($result_set) { |
402 |
if ($result_set) { |
| 376 |
my $model_param = _from_api_param( $param, $result_set ); |
403 |
my $model_param = _from_api_param( $param, $result_set ); |
| 377 |
$param = $model_param if defined $model_param; |
404 |
$param = $model_param if defined $model_param; |
| 378 |
} |
405 |
} |
| 379 |
|
406 |
|
| 380 |
if ( $string =~ m/^\+/ |
407 |
# Step 4: Apply article-ignoring transformation if requested |
| 381 |
or $string =~ m/^\s/ ) |
408 |
if ($ignore_articles) { |
| 382 |
{ |
409 |
$param = _build_article_ignoring_expression($param); |
| 383 |
# asc order operator present |
410 |
} |
| 384 |
return { -asc => $param }; |
|
|
| 385 |
} elsif ( $string =~ m/^\-/ ) { |
| 386 |
|
411 |
|
| 387 |
# desc order operator present |
412 |
# Step 5: Return with appropriate direction |
|
|
413 |
if ( $direction eq 'asc' ) { |
| 414 |
return { -asc => $param }; |
| 415 |
} elsif ( $direction eq 'desc' ) { |
| 388 |
return { -desc => $param }; |
416 |
return { -desc => $param }; |
| 389 |
} else { |
417 |
} else { |
| 390 |
|
|
|
| 391 |
# no order operator present |
| 392 |
return $param; |
418 |
return $param; |
| 393 |
} |
419 |
} |
| 394 |
} |
420 |
} |
| 395 |
|
421 |
|
|
|
422 |
=head3 _build_article_ignoring_expression |
| 423 |
|
| 424 |
my $expression = _build_article_ignoring_expression( $column_name ); |
| 425 |
|
| 426 |
Takes a column name and returns a SQL literal expression that strips leading |
| 427 |
articles based on the ArticleSortStopwords system preference. |
| 428 |
|
| 429 |
Articles are matched case-insensitively and only when followed by a space, |
| 430 |
preventing false matches like "Theater" or "Another". |
| 431 |
|
| 432 |
Returns a DBIx::Class literal SQL expression: C<\[ $sql, @bind_values ]> |
| 433 |
|
| 434 |
If the system preference is empty or not configured, returns the column unchanged. |
| 435 |
|
| 436 |
=cut |
| 437 |
|
| 438 |
sub _build_article_ignoring_expression { |
| 439 |
my ($column) = @_; |
| 440 |
|
| 441 |
# Retrieve stopwords from system preference |
| 442 |
my $stopwords = C4::Context->preference('ArticleSortStopwords') || ''; |
| 443 |
|
| 444 |
# If no stopwords configured, return the column unchanged |
| 445 |
return $column unless $stopwords; |
| 446 |
|
| 447 |
# Trim whitespace |
| 448 |
$stopwords =~ s/^\s+|\s+$//g; |
| 449 |
return $column unless $stopwords; |
| 450 |
|
| 451 |
# Build the REGEXP_REPLACE expression |
| 452 |
# We lowercase both the column and the pattern for case-insensitive matching |
| 453 |
# Pattern: ^(article1|article2|article3) |
| 454 |
# Space after closing paren ensures we only match "The Book" not "Theater" |
| 455 |
# We use lowercase pattern because we're applying LOWER() to the column |
| 456 |
$stopwords = lc($stopwords); |
| 457 |
my $pattern = "^($stopwords) "; |
| 458 |
|
| 459 |
# Return DBIx::Class literal SQL expression |
| 460 |
# Column name must be in the SQL string, not as a bind parameter |
| 461 |
# REGEXP_REPLACE(subject, pattern, replace) - basic syntax for MariaDB 10.0.5+ |
| 462 |
return \[ "REGEXP_REPLACE(LOWER($column), ?, '')", $pattern ]; |
| 463 |
} |
| 464 |
|
| 396 |
=head3 _parse_embed |
465 |
=head3 _parse_embed |
| 397 |
|
466 |
|
| 398 |
my $embed = _parse_embed( $string ); |
467 |
my $embed = _parse_embed( $string ); |