Bug 42310: (follow-up) Add CRUD methods to Koha::Calendar, delegate C4::Calendar

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 <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
This commit is contained in:
Tomás Cohen Arazi 2026-04-08 16:07:44 -03:00 committed by Pedro Amorim
parent 4d23dff433
commit cb7968387a
No known key found for this signature in database
GPG Key ID: 06B75BA7F3894D55
4 changed files with 413 additions and 298 deletions

View File

@ -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;

View File

@ -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

View File

@ -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;
};

View File

@ -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)