From cb7968387a96636eff093a6ca6d427088fa93990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Cohen=20Arazi?= Date: Wed, 8 Apr 2026 16:07:44 -0300 Subject: [PATCH] Bug 42310: (follow-up) Add CRUD methods to Koha::Calendar, delegate C4::Calendar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add to Koha::Calendar: add_weekly_closure, add_repeating_closure, add_single_closure, add_exception, delete_weekly_closure, delete_repeating_closure, delete_single_closure, delete_exception, copy_to, _clear_cache C4::Calendar write methods now delegate to Koha::Calendar instead of using raw DBI. No old table references remain in C4::Calendar. Tests in t/db_dependent/Koha/Calendar.t cover all CRUD operations: create, verify via is_holiday, delete, copy_to (including idempotency). Signed-off-by: Tomás Cohen Arazi Signed-off-by: David Nind Signed-off-by: Martin Renvoize Signed-off-by: Tomás Cohen Arazi Signed-off-by: Pedro Amorim --- C4/Calendar.pm | 384 +++++++++--------------------- Koha/Calendar.pm | 166 ++++++++++++- t/db_dependent/Koha/Calendar.t | 112 ++++++++- t/db_dependent/api/v1/libraries.t | 49 ++-- 4 files changed, 413 insertions(+), 298 deletions(-) diff --git a/C4/Calendar.pm b/C4/Calendar.pm index 0ea3531ae1..74396b5743 100644 --- a/C4/Calendar.pm +++ b/C4/Calendar.pm @@ -21,6 +21,7 @@ use vars qw(@EXPORT); use Carp qw( croak ); use Date::Calc qw( Today ); +use Koha::Calendar; use C4::Context; use Koha::Caches; @@ -64,30 +65,32 @@ sub new { return $self; } +=head2 _init + +Loads calendar data from the database for the given branch. + +=cut + sub _init { my $self = shift @_; my $branch = shift; defined($branch) or die "No branchcode sent to _init"; # must test for defined here and above to allow "" - my $dbh = C4::Context->dbh(); - my $repeatable = $dbh->prepare( - 'SELECT * - FROM repeatable_holidays - WHERE ( branchcode = ? ) - AND (ISNULL(weekday) = ?)' - ); - $repeatable->execute( $branch, 0 ); - my %week_days_holidays; + my $dbh = C4::Context->dbh(); - while ( my $row = $repeatable->fetchrow_hashref ) { + my $weekly_sth = $dbh->prepare('SELECT * FROM library_weekly_closures WHERE library_id = ?'); + $weekly_sth->execute($branch); + my %week_days_holidays; + while ( my $row = $weekly_sth->fetchrow_hashref ) { my $key = $row->{weekday}; $week_days_holidays{$key}{title} = $row->{title}; $week_days_holidays{$key}{description} = $row->{description}; } $self->{'week_days_holidays'} = \%week_days_holidays; - $repeatable->execute( $branch, 1 ); + my $repeating_sth = $dbh->prepare('SELECT * FROM library_repeating_closures WHERE library_id = ?'); + $repeating_sth->execute($branch); my %day_month_holidays; - while ( my $row = $repeatable->fetchrow_hashref ) { + while ( my $row = $repeating_sth->fetchrow_hashref ) { my $key = $row->{month} . "/" . $row->{day}; $day_month_holidays{$key}{title} = $row->{title}; $day_month_holidays{$key}{description} = $row->{description}; @@ -96,29 +99,26 @@ sub _init { } $self->{'day_month_holidays'} = \%day_month_holidays; - my $special = $dbh->prepare( - 'SELECT day, month, year, title, description - FROM special_holidays - WHERE ( branchcode = ? ) - AND (isexception = ?)' - ); - $special->execute( $branch, 1 ); + my $exceptions_sth = + $dbh->prepare('SELECT date, title, description FROM library_closure_exceptions WHERE library_id = ?'); + $exceptions_sth->execute($branch); my %exception_holidays; - while ( my ( $day, $month, $year, $title, $description ) = $special->fetchrow ) { - $exception_holidays{"$year/$month/$day"}{title} = $title; - $exception_holidays{"$year/$month/$day"}{description} = $description; - $exception_holidays{"$year/$month/$day"}{date} = - sprintf( ISO_DATE_FORMAT, $year, $month, $day ); + while ( my $row = $exceptions_sth->fetchrow_hashref ) { + my ( $year, $month, $day ) = split( /-/, $row->{date} ); + $exception_holidays{"$year/$month/$day"}{title} = $row->{title}; + $exception_holidays{"$year/$month/$day"}{description} = $row->{description}; + $exception_holidays{"$year/$month/$day"}{date} = $row->{date}; } $self->{'exception_holidays'} = \%exception_holidays; - $special->execute( $branch, 0 ); + my $single_sth = $dbh->prepare('SELECT date, title, description FROM library_single_closures WHERE library_id = ?'); + $single_sth->execute($branch); my %single_holidays; - while ( my ( $day, $month, $year, $title, $description ) = $special->fetchrow ) { - $single_holidays{"$year/$month/$day"}{title} = $title; - $single_holidays{"$year/$month/$day"}{description} = $description; - $single_holidays{"$year/$month/$day"}{date} = - sprintf( ISO_DATE_FORMAT, $year, $month, $day ); + while ( my $row = $single_sth->fetchrow_hashref ) { + my ( $year, $month, $day ) = split( /-/, $row->{date} ); + $single_holidays{"$year/$month/$day"}{title} = $row->{title}; + $single_holidays{"$year/$month/$day"}{description} = $row->{description}; + $single_holidays{"$year/$month/$day"}{date} = $row->{date}; } $self->{'single_holidays'} = \%single_holidays; return $self; @@ -202,17 +202,13 @@ C<$description> Is the description to store for the holiday formed by $year/$mon sub insert_week_day_holiday { my $self = shift @_; my %options = @_; - - my $weekday = $options{weekday}; - croak "Invalid weekday $weekday" unless $weekday =~ m/^[0-6]$/; - - my $dbh = C4::Context->dbh(); - my $insertHoliday = $dbh->prepare( - "insert into repeatable_holidays (branchcode,weekday,day,month,title,description) values ( ?,?,NULL,NULL,?,? )" + Koha::Calendar->new( branchcode => $self->{branchcode} )->add_weekly_closure( + { + weekday => $options{weekday}, title => $options{title}, description => $options{description}, + } ); - $insertHoliday->execute( $self->{branchcode}, $weekday, $options{title}, $options{description} ); - $self->{'week_days_holidays'}->{$weekday}{title} = $options{title}; - $self->{'week_days_holidays'}->{$weekday}{description} = $options{description}; + $self->{'week_days_holidays'}->{ $options{weekday} }{title} = $options{title}; + $self->{'week_days_holidays'}->{ $options{weekday} }{description} = $options{description}; return $self; } @@ -238,14 +234,11 @@ C<$description> Is the description to store for the holiday formed by $year/$mon sub insert_day_month_holiday { my $self = shift @_; my %options = @_; - - my $dbh = C4::Context->dbh(); - my $insertHoliday = $dbh->prepare( - "insert into repeatable_holidays (branchcode,weekday,day,month,title,description) values (?, NULL, ?, ?, ?,? )" - ); - $insertHoliday->execute( - $self->{branchcode}, $options{day}, $options{month}, $options{title}, - $options{description} + Koha::Calendar->new( branchcode => $self->{branchcode} )->add_repeating_closure( + { + day => $options{day}, month => $options{month}, title => $options{title}, + description => $options{description}, + } ); $self->{'day_month_holidays'}->{"$options{month}/$options{day}"}{title} = $options{title}; $self->{'day_month_holidays'}->{"$options{month}/$options{day}"}{description} = $options{description}; @@ -277,28 +270,16 @@ C<$description> Is the description to store for the holiday formed by $year/$mon sub insert_single_holiday { my $self = shift @_; my %options = @_; - @options{qw(year month day)} = ( $options{date} =~ m/(\d+)-(\d+)-(\d+)/o ) - if $options{date} && !$options{day}; - - my $dbh = C4::Context->dbh(); - my $isexception = 0; - my $insertHoliday = $dbh->prepare( - "insert into special_holidays (branchcode,day,month,year,isexception,title,description) values (?,?,?,?,?,?,?)" + my $date = $options{date}; + unless ($date) { + $date = sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} ); + } + Koha::Calendar->new( branchcode => $self->{branchcode} )->add_single_closure( + { + date => $date, title => $options{title}, description => $options{description}, + } ); - $insertHoliday->execute( - $self->{branchcode}, $options{day}, $options{month}, $options{year}, $isexception, - $options{title}, $options{description} - ); - $self->{'single_holidays'}->{"$options{year}/$options{month}/$options{day}"}{title} = $options{title}; - $self->{'single_holidays'}->{"$options{year}/$options{month}/$options{day}"}{description} = $options{description}; - - # changed the 'single_holidays' table, lets force/reset its cache - my $cache = Koha::Caches->get_instance(); - my $key = $self->{branchcode} . "_holidays"; - $cache->clear_from_cache($key); - return $self; - } =head2 insert_exception_holiday @@ -326,28 +307,15 @@ C<$description> Is the description to store for the holiday formed by $year/$mon sub insert_exception_holiday { my $self = shift @_; my %options = @_; - - @options{qw(year month day)} = ( $options{date} =~ m/(\d+)-(\d+)-(\d+)/o ) - if $options{date} && !$options{day}; - - my $dbh = C4::Context->dbh(); - my $isexception = 1; - my $insertException = $dbh->prepare( - "insert into special_holidays (branchcode,day,month,year,isexception,title,description) values (?,?,?,?,?,?,?)" + my $date = $options{date}; + unless ($date) { + $date = sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} ); + } + Koha::Calendar->new( branchcode => $self->{branchcode} )->add_exception( + { + date => $date, title => $options{title}, description => $options{description}, + } ); - $insertException->execute( - $self->{branchcode}, $options{day}, $options{month}, $options{year}, $isexception, - $options{title}, $options{description} - ); - $self->{'exception_holidays'}->{"$options{year}/$options{month}/$options{day}"}{title} = $options{title}; - $self->{'exception_holidays'}->{"$options{year}/$options{month}/$options{day}"}{description} = - $options{description}; - - # changed the 'single_holidays' table, lets force/reset its cache - my $cache = Koha::Caches->get_instance(); - my $key = $self->{branchcode} . "_holidays"; - $cache->clear_from_cache($key); - return $self; } @@ -368,13 +336,10 @@ C<$description> Is the description to update for the holiday. sub ModWeekdayholiday { my $self = shift @_; my %options = @_; - - my $dbh = C4::Context->dbh(); - my $updateHoliday = - $dbh->prepare("UPDATE repeatable_holidays SET title = ?, description = ? WHERE branchcode = ? AND weekday = ?"); - $updateHoliday->execute( $options{title}, $options{description}, $self->{branchcode}, $options{weekday} ); - $self->{'week_days_holidays'}->{ $options{weekday} }{title} = $options{title}; - $self->{'week_days_holidays'}->{ $options{weekday} }{description} = $options{description}; + my $closure = + Koha::Calendar::WeeklyClosures->search( { library_id => $self->{branchcode}, weekday => $options{weekday} } ) + ->next; + $closure->title( $options{title} )->description( $options{description} )->store if $closure; return $self; } @@ -400,16 +365,9 @@ C<$description> The description to be update for the holiday. sub ModDaymonthholiday { my $self = shift @_; my %options = @_; - - my $dbh = C4::Context->dbh(); - my $updateHoliday = $dbh->prepare( - "UPDATE repeatable_holidays SET title = ?, description = ? WHERE month = ? AND day = ? AND branchcode = ?"); - $updateHoliday->execute( - $options{title}, $options{description}, $options{month}, $options{day}, - $self->{branchcode} - ); - $self->{'day_month_holidays'}->{"$options{month}/$options{day}"}{title} = $options{title}; - $self->{'day_month_holidays'}->{"$options{month}/$options{day}"}{description} = $options{description}; + my $closure = Koha::Calendar::RepeatingClosures->search( + { library_id => $self->{branchcode}, day => $options{day}, month => $options{month} } )->next; + $closure->title( $options{title} )->description( $options{description} )->store if $closure; return $self; } @@ -438,25 +396,12 @@ C<$description> Is the description to update for the holiday formed by $year/$mo sub ModSingleholiday { my $self = shift @_; my %options = @_; - - my $dbh = C4::Context->dbh(); - my $isexception = 0; - - my $updateHoliday = $dbh->prepare( " -UPDATE special_holidays SET title = ?, description = ? - WHERE day = ? AND month = ? AND year = ? AND branchcode = ? AND isexception = ?" ); - $updateHoliday->execute( - $options{title}, $options{description}, $options{day}, $options{month}, $options{year}, - $self->{branchcode}, $isexception - ); - $self->{'single_holidays'}->{"$options{year}/$options{month}/$options{day}"}{title} = $options{title}; - $self->{'single_holidays'}->{"$options{year}/$options{month}/$options{day}"}{description} = $options{description}; - - # changed the 'single_holidays' table, lets force/reset its cache - my $cache = Koha::Caches->get_instance(); - my $key = $self->{branchcode} . "_holidays"; - $cache->clear_from_cache($key); - + my $date = $options{date} || sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} ); + my $closure = Koha::Calendar::SingleClosures->search( { library_id => $self->{branchcode}, date => $date } )->next; + if ($closure) { + $closure->title( $options{title} )->description( $options{description} )->store; + Koha::Caches->get_instance()->clear_from_cache( $self->{branchcode} . '_holidays' ); + } return $self; } @@ -483,27 +428,14 @@ C<$description> Is the description to be modified for the holiday formed by $yea =cut sub ModExceptionholiday { - my $self = shift @_; - my %options = @_; - - my $dbh = C4::Context->dbh(); - my $isexception = 1; - my $updateHoliday = $dbh->prepare( " -UPDATE special_holidays SET title = ?, description = ? - WHERE day = ? AND month = ? AND year = ? AND branchcode = ? AND isexception = ?" ); - $updateHoliday->execute( - $options{title}, $options{description}, $options{day}, $options{month}, $options{year}, - $self->{branchcode}, $isexception - ); - $self->{'exception_holidays'}->{"$options{year}/$options{month}/$options{day}"}{title} = $options{title}; - $self->{'exception_holidays'}->{"$options{year}/$options{month}/$options{day}"}{description} = - $options{description}; - - # changed the 'single_holidays' table, lets force/reset its cache - my $cache = Koha::Caches->get_instance(); - my $key = $self->{branchcode} . "_holidays"; - $cache->clear_from_cache($key); - + my $self = shift @_; + my %options = @_; + my $date = $options{date} || sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} ); + my $exception = Koha::Calendar::Exceptions->search( { library_id => $self->{branchcode}, date => $date } )->next; + if ($exception) { + $exception->title( $options{title} )->description( $options{description} )->store; + Koha::Caches->get_instance()->clear_from_cache( $self->{branchcode} . '_holidays' ); + } return $self; } @@ -529,68 +461,37 @@ C<$year> Is year to make the date to delete. sub delete_holiday { my $self = shift @_; my %options = @_; + my $cal = Koha::Calendar->new( branchcode => $self->{branchcode} ); - # Verify what kind of holiday that day is. For example, if it is - # a repeatable holiday, this should check if there are some exception - # for that holiday rule. Otherwise, if it is a regular holiday, it´s - # ok just deleting it. + if ( $options{day} && $options{month} && $options{year} ) { + my $date = sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} ); - my $dbh = C4::Context->dbh(); - my $isSingleHoliday = $dbh->prepare( - "SELECT id FROM special_holidays WHERE (branchcode = ?) AND (day = ?) AND (month = ?) AND (year = ?)"); - $isSingleHoliday->execute( $self->{branchcode}, $options{day}, $options{month}, $options{year} ); - if ( $isSingleHoliday->rows ) { - my $id = $isSingleHoliday->fetchrow; - $isSingleHoliday->finish; # Close the last query - - my $deleteHoliday = $dbh->prepare("DELETE FROM special_holidays WHERE id = ?"); - $deleteHoliday->execute($id); - delete( $self->{'single_holidays'}->{"$options{year}/$options{month}/$options{day}"} ); - } else { - $isSingleHoliday->finish; # Close the last query - - my $isWeekdayHoliday = $dbh->prepare("SELECT id FROM repeatable_holidays WHERE branchcode = ? AND weekday = ?"); - $isWeekdayHoliday->execute( $self->{branchcode}, $options{weekday} ); - if ( $isWeekdayHoliday->rows ) { - my $id = $isWeekdayHoliday->fetchrow; - $isWeekdayHoliday->finish; # Close the last query - - my $updateExceptions = $dbh->prepare( - "UPDATE special_holidays SET isexception = 0 WHERE (WEEKDAY(CONCAT(special_holidays.year,'-',special_holidays.month,'-',special_holidays.day)) = ?) AND (branchcode = ?)" - ); - $updateExceptions->execute( $options{weekday}, $self->{branchcode} ); - $updateExceptions->finish; # Close the last query - - my $deleteHoliday = $dbh->prepare("DELETE FROM repeatable_holidays WHERE id = ?"); - $deleteHoliday->execute($id); - delete( $self->{'week_days_holidays'}->{ $options{weekday} } ); - } else { - $isWeekdayHoliday->finish; # Close the last query - - my $isDayMonthHoliday = $dbh->prepare( - "SELECT id FROM repeatable_holidays WHERE (branchcode = ?) AND (day = ?) AND (month = ?)"); - $isDayMonthHoliday->execute( $self->{branchcode}, $options{day}, $options{month} ); - if ( $isDayMonthHoliday->rows ) { - my $id = $isDayMonthHoliday->fetchrow; - $isDayMonthHoliday->finish; - my $updateExceptions = $dbh->prepare( - "UPDATE special_holidays SET isexception = 0 WHERE (special_holidays.branchcode = ?) AND (special_holidays.day = ?) and (special_holidays.month = ?)" - ); - $updateExceptions->execute( $self->{branchcode}, $options{day}, $options{month} ); - $updateExceptions->finish; # Close the last query - - my $deleteHoliday = $dbh->prepare("DELETE FROM repeatable_holidays WHERE (id = ?)"); - $deleteHoliday->execute($id); - delete( $self->{'day_month_holidays'}->{"$options{month}/$options{day}"} ); - } + # Try single closure first + if ( Koha::Calendar::SingleClosures->search( { library_id => $self->{branchcode}, date => $date } )->count ) { + $cal->delete_single_closure( { date => $date } ); } + + # Also remove any exception for this date + Koha::Calendar::Exceptions->search( { library_id => $self->{branchcode}, date => $date } )->delete; } - # changed the 'single_holidays' table, lets force/reset its cache - my $cache = Koha::Caches->get_instance(); - my $key = $self->{branchcode} . "_holidays"; - $cache->clear_from_cache($key); + if ( defined $options{weekday} + && Koha::Calendar::WeeklyClosures->search( { library_id => $self->{branchcode}, weekday => $options{weekday} } ) + ->count ) + { + $cal->delete_weekly_closure( { weekday => $options{weekday} } ); + } elsif ( + $options{day} + && $options{month} + && Koha::Calendar::RepeatingClosures->search( + { library_id => $self->{branchcode}, day => $options{day}, month => $options{month} } + )->count + ) + { + $cal->delete_repeating_closure( { day => $options{day}, month => $options{month} } ); + } + $cal->_clear_cache; return $self; } @@ -613,17 +514,8 @@ C<$year> Is year to make the date to delete. sub delete_holiday_range { my $self = shift; my %options = @_; - - my $dbh = C4::Context->dbh(); - my $sth = $dbh->prepare( - "DELETE FROM special_holidays WHERE (branchcode = ?) AND (day = ?) AND (month = ?) AND (year = ?)"); - $sth->execute( $self->{branchcode}, $options{day}, $options{month}, $options{year} ); - - # changed the 'single_holidays' table, lets force/reset its cache - my $cache = Koha::Caches->get_instance(); - my $key = $self->{branchcode} . "_holidays"; - $cache->clear_from_cache($key); - + my $date = $options{date} || sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} ); + Koha::Calendar->new( branchcode => $self->{branchcode} )->delete_single_closure( { date => $date } ); } =head2 delete_holiday_range_repeatable @@ -642,15 +534,8 @@ C<$month> Is month to make the date to delete. sub delete_holiday_range_repeatable { my $self = shift; my %options = @_; - - my $dbh = C4::Context->dbh(); - my $sth = $dbh->prepare("DELETE FROM repeatable_holidays WHERE (branchcode = ?) AND (day = ?) AND (month = ?)"); - $sth->execute( $self->{branchcode}, $options{day}, $options{month} ); - - # changed the 'single_holidays' table, lets force/reset its cache - my $cache = Koha::Caches->get_instance(); - my $key = $self->{branchcode} . "_holidays"; - $cache->clear_from_cache($key); + Koha::Calendar->new( branchcode => $self->{branchcode} ) + ->delete_repeating_closure( { day => $options{day}, month => $options{month} } ); } =head2 delete_exception_holiday_range @@ -673,17 +558,8 @@ C<$year> Is year to make the date to delete. sub delete_exception_holiday_range { my $self = shift; my %options = @_; - - my $dbh = C4::Context->dbh(); - my $sth = $dbh->prepare( - "DELETE FROM special_holidays WHERE (branchcode = ?) AND (isexception = 1) AND (day = ?) AND (month = ?) AND (year = ?)" - ); - $sth->execute( $self->{branchcode}, $options{day}, $options{month}, $options{year} ); - - # changed the 'single_holidays' table, lets force/reset its cache - my $cache = Koha::Caches->get_instance(); - my $key = $self->{branchcode} . "_holidays"; - $cache->clear_from_cache($key); + my $date = $options{date} || sprintf( ISO_DATE_FORMAT, $options{year}, $options{month}, $options{day} ); + Koha::Calendar->new( branchcode => $self->{branchcode} )->delete_exception( { date => $date } ); } =head2 isHoliday @@ -734,47 +610,7 @@ sub isHoliday { sub copy_to_branch { my ( $self, $target_branch ) = @_; - - croak "No target_branch" unless $target_branch; - - my $target_calendar = C4::Calendar->new( branchcode => $target_branch ); - - my ( $y, $m, $d ) = Today(); - my $today = sprintf ISO_DATE_FORMAT, $y, $m, $d; - - my $wdh = $self->get_week_days_holidays; - my $target_wdh = $target_calendar->get_week_days_holidays; - foreach my $key ( keys %$wdh ) { - unless ( grep { $_ eq $key } keys %$target_wdh ) { - $target_calendar->insert_week_day_holiday( weekday => $key, %{ $wdh->{$key} } ); - } - } - - my $dmh = $self->get_day_month_holidays; - my $target_dmh = $target_calendar->get_day_month_holidays; - foreach my $values ( values %$dmh ) { - unless ( grep { $_->{day} eq $values->{day} && $_->{month} eq $values->{month} } values %$target_dmh ) { - $target_calendar->insert_day_month_holiday( %{$values} ); - } - } - - my $exception_holidays = $self->get_exception_holidays; - my $target_exceptions = $target_calendar->get_exception_holidays; - foreach my $values ( grep { $_->{date} gt $today } values %{$exception_holidays} ) { - unless ( grep { $_->{date} eq $values->{date} } values %$target_exceptions ) { - $target_calendar->insert_exception_holiday( %{$values} ); - } - } - - my $single_holidays = $self->get_single_holidays; - my $target_singles = $target_calendar->get_single_holidays; - foreach my $values ( grep { $_->{date} gt $today } values %{$single_holidays} ) { - unless ( grep { $_->{date} eq $values->{date} } values %$target_singles ) { - $target_calendar->insert_single_holiday( %{$values} ); - } - } - - return 1; + return Koha::Calendar->new( branchcode => $self->{branchcode} )->copy_to($target_branch); } 1; diff --git a/Koha/Calendar.pm b/Koha/Calendar.pm index 64a4d23406..6492a2ab75 100644 --- a/Koha/Calendar.pm +++ b/Koha/Calendar.pm @@ -7,6 +7,7 @@ use DateTime; use DateTime::Duration; use C4::Context; use Koha::Caches; +use Koha::DateUtils qw( dt_from_string ); use Koha::Exceptions; use Koha::Exceptions::Calendar; use Koha::Calendar::WeeklyClosures; @@ -422,6 +423,124 @@ sub clear_weekly_closed_days { return; } +sub add_weekly_closure { + my ( $self, $params ) = @_; + return Koha::Calendar::WeeklyClosure->new( { library_id => $self->{branchcode}, %$params } )->store; +} + +sub add_repeating_closure { + my ( $self, $params ) = @_; + return Koha::Calendar::RepeatingClosure->new( { library_id => $self->{branchcode}, %$params } )->store; +} + +sub add_single_closure { + my ( $self, $params ) = @_; + my $result = Koha::Calendar::SingleClosure->new( { library_id => $self->{branchcode}, %$params } )->store; + $self->_clear_cache; + return $result; +} + +sub add_exception { + my ( $self, $params ) = @_; + my $result = Koha::Calendar::Exception->new( { library_id => $self->{branchcode}, %$params } )->store; + $self->_clear_cache; + return $result; +} + +sub delete_weekly_closure { + my ( $self, $params ) = @_; + Koha::Calendar::WeeklyClosures->search( { library_id => $self->{branchcode}, weekday => $params->{weekday} } ) + ->delete; + return $self; +} + +sub delete_repeating_closure { + my ( $self, $params ) = @_; + Koha::Calendar::RepeatingClosures->search( + { library_id => $self->{branchcode}, day => $params->{day}, month => $params->{month} } )->delete; + return $self; +} + +sub delete_single_closure { + my ( $self, $params ) = @_; + Koha::Calendar::SingleClosures->search( { library_id => $self->{branchcode}, date => $params->{date} } )->delete; + $self->_clear_cache; + return $self; +} + +sub delete_exception { + my ( $self, $params ) = @_; + Koha::Calendar::Exceptions->search( { library_id => $self->{branchcode}, date => $params->{date} } )->delete; + $self->_clear_cache; + return $self; +} + +sub copy_to { + my ( $self, $target_branchcode ) = @_; + croak "No target_branchcode" unless $target_branchcode; + + my $today = dt_from_string()->ymd; + + my $weekly = Koha::Calendar::WeeklyClosures->search( { library_id => $self->{branchcode} } ); + while ( my $row = $weekly->next ) { + next + if Koha::Calendar::WeeklyClosures->search( { library_id => $target_branchcode, weekday => $row->weekday } ) + ->count; + Koha::Calendar::WeeklyClosure->new( + { + library_id => $target_branchcode, weekday => $row->weekday, + title => $row->title, description => $row->description, + } + )->store; + } + + my $repeating = Koha::Calendar::RepeatingClosures->search( { library_id => $self->{branchcode} } ); + while ( my $row = $repeating->next ) { + next + if Koha::Calendar::RepeatingClosures->search( + { library_id => $target_branchcode, day => $row->day, month => $row->month } )->count; + Koha::Calendar::RepeatingClosure->new( + { + library_id => $target_branchcode, day => $row->day, month => $row->month, + title => $row->title, description => $row->description, + } + )->store; + } + + my $singles = + Koha::Calendar::SingleClosures->search( { library_id => $self->{branchcode}, date => { '>=' => $today } } ); + while ( my $row = $singles->next ) { + next + if Koha::Calendar::SingleClosures->search( { library_id => $target_branchcode, date => $row->date } ) + ->count; + Koha::Calendar::SingleClosure->new( + { + library_id => $target_branchcode, date => $row->date, + title => $row->title, description => $row->description, + } + )->store; + } + + my $exceptions = + Koha::Calendar::Exceptions->search( { library_id => $self->{branchcode}, date => { '>=' => $today } } ); + while ( my $row = $exceptions->next ) { + next if Koha::Calendar::Exceptions->search( { library_id => $target_branchcode, date => $row->date } )->count; + Koha::Calendar::Exception->new( + { + library_id => $target_branchcode, date => $row->date, + title => $row->title, description => $row->description, + } + )->store; + } + + return 1; +} + +sub _clear_cache { + my ($self) = @_; + Koha::Caches->get_instance()->clear_from_cache( $self->{branchcode} . '_holidays' ); +} + 1; __END__ @@ -598,9 +717,54 @@ allow testing of more configurations =head2 add_holiday Passed a datetime object this will add it to the calendar's list of -closed days. This is for testing so that we can alter the Calenfar object's +closed days. This is for testing so that we can alter the Calendar object's list of specified dates +=head2 CRUD methods + +=head3 add_weekly_closure + + $calendar->add_weekly_closure({ weekday => 0, title => 'Sundays', description => '' }); + +=head3 add_repeating_closure + + $calendar->add_repeating_closure({ day => 25, month => 12, title => 'Christmas', description => '' }); + +=head3 add_single_closure + + $calendar->add_single_closure({ date => '2026-06-15', title => 'Staff day', description => '' }); + +=head3 add_exception + + $calendar->add_exception({ date => '2026-12-25', title => 'Special opening', description => '' }); + +=head3 delete_weekly_closure + + $calendar->delete_weekly_closure({ weekday => 0 }); + +=head3 delete_repeating_closure + + $calendar->delete_repeating_closure({ day => 25, month => 12 }); + +=head3 delete_single_closure + + $calendar->delete_single_closure({ date => '2026-06-15' }); + +=head3 delete_exception + + $calendar->delete_exception({ date => '2026-12-25' }); + +=head3 copy_to + + $calendar->copy_to($target_branchcode); + +Copies all closure rules to another branch. Only copies future single +closures and exceptions. + +=head3 _clear_cache + +Clears the holidays cache for this branch. + =head2 Internal methods =head3 _init diff --git a/t/db_dependent/Koha/Calendar.t b/t/db_dependent/Koha/Calendar.t index c22faf7fec..dc91f06fec 100755 --- a/t/db_dependent/Koha/Calendar.t +++ b/t/db_dependent/Koha/Calendar.t @@ -19,12 +19,16 @@ use Modern::Perl; -use Test::More tests => 3; +use Test::More tests => 4; use Test::Exception; use Test::NoWarnings; use Koha::Database; use Koha::DateUtils qw( dt_from_string ); +use Koha::Calendar::WeeklyClosures; +use Koha::Calendar::RepeatingClosures; +use Koha::Calendar::SingleClosures; +use Koha::Calendar::Exceptions; use t::lib::TestBuilder; BEGIN { @@ -117,3 +121,109 @@ subtest 'has_business_days_between' => sub { $schema->storage->txn_rollback; }; + +subtest 'CRUD methods' => sub { + + plan tests => 20; + + $schema->storage->txn_begin; + + my $library = $builder->build_object( { class => 'Koha::Libraries' } ); + my $calendar = Koha::Calendar->new( branchcode => $library->branchcode ); + + # add_weekly_closure + my $weekly = $calendar->add_weekly_closure( { weekday => 0, title => 'Sundays', description => 'Closed' } ); + isa_ok( $weekly, 'Koha::Calendar::WeeklyClosure', 'add_weekly_closure returns object' ); + is( + Koha::Calendar::WeeklyClosures->search( { library_id => $library->branchcode } )->count, 1, + 'Weekly closure created' + ); + + # add_repeating_closure + my $repeating = + $calendar->add_repeating_closure( { day => 25, month => 12, title => 'Christmas', description => '' } ); + isa_ok( $repeating, 'Koha::Calendar::RepeatingClosure', 'add_repeating_closure returns object' ); + is( + Koha::Calendar::RepeatingClosures->search( { library_id => $library->branchcode } )->count, 1, + 'Repeating closure created' + ); + + # add_single_closure + my $single = $calendar->add_single_closure( { date => '2026-06-15', title => 'Staff day', description => '' } ); + isa_ok( $single, 'Koha::Calendar::SingleClosure', 'add_single_closure returns object' ); + is( + Koha::Calendar::SingleClosures->search( { library_id => $library->branchcode } )->count, 1, + 'Single closure created' + ); + + # add_exception + my $exception = $calendar->add_exception( { date => '2026-12-25', title => 'Special opening', description => '' } ); + isa_ok( $exception, 'Koha::Calendar::Exception', 'add_exception returns object' ); + is( Koha::Calendar::Exceptions->search( { library_id => $library->branchcode } )->count, 1, 'Exception created' ); + + # Verify is_holiday uses the new data + my $cal = Koha::Calendar->new( branchcode => $library->branchcode ); + my $sunday = dt_from_string('2026-06-14'); # a Sunday + is( $cal->is_holiday($sunday), 1, 'Sunday is closed after add_weekly_closure' ); + + my $june15 = dt_from_string('2026-06-15'); + is( $cal->is_holiday($june15), 1, 'Single closure date is closed' ); + + my $xmas = dt_from_string('2026-12-25'); + is( $cal->is_holiday($xmas), 0, 'Exception overrides repeating closure' ); + + # delete methods + $calendar->delete_weekly_closure( { weekday => 0 } ); + is( + Koha::Calendar::WeeklyClosures->search( { library_id => $library->branchcode } )->count, 0, + 'Weekly closure deleted' + ); + + $calendar->delete_repeating_closure( { day => 25, month => 12 } ); + is( + Koha::Calendar::RepeatingClosures->search( { library_id => $library->branchcode } )->count, 0, + 'Repeating closure deleted' + ); + + $calendar->delete_single_closure( { date => '2026-06-15' } ); + is( + Koha::Calendar::SingleClosures->search( { library_id => $library->branchcode } )->count, 0, + 'Single closure deleted' + ); + + $calendar->delete_exception( { date => '2026-12-25' } ); + is( Koha::Calendar::Exceptions->search( { library_id => $library->branchcode } )->count, 0, 'Exception deleted' ); + + # copy_to + $calendar->add_weekly_closure( { weekday => 6, title => 'Saturdays', description => '' } ); + $calendar->add_single_closure( { date => '2027-01-01', title => 'New Year', description => '' } ); + + my $library2 = $builder->build_object( { class => 'Koha::Libraries' } ); + $calendar->copy_to( $library2->branchcode ); + + is( + Koha::Calendar::WeeklyClosures->search( { library_id => $library2->branchcode } )->count, 1, + 'Weekly closure copied' + ); + is( + Koha::Calendar::SingleClosures->search( { library_id => $library2->branchcode } )->count, 1, + 'Single closure copied' + ); + + # copy_to should not duplicate + $calendar->copy_to( $library2->branchcode ); + is( + Koha::Calendar::WeeklyClosures->search( { library_id => $library2->branchcode } )->count, 1, + 'No duplicate after second copy' + ); + is( + Koha::Calendar::SingleClosures->search( { library_id => $library2->branchcode } )->count, 1, + 'No duplicate single after second copy' + ); + + # Verify is_holiday after deletions and re-init + my $cal2 = Koha::Calendar->new( branchcode => $library->branchcode ); + is( $cal2->is_holiday($june15), 0, 'Deleted single closure no longer a holiday' ); + + $schema->storage->txn_rollback; +}; diff --git a/t/db_dependent/api/v1/libraries.t b/t/db_dependent/api/v1/libraries.t index d3e2a60c98..a196d0732b 100755 --- a/t/db_dependent/api/v1/libraries.t +++ b/t/db_dependent/api/v1/libraries.t @@ -28,6 +28,10 @@ use t::lib::Mocks; use List::Util qw(min); use Koha::Caches; +use Koha::Calendar::WeeklyClosures; +use Koha::Calendar::RepeatingClosures; +use Koha::Calendar::SingleClosures; +use Koha::Calendar::Exceptions; use Koha::Libraries; use Koha::Database; @@ -461,41 +465,40 @@ subtest 'list_closed_dates() tests' => sub { my $cache = Koha::Caches->get_instance(); # Clear all closure data so we fully control what exists - $schema->resultset('SpecialHoliday')->search( { branchcode => $branchcode } )->delete; - $schema->resultset('RepeatableHoliday')->search( { branchcode => $branchcode } )->delete; + Koha::Calendar::WeeklyClosures->search( { library_id => $branchcode } )->delete; + Koha::Calendar::RepeatingClosures->search( { library_id => $branchcode } )->delete; + Koha::Calendar::SingleClosures->search( { library_id => $branchcode } )->delete; + Koha::Calendar::Exceptions->search( { library_id => $branchcode } )->delete; $cache->clear_from_cache( $branchcode . '_holidays' ); # Weekly closure: Saturday (6) # 2026-06-13 and 2026-06-20 are Saturdays - $builder->build( + $builder->build_object( { - source => 'RepeatableHoliday', - value => { - branchcode => $branchcode, weekday => 6, day => undef, month => undef, - title => 'Saturdays', description => '', - }, + class => 'Koha::Calendar::WeeklyClosures', + value => { + library_id => $branchcode, weekday => 6, title => 'Saturdays', description => '', + } } ); # Single closure: 2026-06-15 (a Monday) - $builder->build( + $builder->build_object( { - source => 'SpecialHoliday', - value => { - branchcode => $branchcode, day => 15, month => 6, year => 2026, - title => 'Test', description => '', isexception => 0, - }, + class => 'Koha::Calendar::SingleClosures', + value => { + library_id => $branchcode, date => '2026-06-15', title => 'Test', description => '', + } } ); # Exception: 2026-06-20 (a Saturday — normally closed, but open this day) - $builder->build( + $builder->build_object( { - source => 'SpecialHoliday', - value => { - branchcode => $branchcode, day => 20, month => 6, year => 2026, - title => 'Exception', description => '', isexception => 1, - }, + class => 'Koha::Calendar::Exceptions', + value => { + library_id => $branchcode, date => '2026-06-20', title => 'Exception', description => '', + } } ); $cache->clear_from_cache( $branchcode . '_holidays' ); @@ -536,8 +539,10 @@ subtest 'list_closed_dates() tests' => sub { # Defaults work (no from/to) — fresh library with no closures at all my $lib2 = $builder->build_object( { class => 'Koha::Libraries' } ); my $branchcode2 = $lib2->branchcode; - $schema->resultset('RepeatableHoliday')->search( { branchcode => $branchcode2 } )->delete; - $schema->resultset('SpecialHoliday')->search( { branchcode => $branchcode2 } )->delete; + Koha::Calendar::WeeklyClosures->search( { library_id => $branchcode2 } )->delete; + Koha::Calendar::RepeatingClosures->search( { library_id => $branchcode2 } )->delete; + Koha::Calendar::SingleClosures->search( { library_id => $branchcode2 } )->delete; + Koha::Calendar::Exceptions->search( { library_id => $branchcode2 } )->delete; $cache->clear_from_cache( $branchcode2 . '_holidays' ); $t->get_ok("//$userid:$password\@/api/v1/libraries/$branchcode2/closed_dates") ->status_is(200)