From e81e928ee8e036e28b4d42ca9336381433b42339 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 10:47:47 +0200 Subject: [PATCH 01/20] Let group managers create events --- lib/GaletteEvents/Event.php | 4 +- tests/GaletteEvents/tests/units/Event.php | 86 +++++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tests/GaletteEvents/tests/units/Event.php diff --git a/lib/GaletteEvents/Event.php b/lib/GaletteEvents/Event.php index 0fce7485..50281b8c 100644 --- a/lib/GaletteEvents/Event.php +++ b/lib/GaletteEvents/Event.php @@ -249,11 +249,11 @@ public function check(array $values): bool|array } else { if ( empty($values['group']) - || !in_array($values['group'], $this->login->managed_groups) + || !in_array((int)$values['group'], array_map('intval', $this->login->managed_groups), true) ) { $this->errors[] = _T('Please select a group you own!', 'events'); } else { - $this->group = $values['group']; + $this->group = (int)$values['group']; } } diff --git a/tests/GaletteEvents/tests/units/Event.php b/tests/GaletteEvents/tests/units/Event.php new file mode 100644 index 00000000..7027c543 --- /dev/null +++ b/tests/GaletteEvents/tests/units/Event.php @@ -0,0 +1,86 @@ + + */ +class Event extends GaletteTestCase +{ + use EventsFixtures; + + protected int $seed = 20260926151512; + + /** + * Cleanup after each test method + */ + public function tearDown(): void + { + $this->login->logout(); + $this->cleanEvents(); + parent::tearDown(); + } + + /** + * Get values posted from event form + * + * @param array $values Values to override + * + * @return array + */ + private function getFormValues(array $values = []): array + { + return $values + [ + 'name' => 'Event', + 'address' => '', + 'zip' => '', + 'town' => 'Lille', + 'country' => '', + 'comment' => '', + 'color' => '', + 'begin_date' => date('Y-m-d', strtotime('+10 days')), + 'end_date' => date('Y-m-d', strtotime('+11 days')), + 'open' => '1', + 'save' => '1', + ]; + } + + /** + * Group managers create events of the groups they manage + */ + public function testManagerCreatesEvent(): void + { + $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $managed = $this->createGroup('Managed group', [$member_two]); + $other = $this->createGroup('Other group', [], [$member_two]); + $this->logMember($this->dataAdherentTwo()); + + $event = new \GaletteEvents\Event($this->zdb, $this->login); + $this->assertSame( + [_T('Please select a group you own!', 'events')], + $event->check($this->getFormValues(['group' => (string)$other->getId()])) + ); + $this->expectLogEntry(\Analog\Analog::ERROR, 'Some errors has been threw attempting to edit/store an event'); + + $event = new \GaletteEvents\Event($this->zdb, $this->login); + $this->assertTrue($event->check($this->getFormValues(['group' => (string)$managed->getId()]))); + $this->assertTrue($event->store()); + + $event = new \GaletteEvents\Event($this->zdb, $this->login, (int)$event->getId()); + $this->assertSame($managed->getId(), $event->getGroup()); + } +} From 37a6bf887347fdc600d5da67ac4f6906f290196e Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 10:51:06 +0200 Subject: [PATCH 02/20] Initialize entities and accept NULL optional values --- lib/GaletteEvents/Activity.php | 4 +- lib/GaletteEvents/Booking.php | 15 +++--- lib/GaletteEvents/Event.php | 16 +++--- tests/GaletteEvents/tests/units/Activity.php | 18 +++++++ tests/GaletteEvents/tests/units/Booking.php | 55 ++++++++++++++++++++ tests/GaletteEvents/tests/units/Event.php | 33 ++++++++++++ 6 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 tests/GaletteEvents/tests/units/Booking.php diff --git a/lib/GaletteEvents/Activity.php b/lib/GaletteEvents/Activity.php index 1fae14c7..c2536f4e 100644 --- a/lib/GaletteEvents/Activity.php +++ b/lib/GaletteEvents/Activity.php @@ -39,7 +39,7 @@ class Activity private string $name; private bool $active = false; private string $creation_date; - private string $comment; + private string $comment = ''; /** * Default constructor @@ -102,7 +102,7 @@ private function loadFromRS(ArrayObject $r): void $this->name = $r->name; $this->active = (bool)$r->is_active; $this->creation_date = $r->creation_date; - $this->comment = $r->comment; + $this->comment = $r->comment ?? ''; } /** diff --git a/lib/GaletteEvents/Booking.php b/lib/GaletteEvents/Booking.php index d557e540..b02b9ea3 100644 --- a/lib/GaletteEvents/Booking.php +++ b/lib/GaletteEvents/Booking.php @@ -115,12 +115,13 @@ private function loadFromRS(ArrayObject $r): void $this->member = (int)$r->id_adh; $this->date = $r->booking_date; $this->paid = (bool)$r->is_paid; - $this->amount = (float)$r->payment_amount; + $this->amount = $r->payment_amount === null ? null : (float)$r->payment_amount; $this->payment_method = (int)$r->payment_method; $this->bank_name = $r->bank_name; $this->check_number = $r->check_number; - $this->number_people = (int)$r->number_people; - $this->comment = $r->comment; + $this->number_people = (int)($r->number_people ?? 1); + $this->comment = $r->comment ?? ''; + $this->creation_date = $r->creation_date; } /** @@ -255,7 +256,7 @@ public function check(array $values): array|bool && $member !== $this->getMemberId() ) { //group managers book for members of the groups they manage, on events of those groups - $group = $this->getEvent()?->getGroup() ?: null; + $group = $this->getEvent()?->getGroup(); if (!(new Adherent($this->zdb, $member))->canShow($this->login)) { $this->errors[] = _T("- Please select a member from a group you manage."); } elseif ($group === null || !$this->login->isGroupManager($group)) { @@ -768,8 +769,7 @@ private function canBook(Event $event): bool return false; } - //public events have no group, loaded as 0 - $group = $event->getGroup() ?: null; + $group = $event->getGroup(); return $group === null || $this->login->isGroupManager($group) || in_array($group, array_map('intval', Groups::loadGroups($this->login->id, false, false)), true); @@ -793,8 +793,7 @@ public function canEdit(Login $login): bool return true; } - //public events have no group, loaded as 0 - $group = $this->getEvent()?->getGroup() ?: null; + $group = $this->getEvent()?->getGroup(); return $group !== null && $login->isGroupManager($group); } diff --git a/lib/GaletteEvents/Event.php b/lib/GaletteEvents/Event.php index 50281b8c..545f545e 100644 --- a/lib/GaletteEvents/Event.php +++ b/lib/GaletteEvents/Event.php @@ -39,17 +39,17 @@ class Event private int $id; private string $name; - private string $address; - private string $zip; - private string $town; - private ?string $country; + private string $address = ''; + private string $zip = ''; + private string $town = ''; + private ?string $country = null; private string $begin_date; private string $end_date; private string $creation_date; private bool $open = true; - private ?int $group; + private ?int $group = null; private string $comment = ''; - private ?string $color; + private ?string $color = null; /** @var array> */ private array $activities = []; @@ -131,8 +131,8 @@ private function loadFromRS(ArrayObject $r): void $this->end_date = $r->end_date; $this->creation_date = $r->creation_date; $this->open = (bool)$r->is_open; - $this->group = (int)$r->id_group; - $this->comment = $r->comment; + $this->group = $r->id_group === null ? null : (int)$r->id_group; + $this->comment = $r->comment ?? ''; $this->color = $r->color; } diff --git a/tests/GaletteEvents/tests/units/Activity.php b/tests/GaletteEvents/tests/units/Activity.php index 51ca743a..43be19ae 100644 --- a/tests/GaletteEvents/tests/units/Activity.php +++ b/tests/GaletteEvents/tests/units/Activity.php @@ -148,4 +148,22 @@ public function testLoadError(): void $activity = new \GaletteEvents\Activity($this->zdb, $this->login); $this->assertFalse($activity->load(999)); } + + /** + * Activities are stored without comment, and loaded with a NULL one + */ + public function testNoComment(): void + { + $activity = new \GaletteEvents\Activity($this->zdb, $this->login); + $this->assertTrue($activity->check(['name' => 'Dinner', 'active' => '1'])); + $this->assertTrue($activity->store()); + + $update = $this->zdb->update(EVENTS_PREFIX . \GaletteEvents\Activity::TABLE); + $update->set(['comment' => null])->where([\GaletteEvents\Activity::PK => $activity->getId()]); + $this->zdb->execute($update); + + $activity = new \GaletteEvents\Activity($this->zdb, $this->login, (int)$activity->getId()); + $this->assertSame('Dinner', $activity->getName()); + $this->assertSame('', $activity->getComment()); + } } diff --git a/tests/GaletteEvents/tests/units/Booking.php b/tests/GaletteEvents/tests/units/Booking.php new file mode 100644 index 00000000..38c01a5d --- /dev/null +++ b/tests/GaletteEvents/tests/units/Booking.php @@ -0,0 +1,55 @@ + + */ +class Booking extends GaletteTestCase +{ + use EventsFixtures; + + protected int $seed = 20260926151512; + + /** + * Cleanup after each test method + */ + public function tearDown(): void + { + $this->login->logout(); + $this->cleanEvents(); + parent::tearDown(); + } + + /** + * Optional values may be NULL in database + */ + public function testLoadNullValues(): void + { + $member_one = $this->getMemberOne(); + $id = $this->insertBooking( + $this->insertEvent('Event'), + $member_one->id, + ['comment' => null, 'payment_amount' => null, 'number_people' => null, 'creation_date' => '2026-09-01'] + ); + + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $this->assertSame('', $booking->getComment()); + $this->assertNull($booking->getAmount()); + $this->assertSame(1, $booking->getNumberPeople()); + $this->assertSame('2026-09-01', $booking->getCreationDate(false)); + } +} diff --git a/tests/GaletteEvents/tests/units/Event.php b/tests/GaletteEvents/tests/units/Event.php index 7027c543..d242862c 100644 --- a/tests/GaletteEvents/tests/units/Event.php +++ b/tests/GaletteEvents/tests/units/Event.php @@ -83,4 +83,37 @@ public function testManagerCreatesEvent(): void $event = new \GaletteEvents\Event($this->zdb, $this->login, (int)$event->getId()); $this->assertSame($managed->getId(), $event->getGroup()); } + + /** + * Events are stored with mandatory values only + */ + public function testStoreMandatoryValuesOnly(): void + { + $this->logSuperAdmin(); + $event = new \GaletteEvents\Event($this->zdb, $this->login); + $this->assertTrue($event->check([ + 'name' => 'Event', + 'town' => 'Lille', + 'begin_date' => date('Y-m-d', strtotime('+10 days')), + ])); + $this->assertTrue($event->store()); + + $event = new \GaletteEvents\Event($this->zdb, $this->login, (int)$event->getId()); + $this->assertSame('Event', $event->getName()); + $this->assertSame('', $event->getAddress()); + $this->assertNull($event->getGroup()); + } + + /** + * Optional values may be NULL in database + */ + public function testLoadNullValues(): void + { + $id = $this->insertEvent('Event', ['comment' => null, 'country' => null]); + + $event = new \GaletteEvents\Event($this->zdb, $this->login, $id); + $this->assertSame('', $event->getComment()); + $this->assertNull($event->getGroup()); + $this->assertSame('', $event->getColor()); + } } From 8636031ad4430f68fa00be1e3d1be40fa182f95f Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 10:52:56 +0200 Subject: [PATCH 03/20] Store event activities against the stored ones, active ones only --- lib/GaletteEvents/Booking.php | 1 + lib/GaletteEvents/Event.php | 193 +++++++++----------- tests/EventsFixtures.php | 36 ++++ tests/GaletteEvents/tests/units/Booking.php | 40 ++++ tests/GaletteEvents/tests/units/Event.php | 78 ++++++++ 5 files changed, 239 insertions(+), 109 deletions(-) diff --git a/lib/GaletteEvents/Booking.php b/lib/GaletteEvents/Booking.php index b02b9ea3..c9770e6a 100644 --- a/lib/GaletteEvents/Booking.php +++ b/lib/GaletteEvents/Booking.php @@ -726,6 +726,7 @@ public function has(int $activity): bool */ public function loadActivities(): void { + $this->activities = []; $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesbookings', 'acb'); $select->where([self::PK => $this->id]); $results = $this->zdb->execute($select); diff --git a/lib/GaletteEvents/Event.php b/lib/GaletteEvents/Event.php index 545f545e..97bb6a64 100644 --- a/lib/GaletteEvents/Event.php +++ b/lib/GaletteEvents/Event.php @@ -53,8 +53,6 @@ class Event /** @var array> */ private array $activities = []; - /** @var array> */ - private array $activities_removed = []; /** * Default constructor @@ -280,41 +278,41 @@ public function check(array $values): bool|array isset($values['add_activity']) && !empty($values['attach_activity']) ) { - $this->activities[$values['attach_activity']] = [ - 'activity' => new Activity( - $this->zdb, - $this->login, - (int)$values['attach_activity'] - ), - 'status' => Activity::YES - ]; + $activity = new Activity($this->zdb, $this->login, (int)$values['attach_activity']); + if ($activity->getId() !== null && $activity->isActive()) { + $this->activities[$activity->getId()] = [ + 'activity' => $activity, + 'status' => Activity::YES + ]; + } } + $detached = null; if ( isset($values['remove_activity']) && !empty($values['detach_activity']) ) { - unset($this->activities[$values['detach_activity']]); - $this->activities_removed[$values['detach_activity']] = [ - self::PK => $this->id, - Activity::PK => $values['detach_activity'] - ]; - - if (count($values['activities_ids'])) { - unset($values['activities_ids'][array_search($values['detach_activity'], $values['activities_ids'])]); - } + $detached = (int)$values['detach_activity']; + unset($this->activities[$detached]); } if (isset($values['activities_ids'])) { foreach ($values['activities_ids'] as $row => $activity_id) { + $activity_id = (int)$activity_id; + $status = (int)($values['activities_status'][$row] ?? Activity::YES); + if ($activity_id === $detached || !in_array($status, [Activity::NO, Activity::YES, Activity::REQUIRED], true)) { + continue; + } if (isset($this->activities[$activity_id])) { - $this->activities[$activity_id]['status'] = $values['activities_status'][$row]; + $this->activities[$activity_id]['status'] = $status; } else { - $activity = new Activity($this->zdb, $this->login, (int)$activity_id); - $this->activities[$activity_id] = [ - 'activity' => $activity, - 'status' => $values['activities_status'][$row] - ]; + $activity = new Activity($this->zdb, $this->login, $activity_id); + if ($activity->getId() !== null && $activity->isActive()) { + $this->activities[$activity_id] = [ + 'activity' => $activity, + 'status' => $status + ]; + } } } } @@ -414,102 +412,77 @@ public function store(): bool } } - $void = []; - $update = []; - $insert = []; - $key_values = []; - $delete = $this->activities_removed; + $this->storeActivities(); - foreach ($this->activities as $aid => $data) { - $activity = $data['activity']; - $status = $data['status']; - $key_values = [ - self::PK => $this->id, - $activity::PK => $activity->getId() - ]; + $this->zdb->connection->commit(); + return true; + } catch (\Exception $e) { + $this->zdb->connection->rollBack(); + Analog::log( + 'Something went wrong :\'( | ' . $e->getMessage() . "\n" + . $e->getTraceAsString(), + Analog::ERROR + ); + throw $e; + } + } - $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesevents', 'ace'); - $select->where($key_values); - $results = $this->zdb->execute($select); - - foreach ($results as $result) { - $values = [ - Activity::PK => $result[Activity::PK], - self::PK => $this->id, - 'status' => $status - ]; - if (!isset($this->activities[$result[Activity::PK]])) { - $delete[$result[Activity::PK]] = $values; - } elseif ($result['status'] != $this->activities[$result[Activity::PK]]['status']) { - $update[$result[Activity::PK]] = $values; - } else { - $void[$result[Activity::PK]] = $values; - } - } + /** + * Store activities linked to the event, compared to the stored ones + */ + private function storeActivities(): void + { + $table = EVENTS_PREFIX . 'activitiesevents'; - if (!isset($void[$aid]) && !isset($update[$aid]) && !isset($delete[$aid])) { - $insert[$aid] = [ - Activity::PK => $aid, - self::PK => $this->id, - 'status' => $status - ]; - } - } + $stored = []; + $select = $this->zdb->select($table); + $select->where([self::PK => $this->id]); + foreach ($this->zdb->execute($select) as $row) { + $stored[(int)$row[Activity::PK]] = (int)$row['status']; + } - if (count($delete)) { - $stmt = $this->zdb->delete(EVENTS_PREFIX . 'activitiesevents'); - $count = 0; - foreach ($delete as $values) { - $stmt->where($values); - $this->zdb->execute($stmt); - ++$count; - } - Analog::log( - sprintf('%1$s activities removed', $count), - Analog::INFO - ); + $counts = ['added' => 0, 'updated' => 0, 'removed' => 0]; + foreach ($this->activities as $aid => $data) { + $status = (int)$data['status']; + if (!isset($stored[$aid])) { + $insert = $this->zdb->insert($table); + $insert->values([ + self::PK => $this->id, + Activity::PK => $aid, + 'status' => $status + ]); + $this->zdb->execute($insert); + ++$counts['added']; + } elseif ($stored[$aid] !== $status) { + $update = $this->zdb->update($table); + $update->set(['status' => $status])->where([ + self::PK => $this->id, + Activity::PK => $aid + ]); + $this->zdb->execute($update); + ++$counts['updated']; } + } - if (count($update)) { - $stmt = $this->zdb->update(EVENTS_PREFIX . 'activitiesevents'); - $count = 0; - foreach ($update as $values) { - $stmt - ->set($values) - ->where($key_values); - $this->zdb->execute($stmt); - ++$count; - } - Analog::log( - sprintf('%1$s activities updated', $count), - Analog::INFO - ); + foreach (array_keys($stored) as $aid) { + if (!isset($this->activities[$aid])) { + $delete = $this->zdb->delete($table); + $delete->where([ + self::PK => $this->id, + Activity::PK => $aid + ]); + $this->zdb->execute($delete); + ++$counts['removed']; } + } - if (count($insert)) { - $stmt = $this->zdb->insert(EVENTS_PREFIX . 'activitiesevents'); - $count = 0; - foreach ($insert as $values) { - $stmt->values(array_merge($key_values, $values)); - $this->zdb->execute($stmt); - ++$count; - } + foreach ($counts as $action => $count) { + if ($count > 0) { Analog::log( - sprintf('%1$s activities added', $count), + sprintf('%1$s activities %2$s', $count, $action), Analog::INFO ); } - - $this->zdb->connection->commit(); - return true; - } catch (\Exception $e) { - $this->zdb->connection->rollBack(); - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); - throw $e; } } @@ -694,6 +667,7 @@ protected function getTableName(): string public function availableActivities(): array { $select = $this->zdb->select(EVENTS_PREFIX . Activity::TABLE, 'ac'); + $select->where->equalTo('is_active', true); $results = $this->zdb->execute($select); $activities = []; @@ -711,6 +685,7 @@ public function availableActivities(): array */ public function loadActivities(): void { + $this->activities = []; $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesevents', 'ace'); $select->where([self::PK => $this->id]); $results = $this->zdb->execute($select); diff --git a/tests/EventsFixtures.php b/tests/EventsFixtures.php index 0afe2fd9..ca839145 100644 --- a/tests/EventsFixtures.php +++ b/tests/EventsFixtures.php @@ -189,6 +189,42 @@ protected function countBookings(int $event): int return $this->zdb->execute($select)->count(); } + /** + * Get activities linked to an event, and their status + * + * @param int $event Event ID + * + * @return array + */ + protected function getEventActivities(int $event): array + { + $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesevents'); + $select->where([Event::PK => $event])->order(Activity::PK); + $activities = []; + foreach ($this->zdb->execute($select) as $row) { + $activities[(int)$row[Activity::PK]] = (int)$row['status']; + } + return $activities; + } + + /** + * Get activities of a booking, and whether they are checked + * + * @param int $booking Booking ID + * + * @return array + */ + protected function getBookingActivities(int $booking): array + { + $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesbookings'); + $select->where([Booking::PK => $booking])->order(Activity::PK); + $activities = []; + foreach ($this->zdb->execute($select) as $row) { + $activities[(int)$row[Activity::PK]] = (bool)$row['checked']; + } + return $activities; + } + /** * Get an ID from a name * diff --git a/tests/GaletteEvents/tests/units/Booking.php b/tests/GaletteEvents/tests/units/Booking.php index 38c01a5d..eb450aaf 100644 --- a/tests/GaletteEvents/tests/units/Booking.php +++ b/tests/GaletteEvents/tests/units/Booking.php @@ -52,4 +52,44 @@ public function testLoadNullValues(): void $this->assertSame(1, $booking->getNumberPeople()); $this->assertSame('2026-09-01', $booking->getCreationDate(false)); } + + /** + * Activities chosen on a booking are stored and changed + */ + public function testActivitiesSync(): void + { + $this->logSuperAdmin(); + $member_one = $this->getMemberOne(); + $event = $this->insertEvent('Event'); + $dinner = $this->insertActivity('Dinner'); + $lodging = $this->insertActivity('Lodging'); + $this->linkActivity($event, $dinner); + $this->linkActivity($event, $lodging); + + $values = [ + 'event' => (string)$event, + 'member' => (string)$member_one->id, + 'booking_date' => date('Y-m-d'), + 'number_people' => '1', + ]; + $booking = new \GaletteEvents\Booking($this->zdb, $this->login); + $this->assertTrue($booking->check($values + ['activities' => [(string)$dinner]])); + $this->assertTrue($booking->store()); + $id = (int)$booking->getId(); + $this->assertSame([$dinner => true, $lodging => false], $this->getBookingActivities($id)); + + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $this->assertTrue($booking->check($values + ['activities' => [(string)$lodging]])); + $this->assertTrue($booking->store()); + $this->assertSame([$dinner => false, $lodging => true], $this->getBookingActivities($id)); + + //activity removed from event is removed from booking + $delete = $this->zdb->delete(EVENTS_PREFIX . 'activitiesevents'); + $delete->where([\GaletteEvents\Event::PK => $event, \GaletteEvents\Activity::PK => $dinner]); + $this->zdb->execute($delete); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $this->assertTrue($booking->check($values + ['activities' => [(string)$lodging]])); + $this->assertTrue($booking->store()); + $this->assertSame([$lodging => true], $this->getBookingActivities($id)); + } } diff --git a/tests/GaletteEvents/tests/units/Event.php b/tests/GaletteEvents/tests/units/Event.php index d242862c..da832020 100644 --- a/tests/GaletteEvents/tests/units/Event.php +++ b/tests/GaletteEvents/tests/units/Event.php @@ -116,4 +116,82 @@ public function testLoadNullValues(): void $this->assertNull($event->getGroup()); $this->assertSame('', $event->getColor()); } + + /** + * Activities linked to an event are added, changed and removed + */ + public function testActivitiesSync(): void + { + $this->logSuperAdmin(); + $dinner = $this->insertActivity('Dinner'); + $lodging = $this->insertActivity('Lodging'); + $visit = $this->insertActivity('Visit'); + $ids = array_map('strval', [$dinner, $lodging, $visit]); + + $event = new \GaletteEvents\Event($this->zdb, $this->login); + $this->assertTrue($event->check($this->getFormValues([ + 'activities_ids' => $ids, + 'activities_status' => ['1', '1', '2'], + ]))); + $this->assertTrue($event->store()); + $id = (int)$event->getId(); + $this->assertSame([$dinner => 1, $lodging => 1, $visit => 2], $this->getEventActivities($id)); + + //change status of activities that are not the last one + $event = new \GaletteEvents\Event($this->zdb, $this->login, $id); + $this->assertTrue($event->check($this->getFormValues([ + 'activities_ids' => $ids, + 'activities_status' => ['2', '0', '2'], + ]))); + $this->assertTrue($event->store()); + $this->assertSame([$dinner => 2, $lodging => 0, $visit => 2], $this->getEventActivities($id)); + + //remove two activities before storing + $event = new \GaletteEvents\Event($this->zdb, $this->login, $id); + $this->assertTrue($event->check($this->getFormValues([ + 'remove_activity' => '1', + 'detach_activity' => (string)$dinner, + 'activities_ids' => $ids, + 'activities_status' => ['2', '0', '2'], + ]))); + $this->assertTrue($event->check($this->getFormValues([ + 'remove_activity' => '1', + 'detach_activity' => (string)$lodging, + 'activities_ids' => [(string)$lodging, (string)$visit], + 'activities_status' => ['0', '2'], + ]))); + $this->assertTrue($event->store()); + $this->assertSame([$visit => 2], $this->getEventActivities($id)); + + //reloading does not keep activities of the previous event + $other = (int)$this->insertEvent('Other event'); + $this->assertTrue($event->load($other)); + $this->assertSame([], $event->getActivities()); + } + + /** + * Only active activities can be attached to events + */ + public function testInactiveActivities(): void + { + $this->logSuperAdmin(); + $dinner = $this->insertActivity('Dinner'); + $lodging = $this->insertActivity('Lodging'); + $update = $this->zdb->update(EVENTS_PREFIX . \GaletteEvents\Activity::TABLE); + $update->set(['is_active' => $this->zdb->isPostgres() ? 'false' : 0]) + ->where([\GaletteEvents\Activity::PK => $lodging]); + $this->zdb->execute($update); + + $event = new \GaletteEvents\Event($this->zdb, $this->login); + $this->assertSame( + [$dinner], + array_map(fn($row): int => (int)$row[\GaletteEvents\Activity::PK], $event->availableActivities()) + ); + + $this->assertTrue($event->check($this->getFormValues([ + 'add_activity' => '1', + 'attach_activity' => (string)$lodging, + ]))); + $this->assertSame([], $event->getActivities()); + } } From 01400cb34ef1c25e40b04de164ed9b77bcfd0791 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 10:56:48 +0200 Subject: [PATCH 04/20] Store detached activities with the event, as attached ones The form posts every linked activity, the posted list now replaces the loaded one. Refusals of the event form get a status and a message. --- .../Controllers/Crud/EventsController.php | 93 ++++++------ lib/GaletteEvents/Event.php | 71 ++++++---- .../Crud/tests/units/EventsController.php | 133 ++++++++++++++++++ 3 files changed, 215 insertions(+), 82 deletions(-) diff --git a/lib/GaletteEvents/Controllers/Crud/EventsController.php b/lib/GaletteEvents/Controllers/Crud/EventsController.php index c4baceb7..f739ca67 100644 --- a/lib/GaletteEvents/Controllers/Crud/EventsController.php +++ b/lib/GaletteEvents/Controllers/Crud/EventsController.php @@ -241,16 +241,7 @@ public function edit(Request $request, Response $response, ?int $id = null, stri //check if logged-in user can edit event if (!$can) { - $redirect_url = $this->routeparser->urlFor('events_events'); - Analog::log( - sprintf( - 'Member %1$s cannot edit event %2$s', - $this->login->id, - $event->getId() - ) - ); - return $response - ->withHeader('Location', $redirect_url); + return $this->redirectForbidden($response, $event); } // template variable declaration @@ -300,16 +291,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st //check if logged-in user can edit event if (!$can) { - $redirect_url = $this->routeparser->urlFor('events_events'); - Analog::log( - sprintf( - 'Member %1$s cannot edit event %2$s', - $this->login->id, - $event->getId() - ) - ); - return $response - ->withHeader('Location', $redirect_url); + return $this->redirectForbidden($response, $event); } $success_detected = []; @@ -319,46 +301,35 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st // Validation $valid = $event->check($post); - if ($valid !== true) { - $error_detected = array_merge($error_detected, $valid); - } - if (count($error_detected) == 0) { - //all goes well, we can proceed - $new = false; - if ($event->getId() == '') { - $new = true; - } - - if (isset($post['add_activity']) || isset($post['remove_activity'])) { - $this->session->event = $event; - if (isset($post['add_activity'])) { + if (isset($post['add_activity']) || isset($post['remove_activity'])) { + //activities are changed on a form that may not be complete yet, event is stored later + $goto_list = false; + if (isset($post['add_activity'])) { + if (isset($event->getActivities()[(int)($post['attach_activity'] ?? 0)])) { $success_detected[] = _T("Activity has been attached to event.", "events"); $warning_detected[] = _T('Do not forget to store the event', 'events'); } else { - $success_detected[] = _T("Activity has been detached from event.", "events"); + $error_detected[] = _T("Please choose an activity to add", "events"); } - $goto_list = false; + } else { + $success_detected[] = _T("Activity has been detached from event.", "events"); + $warning_detected[] = _T('Do not forget to store the event', 'events'); } - if (isset($post['save']) || isset($post['remove_activity'])) { - $store = $event->store(); - if ($store === true) { - //member has been stored :) - if ($new) { - $success_detected[] = _T("New event has been successfully added.", "events"); - } else { - $success_detected[] = _T("Event has been modified.", "events"); - } + } elseif ($valid !== true) { + $error_detected = array_merge($error_detected, $valid); + } elseif (isset($post['save'])) { + $new = $event->getId() === null; + if ($event->store() === true) { + if ($new) { + $success_detected[] = _T("New event has been successfully added.", "events"); } else { - //something went wrong :'( - $error_detected[] = _T("An error occurred while storing the event.", "events"); + $success_detected[] = _T("Event has been modified.", "events"); } + } else { + $error_detected[] = _T("An error occurred while storing the event.", "events"); } - } - - if (!isset($post['save'])) { - $this->session->event = $event; - $error_detected = []; + } else { $goto_list = false; } @@ -409,6 +380,26 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st ->withHeader('Location', $redirect_url); } + /** + * Redirect when current logged-in user cannot edit an event + * + * @param Event $event Event + */ + private function redirectForbidden(Response $response, Event $event): Response + { + Analog::log( + 'Logged in member ' . $this->login->login + . ' has tried to edit event #' . $event->getId() + . ' without the right to do so.', + Analog::WARNING + ); + return $this->redirectWithErrors( + response: $response, + errors: [_T("You do not have permission for requested URL.")], + redirect_url: $this->routeparser->urlFor('events_events') + ); + } + // /CRUD - Update // CRUD - Delete diff --git a/lib/GaletteEvents/Event.php b/lib/GaletteEvents/Event.php index 97bb6a64..ec23ea2d 100644 --- a/lib/GaletteEvents/Event.php +++ b/lib/GaletteEvents/Event.php @@ -274,48 +274,46 @@ public function check(array $values): bool|array } } + //the form posts every linked activity: posted list replaces the current one + $detached = null; if ( - isset($values['add_activity']) - && !empty($values['attach_activity']) + isset($values['remove_activity']) + && !empty($values['detach_activity']) ) { - $activity = new Activity($this->zdb, $this->login, (int)$values['attach_activity']); - if ($activity->getId() !== null && $activity->isActive()) { - $this->activities[$activity->getId()] = [ + $detached = (int)$values['detach_activity']; + } + + $activities = []; + foreach ($values['activities_ids'] ?? [] as $row => $activity_id) { + $activity_id = (int)$activity_id; + $status = (int)($values['activities_status'][$row] ?? Activity::YES); + if ($activity_id === $detached || !in_array($status, [Activity::NO, Activity::YES, Activity::REQUIRED], true)) { + continue; + } + //already linked activities stay, even if they have been deactivated since + $activity = $this->activities[$activity_id]['activity'] ?? $this->getActiveActivity($activity_id); + if ($activity !== null) { + $activities[$activity_id] = [ 'activity' => $activity, - 'status' => Activity::YES + 'status' => $status ]; } } - $detached = null; if ( - isset($values['remove_activity']) - && !empty($values['detach_activity']) + isset($values['add_activity']) + && !empty($values['attach_activity']) + && !isset($activities[(int)$values['attach_activity']]) ) { - $detached = (int)$values['detach_activity']; - unset($this->activities[$detached]); - } - - if (isset($values['activities_ids'])) { - foreach ($values['activities_ids'] as $row => $activity_id) { - $activity_id = (int)$activity_id; - $status = (int)($values['activities_status'][$row] ?? Activity::YES); - if ($activity_id === $detached || !in_array($status, [Activity::NO, Activity::YES, Activity::REQUIRED], true)) { - continue; - } - if (isset($this->activities[$activity_id])) { - $this->activities[$activity_id]['status'] = $status; - } else { - $activity = new Activity($this->zdb, $this->login, $activity_id); - if ($activity->getId() !== null && $activity->isActive()) { - $this->activities[$activity_id] = [ - 'activity' => $activity, - 'status' => $status - ]; - } - } + $activity = $this->getActiveActivity((int)$values['attach_activity']); + if ($activity !== null) { + $activities[(int)$values['attach_activity']] = [ + 'activity' => $activity, + 'status' => Activity::YES + ]; } } + $this->activities = $activities; if (isset($values['open'])) { $this->open = true; @@ -427,6 +425,17 @@ public function store(): bool } } + /** + * Get an activity that can be attached to the event + * + * @param int $id Activity ID + */ + private function getActiveActivity(int $id): ?Activity + { + $activity = new Activity($this->zdb, $this->login, $id); + return $activity->getId() !== null && $activity->isActive() ? $activity : null; + } + /** * Store activities linked to the event, compared to the stored ones */ diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php index 9266ae23..772d9424 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php @@ -10,6 +10,7 @@ namespace GaletteEvents\Controllers\Crud\tests\units; +use Analog\Analog; use Galette\Tests\GaletteRoutingTestCase; use GaletteEvents\tests\EventsFixtures; @@ -35,6 +36,46 @@ public function tearDown(): void parent::tearDown(); } + /** + * Post an event + * + * @param ?int $id Event ID, null to add a new one + * @param array $data Posted data + */ + private function postEvent(?int $id, array $data): \Psr\Http\Message\ResponseInterface + { + if ($id === null) { + $request = $this->createRequest('events_storeevent_add', [], 'POST'); + } else { + $request = $this->createRequest('events_storeevent_edit', ['id' => (string)$id], 'POST'); + $data += ['id' => (string)$id]; + } + return $this->app->handle($request->withParsedBody($data)); + } + + /** + * Get values posted from event form + * + * @param array $values Values to override + * + * @return array + */ + private function getFormValues(array $values = []): array + { + return $values + [ + 'name' => 'Event', + 'address' => '', + 'zip' => '', + 'town' => 'Lille', + 'country' => '', + 'comment' => '', + 'color' => '', + 'begin_date' => date('Y-m-d', strtotime('+10 days')), + 'end_date' => date('Y-m-d', strtotime('+11 days')), + 'open' => '1', + ]; + } + /** * Calendar event description is HTML: values typed by users must be escaped */ @@ -76,4 +117,96 @@ public function testCalendarDescriptionIsEscaped(): void $this->assertSame('Party name', $events[0]['name']); $this->assertSame('Party name', $events[0]['title']); } + + /** + * Detaching an activity waits for the event to be stored, as attaching does + */ + public function testDetachActivityWaitsForStore(): void + { + $this->logSuperAdmin(); + $dinner = $this->insertActivity('Dinner'); + $lodging = $this->insertActivity('Lodging'); + $event = $this->insertEvent('Event'); + $this->linkActivity($event, $dinner); + $this->linkActivity($event, $lodging); + + $test_response = $this->postEvent($event, $this->getFormValues([ + 'remove_activity' => '1', + 'detach_activity' => (string)$dinner, + 'activities_ids' => [(string)$dinner, (string)$lodging], + 'activities_status' => ['1', '1'], + ])); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('events_event_edit', ['id' => (string)$event])]], + $test_response->getHeaders() + ); + $this->expectFlashData([ + 'warning_detected' => ['Do not forget to store the event'], + 'success_detected' => ['Activity has been detached from event.'], + ]); + $this->assertSame([$dinner => 1, $lodging => 1], $this->getEventActivities($event)); + + $this->postEvent($event, $this->getFormValues([ + 'save' => '1', + 'activities_ids' => [(string)$lodging], + 'activities_status' => ['1'], + ])); + $this->expectFlashData(['success_detected' => ['Event has been modified.']]); + $this->assertSame([$lodging => 1], $this->getEventActivities($event)); + } + + /** + * Activities are attached to an event whose form is not complete yet + */ + public function testAttachActivityOnIncompleteEvent(): void + { + $this->logSuperAdmin(); + $dinner = $this->insertActivity('Dinner'); + + $test_response = $this->postEvent(null, $this->getFormValues([ + 'name' => '', + 'add_activity' => '1', + 'attach_activity' => (string)$dinner, + ])); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('events_event_add')]], + $test_response->getHeaders() + ); + $this->expectFlashData([ + 'warning_detected' => ['Do not forget to store the event'], + 'success_detected' => ['Activity has been attached to event.'], + ]); + $this->expectLogEntry(Analog::ERROR, 'Some errors has been threw attempting to edit/store an event'); + $this->assertSame([$dinner], array_keys($this->session->event->getActivities())); + + //an unknown activity is not attached + $this->postEvent(null, $this->getFormValues([ + 'add_activity' => '1', + 'attach_activity' => (string)($dinner + 1000), + ])); + $this->expectFlashData(['error_detected' => ['Please choose an activity to add']]); + } + + /** + * Group managers cannot store events of groups they do not manage + */ + public function testManagerCannotStoreOtherEvent(): void + { + $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $this->createGroup('Managed group', [$member_two]); + $event = $this->insertEvent('Public event'); + $this->logMember($this->dataAdherentTwo()); + + $test_response = $this->postEvent($event, $this->getFormValues(['name' => 'Changed', 'save' => '1'])); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('events_events')]], + $test_response->getHeaders() + ); + $this->assertSame(301, $test_response->getStatusCode()); + //message comes in the language of the logged-in member + $this->expectFlashData(['error_detected' => [_T('You do not have permission for requested URL.')]]); + $this->expectLogEntry(Analog::WARNING, 'has tried to edit event #' . $event); + $this->expectNoLogEntry(); + } } From 7f8c209da2acfa5e5d3d93526913251a675ebdb8 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 10:57:22 +0200 Subject: [PATCH 05/20] Do not close past events when they are stored --- lib/GaletteEvents/Event.php | 9 ++++++++ templates/default/event.html.twig | 4 ++-- .../Crud/tests/units/EventsController.php | 21 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/GaletteEvents/Event.php b/lib/GaletteEvents/Event.php index ec23ea2d..2cf3260b 100644 --- a/lib/GaletteEvents/Event.php +++ b/lib/GaletteEvents/Event.php @@ -630,6 +630,15 @@ public function hasActivity(int $activity): bool return $this->activities[$activity]['status'] != Activity::NO; } + /** + * Has event been flagged as open? + * Unlike isOpen(), whatever its dates + */ + public function isOpenFlag(): bool + { + return $this->open; + } + /** * Is event open? * Will return false once the begin date has been exceeded diff --git a/templates/default/event.html.twig b/templates/default/event.html.twig index e0ae0962..f790e8ae 100644 --- a/templates/default/event.html.twig +++ b/templates/default/event.html.twig @@ -16,9 +16,9 @@
{% include "components/forms/checkbox.html.twig" with { id: 'open', - value: event.isOpen(), + value: 1, label: _T("Is open", "events"), - checked: event.IsOpen() + checked: event.isOpenFlag() } %} {% include "components/forms/text.html.twig" with { diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php index 772d9424..5a941609 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php @@ -209,4 +209,25 @@ public function testManagerCannotStoreOtherEvent(): void $this->expectLogEntry(Analog::WARNING, 'has tried to edit event #' . $event); $this->expectNoLogEntry(); } + + /** + * Past events stay open in their form, so storing them does not close them + */ + public function testPastEventFormKeepsOpenFlag(): void + { + $this->logSuperAdmin(); + $event = $this->insertEvent( + 'Past event', + ['begin_date' => date('Y-m-d', strtotime('-2 days')), 'end_date' => date('Y-m-d', strtotime('-1 day'))] + ); + + $test_response = $this->app->handle($this->createRequest('events_event_edit', ['id' => (string)$event])); + $this->assertSame(200, $test_response->getStatusCode()); + $this->assertSame( + 1, + preg_match('@]*id="open"[^>]*>@s', (string)$test_response->getBody(), $matches) + ); + $this->assertStringContainsString(' checked', $matches[0]); + $this->assertStringContainsString('value="1"', $matches[0]); + } } From 043372b99d9566b950c65ec9f528827c397a190c Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 10:58:10 +0200 Subject: [PATCH 06/20] Accept cleared, zero and comma separated booking amounts --- lib/GaletteEvents/Booking.php | 14 ++++++-- tests/GaletteEvents/tests/units/Booking.php | 40 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/lib/GaletteEvents/Booking.php b/lib/GaletteEvents/Booking.php index c9770e6a..126e48b0 100644 --- a/lib/GaletteEvents/Booking.php +++ b/lib/GaletteEvents/Booking.php @@ -220,11 +220,19 @@ public function check(array $values): array|bool $this->paid = false; } - if (isset($values['amount']) && !empty($values['amount'])) { - $this->amount = (float)$values['amount']; + if (isset($values['amount'])) { + //accept comma as decimal separator + $amount = strtr(trim((string)$values['amount']), ',', '.'); + if ($amount === '') { + $this->amount = null; + } elseif (is_numeric($amount)) { + $this->amount = (float)$amount; + } else { + $this->errors[] = _T('Amount must be a number.', 'events'); + } } - if ($this->paid && !$this->amount) { + if ($this->paid && $this->amount === null) { $this->errors[] = _T('Please specify amount if booking has been paid ;)', 'events'); } diff --git a/tests/GaletteEvents/tests/units/Booking.php b/tests/GaletteEvents/tests/units/Booking.php index eb450aaf..f2f7135c 100644 --- a/tests/GaletteEvents/tests/units/Booking.php +++ b/tests/GaletteEvents/tests/units/Booking.php @@ -92,4 +92,44 @@ public function testActivitiesSync(): void $this->assertTrue($booking->store()); $this->assertSame([$lodging => true], $this->getBookingActivities($id)); } + + /** + * Amounts are cleared, use comma as decimal separator, and may be zero once paid + */ + public function testAmount(): void + { + $this->logSuperAdmin(); + $member_one = $this->getMemberOne(); + $values = [ + 'event' => (string)$this->insertEvent('Event'), + 'member' => (string)$member_one->id, + 'booking_date' => date('Y-m-d'), + 'number_people' => '1', + ]; + + $booking = new \GaletteEvents\Booking($this->zdb, $this->login); + $this->assertTrue($booking->check($values + ['amount' => '12,50'])); + $this->assertSame(12.5, $booking->getAmount()); + $this->assertTrue($booking->store()); + $id = (int)$booking->getId(); + + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $this->assertTrue($booking->check($values + ['amount' => ''])); + $this->assertTrue($booking->store()); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $this->assertNull($booking->getAmount()); + + $this->assertTrue($booking->check($values + ['amount' => '0', 'paid' => '1'])); + $this->assertSame(0.0, $booking->getAmount()); + + $this->assertSame( + [_T('Please specify amount if booking has been paid ;)', 'events')], + $booking->check($values + ['amount' => '', 'paid' => '1']) + ); + $this->assertSame( + [_T('Amount must be a number.', 'events')], + $booking->check($values + ['amount' => 'ten']) + ); + $this->expectLogEntry(\Analog\Analog::ERROR, 'Some errors has been threw attempting to edit/store a booking'); + } } From efdeb5e38f46f8f34457e0af722ee30c8a78b1ab Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 10:58:35 +0200 Subject: [PATCH 07/20] Check event edition rights for the given login --- lib/GaletteEvents/Event.php | 7 +------ tests/GaletteEvents/tests/units/Event.php | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/GaletteEvents/Event.php b/lib/GaletteEvents/Event.php index 2cf3260b..ad660a71 100644 --- a/lib/GaletteEvents/Event.php +++ b/lib/GaletteEvents/Event.php @@ -784,12 +784,7 @@ public function canEdit(Login $login): bool return false; } - if ($this->group) { - $groups = $this->login->getManagedGroups(); - return (in_array($this->group, $groups)); - } - - return false; + return $this->group !== null && $login->isGroupManager($this->group); } /** diff --git a/tests/GaletteEvents/tests/units/Event.php b/tests/GaletteEvents/tests/units/Event.php index da832020..e0ad3206 100644 --- a/tests/GaletteEvents/tests/units/Event.php +++ b/tests/GaletteEvents/tests/units/Event.php @@ -194,4 +194,26 @@ public function testInactiveActivities(): void ]))); $this->assertSame([], $event->getActivities()); } + + /** + * Edition rights are checked for given login, not for the one the event has been loaded with + */ + public function testCanEditChecksGivenLogin(): void + { + $member_one = $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $managed = $this->createGroup('Managed group', [$member_two]); + $other = $this->createGroup('Other group', [$member_one]); + $managed_event = $this->insertEvent('Managed event', ['id_group' => $managed->getId()]); + $other_event = $this->insertEvent('Other event', ['id_group' => $other->getId()]); + + //events are loaded with member one logged in + $this->logMember($this->dataAdherentOne()); + $manager = new \Galette\Core\Login($this->zdb, $this->i18n); + $this->assertTrue($manager->login($this->dataAdherentTwo()['login_adh'], $this->dataAdherentTwo()['mdp_adh'])); + + $this->assertTrue((new \GaletteEvents\Event($this->zdb, $this->login, $managed_event))->canEdit($manager)); + $this->assertFalse((new \GaletteEvents\Event($this->zdb, $this->login, $other_event))->canEdit($manager)); + $this->assertFalse((new \GaletteEvents\Event($this->zdb, $this->login, $this->insertEvent('Public event')))->canEdit($manager)); + } } From 5482e57c5ea97937c66f3d2635094f97dccfdf20 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 11:00:52 +0200 Subject: [PATCH 08/20] Keep bookings own values in bookings list --- lib/GaletteEvents/Repository/Bookings.php | 14 ++++++++------ .../Repository/tests/units/Bookings.php | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/GaletteEvents/Repository/Bookings.php b/lib/GaletteEvents/Repository/Bookings.php index 753fc435..1dfebb94 100644 --- a/lib/GaletteEvents/Repository/Bookings.php +++ b/lib/GaletteEvents/Repository/Bookings.php @@ -121,17 +121,19 @@ private function buildSelect(?array $fields, bool $count = false): Select $select = $this->zdb->select(EVENTS_PREFIX . Booking::TABLE, 'b'); $select->columns($fieldsList); + //joined tables are used for filtering and ordering only, their columns would override bookings ones $select->join( ['a' => PREFIX_DB . Adherent::TABLE], - 'b.' . Adherent::PK . '= a.' . Adherent::PK + 'b.' . Adherent::PK . '= a.' . Adherent::PK, + [] ); $select->join( ['e' => PREFIX_DB . EVENTS_PREFIX . Event::TABLE], - 'b.' . Event::PK . '= e.' . Event::PK + 'b.' . Event::PK . '= e.' . Event::PK, + [] ); $this->buildWhereClause($select); - $select->order(self::buildOrderClause()); $this->calculateSum($select); @@ -142,7 +144,7 @@ private function buildSelect(?array $fields, bool $count = false): Select return $select; } catch (\Exception $e) { Analog::log( - 'Cannot build SELECT clause for contributions | ' . $e->getMessage(), + 'Cannot build SELECT clause for bookings | ' . $e->getMessage(), Analog::WARNING ); throw $e; @@ -150,7 +152,7 @@ private function buildSelect(?array $fields, bool $count = false): Select } /** - * Calculate sum of all selected contributions + * Calculate sum of all selected bookings * * @param Select $select Original select */ @@ -323,7 +325,7 @@ private function buildOrderClause(?array $fields = null): array } break; case self::ORDERBY_PAID: - if ($this->canOrderBy('id_paid', $fields)) { + if ($this->canOrderBy('is_paid', $fields)) { $order[] = 'is_paid ' . $this->filters->getDirection(); } break; diff --git a/tests/GaletteEvents/Repository/tests/units/Bookings.php b/tests/GaletteEvents/Repository/tests/units/Bookings.php index f1b6e56d..a5b7d60c 100644 --- a/tests/GaletteEvents/Repository/tests/units/Bookings.php +++ b/tests/GaletteEvents/Repository/tests/units/Bookings.php @@ -89,4 +89,20 @@ public function testListScope(): void $this->getVisibleBookings() ); } + + /** + * Listed bookings keep their own values, not the ones of their event + */ + public function testListKeepsBookingValues(): void + { + $member_one = $this->getMemberOne(); + $event = $this->insertEvent('Event', ['comment' => 'Event comment', 'creation_date' => '2026-01-01']); + $this->insertBooking($event, $member_one->id, ['comment' => 'Booking comment', 'creation_date' => '2026-02-01']); + + $this->logSuperAdmin(); + $list = (new \GaletteEvents\Repository\Bookings($this->zdb, $this->login))->getList(); + $this->assertCount(1, $list); + $this->assertSame('Booking comment', $list[0]->getComment()); + $this->assertSame('2026-02-01', $list[0]->getCreationDate(false)); + } } From 6c887b136e1d47804a52a13918bb487d72e1f9af Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 14:44:06 +0200 Subject: [PATCH 09/20] Show members the events they can see in calendar, in requested dates Past and closed events included. Group managers also get events of groups they belong to, and bookings events lists are not paginated anymore. --- .../Controllers/Crud/BookingsController.php | 2 +- .../Controllers/Crud/EventsController.php | 40 ++---- lib/GaletteEvents/PluginGaletteEvents.php | 2 +- lib/GaletteEvents/Repository/Events.php | 118 +++++++---------- .../Crud/tests/units/EventsController.php | 16 +++ .../Repository/tests/units/Events.php | 125 ++++++++++++++++++ 6 files changed, 197 insertions(+), 106 deletions(-) create mode 100644 tests/GaletteEvents/Repository/tests/units/Events.php diff --git a/lib/GaletteEvents/Controllers/Crud/BookingsController.php b/lib/GaletteEvents/Controllers/Crud/BookingsController.php index 41711f40..b73a0043 100644 --- a/lib/GaletteEvents/Controllers/Crud/BookingsController.php +++ b/lib/GaletteEvents/Controllers/Crud/BookingsController.php @@ -139,7 +139,7 @@ public function listBookings(Response $response, string|int $event, ?string $opt 'eventid' => $linked_event, 'require_dialog' => true, 'filters' => $filters, - 'events' => $events->getList(), + 'events' => $events->getList(full: true), 'groups' => $groups_list ] ); diff --git a/lib/GaletteEvents/Controllers/Crud/EventsController.php b/lib/GaletteEvents/Controllers/Crud/EventsController.php index f739ca67..d221715d 100644 --- a/lib/GaletteEvents/Controllers/Crud/EventsController.php +++ b/lib/GaletteEvents/Controllers/Crud/EventsController.php @@ -121,31 +121,6 @@ public function calendar( ?string $option = null, string|int|null $value = null ): Response { - if (isset($this->session->filter_events_calendar)) { - $filters = $this->session->filter_events_calendar; - } else { - $filters = new EventsList(); - } - $filters->calendar_filter = true; - - if ($option !== null) { - switch ($option) { - case 'page': - $filters->current_page = (int)$value; - break; - case 'order': - $filters->orderby = $value; - break; - } - } - - $events = new Events($this->zdb, $this->login, $filters); - - //assign pagination variables to the template and add pagination links - $filters->setViewPagination($this->routeparser, $this->view, false); - - $this->session->filter_events_calendar = $filters; - //check if JS has been generated if (!file_exists(__DIR__ . '/../../../../webroot/js/calendar.bundle.js')) { $this->flash->addMessageNow( @@ -161,9 +136,6 @@ public function calendar( [ 'page_title' => _T("Events calendar", "events"), 'require_dialog' => true, - 'events' => $events->getList(), - 'nb_events' => $events->getCount(), - 'filters' => $filters, 'module_id' => $this->getModuleId() ] ); @@ -176,10 +148,16 @@ public function calendar( public function ajaxCalendar(Request $request, Response $response): Response { $get = $request->getQueryParams(); - $filters = $this->session->filter_events_calendar ?? new EventsList(); + $start = strtotime((string)($get['start'] ?? '')); + $end = strtotime((string)($get['end'] ?? '')); + if ($start === false || $end === false) { + return $this->withJson($response, [], 400); + } + + $filters = new EventsList(); $filters->calendar_filter = true; - $filters->start_date_filter = date(__("Y-m-d"), strtotime($get['start'])); - $filters->end_date_filter = date(__("Y-m-d"), strtotime($get['end'])); + $filters->start_date_filter = date(__("Y-m-d"), $start); + $filters->end_date_filter = date(__("Y-m-d"), $end); $events = new Events($this->zdb, $this->login, $filters); diff --git a/lib/GaletteEvents/PluginGaletteEvents.php b/lib/GaletteEvents/PluginGaletteEvents.php index 99fd8700..9cefa598 100644 --- a/lib/GaletteEvents/PluginGaletteEvents.php +++ b/lib/GaletteEvents/PluginGaletteEvents.php @@ -189,7 +189,7 @@ public function getNews(): ?Entry { $filters = new EventsList(); $now = new \DateTime(); - $filters->start_date_filter = $now->format(_T('Y-m-d')); + $filters->start_date_filter = $now->format(__('Y-m-d')); $events = new Events($this->zdb, $this->login, $filters); $posts = []; diff --git a/lib/GaletteEvents/Repository/Events.php b/lib/GaletteEvents/Repository/Events.php index 132678a9..9943cb2c 100644 --- a/lib/GaletteEvents/Repository/Events.php +++ b/lib/GaletteEvents/Repository/Events.php @@ -63,12 +63,17 @@ public function __construct(Db $zdb, Login $login, ?EventsList $filters = null) /** * Get events list * - * @param bool $onlyevents get events member has booking on + * Members get open and upcoming events that are public or restricted to their groups, + * and the ones they have booked; group managers also get events of the groups they manage. + * In calendar, members get all those events, even past or closed ones, in requested dates. + * + * @param bool $bookable get only events current logged-in user can book, not paginated * @param bool $fullcalendar get events for fullcalendar display (ie. end date +1 day) + * @param bool $full get full list, not paginated * * @return array> */ - public function getList(bool $onlyevents = false, bool $fullcalendar = false): array + public function getList(bool $bookable = false, bool $fullcalendar = false, bool $full = false): array { try { $select = $this->zdb->select(EVENTS_PREFIX . Event::TABLE, 'e'); @@ -80,94 +85,61 @@ public function getList(bool $onlyevents = false, bool $fullcalendar = false): a $select::JOIN_LEFT ); - $groups = null; if (!$this->login->isAdmin() && !$this->login->isStaff()) { - if ($this->login->isGroupManager()) { - $groups = $this->login->managed_groups; + $managed = array_map('intval', $this->login->managed_groups); + $groups = array_unique(array_merge( + array_map('intval', Groups::loadGroups((int)$this->login->id, false, false)), + $managed + )); + + $visible = [new Predicate\IsNull('e.' . Group::PK)]; + if (count($groups)) { + $visible[] = new Predicate\In('e.' . Group::PK, $groups); + } + $visible = new PredicateSet($visible, PredicateSet::OP_OR); + + $booked = new Predicate\Operator('b.' . Adherent::PK, '=', $this->login->id); + + if ($this->filters->calendar_filter) { + $set = [$visible, $booked]; + } else { $set = [new PredicateSet( [ - new Predicate\IsNull(Group::PK), - new Predicate\Operator( - 'is_open', - '=', - true - ), - new Predicate\Operator( - 'begin_date', - '>=', - date('Y-m-d') - ) + new Predicate\Operator('e.is_open', '=', true), + new Predicate\Operator('e.begin_date', '>=', date('Y-m-d')), + $visible ] )]; - - if (count($groups)) { - $set[] = new Predicate\In( - Group::PK, - $groups - ); + if (!$bookable) { + if (count($managed)) { + //managers get events of their groups, whatever their state + $set[] = new Predicate\In('e.' . Group::PK, $managed); + } + $set[] = $booked; } + } - if ($onlyevents === false) { - //get events member has booking on - $set[] = new Predicate\Operator( - 'b.' . Adherent::PK, - '=', - $this->login->id - ); - } + $select->where(new PredicateSet($set, PredicateSet::OP_OR)); + } - $select->where( - new PredicateSet( - $set, - PredicateSet::OP_OR - ) - ); + if ($this->filters->raw_start_date_filter !== null) { + if ($this->filters->calendar_filter) { + //events overlapping requested dates + $select->where->greaterThanOrEqualTo('e.end_date', $this->filters->raw_start_date_filter); } else { - $select->where( - 'is_open', - //@phpstan-ignore-next-line - new Expression('true') - ); - $select->where->greaterThanOrEqualTo('begin_date', date('Y-m-d')); - - $set = [new Predicate\IsNull(Group::PK)]; - $groups = Groups::loadGroups($this->login->id, false, false); - if (count($groups)) { - $set[] = new Predicate\In( - Group::PK, - $groups - ); - } - - if ($onlyevents === false) { - //get events member has booking on - - $set[] = new Predicate\Operator( - 'b.' . Adherent::PK, - '=', - $this->login->id - ); - } - - $select->where( - new PredicateSet( - $set, - PredicateSet::OP_OR - ) - ); - } - } else { - if (isset($this->filters->start_date_filter) && $this->filters->raw_start_date_filter !== null) { - $select->where->greaterThanOrEqualTo('begin_date', $this->filters->raw_start_date_filter); + $select->where->greaterThanOrEqualTo('e.begin_date', $this->filters->raw_start_date_filter); } } + if ($this->filters->raw_end_date_filter !== null) { + $select->where->lessThanOrEqualTo('e.begin_date', $this->filters->raw_end_date_filter); + } $select->group(['e.' . Event::PK]); $select->order($this->buildOrderClause()); $this->proceedCount($select); - if (!$this->filters->calendar_filter) { + if (!$this->filters->calendar_filter && !$bookable && !$full) { $this->filters->setLimits($select); } $results = $this->zdb->execute($select); diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php index 5a941609..d99f50d9 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php @@ -230,4 +230,20 @@ public function testPastEventFormKeepsOpenFlag(): void $this->assertStringContainsString(' checked', $matches[0]); $this->assertStringContainsString('value="1"', $matches[0]); } + + /** + * Calendar requires dates + */ + public function testCalendarRequiresDates(): void + { + $this->getMemberOne(); + $this->logMember($this->dataAdherentOne()); + + foreach ([[], ['start' => 'soon', 'end' => 'later']] as $query) { + $test_response = $this->app->handle($this->createRequest('ajax-events_calendar', query_params: $query)); + $this->assertSame(400, $test_response->getStatusCode()); + $this->assertSame('[]', (string)$test_response->getBody()); + } + $this->expectNoLogEntry(); + } } diff --git a/tests/GaletteEvents/Repository/tests/units/Events.php b/tests/GaletteEvents/Repository/tests/units/Events.php new file mode 100644 index 00000000..b842b612 --- /dev/null +++ b/tests/GaletteEvents/Repository/tests/units/Events.php @@ -0,0 +1,125 @@ + + */ +class Events extends GaletteTestCase +{ + use EventsFixtures; + + protected int $seed = 20260926151512; + + /** + * Cleanup after each test method + */ + public function tearDown(): void + { + $this->login->logout(); + $this->cleanEvents(); + parent::tearDown(); + } + + /** + * Get names of listed events + * + * @param bool $bookable Bookable events only + * + * @return array + */ + private function getListed(bool $bookable = false): array + { + $events = new \GaletteEvents\Repository\Events($this->zdb, $this->login); + $names = array_map(fn(\GaletteEvents\Event $event): string => (string)$event->getName(), $events->getList($bookable)); + sort($names); + return $names; + } + + /** + * Get names of events displayed in calendar, from last month to next one + * + * @return array + */ + private function getCalendar(): array + { + $filters = new EventsList(); + $filters->calendar_filter = true; + $filters->start_date_filter = date(__('Y-m-d'), strtotime('-1 month')); + $filters->end_date_filter = date(__('Y-m-d'), strtotime('+1 month')); + $events = new \GaletteEvents\Repository\Events($this->zdb, $this->login, $filters); + $names = array_map(fn(\ArrayObject $event): string => $event['name'], $events->getList(false, true)); + sort($names); + return $names; + } + + /** + * Events listed, bookable and displayed in calendar, depending on who is logged in + */ + public function testVisibility(): void + { + $member_one = $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $own = $this->createGroup('Own group', [], [$member_one]); + $other = $this->createGroup('Other group', [], [$member_two]); + $managed = $this->createGroup('Managed group', [$member_two]); + $past = [ + 'begin_date' => date('Y-m-d', strtotime('-10 days')), + 'end_date' => date('Y-m-d', strtotime('-9 days')), + ]; + + $this->insertEvent('public'); + $this->insertEvent('public closed', ['is_open' => false]); + $this->insertBooking($this->insertEvent('public past', $past), $member_one->id); + $this->insertEvent( + 'public far', + ['begin_date' => date('Y-m-d', strtotime('+3 months')), 'end_date' => date('Y-m-d', strtotime('+3 months'))] + ); + $this->insertEvent('own group', ['id_group' => $own->getId()]); + $this->insertEvent('other group', ['id_group' => $other->getId()]); + $this->insertEvent('managed', ['id_group' => $managed->getId()]); + $this->insertEvent('managed past closed', ['id_group' => $managed->getId(), 'is_open' => false] + $past); + + $this->logMember($this->dataAdherentOne()); + $this->assertSame(['own group', 'public', 'public far', 'public past'], $this->getListed()); + $this->assertSame(['own group', 'public', 'public far'], $this->getListed(true)); + $this->assertSame(['own group', 'public', 'public closed', 'public past'], $this->getCalendar()); + $this->login->logout(); + + //member two manages a group, and belongs to another one + $this->logMember($this->dataAdherentTwo()); + $this->assertSame( + ['managed', 'managed past closed', 'other group', 'public', 'public far'], + $this->getListed() + ); + $this->assertSame(['managed', 'other group', 'public', 'public far'], $this->getListed(true)); + $this->assertSame( + ['managed', 'managed past closed', 'other group', 'public', 'public closed', 'public past'], + $this->getCalendar() + ); + $this->login->logout(); + + $this->logSuperAdmin(); + $all = [ + 'managed', 'managed past closed', 'other group', 'own group', + 'public', 'public closed', 'public far', 'public past' + ]; + $this->assertSame($all, $this->getListed()); + $this->assertSame($all, $this->getListed(true)); + $this->assertSame(array_values(array_diff($all, ['public far'])), $this->getCalendar()); + } +} From 3c51103da44ed6876f08188322ed8b41c530a3dc Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 14:45:18 +0200 Subject: [PATCH 10/20] Fix month end date filter and bookings list pagination --- lib/GaletteEvents/Filters/BookingsList.php | 8 +--- lib/GaletteEvents/Filters/EventsList.php | 2 +- lib/GaletteEvents/Repository/Activities.php | 3 +- .../Crud/tests/units/BookingsController.php | 27 +++++++++++ .../Filters/tests/units/EventsList.php | 47 +++++++++++++++++++ 5 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 tests/GaletteEvents/Filters/tests/units/EventsList.php diff --git a/lib/GaletteEvents/Filters/BookingsList.php b/lib/GaletteEvents/Filters/BookingsList.php index ea9f9621..7b872d79 100644 --- a/lib/GaletteEvents/Filters/BookingsList.php +++ b/lib/GaletteEvents/Filters/BookingsList.php @@ -154,7 +154,7 @@ public function __set(string $name, mixed $value): void /** * Build href - * Override to add "event" parameter + * Override to keep "event" parameter * * @param int $page Page */ @@ -163,13 +163,9 @@ protected function getHref(int $page): string $args = [ 'option' => 'page', 'value' => (string)$page, - 'event' => 'all' + 'event' => $this->event_filter === null ? 'all' : (string)$this->event_filter ]; - if ($this->view->getEnvironment()->getGlobals()['cur_subroute']) { - $args['type'] = $this->view->getEnvironment()->getGlobals()['cur_subroute']; - } - $href = $this->routeparser->urlFor( $this->view->getEnvironment()->getGlobals()['cur_route'], $args diff --git a/lib/GaletteEvents/Filters/EventsList.php b/lib/GaletteEvents/Filters/EventsList.php index d079909a..e7174b64 100644 --- a/lib/GaletteEvents/Filters/EventsList.php +++ b/lib/GaletteEvents/Filters/EventsList.php @@ -186,7 +186,7 @@ public function __set(string $name, mixed $value): void if ($y === false && $ym !== false) { $day = 1; if ($name === 'end_date_filter') { - $day = $ym->format('t'); + $day = (int)$ym->format('t'); } $ym->setDate( (int)$ym->format('Y'), diff --git a/lib/GaletteEvents/Repository/Activities.php b/lib/GaletteEvents/Repository/Activities.php index adf482d6..593242e7 100644 --- a/lib/GaletteEvents/Repository/Activities.php +++ b/lib/GaletteEvents/Repository/Activities.php @@ -19,7 +19,6 @@ use Laminas\Db\Sql\Expression; use Galette\Core\Login; use Galette\Core\Db; -use GaletteEvents\Filters\EventsList; use Laminas\Db\Sql\Select; /** @@ -50,7 +49,7 @@ public function __construct(Db $zdb, Login $login, Preferences $preferences, ?Ac parent::__construct($zdb, $preferences, $login, 'Activity', 'GaletteEvents', EVENTS_PREFIX); if ($filters === null) { - $this->filters = new EventsList(); + $this->filters = new ActivitiesList(); } else { $this->filters = $filters; } diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php index 84faa654..0fd764e5 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php @@ -466,4 +466,31 @@ public function testManagerBatchAsCoreAllows(): void ); $this->assertSame(307, $batch('csv')->getStatusCode()); } + + /** + * Bookings list of an event keeps the event in its pagination + */ + public function testListPaginationKeepsEvent(): void + { + $member_one = $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $event = $this->insertEvent('Event'); + $this->insertBooking($event, $member_one->id); + $this->insertBooking($event, $member_two->id); + + $this->logSuperAdmin(); + $filters = new \GaletteEvents\Filters\BookingsList(); + $filters->show = 1; + $this->session->filter_bookings = $filters; + + $test_response = $this->app->handle($this->createRequest('events_bookings', ['event' => (string)$event])); + $this->assertSame(200, $test_response->getStatusCode()); + $this->assertStringContainsString( + 'href="' . $this->routeparser->urlFor( + 'events_bookings', + ['event' => (string)$event, 'option' => 'page', 'value' => '2'] + ) . '"', + (string)$test_response->getBody() + ); + } } diff --git a/tests/GaletteEvents/Filters/tests/units/EventsList.php b/tests/GaletteEvents/Filters/tests/units/EventsList.php new file mode 100644 index 00000000..54751501 --- /dev/null +++ b/tests/GaletteEvents/Filters/tests/units/EventsList.php @@ -0,0 +1,47 @@ + + */ +class EventsList extends GaletteTestCase +{ + protected int $seed = 20260926151512; + + /** + * Date filters accept a year, a month or a day + */ + public function testDateFilters(): void + { + $filters = new \GaletteEvents\Filters\EventsList(); + + $filters->start_date_filter = '2026'; + $filters->end_date_filter = '2026'; + $this->assertSame('2026-01-01', $filters->raw_start_date_filter); + $this->assertSame('2026-12-31', $filters->raw_end_date_filter); + + $filters->start_date_filter = '2026-02'; + $filters->end_date_filter = '2026-02'; + $this->assertSame('2026-02-01', $filters->raw_start_date_filter); + $this->assertSame('2026-02-28', $filters->raw_end_date_filter); + + $filters->start_date_filter = '2026-02-10'; + $this->assertSame('2026-02-10', $filters->raw_start_date_filter); + + $filters->end_date_filter = ''; + $this->assertNull($filters->raw_end_date_filter); + } +} From f7554afa9d8be6a04d1bdd6c918cbd99b4917be2 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 14:47:25 +0200 Subject: [PATCH 11/20] Prefix plugin session keys The edited activity was stored under the same key as plugin-activities one. --- .../Controllers/Crud/ActivitiesController.php | 20 +++++++------- .../Controllers/Crud/BookingsController.php | 26 +++++++++---------- .../Controllers/Crud/EventsController.php | 20 +++++++------- .../Controllers/CsvController.php | 16 +++++++++--- .../Crud/tests/units/ActivitiesController.php | 14 ++++++++++ .../Crud/tests/units/BookingsController.php | 2 +- .../Crud/tests/units/EventsController.php | 2 +- 7 files changed, 62 insertions(+), 38 deletions(-) diff --git a/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php b/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php index eea220bb..8ea3de7e 100644 --- a/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php +++ b/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php @@ -61,8 +61,8 @@ public function doAdd(Request $request, Response $response): Response */ public function list(Request $request, Response $response, ?string $option = null, string|int|null $value = null): Response { - if (isset($this->session->filter_activities)) { - $filters = $this->session->filter_activities; + if (isset($this->session->{$this->getFilterName('activities')})) { + $filters = $this->session->{$this->getFilterName('activities')}; } else { $filters = new ActivitiesList(); } @@ -84,7 +84,7 @@ public function list(Request $request, Response $response, ?string $option = nul //assign pagination variables to the template and add pagination links $filters->setViewPagination($this->routeparser, $this->view, false); - $this->session->filter_activities = $filters; + $this->session->{$this->getFilterName('activities')} = $filters; // display page $this->view->render( @@ -107,8 +107,8 @@ public function list(Request $request, Response $response, ?string $option = nul public function filter(Request $request, Response $response): Response { $post = $request->getParsedBody(); - if (isset($this->session->filter_activities)) { - $filters = $this->session->filter_activities; + if (isset($this->session->{$this->getFilterName('activities')})) { + $filters = $this->session->{$this->getFilterName('activities')}; } else { $filters = new ActivitiesList(); } @@ -123,7 +123,7 @@ public function filter(Request $request, Response $response): Response } } - $this->session->filter_activities = $filters; + $this->session->{$this->getFilterName('activities')} = $filters; return $response ->withStatus(301) @@ -141,9 +141,9 @@ public function filter(Request $request, Response $response): Response */ public function edit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response { - if ($this->session->activity !== null) { - $activity = $this->session->activity; - $this->session->activity = null; + if ($this->session->plugin_events_activity !== null) { + $activity = $this->session->plugin_events_activity; + $this->session->plugin_events_activity = null; } else { $activity = new Activity($this->zdb, $this->login); } @@ -241,7 +241,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st $redirect_url = $this->routeparser->urlFor('events_activities'); } else { //store entity in session - $this->session->activity = $activity; + $this->session->plugin_events_activity = $activity; if ($activity->getId()) { $redirect_url = $this->routeparser->urlFor( diff --git a/lib/GaletteEvents/Controllers/Crud/BookingsController.php b/lib/GaletteEvents/Controllers/Crud/BookingsController.php index b73a0043..7177f3da 100644 --- a/lib/GaletteEvents/Controllers/Crud/BookingsController.php +++ b/lib/GaletteEvents/Controllers/Crud/BookingsController.php @@ -83,7 +83,7 @@ public function list(Request $request, Response $response, ?string $option = nul */ public function listBookings(Response $response, string|int $event, ?string $option = null, string|int|null $value = null): Response { - $filters = $this->session->filter_bookings ?? new BookingsList(); + $filters = $this->session->{$this->getFilterName('bookings')} ?? new BookingsList(); if ($event == 'guess') { $linked_event = $filters->event_filter; @@ -124,7 +124,7 @@ public function listBookings(Response $response, string|int $event, ?string $opt //assign pagination variables to the template and add pagination links $filters->setViewPagination($this->routeparser, $this->view, false); - $this->session->filter_bookings = $filters; + $this->session->{$this->getFilterName('bookings')} = $filters; // display page $this->view->render( @@ -163,8 +163,8 @@ public function filter(Request $request, Response $response): Response public function filterBookings(Request $request, Response $response, string|int $event): Response { $post = $request->getParsedBody(); - if (isset($this->session->filter_bookings)) { - $filters = $this->session->filter_bookings; + if (isset($this->session->{$this->getFilterName('bookings')})) { + $filters = $this->session->{$this->getFilterName('bookings')}; } else { $filters = new BookingsList(); } @@ -204,7 +204,7 @@ public function filterBookings(Request $request, Response $response, string|int } } - $this->session->filter_bookings = $filters; + $this->session->{$this->getFilterName('bookings')} = $filters; return $response ->withStatus(301) @@ -238,13 +238,13 @@ public function handleBatch(Request $request, Response $response): Response } if (isset($post['entries_sel'])) { - if (isset($this->session->filter_bookings)) { - $filters = clone $this->session->filter_bookings; + if (isset($this->session->{$this->getFilterName('bookings')})) { + $filters = clone $this->session->{$this->getFilterName('bookings')}; } else { $filters = new BookingsList(); } - //$this->session->filter_bookings = $filters; + //$this->session->{$this->getFilterName('bookings')} = $filters; $filters->selected = $post['entries_sel']; //selection is restricted to bookings current logged-in user can list @@ -363,9 +363,9 @@ public function edit(Request $request, Response $response, ?int $id = null, stri $get = $request->getQueryParams(); $route_params = []; - if ($this->session->booking !== null) { - $booking = $this->session->booking; - $this->session->booking = null; + if ($this->session->plugin_events_booking !== null) { + $booking = $this->session->plugin_events_booking; + $this->session->plugin_events_booking = null; } else { $booking = new Booking($this->zdb, $this->login); } @@ -519,7 +519,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st } if (!isset($post['save'])) { - $this->session->booking = $booking; + $this->session->plugin_events_booking = $booking; $error_detected = []; $goto_list = false; $warning_detected[] = _T('Do not forget to store the booking', 'events'); @@ -558,7 +558,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st ); } else { //store entity in session - $this->session->booking = $booking; + $this->session->plugin_events_booking = $booking; if ($booking->getId()) { $route = 'events_booking_edit'; diff --git a/lib/GaletteEvents/Controllers/Crud/EventsController.php b/lib/GaletteEvents/Controllers/Crud/EventsController.php index d221715d..014bfe84 100644 --- a/lib/GaletteEvents/Controllers/Crud/EventsController.php +++ b/lib/GaletteEvents/Controllers/Crud/EventsController.php @@ -69,8 +69,8 @@ public function doAdd(Request $request, Response $response): Response */ public function list(Request $request, Response $response, ?string $option = null, string|int|null $value = null): Response { - if (isset($this->session->filter_events)) { - $filters = $this->session->filter_events; + if (isset($this->session->{$this->getFilterName('events')})) { + $filters = $this->session->{$this->getFilterName('events')}; } else { $filters = new EventsList(); } @@ -92,7 +92,7 @@ public function list(Request $request, Response $response, ?string $option = nul //assign pagination variables to the template and add pagination links $filters->setViewPagination($this->routeparser, $this->view, false); - $this->session->filter_events = $filters; + $this->session->{$this->getFilterName('events')} = $filters; // display page $this->view->render( @@ -170,8 +170,8 @@ public function ajaxCalendar(Request $request, Response $response): Response public function filter(Request $request, Response $response): Response { $post = $request->getParsedBody(); - if (isset($this->session->filter_events)) { - $filters = $this->session->filter_events; + if (isset($this->session->{$this->getFilterName('events')})) { + $filters = $this->session->{$this->getFilterName('events')}; } else { $filters = new EventsList(); } @@ -186,7 +186,7 @@ public function filter(Request $request, Response $response): Response } } - $this->session->filter_events = $filters; + $this->session->{$this->getFilterName('events')} = $filters; return $response ->withStatus(301) @@ -204,9 +204,9 @@ public function filter(Request $request, Response $response): Response */ public function edit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response { - if ($this->session->event !== null) { - $event = $this->session->event; - $this->session->event = null; + if ($this->session->plugin_events_event !== null) { + $event = $this->session->plugin_events_event; + $this->session->plugin_events_event = null; } else { $event = new Event($this->zdb, $this->login); } @@ -341,7 +341,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st $redirect_url = $this->routeparser->urlFor('events_events'); } else { //store entity in session - $this->session->event = $event; + $this->session->plugin_events_event = $event; if ($event->getId()) { $redirect_url = $this->routeparser->urlFor( diff --git a/lib/GaletteEvents/Controllers/CsvController.php b/lib/GaletteEvents/Controllers/CsvController.php index 389c35ab..4cf1c06b 100644 --- a/lib/GaletteEvents/Controllers/CsvController.php +++ b/lib/GaletteEvents/Controllers/CsvController.php @@ -11,6 +11,8 @@ namespace GaletteEvents\Controllers; use Analog\Analog; +use DI\Attribute\Inject; +use Galette\Core\PluginControllerTrait; use Slim\Psr7\Request; use Slim\Psr7\Response; use Galette\IO\Csv; @@ -26,6 +28,14 @@ */ class CsvController extends \Galette\Controllers\CsvController { + use PluginControllerTrait; + + /** + * @var array + */ + #[Inject("Plugin Galette Events")] + protected array $module_info; + /** * Bookings CSV exports * @@ -55,9 +65,9 @@ public function bookingsExport(Request $request, Response $response, ?int $id = $csv = new CsvOut(); //filters come from bookings list, or from its batch actions - $session_var = $post['session_var'] ?? $get['session_var'] ?? 'filter_bookings'; - if (!in_array($session_var, ['filter_bookings', 'plugin-events-bookings'], true)) { - $session_var = 'filter_bookings'; + $session_var = $post['session_var'] ?? $get['session_var'] ?? $this->getFilterName('bookings'); + if (!in_array($session_var, [$this->getFilterName('bookings'), 'plugin-events-bookings'], true)) { + $session_var = $this->getFilterName('bookings'); } if ($id === null && ($this->session->$session_var ?? null) instanceof BookingsList) { $filters = $this->session->$session_var; diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php index 2aa8c058..02c9f105 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php @@ -86,4 +86,18 @@ public function testMemberCannotStoreActivity(): void $this->expectAuthMiddlewareRefused($this->postActivity(['name' => 'Created by a member', 'active' => '1', 'comment' => ''])); $this->assertSame(0, $this->countActivities('Created by a member')); } + + /** + * Session keys do not collide with other plugins ones + */ + public function testSessionKeysArePrefixed(): void + { + $this->logSuperAdmin(); + //plugin-activities stores its own entity under this key + $this->session->activity = new \stdClass(); + + $test_response = $this->app->handle($this->createRequest('events_activity_add')); + $this->assertSame(200, $test_response->getStatusCode()); + $this->expectNoLogEntry(); + } } diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php index 0fd764e5..613dfebf 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php @@ -481,7 +481,7 @@ public function testListPaginationKeepsEvent(): void $this->logSuperAdmin(); $filters = new \GaletteEvents\Filters\BookingsList(); $filters->show = 1; - $this->session->filter_bookings = $filters; + $this->session->plugin_events_bookings_filter = $filters; $test_response = $this->app->handle($this->createRequest('events_bookings', ['event' => (string)$event])); $this->assertSame(200, $test_response->getStatusCode()); diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php index d99f50d9..1c2ce660 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php @@ -177,7 +177,7 @@ public function testAttachActivityOnIncompleteEvent(): void 'success_detected' => ['Activity has been attached to event.'], ]); $this->expectLogEntry(Analog::ERROR, 'Some errors has been threw attempting to edit/store an event'); - $this->assertSame([$dinner], array_keys($this->session->event->getActivities())); + $this->assertSame([$dinner], array_keys($this->session->plugin_events_event->getActivities())); //an unknown activity is not attached $this->postEvent(null, $this->getFormValues([ From e274324b76bfebfff8e49ce65fbe511b5f936fe6 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 14:48:14 +0200 Subject: [PATCH 12/20] Test activity removal --- .../Crud/tests/units/ActivitiesController.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php index 02c9f105..6ef6cc08 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php @@ -100,4 +100,23 @@ public function testSessionKeysArePrefixed(): void $this->assertSame(200, $test_response->getStatusCode()); $this->expectNoLogEntry(); } + + /** + * Activities are removed from the posted id + */ + public function testRemoveActivity(): void + { + $this->logSuperAdmin(); + $id = $this->insertActivity('Dinner'); + + $request = $this->createRequest('events_do_remove_activity', [], 'POST') + ->withParsedBody(['id' => (string)$id, 'confirm' => '1']); + $test_response = $this->app->handle($request); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('events_activities')]], + $test_response->getHeaders() + ); + $this->expectFlashData(['success_detected' => ['Successfully deleted!']]); + $this->assertSame(0, $this->countActivities('Dinner')); + } } From ed91b25cda9cfd35c7047d2a08e584cc6406e7f7 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 14:48:56 +0200 Subject: [PATCH 13/20] Export bookings without a known payment method --- lib/GaletteEvents/Booking.php | 10 ++++++++-- .../Controllers/tests/units/CsvController.php | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/GaletteEvents/Booking.php b/lib/GaletteEvents/Booking.php index 126e48b0..8250c1c1 100644 --- a/lib/GaletteEvents/Booking.php +++ b/lib/GaletteEvents/Booking.php @@ -638,8 +638,14 @@ public function getPaymentMethod(): int */ public function getPaymentMethodName(): string { - $pt = new PaymentType($this->zdb, (int)$this->payment_method); - return $pt->getname(); + //payment method may be missing: 0 is the column default, and types can be removed + $select = $this->zdb->select(PaymentType::TABLE); + $select->where([PaymentType::PK => $this->payment_method]); + if ($this->zdb->execute($select)->count() === 0) { + return ''; + } + $pt = new PaymentType($this->zdb, $this->payment_method); + return $pt->getName(); } /** diff --git a/tests/GaletteEvents/Controllers/tests/units/CsvController.php b/tests/GaletteEvents/Controllers/tests/units/CsvController.php index 5a84ac68..cc4b1a12 100644 --- a/tests/GaletteEvents/Controllers/tests/units/CsvController.php +++ b/tests/GaletteEvents/Controllers/tests/units/CsvController.php @@ -123,4 +123,18 @@ public function testExportReadsBookingsFiltersOnly(): void $this->assertSame(200, $test_response->getStatusCode()); $this->assertSame(['text/csv'], $test_response->getHeader('Content-Type')); } + + /** + * Bookings without a known payment method are exported + */ + public function testExportUnknownPaymentMethod(): void + { + $member_one = $this->getMemberOne(); + $event = $this->insertEvent('Event'); + $this->insertBooking($event, $member_one->id, ['payment_method' => 0]); + + $this->logSuperAdmin(); + $this->assertStringContainsString($member_one->email, $this->exportEvent($event)); + $this->expectNoLogEntry(); + } } From 4f1a2e6863c3c99560b8f99029bb71d7d5a5e1e5 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 14:49:30 +0200 Subject: [PATCH 14/20] Offer to edit events from calendar only when they can be edited --- calendar.js | 18 +++++++------ lib/GaletteEvents/Repository/Events.php | 1 + templates/default/calendar.html.twig | 17 +++++------- .../Crud/tests/units/EventsController.php | 26 +++++++++++++++++++ 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/calendar.js b/calendar.js index 744a59e8..8ebce7e4 100644 --- a/calendar.js +++ b/calendar.js @@ -35,14 +35,16 @@ $(function() { var _infos = JSON.parse(JSON.stringify(info.event.extendedProps)); _infos.url = _calendar_event_url.replace('PLACEBO', _infos.id_event); _infos.booking = _calendar_booking_url.replace('PLACEBO', _infos.id_event); - _booking_action = function() { - window.location.href = _infos.booking; - }; - if (_modal_actions.length == 2) { - _modal_actions[0].click = _booking_action; - } else { - _modal_actions[1].click = _booking_action; + var _actions = []; + if (_infos.can_edit) { + _actions.push(_edit_action); } + _actions.push(Object.assign({}, _booking_action, { + click: function() { + window.location.href = _infos.booking; + } + })); + _actions.push(_close_action); //description is built and escaped server side, other values must be displayed as text var _elt = $(''); _elt.find('.header').text(_infos.name + ' (' + _infos.begin_date_fmt + ' - ' + _infos.end_date_fmt + ')'); @@ -52,7 +54,7 @@ $(function() { onApprove: function() { window.location.href = _infos.url; }, - actions: _modal_actions + actions: _actions }).modal('show'); }, eventMouseEnter: function(info) { diff --git a/lib/GaletteEvents/Repository/Events.php b/lib/GaletteEvents/Repository/Events.php index 9943cb2c..8f3dcb52 100644 --- a/lib/GaletteEvents/Repository/Events.php +++ b/lib/GaletteEvents/Repository/Events.php @@ -153,6 +153,7 @@ public function getList(bool $bookable = false, bool $fullcalendar = false, bool } else { //required entries for fullcalendar $row['title'] = $row['name']; + $row['can_edit'] = $event->canEdit($this->login); $row['start'] = $row['begin_date']; $end_date = new \DateTime($event->getEndDate(false)); if ($fullcalendar === true) { diff --git a/templates/default/calendar.html.twig b/templates/default/calendar.html.twig index f895e50c..c95d1a48 100644 --- a/templates/default/calendar.html.twig +++ b/templates/default/calendar.html.twig @@ -23,15 +23,12 @@ today: { buttonText: '{{ _T("Today", "events")|e('js') }}' } } var _fullcalendar_locale = '{{ i18n.getWebID() }}'; - var _modal_actions = []; - {% if login.isAdmin() or login.isStaff() or login.isGroupManager() %} - var _edit_action = { - text : '{{ _T("Edit")|e('js') }}', - icon : 'edit', - class : 'icon labeled blue approve' - }; - _modal_actions.push(_edit_action); - {% endif %} + //edit action is added for events current user can edit + var _edit_action = { + text : '{{ _T("Edit")|e('js') }}', + icon : 'edit', + class : 'icon labeled blue approve' + }; var _booking_action = { text : '{{ _T("New booking", "events")|e('js') }}', icon : 'plus circle', @@ -42,8 +39,6 @@ icon : 'times', class : 'icon labeled cancel' }; - _modal_actions.push(_booking_action); - _modal_actions.push(_close_action); {% endblock %} diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php index 1c2ce660..d2f2c6e9 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php @@ -246,4 +246,30 @@ public function testCalendarRequiresDates(): void } $this->expectNoLogEntry(); } + + /** + * Calendar tells which events can be edited + */ + public function testCalendarCanEdit(): void + { + $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $managed = $this->createGroup('Managed group', [$member_two]); + $this->insertEvent('Managed event', ['id_group' => $managed->getId()]); + $this->insertEvent('Public event'); + $this->logMember($this->dataAdherentTwo()); + + $request = $this->createRequest( + 'ajax-events_calendar', + query_params: [ + 'start' => date('Y-m-d'), + 'end' => date('Y-m-d', strtotime('+1 month')), + ] + ); + $events = json_decode((string)$this->app->handle($request)->getBody(), true); + $this->assertIsArray($events); + $can_edit = array_column($events, 'can_edit', 'name'); + ksort($can_edit); + $this->assertSame(['Managed event' => true, 'Public event' => false], $can_edit); + } } From aadc738d6a86af8163de596df90949d998931b23 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 14:53:39 +0200 Subject: [PATCH 15/20] Remove dead code and obsolete files --- .composer-require-checker.config.json | 119 ------------ .scrutinizer.yml | 173 ------------------ _routes.php | 11 -- bin/release | 3 - bin/twig-cache | 114 ------------ .../Controllers/Crud/BookingsController.php | 1 - lib/GaletteEvents/Event.php | 14 -- lib/GaletteEvents/Filters/EventsList.php | 17 +- package-lock.json | 4 +- package.json | 7 +- requirements.txt | 2 - 11 files changed, 6 insertions(+), 459 deletions(-) delete mode 100644 .composer-require-checker.config.json delete mode 100644 .scrutinizer.yml delete mode 100755 bin/twig-cache delete mode 100644 requirements.txt diff --git a/.composer-require-checker.config.json b/.composer-require-checker.config.json deleted file mode 100644 index f03dabfc..00000000 --- a/.composer-require-checker.config.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "php-core-extensions": [ - "core", "standard", - - "ctype", "date", "fileinfo", "filter", "iconv", "json", "pcre", "reflection", "session", "simplexml", "spl", "xml", - - "curl", "gd", "intl", "mbstring", "mysqli", "zlib", "pdo", "pgsql", "gettext", - - "exif", "pcntl" - ], - "symbol-whitelist": [ - "// PHP symbols", - "null", - "false", "true", - "static", "self", "parent", - "array", "bool", "callable", "float", "int", "iterable", "object", "string", "void", - - "// random native PHP functions", - "// these are moved in random native extension in PHP 8.2, but this extension does not exists in previous PHP versions", - "mt_rand", - "rand", - "random_int", - - "// Galette constants (not detected as they are dynamically declared)", - "GALETTE_ATTACHMENTS_PATH", - "GALETTE_ADAPTATIVE_CARDS", - "GALETTE_BASE_PATH", - "GALETTE_CACHE_DIR", - "GALETTE_CARD_COLS", - "GALETTE_CARD_HEIGHT", - "GALETTE_CARD_ROWS", - "GALETTE_CARD_WIDTH", - "GALETTE_COMPAT_VERSION", - "GALETTE_CONFIG_PATH", - "GALETTE_SYSCONFIG_PATH", - "GALETTE_DATA_PATH", - "GALETTE_DB_VERSION", - "GALETTE_DOCUMENTS_PATH", - "GALETTE_DOWNLOADS_URI", - "GALETTE_EXPORTS_PATH", - "GALETTE_FILES_PATH", - "GALETTE_HOSTED", - "GALETTE_IMPORTS_PATH", - "GALETTE_LOGS_PATH", - "GALETTE_MODE", - "GALETTE_DEBUG", - "GALETTE_NIGHTLY", - "GALETTE_PHOTOS_PATH", - "GALETTE_PLUGINS_PATH", - "GALETTE_PLUGINS_DATA_PATH", - "GALETTE_ROOT", - "GALETTE_TELEMETRY_URI", - "GALETTE_TEMPIMAGES_PATH", - "GALETTE_THEME", - "GALETTE_THEMES_PATH", - "GALETTE_VERSION", - "GALETTE_X_FORWARDED_FOR_INDEX", - "_CURRENT_THEME_PATH", - "GALETTE_URI", - "GALETTE_MYSQL_MIN", - "GALETTE_MARIADB_MIN", - "GALETTE_PGSQL_MIN", - "GALETTE_TESTS", - "GALETTE_DISPLAY_VERSION", - "GALETTE_PHP_MIN", - - "// Galette db constants (not detected as they are dynamically declared)", - "HOST_DB", - "NAME_DB", - "PORT_DB", - "PREFIX_DB", - "PWD_DB", - "TYPE_DB", - "USER_DB", - - "//XHProf constants", - - "//know but not detected Galette function,", - "remove_remarks", - "isValidWebUrl", - "remove_remarks", - "split_sql_file", - "_T", - "__", - "_Tn", - "_Tx", - "_Tnx", - - "// XHProf PECL extension", - "XHProfRuns_Default", - "xhprof_disable", - "xhprof_enable", - "GALETTE_XHPROF_PATH", - "XHPROF_PATH", - "XHPROF_URL", - "XHPROF_FLAGS_CPU", - "XHPROF_FLAGS_MEMORY", - "XHPROF_FLAGS_NO_BUILTINS", - "XHPROF_HOST", - - "// DI functions", - "DI\\autowire", - "DI\\Container", - "DI\\ContainerBuilder", - "DI\\Bridge\\Slim\\App", - "DI\\Attribute\\Inject", - - "// Not detected, do not know why.", - "Psr\\Http\\Server\\RequestHandlerInterface", - "Install" - ], - "scan-files": [ - "*.php", - "lib/**/.php", - "scripts/*.php", - "scripts/**/*.php", - "webroot/*.php" - ] -} diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 08e47309..00000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,173 +0,0 @@ -#tools: -# external_code_coverage: -# timeout: 600 # Timeout in seconds. - -build: - dependencies: - override: - - composer self-update - - git clone --depth=1 https://github.com/galette/galette -b develop galette && pushd galette - - nodes: - - analysis: - environment: - php: - version: 8.2 - - project_setup: - override: - - 'true' - tests: - override: - - php-scrutinizer-run - - js-scrutinizer-run - - cache: - directories: - - vendor # Cache for already installed composer package -> speed up composer install - - galette/vendor # Same ^ - - bin # As vendor directory is cached, bin directory must be also cached (as some dependency will not installed if they are already present and so, related binary will not be linked) - - ~/.composer # Composer home directory (avoid fetching already fetched packages) -filter: - excluded_paths: - - 'galette/*' - - 'tests/*' - - 'bin/*' - - '*.min.js' - dependency_paths: - - "galette/vendor/" - - "vendor/" - -checks: - php: - fix_php_opening_tag: false - remove_php_closing_tag: false - one_class_per_file: false - side_effects_or_types: false - no_mixed_inline_html: false - require_braces_around_control_structures: false - php5_style_constructor: false - no_global_keyword: false - avoid_usage_of_logical_operators: false - psr2_class_declaration: false - no_underscore_prefix_in_properties: false - no_underscore_prefix_in_methods: false - blank_line_after_namespace_declaration: false - single_namespace_per_use: false - psr2_switch_declaration: false - psr2_control_structure_declaration: false - avoid_superglobals: false - security_vulnerabilities: false - no_exit: false - fix_use_statements: - remove_unused: true - preserve_multiple: false - preserve_blanklines: true - order_alphabetically: false - javascript: true - -coding_style: - php: - indentation: - general: - use_tabs: false - size: 4 - switch: - indent_case: true - spaces: - general: - linefeed_character: newline - before_parentheses: - function_declaration: false - closure_definition: true - function_call: false - if: true - for: true - while: true - switch: true - catch: true - array_initializer: false - around_operators: - assignment: true - logical: true - equality: true - relational: true - bitwise: true - additive: true - multiplicative: true - shift: true - unary_additive: false - concatenation: true - negation: false - before_left_brace: - class: true - function: true - if: true - else: true - for: true - while: true - do: true - switch: true - try: true - catch: true - finally: true - before_keywords: - else: true - while: true - catch: true - finally: true - within: - brackets: false - array_initializer: false - grouping: false - function_call: false - function_declaration: false - if: false - for: false - while: false - switch: false - catch: false - type_cast: false - ternary_operator: - before_condition: true - after_condition: true - before_alternative: true - after_alternative: true - in_short_version: false - other: - before_comma: false - after_comma: true - before_semicolon: false - after_semicolon: true - after_type_cast: false - braces: - classes_functions: - class: undefined - function: undefined - closure: undefined - if: - opening: undefined - always: true - else_on_new_line: false - for: - opening: undefined - always: true - while: - opening: undefined - always: true - do_while: - opening: undefined - always: true - while_on_new_line: false - switch: - opening: undefined - try: - opening: undefined - catch_on_new_line: false - finally_on_new_line: false - upper_lower_casing: - keywords: - general: undefined - constants: - true_false_null: undefined diff --git a/_routes.php b/_routes.php index b171427d..c8cb3275 100644 --- a/_routes.php +++ b/_routes.php @@ -9,17 +9,6 @@ declare(strict_types=1); use Galette\Middleware\Authenticate; -use Galette\Repository\Groups; -use GaletteEvents\Filters\BookingsList; -use GaletteEvents\Filters\ActivitiesList; -use GaletteEvents\Event; -use GaletteEvents\Booking; -use GaletteEvents\Activity; -use GaletteEvents\Repository\Bookings; -use GaletteEvents\Repository\Activities; -use Galette\Repository\Members; -use Galette\Filters\MembersList; -use Galette\Entity\Adherent; use GaletteEvents\Controllers\Crud\EventsController; use GaletteEvents\Controllers\Crud\ActivitiesController; use GaletteEvents\Controllers\Crud\BookingsController; diff --git a/bin/release b/bin/release index c442bcbf..02e0202b 100755 --- a/bin/release +++ b/bin/release @@ -409,14 +409,11 @@ def add_libs(rel_name, galette_archive): # development only, nothing a Galette installation needs todrop = [ - '.composer-require-checker.config.json', '.docheader', '.gitignore', '.php-cs-fixer.dist.php', '.phpcs.xml', - '.scrutinizer.yml', 'calendar.js', - 'composer.json.checker', 'package-lock.json', 'package.json', 'phpstan.neon', diff --git a/bin/twig-cache b/bin/twig-cache deleted file mode 100755 index 6074b018..00000000 --- a/bin/twig-cache +++ /dev/null @@ -1,114 +0,0 @@ -#!/bin/php -isDir() && !$fpath->isLink()) ? rmdir($fpath->getPathname()) : unlink($fpath->getPathname()); - } - - rmdir($path); - return true; - } - return false; -} - -/** - * Return a custom Twig cache handler. - * This handler is useful to be able to preserve filenames of compiled files. - * - * @param string $directory - * - * @return CacheInterface - */ -function getTwigCacheHandler(string $directory): CacheInterface -{ - return new class($directory) extends FilesystemCache { - - private string $directory; - - public function __construct(string $directory, int $options = 0) - { - $this->directory = rtrim($directory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - parent::__construct($directory, $options); - } - - public function generateKey(string $name, string $className): string - { - return $this->directory . $name; - } - }; -} - -$directory = sprintf('%s/../templates/default', __DIR__); -$cache_dir = sprintf('%s/../tempcache', __DIR__); -$cache = getTwigCacheHandler($cache_dir); -if (file_exists($cache_dir)) { - rmdir_recursive($cache_dir); -} -mkdir($cache_dir); - -$iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($directory), - RecursiveIteratorIterator::LEAVES_ONLY -); - -$loader = new FilesystemLoader($directory); -$twig = new Environment( - $loader, - [ - 'cache' => $cache, - 'auto_reload' => true, - ] -); - -$twig_functions = [ - '__', - '_T', - '_Tn', - '_Tx', - '_Tnx', - 'url_for', - 'memberName', - 'callstatic', - 'is_current_url', - 'get_class', - 'base_path', - 'file_exists' -]; - -foreach ($twig_functions as $function) { - $twig->addFunction(new TwigFunction($function, $function)); -} - -/** @var SplFileInfo $file */ -foreach ($iterator as $file) { - if ($file->isFile()) { - $twig->load(str_replace($directory . '/', '', $file)); - } -} diff --git a/lib/GaletteEvents/Controllers/Crud/BookingsController.php b/lib/GaletteEvents/Controllers/Crud/BookingsController.php index 7177f3da..7e5d60a2 100644 --- a/lib/GaletteEvents/Controllers/Crud/BookingsController.php +++ b/lib/GaletteEvents/Controllers/Crud/BookingsController.php @@ -244,7 +244,6 @@ public function handleBatch(Request $request, Response $response): Response $filters = new BookingsList(); } - //$this->session->{$this->getFilterName('bookings')} = $filters; $filters->selected = $post['entries_sel']; //selection is restricted to bookings current logged-in user can list diff --git a/lib/GaletteEvents/Event.php b/lib/GaletteEvents/Event.php index ad660a71..4601e5eb 100644 --- a/lib/GaletteEvents/Event.php +++ b/lib/GaletteEvents/Event.php @@ -28,10 +28,6 @@ class Event public const string TABLE = 'events'; public const string PK = 'id_event'; - public const int ACTIVITY_NO = 0; - public const int ACTIVITY_YES = 1; - public const int ACTIVITY_REQUIRED = 2; - private Db $zdb; private Login $login; /** @var array */ @@ -659,16 +655,6 @@ public function isOpen(): bool return false; } - /** - * Set name - * - * @param string $name Event name - */ - public function setName(string $name): void - { - $this->name = $name; - } - /** * Get table's name */ diff --git a/lib/GaletteEvents/Filters/EventsList.php b/lib/GaletteEvents/Filters/EventsList.php index e7174b64..828a77f7 100644 --- a/lib/GaletteEvents/Filters/EventsList.php +++ b/lib/GaletteEvents/Filters/EventsList.php @@ -24,32 +24,24 @@ * @property bool $calendar_filter * @property ?string $start_date_filter * @property ?string $raw_start_date_filter + * @property ?string $end_date_filter + * @property ?string $raw_end_date_filter */ class EventsList extends Pagination { //filters - private ?string $name_filter = null; private ?string $start_date_filter = null; private ?string $end_date_filter = null; - private int $group_filter = 0; - private ?string $meal_filter = null; - private ?string $lodging_filter = null; - private ?string $open_filter = null; private bool $calendar_filter = false; private string $query; /** @var array */ protected array $list_fields = [ - 'name_filter', 'start_date_filter', 'raw_start_date_filter', 'end_date_filter', 'raw_end_date_filter', - 'group_filter', - 'meal_filter', - 'lodging_filter', - 'open_filter', 'calendar_filter' ]; @@ -85,13 +77,8 @@ protected function getDefaultDirection(): SQLOrder public function reinit(): void { parent::reinit(); - $this->name_filter = null; $this->start_date_filter = null; $this->end_date_filter = null; - $this->group_filter = 0; - $this->meal_filter = null; - $this->lodging_filter = null; - $this->open_filter = null; $this->calendar_filter = false; } diff --git a/package-lock.json b/package-lock.json index a1b89d00..bd2f51b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "plugin-events", - "version": "1.0.0", + "version": "2.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "plugin-events", - "version": "1.0.0", + "version": "2.2.1", "license": "GPL-3.0-or-later", "dependencies": { "fullcalendar": "^7.1.0", diff --git a/package.json b/package.json index d98c3137..9bc4b543 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,8 @@ { "name": "plugin-events", - "version": "1.0.0", + "version": "2.2.1", "description": "A [Galette](https://galette.eu) plugin to manage events and bookings", "main": "index.js", - "directories": { - "lib": "lib" - }, "scripts": { "build": "webpack", "build-dist": "webpack --mode=production", @@ -14,7 +11,7 @@ }, "repository": { "type": "git", - "url": "ssh://git.tuxfamily.org/gitroot/galette/plugin-events.git" + "url": "https://github.com/galette-plugins/plugin-events.git" }, "author": "Johan Cwiklinski ", "license": "GPL-3.0-or-later", diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index af389325..00000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -gitpython -urlgrabber From 0ef24e47fd5633c9bc05d1c98e74c842d917f976 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 14:53:50 +0200 Subject: [PATCH 16/20] Return initialized event and activity values as is --- lib/GaletteEvents/Activity.php | 2 +- lib/GaletteEvents/Event.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/GaletteEvents/Activity.php b/lib/GaletteEvents/Activity.php index c2536f4e..afdac6ea 100644 --- a/lib/GaletteEvents/Activity.php +++ b/lib/GaletteEvents/Activity.php @@ -325,7 +325,7 @@ protected function getTableName(): string */ public function getComment(): string { - return $this->comment ?? ''; + return $this->comment; } /** diff --git a/lib/GaletteEvents/Event.php b/lib/GaletteEvents/Event.php index 4601e5eb..f2e299a7 100644 --- a/lib/GaletteEvents/Event.php +++ b/lib/GaletteEvents/Event.php @@ -510,25 +510,25 @@ public function getName(): ?string /** * Get event address */ - public function getAddress(): ?string + public function getAddress(): string { - return $this->address ?? null; + return $this->address; } /** * Get event zip */ - public function getZip(): ?string + public function getZip(): string { - return $this->zip ?? null; + return $this->zip; } /** * Get event town */ - public function getTown(): ?string + public function getTown(): string { - return $this->town ?? null; + return $this->town; } /** From 6c8286b975b7d045350ee6ff4e3e8d4e70ef4b66 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sat, 26 Sep 2026 14:55:25 +0200 Subject: [PATCH 17/20] Remove stray quote from event removal title --- lang/events.pot | 2 +- lang/events_ar.utf8.po | 2 +- lang/events_br.utf8.po | 2 +- lang/events_ca.utf8.po | 2 +- lang/events_de_DE.utf8.po | 2 +- lang/events_en_US.po | 2 +- lang/events_es.utf8.po | 2 +- lang/events_fr_FR.utf8.po | 2 +- lang/events_it_IT.utf8.po | 2 +- lang/events_nb_NO.utf8.po | 2 +- lang/events_oc.utf8.po | 2 +- lang/events_ota.utf8.po | 2 +- lang/events_pt.utf8.po | 2 +- lang/events_pt_BR.utf8.po | 2 +- lang/events_ru.utf8.po | 2 +- lang/events_si.utf8.po | 2 +- lang/events_sl.utf8.po | 2 +- lang/events_ta.utf8.po | 2 +- lang/events_tr.utf8.po | 2 +- lang/events_uk.utf8.po | 2 +- .../Controllers/Crud/EventsController.php | 2 +- .../Crud/tests/units/EventsController.php | 14 ++++++++++++++ 22 files changed, 35 insertions(+), 21 deletions(-) diff --git a/lang/events.pot b/lang/events.pot index a89b599c..9c786da6 100644 --- a/lang/events.pot +++ b/lang/events.pot @@ -136,7 +136,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_ar.utf8.po b/lang/events_ar.utf8.po index 1a68933e..6b405b55 100644 --- a/lang/events_ar.utf8.po +++ b/lang/events_ar.utf8.po @@ -138,7 +138,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_br.utf8.po b/lang/events_br.utf8.po index 7cff5fbb..9728af63 100644 --- a/lang/events_br.utf8.po +++ b/lang/events_br.utf8.po @@ -138,7 +138,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_ca.utf8.po b/lang/events_ca.utf8.po index 6fb83280..7f0f2a68 100644 --- a/lang/events_ca.utf8.po +++ b/lang/events_ca.utf8.po @@ -137,7 +137,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_de_DE.utf8.po b/lang/events_de_DE.utf8.po index 11693b23..480d85cc 100644 --- a/lang/events_de_DE.utf8.po +++ b/lang/events_de_DE.utf8.po @@ -144,7 +144,7 @@ msgstr "Ein Fehler ist beim Speichern des Events aufgetreten." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Lösche Ereignis '%1$s'\"" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_en_US.po b/lang/events_en_US.po index c9d20ad7..cfea5fd8 100644 --- a/lang/events_en_US.po +++ b/lang/events_en_US.po @@ -137,7 +137,7 @@ msgstr "An error occurred while storing the event." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Remove event '%1$s'\"" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_es.utf8.po b/lang/events_es.utf8.po index 6acb5f5f..bdfcc97a 100644 --- a/lang/events_es.utf8.po +++ b/lang/events_es.utf8.po @@ -148,7 +148,7 @@ msgstr "Ha ocurrido un error al almacenar el evento." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Eliminar evento «%1$s»" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_fr_FR.utf8.po b/lang/events_fr_FR.utf8.po index 298bee0d..86cdf2d5 100644 --- a/lang/events_fr_FR.utf8.po +++ b/lang/events_fr_FR.utf8.po @@ -140,7 +140,7 @@ msgstr "Une erreur est survenue à l'enregistrement de l'évènement." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Supprimer l'évènement «%1$s»" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_it_IT.utf8.po b/lang/events_it_IT.utf8.po index ce388fd8..db4771f6 100644 --- a/lang/events_it_IT.utf8.po +++ b/lang/events_it_IT.utf8.po @@ -138,7 +138,7 @@ msgstr "Si è verificato un errore durante la memorizzazione dell'evento." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Rimuovi evento '%1$s'\"" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_nb_NO.utf8.po b/lang/events_nb_NO.utf8.po index 92447fce..621e65a4 100644 --- a/lang/events_nb_NO.utf8.po +++ b/lang/events_nb_NO.utf8.po @@ -134,7 +134,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_oc.utf8.po b/lang/events_oc.utf8.po index 2c7d5b3a..379a0b4f 100644 --- a/lang/events_oc.utf8.po +++ b/lang/events_oc.utf8.po @@ -138,7 +138,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_ota.utf8.po b/lang/events_ota.utf8.po index ef6d0394..294895ea 100644 --- a/lang/events_ota.utf8.po +++ b/lang/events_ota.utf8.po @@ -134,7 +134,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_pt.utf8.po b/lang/events_pt.utf8.po index 279d81da..5f43559b 100644 --- a/lang/events_pt.utf8.po +++ b/lang/events_pt.utf8.po @@ -143,7 +143,7 @@ msgstr "Ocorreu um erro ao armazenar o evento." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Remover evento '%1$s'\"" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_pt_BR.utf8.po b/lang/events_pt_BR.utf8.po index 344ae6d5..9626a832 100644 --- a/lang/events_pt_BR.utf8.po +++ b/lang/events_pt_BR.utf8.po @@ -140,7 +140,7 @@ msgstr "Ocorreu um erro ao armazenar o evento." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Remover evento '%1$s'\"" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_ru.utf8.po b/lang/events_ru.utf8.po index 80381e9b..1360377b 100644 --- a/lang/events_ru.utf8.po +++ b/lang/events_ru.utf8.po @@ -145,7 +145,7 @@ msgstr "При сохранении мероприятия произошла о #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Удалите мероприятие '%1$s'\"" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_si.utf8.po b/lang/events_si.utf8.po index 3d31376c..e9b83cda 100644 --- a/lang/events_si.utf8.po +++ b/lang/events_si.utf8.po @@ -134,7 +134,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_sl.utf8.po b/lang/events_sl.utf8.po index 701a317a..21cb5e7a 100644 --- a/lang/events_sl.utf8.po +++ b/lang/events_sl.utf8.po @@ -140,7 +140,7 @@ msgstr "Pri shranjevanju dogodka je prišlo do napake." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Odstrani dogodek '%1$s'" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_ta.utf8.po b/lang/events_ta.utf8.po index f3f73177..0fc19489 100644 --- a/lang/events_ta.utf8.po +++ b/lang/events_ta.utf8.po @@ -141,7 +141,7 @@ msgstr "நிகழ்வை சேமிக்கும் போது பி #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "நிகழ்வை '%1$s' '" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_tr.utf8.po b/lang/events_tr.utf8.po index bb879997..1f52cb36 100644 --- a/lang/events_tr.utf8.po +++ b/lang/events_tr.utf8.po @@ -137,7 +137,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_uk.utf8.po b/lang/events_uk.utf8.po index 1871ff5e..58efeb45 100644 --- a/lang/events_uk.utf8.po +++ b/lang/events_uk.utf8.po @@ -146,7 +146,7 @@ msgstr "Під час зберігання події сталася помил #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Вилучити подію '%1$s'" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lib/GaletteEvents/Controllers/Crud/EventsController.php b/lib/GaletteEvents/Controllers/Crud/EventsController.php index 014bfe84..41c4ef5a 100644 --- a/lib/GaletteEvents/Controllers/Crud/EventsController.php +++ b/lib/GaletteEvents/Controllers/Crud/EventsController.php @@ -414,7 +414,7 @@ public function confirmRemoveTitle(array $args): string $event = new Event($this->zdb, $this->login, (int)$args['id']); return sprintf( //TRANS: %1$s is the event name - _T('Remove event \'%1$s\'"', 'events'), + _T('Remove event \'%1$s\'', 'events'), $event->getName() ); } diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php index d2f2c6e9..6821bd44 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php @@ -272,4 +272,18 @@ public function testCalendarCanEdit(): void ksort($can_edit); $this->assertSame(['Managed event' => true, 'Public event' => false], $can_edit); } + + /** + * Removal confirmation page names the event + */ + public function testRemovalTitle(): void + { + $this->logSuperAdmin(); + $event = $this->insertEvent('Party'); + + $test_response = $this->app->handle($this->createRequest('events_remove_event', ['id' => (string)$event])); + $this->assertSame(200, $test_response->getStatusCode()); + $body = html_entity_decode((string)$test_response->getBody(), ENT_QUOTES); + $this->assertStringContainsString("Remove event 'Party' - ", $body); + } } From 6e612ab627c03a5bedc9aa0d8dfccfe0b4644e18 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski <johan@x-tnd.be> Date: Sat, 26 Sep 2026 14:55:47 +0200 Subject: [PATCH 18/20] Compile locales --- lang/ar.utf8/LC_MESSAGES/events.mo | Bin 2053 -> 2029 bytes lang/br.utf8/LC_MESSAGES/events.mo | Bin 358 -> 798 bytes lang/ca.utf8/LC_MESSAGES/events.mo | Bin 1593 -> 1561 bytes lang/de_DE.utf8/LC_MESSAGES/events.mo | Bin 9273 -> 9253 bytes lang/en_US/LC_MESSAGES/events.mo | Bin 8943 -> 8942 bytes lang/es.utf8/LC_MESSAGES/events.mo | Bin 9670 -> 9646 bytes lang/fr_FR.utf8/LC_MESSAGES/events.mo | Bin 10129 -> 10110 bytes lang/it_IT.utf8/LC_MESSAGES/events.mo | Bin 9723 -> 9699 bytes lang/nb_NO.utf8/LC_MESSAGES/events.mo | Bin 361 -> 481 bytes lang/oc.utf8/LC_MESSAGES/events.mo | Bin 1212 -> 1212 bytes lang/ota.utf8/LC_MESSAGES/events.mo | Bin 359 -> 477 bytes lang/pt.utf8/LC_MESSAGES/events.mo | Bin 9595 -> 9577 bytes lang/pt_BR.utf8/LC_MESSAGES/events.mo | Bin 9631 -> 9603 bytes lang/ru.utf8/LC_MESSAGES/events.mo | Bin 12287 -> 12263 bytes lang/si.utf8/LC_MESSAGES/events.mo | Bin 358 -> 464 bytes lang/sl.utf8/LC_MESSAGES/events.mo | Bin 9693 -> 9663 bytes lang/ta.utf8/LC_MESSAGES/events.mo | Bin 16031 -> 15981 bytes lang/tr.utf8/LC_MESSAGES/events.mo | Bin 733 -> 705 bytes lang/uk.utf8/LC_MESSAGES/events.mo | Bin 11823 -> 11801 bytes 19 files changed, 0 insertions(+), 0 deletions(-) diff --git a/lang/ar.utf8/LC_MESSAGES/events.mo b/lang/ar.utf8/LC_MESSAGES/events.mo index b4010e6c8a4b9836cf3f2e536197c11318f6e903..4027058a3d31176dc974e9e3d83f1e293ed6a546 100644 GIT binary patch delta 314 zcmZn_c*{TGOZ_8828O%L3=C`x3=9WY85jhB^l>0938ZfTX&xZ`2}sKV>Hk2Q4@k=b z6@zFUC~Xa-xqy5xAPv$N3Kh?X^6P-KI8b~BkQM~e8-TP0kUqr55C)d`4HS?D3J9}9 z4AKYEAPrVP8l)izNGk#96d)}Dq}zeCI*?ufq(K&+fYO(NG{|B1HlJinVRAFmHL%n* zFjg=$urjsOHUI)HpTy!4-H@WhyyBe1lKdho1+V;!#5@J(^33d<%)H|4Oa+^gqQv5i h)FOup-IBZ%y`<F5T`aDQY(@q~W_p&Jud(Vg0RX^AGn4=T delta 338 zcmaFM-zqTSOZ_iK28O%L3=C`x3=B6|85jhB^nD;L38X&&X&xZW1(cKp(&9jx4@jFr zX%{FR45Yb$;z>Xnq%RjL-VWtY1JdF^@eM#)5J;Z@(iTAa78^qtSVEYcfk7H5pbex! z2Dt-ikcJ>24bqSWq?Lel4Um=q(({3|I*{H0q(SN*K<U>&8sxxln@=*PFvXha8d&NY znkX2WS{Ybs8vp^9PhxS2Zb(sLUU5!hNq&))f>UWpNorAAPNjdETV_$Jf=yC>S{jhy zP+FW?RIHbmUzA#qQ>mAcUtE%!qF0`p1k{(RmtT~=xtPV3k=<0!LN_I~Z1X-=eI@{s Cq&-{! diff --git a/lang/br.utf8/LC_MESSAGES/events.mo b/lang/br.utf8/LC_MESSAGES/events.mo index 37604cdcaab5336af091cd095f5546e4417d150f..a6f1255dcd2ae4f6d8e297d99810ff37b4a915fc 100644 GIT binary patch literal 798 zcmZWnO>fgc5DgzEA31XvB&ce^#%re~*-czXOIk$`8U<6hI?2>Aa(1oVbs<v!27iwe zKZM;nYJxk`lXu2%-p(8CpJz{hw`@GJcy961;)%r@3l}|$zmHojX1|VeA5$fN;KGpc zl3bzExfC3x!?TaWixFInW?#mapS>BDQW-MQS-vEvRi;TIIe61cGQ*qP^^gy#K@Jg( z-V(|P?LbKRF?&sIc%M>jNTO1qi_}QPVJM{7tYxKPAf&>w*u4LVbNdt9Qe`K$L1a~$ zAxSV@b2wFKq=3Q7m{Ri~SV?Vg+1_gj%G!kvva~>BFu)rWMw7CrvRnjn73@?w%#}A8 zPewb#VVinqQW)Dql1*uy82n{|vPg5$hs7#Y8cqB)IVZgx&l$Q#MMh#Fm$}F|^yazo zzLN_SsA{L*YFl@UX)|wHR4OgVxm0V-LD=bRs^jB6)W`Txz$?2th+_!h-~j5B!V8-> z3*Bacg+9Ck3LFmibfTtCv(Rl8SmboVJDq){4&2^uCvpQG`Tyt4cQ<WsrZ8|m?i(na qK=^>sZ7{kI7_7ly%y+{@Ta$J#bkCoAZf-Q{I33G5@_oPWwf+HQg~_A< delta 125 zcmbQo_KazQ3X=}wM74yVNLR-oU0;7)7e@sTe=A>4?I?dgS1zB#;u77EqQt!7oWzp+ zA}a;Q(vtk##FEV9#GIT;h4j?C)S|?a)D)n?y!6t<^i<uD)WlpXg}nT{)X7SWrIIkc VsR|+a`8nB{B?`uR26_fu3;_3yDCYnG diff --git a/lang/ca.utf8/LC_MESSAGES/events.mo b/lang/ca.utf8/LC_MESSAGES/events.mo index c5f2e0f3c56ba3ccecb6b58ee3b925b5f0609aca..91fd95225b065e12d155bdab6dbceef26f54b8b1 100644 GIT binary patch delta 274 zcmdnVGm~e+nffJ+3=9Iy3=Db<3=CUY7#O&K^b;V>38ddc>7P)VnU#To2Pn=9q(Smh zKpLc81xN!CgFcWD0Me#F8mNWA4M>XuX>TAc3#1c)G%t{@2h!|7x|bDbtuc@>6(|67 z1H(xm%>ks(0%-#veGN#1EMRA2U;rWpq0N<y7a1ofGD|vH>KYg;7#diaT51~r0hdo= zafxn7QDR<kPGU)Zk(EMVX=+hoPKknhVo_0MdVaBjO*)iS2$9)*n3;o-&B(yWOwV%j IWtKEX08u6_LI3~& delta 309 zcmXZWF-yZh6bJBkB-L0Vb`fz94jgp3a7nRXaB%3Tv<iNJ2VNx@PfRWaC-EByPMw@O zb_x#8I=VRcAso7P_8$m5?sxZp$Gw|{AK}aQ)0ZIXc|;Kr{rW^*=y!-V;8yEy>pl#T ztM;72=Kc_(XoRH)uOObH3EYOaa1TDiP51`a;i7}FeQe$l;1Bc%1Gt15u3$6ZV2ubx zN&P5(MCuB*WzH_Ux8>;#=f*h`nW{S;sY^4>O5|$D&ZIKY$w28?^|jQ;sg7Uoyq&`{ qt_qeHRc0Mqmgii2F04FBw2t$p_0V?exfh7wD2fN_+Dz-(pNM}Wz&U3C diff --git a/lang/de_DE.utf8/LC_MESSAGES/events.mo b/lang/de_DE.utf8/LC_MESSAGES/events.mo index d5c1569bdd96a407c5e1fc62f6123d75a9e96ec6..f030e0ff097426a51b8ef028e72808ecf88f725c 100644 GIT binary patch delta 1316 zcmXZcT}abW7{~E5%D0&>m0PZ6tqe=&bZNd!f?6qh5o#ty1l7oqf-mJ}kQIN3AnHbX zA*zeQEP^C323kZ(7rj7r6O=@OT__n;N`X*UeSdCa{y%3A=bY!9=bXQf8}2rYTv=o- zH^wC87{k}Nc;sUVreX_L;C{@)i<pLkn2y7^7RN9NKjC7W#HBce8aI!cFWF(t63j~I zb{NZK5GW*Y%Vta!YQhPpF{|(|deN~WJ}`)@`Q3=>*M%iGfXVm_mEZ+3m3fbLoI$?k z4-X%v=f>v^<XXnK34{r#!x=2Y8<>Lca2b9;C78hg&SE7Ny5bc&gi3T3RhdE5#vY&+ z^aQn`*Qj}?kfN9$77cZ@tu$s0W+8K%Qq&F_P#q7TN*_U$_B3i?1E|v8MkN}?4fqI4 z@hj?RvdOOTPSip@sH3vHG_q;<Q5}O=jP<C}Mp21|P>DuSJ0C|)IG>2CjOv$$s-!#d zdo$+oyB^uA2`AzfQc=rXqM-_0M_rbCSdC*?fpb`d1<X=~yRZPyqbf9l+VM+N<$j@# zDus)pQfK1A&J%htK)eW@<Tq_J77;j!3nf9V^ek#eHxu!PsDz`ag}%jd{DLZdHs_~> z<)OyaqK;}Ws*)YJ5wD>x;d3muXw1@Bhi*^&4s1m&;2`e66Q~M3MZK<XP)9Y1+Tk=R z{u9;T#;Xy;I^2!tP<P=oZpU<zsZxz-De;j+-~w`-W(fT_jw;<8w&Fij1)BILp#_Ce zw>^UE(LyckPQv@h8*E09Q!%elm6}G4|DA6#7aAI^j+vKTsb1UBSY7Hjo1>qAR&k0y zeU9l73)&~rVy>KhwmtUdCSObVct@n8r`uN>iiT@EW&W~CpFiO9mwPt(YbvU}em?e{ zp-9x%+8gSQbcUimy)~ZT;ZR$+i(#HET_x>3o&D{hzDT%xTeL44ihXos*ldB=cV~0P Ee?a1uV*mgE delta 1317 zcmXZcOGwmF6vy#1`L;AG$7iPL(8K5We$Yd+z(s+eLZX||1_=izbqwvn(8@LuH4#D7 zBB@1KghOcrMPLLLM2SQeWQ&SuQBY8Vi@rab!I{sU!@c*Md+vX{+48XE-leEus+q;` zA+z`>z%q=-Hmt^OT#pwq0dL@1oW!j-gE6>>(YS;g@H1-NAJl$vPP28G5;4bVK}#Y~ zNMOogR)^Z~dzx7`CZ(Gd;ZD@T4%~u0sCj3w6elni-=OBbL$<Onn20OLpT%XEm0&Jv z-$NNev&{^~2x!7(tiZb%ho5jGenTZ#!Fr6!G^@egSd9TxqT8s>+(#YkDXO4Xr~`dK z?fVyLidiu0|3oL|^1T4bVa=!$bfYE?p;|wVYV8G7VH2p<K0qaUg4^*Wmf>&I)wsy6 z^@XTHt58=J+{++^!G6@l4lKqlRBO+o5>2BL&7n>{kJ>Pi_9>o<nwNv>q&xDx1#|e` zg&f+(BJmK?(V*R8pjO>MJuHu~4qu>J8_nuMY`|Ld;x@d7>d-9e#EYoTt>FRDRb``E zosX+0k64BE#P?tt`E8Uz6oK=omV{81UO}B`DiVK&N;roqbO|f*C#v->?oWl4qt+cp zT~$A-lc#Y9PNE*d1uPCSh{~f%bR#iqLlrQL&3F#gq1U(xKccSc7wUw|sJMflgyuW3 z2|KY3ucDrXAGi;5Nv2NqU{HxqMgrH7Yqe=~<2<T$(fm_r$7ECodQk<9p`P|}bYTcp z*i6L7$S>Juk=wENs7@`T*2lUmd3BJ=YE8@!WyTjdEPNpTtHXJWfT}pfTdzUhC&EpM zZxbB(;f&NyM|xn$GcfG)1djR#h8z1lK5ro4E%lCs2A!EzH6<10dG5*vcX^SUSK>j> xaG<0;vcNyokk{(>jpgkc^7r`sqrQ<|Pk&=y__;I55nEe&#CzNwUPx<A`UeL`l+FME diff --git a/lang/en_US/LC_MESSAGES/events.mo b/lang/en_US/LC_MESSAGES/events.mo index 0b01990f205accfd6f39a036b946d2fec5f557e5..e78365dc3bc1c876aa52ba305062a2a305ca3a9f 100644 GIT binary patch delta 1312 zcmXZcO-R&17{~F)eBIWU%$L>7+-wOq+|ug5P((=K(m{}-LOet$5ky23iCOR`vJjy> z*c(&_gRBnW$`*L34y8qx4l6;Y){qDS7di;N()XtWyMAWInR%XhW;ENr(4L&l^()Nm zOqE$BUd0V~7mM%}diV~@@E-=SwAw6)HMkxln2!m}!wxLR{it=lsQG76`}$anL)GRl zHEuFc!oUOca?FxgPycJkYz^ksm@#AxsDN=)B3*902iMX+kD7lAb+Ac{;xrau7FXgT zau&;}^_MdW)p9BZ!Wh9VxDgLx9bUjycpKN@V=TnCsFQv`1(?Gae#2(0CYw#T7nSfS zR0933f7y2f*O0^79n{82R7IvxrJ6xLmUb?nP8c9NmAD>N+L-IdQNK^P@ov;Uy-03$ z8dWiWn1=4|5i0Og)JD%;|0ODs8C2%8s5}0QjW~~CEMzwYXhN;`P?gw?I{8UdoIccB zxQN_>Z$mUx!ZFlIQn(slqXK_)&ZA2F12yj_YF&Vbq7v7k9&ZA5!V^dU8$`XG`=~pA zhN|>?tRTL9p`pwcQJMciWt`7Nt0HCCgdx-$X+zEHM9u3$mAc3MzR!6DRgoL0ijAQX z9(UtY7$v?<(@;sWs6f9_8%G+J7i>m7#x_*u$6en?rrJ2_ZA_z1o<Sw_3%6luczN9( zRN~#JM0(LzX6I<g0n`b7<YU+Qp^{GEAxxtJM)@0T#eJy%8v{rz8*%*!=Nr`ePpH6u z(Zd2Bj;?T5qa9f~X$J$Uz+q&L^`Nd~5S7?4D&rB<!ZFnRNz}$KaOtnanMS>x%;=M% z@8$ipRJUBd&3rGV9tJYO6?S0sXmMrgUe&Rjl6b6TM{KJXZrQ%G)k~Fz=7RqLF!+Nn delta 1309 zcmXZcO-R&17{~F)OkFiovvjx2R?BE*x0KqHAVNx(4uS*{B6L{~K|~ahmBoL9EJP?l zdV}b&5#}XUt-wn~<XUv;un~09LL!0`Itaagf4c*_erCp*d7gP@^n1r*M=F!=tu?b# z>&)a;+>Ce8hc7USZ?GKyVKJ6ho0Vb!>#-3FFor9!6D#o$YF!^{{%O>{30#Fk)#haz zHyJ2n;66t4%qDOn{Vz3U>#@LZ#*hV30b5at^tkaj*3v(Zntuzca1uk9#zI`e5?n^k zVk-h(E+c=yY&8S*xCNVWD|X`synq#W8|!c!i|{q-r0-AxK4ApEVi;@4rU?(A5<Y=S z;GFAU_T0cV<gj)JwebY1BGaf+%_1MmI2TbTEFn9UxDHj??XKU7`hCoe_oDXcLvpi| zsET>RG<0_lQGp+!HhSv%&rylYqB4Jvy5l))zy%DVkKGg?gj&BFRf&D5lOIRL=|{bV zi^wf_Hbg@uOrlQm7}ww{RNxQJ1yqTDpyvHVtt;W7sKm9X#~VYP@E8)n22pS49_r4Y zpep?qtB7x(X(+R0ROWwB85eTVsz?QHLqF<`w4>&Aq2~3VN*#B<?{{87RpbV$Vo6lO zV{UvJL&Uc<4V7dG73eo=<Hk+71v^lWu^pB9QP)o(Q*8|OHqxk*&!ZChg*&l4m|M3W zm3S{Ikv{a4*%=yg0CmCy^0DjuP)SqRjTuzHCjJIn@F42{#sCt_MqEGToI$Pshzk4{ zqgcel(G~7(up`-%b~2y}97g6?9CamwsKka*8IPbACQ<VzQ5!$Q>|cp9gL*sjqvO7B vm6vF#Zuxw}e9uljD4r{wdRZUN8%<Srl|>`XZIPDNU~}7^=&q@<n)%ZIT&08x diff --git a/lang/es.utf8/LC_MESSAGES/events.mo b/lang/es.utf8/LC_MESSAGES/events.mo index 2a28cb540422d1eede11060b18bb9b552e34f35d..97a4bb92bdfe145edb87166770887092197e28b5 100644 GIT binary patch delta 1437 zcmXZcTSyd99LMovR_49is+HDQc5iGomAn)sVUX2U7D!k$)}$L=VrPvm&=5oykc^~W zj0iz2h%)x6hq8x~pdJ#sKv9HHba}5Jh`zsemSsNYoS8Z2|Noz}JJxu&apYC1Kg$>s z3mG#T&*35*z-c&&mG}&E@h_%hPM$HDSb)V?f>W>oC*xL}fjd#}9Yj6vq2~253oqmu z|9|5u1KA7=;$Vs~Be;NmQ-Lva(L=uG5|`yTjGAy9=irB6{1;mEiy17!dR&SJa4KFv zz1N5NI55XgW|Uw+m3W9+U_9^-TJ#qa1<9KR)PyaVfoD+@^`SBw#tIxk8$aVJoHZ|* z;09Cx+fkL->j#Y^$k&|Yl7`oCI^GDphg$eC&d0AvvL>xC8MuWCpd#q63*3nc)D8Ma zaUuP)s0#VRG_-e5Q4c;xMffHdpTL>)zhVyl3C1&cTY*eRO|+Ve?r%eO&$QrL^iUPL zgW7^8sLY=u#qrJi;DMj0338ZKr7lAC*PsHZM^$D|aDN|a%RH>a^SBHjVhMghWtz!B zqG?J|fo#Ji=wgWVO)m{qU=UTpTd31{A9W}nVHv(aZOLEM^SM^CQX%A+m||2wwWxq2 zs7zZ?na5CDb^<ki1#O?kAPp__62tfr6+murGU7tiVX8p3-L#^fJB=&w9xldrsGqOW zW|vF}R^dieC605^&vyV7=n(qK<N*yuIEu<}3|Hb?)agxS)@BT&N_P>pw<D;MenSgW zsJk*;hzg(<6?ijh;d8hNFQfi%UYD5t|1<y2fENCZRM(`FPAV=(1!kkpL<Q=PWeci= zEvPfnfqMTGDzjcxMQ`@sp7vwLaasyfGb)`kiJ|mQnUl<}{sURF6W2qL6hC7}#NOo| zi+a)SE_;&`cWbQjaCwy-uC~K#tg>)TW%bH1pNy@J7q@rEoG!1!iFe0ptj%t$)3xI5 zu64L4W_fWZX4N%Bnwr`)UV6xF-4O4IJEhU?^}O5F*5kCf_HNhdtg)JoI$cq(-KuMk S_xt%1Q?+1ebs}1@FZ&;}Z>s44 delta 1414 zcmXZcTS!zv9LMo7yO>#4-qvm&HQjD2>(*9Efq_||Q7M55Nt>QzN!{2zN(6B#f-b&P z3%w*==wkH{#X?jN`VeLG6lC>K5!6Ew6%_Rlbou_=1Izi$%+8tl|Nk?)Hya-{-g=(t z<{M+;1;)suSdN33g(DcoCzy->a4O~&8Z!-xu@ozC5^lhWxCv)rGwQug)O-iE?m^7K z<Auf@Z=7LZIs=#RUWPHZv4s8}pD}ZA0Qs1!JQm|HYQaxfj6c2cTsjv08k~=9=*N9H z884vT8^R(?6}#z-o-&Zfz!++SPo8<~ZqZ+hByZYL3p$vMgQ$guP?-(m3LM1%{=sFq zbZ$DqR#X7HQI(0iUgI$GF{gP<!J9Y}?|42&ZTt!spqZDhNC_(N)u;fPy#6-NC@Rnc zUjG=D(H}%r$Q`DkvwMS@_#PGES8x0`&Ze)~GawH&zYG<~BIIK>@X+@*a(c$W)i{8v z&_mP_yhdgI9x0A%es~jRu$mUAM3p-1^&3$Ew4o}~<9*+U%6tIBcpeww7%s%Ws7%Yb zNHk3YDi9kh@c<UEzqw398Qn*fa0GQbpP(+~3#`Tus3R$~()0bON(GSXVrozUwV(p- zLS?!imH7$eo|;pr@f#R$Y22rwjXq)!f1v{K^V?FyD^QoI2|0eVA2sg`F2ToGf!|R- zUmt06YGw_t#O<g`oaCXOZweLY19X-7Ga8EUB`U+WxD>ykZtq-HZN-gPi5F34JBkW0 zgL+$7jLNVU6+jCr@V%&w&tW}YL;c@;S!mkFGoL{C+IS{XWmAfoxDFNAdeoh0LjAGq zLY2@#-I*h(_mim1E~6@XH+enl^Q=R(6sA^GI{Q*rroNk&S!SA(J9F|=XA3rG)Hv}- zZ=yTmL}R^)x~@pK?KpOo-Iu*(bD-Jok0uxo)JGh<)(QnfD+9shfl$z@4%G&eIYmBy z18;Xk6CE+D#g4}#ow2yJW_QAC)^+tnqTN*;v7Y3nqQ6<;s_IpNPP;#~)wg5%f2dTb A3;+NC diff --git a/lang/fr_FR.utf8/LC_MESSAGES/events.mo b/lang/fr_FR.utf8/LC_MESSAGES/events.mo index 7aa5b894cda0d1b7c16c572f8b80fa0514d508f5..48d86ce2b2069891ea699347e9fb6b065462e150 100644 GIT binary patch delta 1415 zcmXZbduYvZ7{~EvH<y{Q8QaX|=eE(D<2bXeZDegaX_m_na>=nB?bte-b51PLK@vjg zgoHxLEm2b_w@9SOAKO2Y{Noa8B}vqHf9$tfukYpedw$>NdA{e^lK#avd(sl4%q%+E zED!hN6g-VX@FAAta~y&FI25zTm<_`MoP<*`0|S_jjhKVWQ13OPo{yp8CNK*RjIqT3 z!EqLbvv2_~r<mQu3C!0N5FER3GM>d!e1r=44ITW2)3LB{z&aeyybb+$5C`Es%)}?i zC+t;WVj!arEabA#hqEwsteFq}sDm`4Uf7FecnSyOOLVZ;o&Urd=KVMytBcI$;SN;B zcQ6~D;z;aCxC<XpC;owYFn638)wYSK)|BHkY(Q0_6FI9TU9Y1m_zd-Yue<&Qm2o=3 zmEZ_eizlPvB?1hTNjs{faa5q)s9qjL^)l&t(OtidYQ;UAfuE5sST5yKoKjqXm8f_- z-StDLJ9h#n@_oC?fOqW$D!^yg6m~Tqi}_fFs>~v!NVeMDkK=6SM^LSMh&oswda=kE zxDySiip8)Pk6;1cw_6N!IbNbF(Ss`4Th~um!~8p{5?-#nGOR`QvJrJR)}r>Ks8a7k z-Tsr9hL_y^YwrGioWu9+H3RiJgT_{35h}oPRIfIpF5Mv{DSLqB_zsnM9uFzA0Mc&T zh-}#|4B$C;{}U>)6n;`_K^7*Ixx+w~pi1pU^)!GgWhbu3Gw8?PSdSiV5EtFH;!He@ z4&HTrgF5&hRK;c$n=Ql|)Is+Z+lK#_<2(!cIov>=wMVEIK4J!bN8RdQs7ehl8K}?{ z)N>W6{W^Dj6)M9m$ty#?=5#SqT50_2n0vY}4t+l?)tZtUv+}ynj9!_tE_3M;Pg7`H zI2P`RcoqcXp=!tP^H+F$RUW_J@l{oqm6!VX%WMqB;+_@JU?kQajCVw<oraFqV8p54 z9&T$7M`CSZXKp+ijJ1ZMwOyWgq{X`_l-!*EH%rmHvwhx{&^BMPt)MUU@wx7`g%#QV E007IXUH||9 delta 1417 zcmXZbSxA&o6u|ML=DuaAqiud#YGE_!*cfPwZDyecmW2;mqGMU+z&J$(HHDP~Ma57) zq+~>b5tP_N6ovE>Entg81cA|mLWK2ZO4<K7cbMUKx9@)UoO9=2)wP`uN5cHcBH~RE zQ9E%dc4HI{p$lJOBF<nmCZ>wSVmfAGHiqLioP(8^gw?3eHKDHeq3(MI<M3jt_-7k8 z7>H-!4n7SL8Nem<kEDq#!%LWj1DJ;+s2fVUhy`)D9POBkdvGzHMkjV-1P)^ienMU$ z-_rfTh$b1B$3W-;kyV(2`M4GJAjeQ2xQ>N*A0u%TEgU!f=!GIB^pkJ{R^mFmj2iJ9 zOu)}L7svc&U=sDjk&A*0vyp6z6SXwun2mL)ndm^yEPciy)C_(>T|aKd|Dr~0V{;8K z3$?^9)cyRmG&B-FYDzm%HwvKE@-}KM`;3px_-oWs4C4x%Myev&q)Yc%k6UmD>V8+u zcn{8}-;Wu*R-Vz|S^0{(!L%`%S?$}=hGnQJtVfb1hs}H^7SQiSE!_vygN0fm4s@X2 zjXKnfUBDdd#Wdb8FKOuI7)8y*7;4HUj8j-bKRh#-iH)cc?n15QKGeI>X6DbMrn(FD z_TR%Wd}8LGoB4ORn)l0h8d~c#3cCg!s2en+*6IZ6rRzb0llSPt->8x2vPdJVManJh z$dp{gG8{DXQ>cL@^Ow>RWT0OoFQlOsqo#TzYE5fVQ`v!a_yC<4&&}D4rO3sXbGQ<3 zp@naaKT!{!kQ2<<CM>64g?iAg9I2nZ9FG{#zrzdU8Tp8s;y)OU;km(E9gCVN3pGPU zsOz?y`5H5R2sOe}f&Qo;NoQ$kSYiD1n8ye2MvumZWl42le_Zn5&6JuDo6p<O*52CS zYw@(TmmX+nb^Cm7hx=&YS88gx%U<BLI}0qQvp7FLFP~3LMMJyKzS}(G@fKUnUU!q* x+tO%n^m<OZeb(A$N3d1ec+k`8^Rzjdy@3kbOoYo(z&EfrZ6d@PEKe^=_z%`qs@DJj diff --git a/lang/it_IT.utf8/LC_MESSAGES/events.mo b/lang/it_IT.utf8/LC_MESSAGES/events.mo index af35225f70255bba1d2a70e2d6a4225541b6d6aa..04f6de9ba69d383b2ca853c43bee945f7fb0131e 100644 GIT binary patch delta 1403 zcmXZcT}V@59LMoTE#I}Q)M=%s>{Xl1g_$$L$X-xElcI<ftsFGe4YpMZ)*%FwE&`c( zQv?+jRu{$WuJEF$unUP^t}2QlT?Eo?7}EFGCWrGm&pFR|UVi^G7uphSPd;YYD~&PX zVq;d}FjnDB%*JV~$G5l)7cmD5ON_}w2d={nxD*fI5^TpJJdAqpEUJG5HLi{MI8tKl z{}(s7QNWG6cstFQC%Be&ha<V5J}jj@ikoo~HQ;-!z^|x<|HX2&%8cPKO`M!~6f<!M zwSjB60>{ei<c$ZZ8&7d74_;vn&ZC9pjH-m1a2s}_2D*fFF;_7Q?_di~;vt;Jz1Xli zx#I!UMu%}Zj@eukaxsC5atamAXC!TtKu!Dyw_xEK;>HH#FkVhdW&rixDC)i2DW@=( z_6({dpHTDsL<{X4a#l3esM^$_cIH9Vv<3BID=IQCs>UI##VD#I_fQkRLoMV7?!$T1 zj@{Hr^E9KriG#=^+Z^M9vYGR!j@MJ|TdDRl)B;|j2Kba}e?bj2kGpUYRhov%<PN)0 zNev^9%p=r1GpTk0i<#e~TgibPs01oe32aH}Mi=efsG9Ymo?k{qJAxWGhFZ{LR8r4T zrJY43_6=341ghTxZeo6uM?v(08@J+qRD>ba$2ExCaU8Xi*Qk$k4z=T=^~rvFkg}TN zXyJKOGUKSy%%T=Nhf4G}+FDsUza2%Jh5FbEk+Dq`a+v*`PGA7FfLGXr3FHGemE>HF z9^{v6{HSpTQ41YKCH4ZF@eOK0*&EHN|2wVNn2e+bHE|Oz!IP+kok1lLKt(r%N-TyM zcpo+4L)1UCX;i-tvB~UTMg3eUc=c3XSK<>n-}BN<SL}5Ds`#zqjx=v>XNR-Pe<2VF zgo4f%U)1lh+%ES{r>oKF+F{kXJoSw=F8*@ceUYg1NZ1#Q^!lQqu*Yf*o%02)eHQ~g py@6n)Ct&T4hJBH9{%~`@GaBr!J>!piOViRaHC%0D-0v7K_y@*hqG$jB delta 1409 zcmXZbOGs2<7{>8etjR1Z%S+nrSZP_!G-KJtT1**T44D`sBDzgAlu(X1r;sQn7a~C} z#ByU;6a`{PF%?8~ZzI^QXi-$uLPBa)K@3v=#~C=B-}jyQ&Ubm<Gv9WmcHVuPohUG~ zmO?Yxfs61e=HPRz#vz=J|8O!+FEX2gC0K^#I1#tu1gyt=45QvVh8pjp=DmPZ@nVrB z#v466$m79He2`^!59iT8K0Cdkb}Xjfk4x|cYQpb0AAh4ZUQl9IitCWe8n}7!6nd~1 zb$}Z<6YrEH(hr_x9t>e6KYYaH=wUVot569w;0io}n&=8L)UM$qe1u!@1#U;r9J5Ur zLY=q^b<jSXfp-!#rqOtUigFk=@F$YCrBDm!&ox_$6}T8f$Ym}!CDVm^uOIc^!;Hh2 zOaCjXBtKB={6hy5W#p`AHlu2@19h@6s;2u<FCIZf=Avrcfj+#1D#>Hi!rxFE`HQva zp*}is9crDusBhvBG9+O!8kEmEQ3G#h`u8*aH>eGKKuz!?)BlB<$g_ZYV-c>v5bA{I zP)YS6L+ur6ov)dG3JY1^N}cq?epCW$Q3(Vy)}f#NZdA?MQNQ<~qP>oqcmTDb*Qli4 zqDni0O6(7+R4LTBS?scc^=%Oiy-<ghcmNe)2kPUxj4N>vb&^l0k8=!l;>v~TaeI)m zS~EJ>iArV=RhkjhhR0Be=J1zEXlJD~6zu}k$5w&NZ5xow4shFtZKw@=#0{82K6G14 z&dV^2{B!L*YM#rejrOAwdxt^%jM|V>ZU@Itx~4oGNiAyO2AqJWQ5!ppN}>%FT`wxJ z0nEmys0Cl*EPRg|H`?8wGn(H<OTlZT@@h@qn*3!-_CgDHAD&u}>@5ss)wnH@rufN- z+Zbz#2aiWiMqM}Ri=NKi74n9oXBy)?_il-}(Hf`9Usdf5_`QKuj(>ekz~7x$T<p|G wT8_q?+Q!JK=7_WLsBCNvHpk*_H0EoHy2-7@Sy?&NzQAhlvFMp({p_B+|15l|e*gdg diff --git a/lang/nb_NO.utf8/LC_MESSAGES/events.mo b/lang/nb_NO.utf8/LC_MESSAGES/events.mo index 76fda090d860bbe1487a47f89556188fc34ce25f..69937efa4b491943abf4b24628b607d231a510fd 100644 GIT binary patch delta 244 zcmXYrO-=$q5QY8O%_+(RSEQ$#A(~9X5OLwkfD00qrqN1+_Eb%}dq7rRK@MR&h?j8d z(p%63-|AJ~`%>@k?DzbmbNaQrH1CBfu7xUv>O)WItFo(-vU?Mvn0Z6NnaEzln3%RT zw*iL49vip4+2A6Dn6tN|1dwfwMKMEDYnZU#pj{b)TYjy-KOGF0F*ZR<x#SS>Isd1- z2V6FuU+^ggX^q1eQQ{gY2GO|Iu1M12aWa*o-OpP&KR$)S?7Yk#T<eV!ckJt+fl@!6 Rp(+M&v=zZc1-(IbvIi{sR5kzr delta 124 zcmaFJ{E}&c3Zwo+)x@AkSH~b-Uw>T}M+FamD_>9TD1Sd!E}z8W65WuZ#Ju91#FG3X zD+R~WlKkAnlFa19oSaI9^whl6qQsKa6rjSq^wPxiRNau S+dy!^b>$p(xSk}$og U3L*LVIoX*d3dVW{dInq!02lZvJOBUy diff --git a/lang/oc.utf8/LC_MESSAGES/events.mo b/lang/oc.utf8/LC_MESSAGES/events.mo index cc5d75b6ee7edb978210fae8564cd21f2fe94fa0..14357a852fc05d4201af7a465d4d24429a6e57d4 100644 GIT binary patch delta 37 tcmdnPxrcMZdL~{oT?0#917ih411nR@&3l;a8QF{sjLh^bCo{6_1^~rH3Aq3O delta 37 tcmdnPxrcMZdL~{IT|)z1LsJDqODiLb&3l;a8QDzrEOd*KCo{6_1^~xa3L^jj diff --git a/lang/ota.utf8/LC_MESSAGES/events.mo b/lang/ota.utf8/LC_MESSAGES/events.mo index d827dd049b9cf3e6a1357dd6a6052fdc02a595a0..5228cf99f9476346940e4c0e4a27f80c5e7898c3 100644 GIT binary patch delta 242 zcmXYrO-{ow5QX{K<rISi5*2dn27wf}sbIqjsVpL~9$@4Y96Rz%in`(o^bA~z4QsA} ztMXQF^xmg=zehjEpQndk+cW&AOm(eHt;{7%jlHTbOjdD^EUHZy*rUW`;Wosu>Ep;S z4>8f;H#hI}ltU_Ga*GTEH=-le6809_F}<z0gY%_G>@iGW-iZusweBJdy_Em-WQXe^ z#aDV3)($<1kft>SVKw;Ct%8n%da?ap*N^*`up3?0#e*LcdUY35&lW;%=aZXi27B8C LIB8%yD-Qku3OQ1? delta 124 zcmcc1{G4fm3Zw2s)x@AkSH~b-Uw>T}M+FamD_>9TD1Sd!E}z8W65WuZ#Ju91#FG3X zD+R~WlKkAnlFa19oSaI9^whl6qQsKa6rjSq^wPxiRNau S+dy!^b>$$E@sk}$og U3L*LVIoX*d3dVZIdS+Y<02A9OIRF3v diff --git a/lang/pt.utf8/LC_MESSAGES/events.mo b/lang/pt.utf8/LC_MESSAGES/events.mo index b2d9727d91ddb6d3bb2c14bf429e488a4ae83122..a42a91918572927ee5333be27249629c3a815dd0 100644 GIT binary patch delta 1397 zcmXZbOGs2v9LMo9YUZ=jCNs^t+Vr%sQ7Lf>H7plqTJ|6)qOk-Ab*5&f6f!PCiA4n^ zxhN|L(xOGeMASmsNEAU4)Izd|pdgACE$l_n_vehHpL5PV_uT*E^r!xF{phPid#*7i zG|w0(_Tdt|f+_e2*WweLjsGweXXO}^j=5NfE}VgNn1l_Oh1*f@9YpO9qt3N41N(D~ zJw3R@LM970@T$X@QC!5lG1nLiJCT3pJb#wq2x|Xx%)?2v@CWL>+y%yPgsH$4*oX?S z3)Aofl8fnIV8=4L#)2wv3tc#dOYsBdV+vo<a~H0}N>n8dqMmo+OgxPW@B-?b2l4q^ zRKQ7Wo`u<{d7;ffC0c_jWi{#`A7-OJ{=5f^n4d*rGIwzyK1J>OgevVXoR0}SRBQ84 zr7uCX#ES~7A+CLdK_Ux-sEwCVy%|Q8EQ)&ZAu8f$s2)$CD)bJOaZ*7nvtpdX+>0A= z3+nxIn2gus^I@c0wt2vSGtA4_g87Dud?t-lkMdCml%gtAi|XwbROT&Mibrudj-V>_ z0xkTGi*YW0RJn3grMIG!@0%b4WqK0zfitK|Ttrpk8hY>!s#TMyN_<82@;8!+Nws2` z7oh?xN4;N%s@M+H9omC>uM=0<40;%7;|QuZ_fP>$pnCZc_22R@l8h-N{54pE#dsKb zWQI`B$B=7aCUG<VL{+$ovgp$8MOC&HZDoF(fg<chW!Q)Mz+l|RxSRPDR^V2mslrpJ zR*WOxFmK}Xzo?2h$wn=4qY~VO)!2e+@eP;RKV8cEEa=jVqlK?A38zq(<`3$?1a6$} zLO!YmZcN1WsKBdHA8LxbZ{T*ym#l79R7dSx&2Mk?X6pO&1k*flAj26Qn%Csmo3^dV z-Q0T2ANB_W?oGZ(tJf;?l&y1nD&3xPtF+YX@f3UbNo(+hBkr9cUm)D!iv&Ynt3KG~ s3s|*X{`L-kAl&Y^Hbg?ca9eArrrR9}94cvvHqTFXI4Yz2a|<&61Cc16H~;_u delta 1397 zcmXZbOGs2v9LMn^W9Bon#_^SPGqp0u9L))&QmG*_#Rn8^d`viqVG7PwgBCYyAuh6o zV3AUR1qC6>w9p`^MNzhJ6B0yVw1|+bMNkk_-=8xuGoN$LJ?Gs2`Tx&+t^ZR$_s-?G zjWGk6#>i3JgVUIZ&#?;MU^@Q8Buw`hlY%+88;fugp2T=;#0+djt?NR~w^93E!Biac z7-yp~$v_$dGdLS(%slR--<fTUg%0vD_xLHsMb!LN%*A!IFexXxt_0b_oWgzBi3)HG zlko;}7IQntiJoYV0cGGR7UD7%;ZMxNT;8JRe%z0zQJEM-J$G<3PN4#vLG7~?>wiZD z>|yd2EJXDy9U4ke9V(SAsExw74KK%@k7EJ-J4j6C1@6EPsCmCpsZC)p3*ERKOHrw> zMU|uh6<B*r=Nb(c0}oLXAERosfJ)g*)WWx@h(DrgyoSoq57dc0`O%Xd#jW%kunyZ$ z>+fO$&d2%-NYxy(M1wud=jgyj0Tp=;7eF=gqBb~=%FG#5ZQD>Mx6y~AxEB{unfinl z#_uwQk166unG2vY-GOf2Z$@b7q?4!@OrtV!AC-wYEXQZ4Qmvyh@fTIgR4aNp^H3*0 zgbFNxT7M3eu@LGGT|ljKu*9J;PD2wHu@GOO0$4-U@)zoFnNHXo%2eV(Y{dh36)l`a zJzqwygIUJ_CX)_jxEXb6FQPI#jE+uzorWTuK%MXw>IDyDzQ?ol6N%z5cHl9*i7Lek z@-Fi&)=%dY)YcTEN>YP5U^mvFjVkezLesvH%GV4ibt`D$7mUY5ZkR4j8frr~>MnRu zC8$9cHlhM=LA@v#vv1;2;#$TCBg&&@mh!z6d62Z4;wmt$6P>B<$W&%iT(vzA><jk> z?VkR=aBX+6H)PwPve001bCb6<G}IGj+<PKuhpMfL@(RDVyvkeQw|xF;pKl^9E6WoO j2m9>)uyrKXs2vWSFYAf~vJ&DF{AFFCq4G#Wc7ED_t~Q;- diff --git a/lang/pt_BR.utf8/LC_MESSAGES/events.mo b/lang/pt_BR.utf8/LC_MESSAGES/events.mo index f17c87f531c50b4b8a78aa137736a95bc6f838f7..8b6d3b7465e0787d30361227f52aa672b7a4ca0f 100644 GIT binary patch delta 1398 zcmXZbT}YEr9LMpqEY0_<nX}BE%+j*eV^);e8=2-5OsObRan_(=OxlJT6c1ul^m0*B z6eUuSQA7#@DG&;Ru#3Qp2qFrDZbB;RqVA;c&rNinbI#6lp8v~nsJ_2`^nH@E$QTpO zHO7q>a3$Ww`S=ny;{;~o45nh%Vq?<LgDbEY=V1-b#Rkm5Ce(AsQTrpPa~;gUON)&& zJGjY0CJPVnsmqvAEM(s5F~-8P$gdgXV>OPU_J77DIE@x2=Nq#eOOPW>EtX;%D!_}F zhS!i>%<X(9p3yTFRDtJMjPGz2{=uc_;Vt@p6RyEJR3##)@6TZh-a!R;A9c<|V*Uda zu$#?UScICFI}B8!U8qtvq7FKZ3-L_i`>R;Q{2mgMd5H!15w-6xs<f#r=HWtAYuBPm zzZ2CGA1bhxgiarWBo>BI8=s(hGlnYJYt(~pQ4xPa_4qrgLO)R%yO+f?+l~vE`*05) zLp?u;$vBdjk0D)i%mf3@Fkj;fW+q`C7eGC-Q3uqZDsu$Y+jdmuJy?#Hu>{9amHLJj zrf?y2CrbHH<@Td0-GOf2Z_YDNrgu;;7(!Lz5vmd~wDA?HRliY{a9Qz;vrz{ZqB7r# z3ak$Gd<&{#LDU@zq3%FGu67vQV4#gJP`!DB3Sb)5%YUeU%WT3XDN}(8s2R(!A1xd~ zeLsm@2lET}VLIhdg_}^9HiQbW7ae7Og@Gcxj>_;BZouJ$AMh~qWTM!CtyqnBQLUIl z-evwJ=J{lyirA=@_)rOku?ij3<s2_IZL_7EWI?6-f)@V5xwwEErc0BHTozM+x(l08 zEvUsLJcJ7TDC$MQgxv#==TGPKvZ6X_=Q{rSVh>ZN(&w1wf%Xh{>|So8t0nC~qqjNG z9gGCKLf&eBG*D@6v^Q??+S|N#g;ic&Y1?HsKWPpANYr~U><>jw`J-LoO3T-E(jT(+ q_5?dm1w)a}pj8zO`y(d<;oZI7Xz0ZHjzFw6&+T$;i}iR)Gyelc!ktwB delta 1408 zcmXZbSx8h-9LMn^=Dy{=ly%(=mu4&_#KK7JK}C~DNQfrq8j_8q;}+2rA(2o)RumQn z(&9r=nUq8j21WT0h=EX0ggrz)1rbG2-=8y}e$F}f+;ji`|M`2We!TwvLa;B%7}Jw% zj697wcn!nxIhNyVjKhBzfpICuL}5B+VIGFyPF#hJ7>_Ne_uQ!cUeviKFdEOK7~jg^ zG7B**OyH{^V;<l-=0j=5Sa=2bF*E$h$9dHLUzmYm>HdA$xSshA<OtJ_g?JPd-~>kE zB$A7Hl<xCq^o9jhU;%USE9PM=r?0`ysOQbN0c}(z&Ig`f#W3d6r~sd#&RGh~BRE6> zJ1`cv1?G)D1}ae-s+5ON2c5+PycBqT2eX+!L1HuSaV`Ep?Mq;y(&k|*mSQ5-p-R6O zbtN6Bzy<^Qt}_T`VHUOVCF*YGQ6>9`dhrV?gP*874$1UaC=!*i1C?1TuEq}BjsvLo zXD}4!0`qy~_IzfE0cV=O{soi4#q+>yLfugV>VN~N%6L(CdkmHNC_3>L7T_YPQfdSX za!@@{!yi@7jjHr<Oyc|I1_Na}jrzb-R3%=ZD)AN__yKiQF_ym)nW&75PzP6{GT)5~ z%tpOGh^p8ys)t5U?~P-=kHI|#+V~E0@iQubFv8SbCZhf=i;(0@6DlAN7U4KrIEQ+^ zjI@P`q71cIfU59eRMU>20=$SmWqzB1BD{;r@F8x**?`}15A$rI*os3~gO5>H@dx>) zNhEsB%TX0+KwU`(D#3GDg=47ZT+B85R+?&=1(ohMS{Rk*52zT`G)`1cY(e$HE>ttN zV=x{;1>T4H&~U&Dqm$v^;!m=oI%;Pzzk^dZBR)k1XPcJM{n1HNW64cHmEIngr?<=H z?d<mSRv&bA*<P<*YWGDpH<h&5{hht6m(;ktcBNJ3D61%Ol$SUwEQhnw=^TwoP0gxx u_jY@%*6ywjtIF1RfMIoiXTPh<-C63fy;F6mNkO3%rEa_5G1Zn<81o->KBC9~ diff --git a/lang/ru.utf8/LC_MESSAGES/events.mo b/lang/ru.utf8/LC_MESSAGES/events.mo index cb97dde3a392cd76c596dbbb380d54be390f07a8..d718a6e42b888480c9ec8ee5d8c51f8158b648c8 100644 GIT binary patch delta 1403 zcmXZbOGuPa6u|K_TH-6s@|C7erajb|##c0&Jxq;E@R5m`q+<$>+88sVsVxRcC1@q0 z1WAiXM354VAY4d@5G~piMnMsQC`oM!f{5&YoKNuk-Sa)pJ@?!@TTSatbMFFudqu<@ zBNB}xcn}|8FfL&}zQRcShangdD-wqBcmUIICzjz3tiUL&M*Z$O>iZtlygm%ayRqWi z9*lEi7dIZ^x=CaXD~a#Ni)7;lUc!WZBBkg;YLREC1-`};C<!9TU`H*i9?Nk6cjF4` zLRaw^ZYKCx1%t#yk;9mWI&q`nEb5DYjpteWMQVs^F%+lKjPpkP7OROr<7KoZ`3oPx zJ;WbTJN_L>DJCD~>PnI@3v*EmYQ{|LNBv+5?f3#S@F%8VG8cXC6ly^osQG$O3%`dt z@gi!TH+UAm;%W4ynf-gxigaCWU>pvi?$IRDU70~`=`!jbuAwISfttw1!*slfy0Qsu z#d)L|5|`@FpNB_@FCts(lO6_EZcL*lSVar|HR23Tq<&C^THt^YkD{({5le6rwL`Wv z{~77P3&eAH9{-@`DP|J23sab1CK%{SpQE;F86$Aja1G0df8il4<f2>Dg4&tes0qiB zZIvaY7Wrgo;$Q_4rz2gLEW-*cW`5~npnLWd%WwthnuH(mpKdFb5VxYPc*eN@0rmYz zX4ekZA;&_74Ig72@iOX}N#LUMlp;R>xr#nrX)gm^(J-3vA=cv(YJzx9*eG^m1DZIg z{n&`qAgfq}Tc}%C#6|OUqW)TjP&@k`UHA=Iqg=7dwe3?mNcp<A<5-BZ7=arYioa1? z7r^rQR#K7OkSYwvUeu1=K|Op^#`BkBGr^ltecaI_9mqcw%HYCO$d|AHsTpeyk6xIJ zsWdf)R#jSR+IpQHXSd5z;_$W=m~(77$1OIy#g=Q%wiV>tGj05ZRyaIfORd}C@^m@8 z-R=T&d3U?RWj=Sy+1cfEc{-iulU}#O)86Jj(`WIzTCFW@3k`AQrXWpcwJ$u0ua5f< DFd49q delta 1414 zcmXZbT}YE*6u|LgD={_Anr_q5Wtx3$ZHl6%WU|yuF(*j~eGDBYOe?p<LRjC{g@O)( zP`imPy3iD(tHcDM4<ZNy2@#@;x(JMdx(Let$Gxz<zvn!A-}9d5JkP$KSv@oP!4x)& zNO!7;+K)SN6yxv(7UFA6##P*i$!Q`9Xu}<tiyN>S*JC}VpcnPIHq`q=sClnqBHm4t z@Y>)W6PuWLi2q_lrm%r{(kkLcv57Q6J|4l_NG<XfwZP9<jMj9KOl(9g>;l%}FmA?W z)P=6#0W@WVd5A$lhDaXPp}sg|IFEXxd0X^%4|<6M7>~1P$2Ug&4I7Dn;|Z+U9xeO< zZXy1OTX6$bl3HwG2D*|$+>ggl3+lyvIE4DZ43^+~+=~fWB7CHXi{96aTF`aWd;_S3 zKSX`;0&1Qwco_fSAq<z<qkGbabX{(t1xHc$=qb`&nL};q66zlQMNJgTq$aAtT)cz2 zvI%U%H%K$&Kz1~L9qu7+MYc990}Px@%%Ub(K?j;Si5gd;K5!1Tz+oePjJm=F^q?sx z+MycMGjbh|;w!AiEhN)CErvHSi}%X}16}Dm)K)EF60R8j#VX>&T_Sv>iHmO071Yi| zP!o?M+bfGmP4dgo#=(juE=Rg99>X@Q<o$A!f$rH$tiolaYm&1&db+F7L)?eD;yGjf z2kQNKyj?rkg&YqVGklKC#7n4WCZCJG=M?e>kRXP2rGpG~MPq2kXLu49Q4_d0VMExD zEokGUUd9kogREdVTG%(;x)WS9-wo7%%P4ARzhf7!qgvJ|7oBo??Nr`pLictY58^x~ zp)NNbl2Kcij^vP1WLM-I>edaSc5DRo@XZ*%e~dhi`<8Nz89mY_zEmo`(~mZON-$-M zH*z-7JUx=y5K|iJ_H_jVzEH>cu3$yGFW?V_{7(O+_~Q)@ufL}w$h^bj3;9d!`L6sz zhpWWlDzLlUr7m}5lf{x*-`y1G=qW0(mvwdr*G5Gp744nAj)1fEeCKqPr8Xv^(CIF4 Tx*cu)p7_X=)jIveT5S0bOj)w< diff --git a/lang/si.utf8/LC_MESSAGES/events.mo b/lang/si.utf8/LC_MESSAGES/events.mo index 94286e0ede0f4f67f51d77dcb9acac48f955819f..c834683e6b7cea04f509a0b0b537a34f90c7134c 100644 GIT binary patch delta 230 zcmYjLJ5B>J6y($GA<!#Ag}inafmkP5K|@7>L?r4Lc=9gtKCk?2P%5rK8qP(@2{;Ev zX_#t8GozWGqp#zS+2QBz6yF=uTq@Hla}IN3FWNIxP~D*<wT{T*F@<R1h68WMkOh_; zB29jK{YIN&QXQh}N`YNQM`}$tT6h?^M~|@VQ<}tT-3LkZT>tCnDTWt1c}k)kdP<2j zy%Hx;lh1BoErafM-}&lszXN|y>$bZ0Iigp$A&z2!OSni`H4E6&6+;K}MRo8CAIVOM delta 124 zcmcb>{ETUW3Zu?M)x@AkSH~b-Uw>T}M+FamD_>9TD1Sd!E}z8W65WuZ#Ju91#FG3X zD+R~WlKkAnlFa19oSaI9^whl6qQsKa6rjSq^wPxiRNau S+dy!^b>$;ynSk}$og U3L*LVIoX*d3dVZIdS+Y<01HMb9smFU diff --git a/lang/sl.utf8/LC_MESSAGES/events.mo b/lang/sl.utf8/LC_MESSAGES/events.mo index aa520aad9890b5fcfc378088c54920fbd17105ec..775b9b393e84af3f9bcd09b7a6191eab7385e5b2 100644 GIT binary patch delta 1376 zcmXZbOGs2v9LMo9I+m|A%jcwZ%U4As<B(P+DWVl=_~=1Jh53kvAj1g}BQFwYwrMd8 z6uK%4icn*q2nwN1ZW4+XRthaN1R;XTXp!{&>0Qix&N=hH=l=iyb7r=7qIUdaw7t<7 z)0t*WD)wV84q+@#U<poQGA?5rCZ!vbfSI@z^DqVvq8sZl1shS%wW8vKsC{ir#9Qgc zUKxa0SjWPBoOT&Ajt7_zW*Tz>CsB!P&f<f|QSm`k0exs;7?sd-^x!lW;7?qO9v0(q z8*V_a$6l?dk_Fyjj$l5vIu4><IELh8rf@sX;~Grbv|4EzYF>!NScHCT#v%-%I`9Tn z`4p;?Uu*_R4Cav}%`a47Qub<w9#kv6n1dB);W<=%FDmi7sOO%cI`SHo@OxC{3(oov zQ~_=_+l985fzIv>>TH@(nFUY}22mSdN40za6(2?mhmbwZD^#MNP;rY`j(<@FmeNM; zSA)81N0BDlriB6b%G|*97($)Rh%<kIs%R3m!Hly$ht<q~;|?s_!ntA_=HUS9D4(J( z^#{~@mXMrGqNPM^V|Fso#ucc_51}qeqccB?I>U>o1jDFKJwnBgp(=Wbs&on!H;cOc z-!U4OQNKrVLuCpU@_u76&;y52m7GSk{3g;Oa|cz~6IAB!um~5?LJvRc2==4S{FGw{ zRx<BLCHNM#?+ogQ7SPtMU1Fe${-Ij5hJNXiWTP%g4OU_&7GMN-<7dpkWcsa^7xJZf z9qMwnp^l&z)u|EOgQKW|zvY?pD}RSSET~m(qAALxVhol#mZKi1LRE4M6@LkJ>AD?n zI}W4bM?;Te7gM?!DXnNeDyJ{<Fz#zYlxYezC#FV*)9PKh@eTFfruMEtFwk+uTYceL zdzt0;`AfaNeO`Zw<@cBQ_D1qEg084yU*uk9l`GB)^jJMDfzHdJxvXaQ$&R-6NS&u4 P+TC?E5R5#?DU1CNUCfxk delta 1406 zcmXZbPe@cz6vy#9jhUmCS>xX_UzU~8*vyn!Lx@O`5s`>O8HF#K!at1Vj3x?=i4_G! zAV^V*B87sA;;m{^NQ)9(1Vs@<Z3-f(Ru#6W?@!-CKlfhmefOMmk5Bh}*z@qUH|aOV zbmbT$&*Lh*fob>*EAS;|;Xm|YR<1D_Sb!_B6zAhEOu@aFjrFK=M^XC|sP7J7CSJ%j z$=SgO3kzAejo&@SJi;pGH}j2Y#E+;zDhiBQf$gaM7jY?GMGNnt0-C}?{DEsQoyh_W zVLEO@t}r_alXDpzWPv+O6ie~6>nQ5N3DkkFxDL%SV^X0Em1)qO*I<bGZVY2Tmg56d z1wNoM|B9;QuOx#-49s%M30bI(Ww-<@P#4r<5jLTPy{P@yP=PzBbCalwyh8;%jmq51 zVXbGN5-7$En5<=>x9di|O+PBKA=JUkr~pS$B_BiWchJJe$Q|ZAD$w7keLl*z9rIA< z8&KbCLG@ZYQp}_oU_f7)>$n&np)wzL=WkGve?fg<#$BIR%=<B4ie<PTOK=cZ;~45G zCs0lO6LpW@nych`^mD(dWuOlqLS-IBHA$yC@56HDL#O~9RHdGy_D`WQdW*_*8ny2) zs{1qPNG~o&{oQiih-<muR58$jW2j6{q2BTqQX+F7mDvlVy5<`yARmhsZbd!80o0rK zxL(3)=A)<pKcl`ogL<M=%92zU`xz*sWvCLZMm0$U)g&!gjaN|rPOoq?{=qy9P;Zrd zC%-grLpA3h>IrV3Dm9LkIEhL)rPLgr{ddS=mr7NFBxk}n9~)eoPzRb(nVdxJKZk0% z5!XAePf+_Ohwr9+%Ra+MVR`vcI=#-A?^A}i#MBQrXZoG%Ir}`3M3)_lci4&6&RD$q zxZM#=B%<Zf?)18Sf%<4qYn=7K4m%NzSm9uJOCVSo2vu0Y%E+dWvoJ62@oaSl^J_f5 qM61=>XC1NohNlXe(;827**zU?wi7C>^Dc<l@q`^~cg_|?(*6VJiJ<8K diff --git a/lang/ta.utf8/LC_MESSAGES/events.mo b/lang/ta.utf8/LC_MESSAGES/events.mo index dd5a9c6ec765da61b83d030b0a083e8c54d8edf2..98c1b2f02f360239eac7cd181d5c59f1cc6f9346 100644 GIT binary patch delta 1402 zcmXZbX-Ly?7{KvoTH&S^na4Ue&CIgZoY`R}#WZMH9$gIVm^N}LTWu>1jA3-rs}x^E zUPKa6fgwTk#(F1Gks(47B^8351VO#1===0n*gn7Cv;BVm=a@ZbzO?j@#>eJ~NH|R- z6))gYyp9Pth=n+WbMP;kadx^$B3dyMvvDSF!x>nPbFm8bTpjB7BdGVrFbOZEOYHxm zgFrHY8+gqm(vKeY9;;yomav~dFWTmdxbQS?#=BUBA8{`hFA#BK8-{QU$z8TBBw4IM zJ=eN0W&{QaY$xym^}y;yBImFh{kU|oaq(sBVgD9WFt)@<tPQoI5p2RYcmns?L>h1i z9k|xc-B^o+<Z5hsaUV+wj37BkI(^lIT$qb3xC{HR5`UmxRF-8V7DcUW7_~Fsa4lvr zPjZp{NG);-1K5sr82iIQXXN9ww&(=XeYuEQQ8(&??%;YHNBw@vGGi23xSM?o7U2uj z$`W%#?qMF%EO~``|4(#d@^X<wJYT9<XodGtKNv^7XbC~xd$9t$P%HU@n=xsHk&p++ z*q_CI^sW>+hL${$<9HGe;v|xrl&vy4(||UfFTE_Z(#NQ+e}+6mrch^=!do3~!F;@l z`u#CX#`ma+e#a6lq+NPGiaLTW)Q-KJ-oHa~k&HF`2k?Axv(Ug(4d5@-Ok15ITk#g2 z#VI_1jbx$wVeG}747>)vq8E1*7(c>3G?{2WSyU3Y6^am&<EW#&i7_*a*DSQ<lSt8$ zxz6~J>_V;lAx_~UPF=xS8$>#=4L4#gZ`D|fI-&<yfuq=o*<7w2dxCK!^c=nHM>a7T z%_Of_#Ex}%6wl!t{5<V%)K(@jcy0Ao)ctwXPCY_x`NVYG%pB;ll(gSY_%yefRR*tk zURqx3K%aRcF;1%64=1G#bf#69YAlr%j%xp@KqL?fI!b*}f05ngbggqZH#(dJ_Wb-J zr!&vVpQYRvi8}U#eZfehFB%FL**&3pU(oJu3N$nZf{})Ry*L{7Me6<GEzOQ-@W|>~ V|3D-o&SbK*cUm(CI<4_J{{Y`Iz^niO delta 1439 zcmXZaOKePG6u|K_osvGLs!d<Cbcz;bMrWv2wM8^u34I|UN+<1ThCb+wwu{PmB}l_& zu<@vPi@2?f5V0Up5jGl;HWuV2U5M8LJ89JaxLwTrzH{b&_dDM?^P{)_@ZfuUI88)4 z(?!&iI2SKr96rZle2vLCf$^A}A(DtQFc0%_3a-ak+>9w$hq|vB_4yF$dyk+4PiIK@ z|Kb9PBof#0jZNeMZX)i@vV4V=#Hlkxc3>@f@g7#-C#=PsnIhY;9oOOzc3@n#2(?QO z&cm~)`=Z%lEAf-WMiLo0)`mtrK|G9p+&0U4@l)(4PMJ+H_yD!AA=De$b49vwIv&JR zxF7$Z3->xj+;|R2$&2vhVhpQD*y$&=$OhDcnsEUdxCKYC1`Fm`FX~3U@h#MwjiYuZ zk2x;Gtw?Qh8o7(y!2rI*CJZlOm(D1H+M?@7_vH!djfPPx{)i>$$hV%aLLJ3ctVaXO zFs8t|Sq0uFZa|tQiSw=ZFT}OPt8fqJ%W)QZ!?DR990dBJ8q^2-uo~Z@-XwRSHM>gG zf_iZTAL1bP6^iV`@<k#C@HXzoti}9Mu^Y8Bmob;~<sAzLiC?I#pFqx%YzC$?tKv&r z)rlS)L47`swj^OXYN2zn5?fIB-$Fg;Eoz5jmsoKcQj=`NQ=Bh{CKG=(L0nj5RT{-L z#2@e&W^;N2UO|n=@hXNH_%3uVwMKFn^-nm8HXGfeiW=hDVi8hu4YhM47>;K#ZMoI* zEadW1j{!V_dh@UN1-G!P4@*l$&f^fSz@2>6;W^Y1ea32>${a7@cGQmj#uy5cl~$ql zawemdG?Y_4p2rq^gvmI&!qSb}%1YE$2Pfmls5AYJ+VWKTug|SSx-JJIcjLaL9A%Zs z$IeHu*Au-NZzkI3NL{4Skrus}UTrH2b^6+at-es8qdmB~#n<W&h5T;+p@f=hSDn8r z5G3!a@`e0mPH&NSxvQwuwbbMEc*}}Ck)+Jb64SV08ir{+GmVGlz!}rHV-B1&jX_Ng zoN<<id~Jc&c7La@&AY5*bxWHs(CTjLXp64PjIkxUoBdry36k%b7mV1vInjMt_JV(1 CG|#L6 diff --git a/lang/tr.utf8/LC_MESSAGES/events.mo b/lang/tr.utf8/LC_MESSAGES/events.mo index d8acd338e1ae07338d85845c92567e56f9ee61f5..da762800727e6253cdb2e3ab9c50fe7f80c267ed 100644 GIT binary patch delta 94 zcmcc1dXRO3i|RZ^28N3a3=G0RT)@P@zzw8ZfV38no&cm-f%NW)nTwRU0{nG@Qp+-n qGxPIwT@p)DtrUz5jLdWmEOiZx6$}lmOf5INGTJjTT2Ag_3I+g?2o#F| delta 102 zcmX@edY5&Ai|Q^$28N3a3=G0R+{47czzw7q0BJ2Cy#`3L0_p1$GZ%@M6eZ>r=OmV7 y=I0eVq$lR2mXxIGrIt?SW|ZVI*EKLwFfg|=Fx;%iXwS%Iu9uQpW-vLADHs61-x)6e diff --git a/lang/uk.utf8/LC_MESSAGES/events.mo b/lang/uk.utf8/LC_MESSAGES/events.mo index cb34bfb5057fe74c65ac1805d4cb6da7c3f54bda..0c314ff57a9b62fae5468e881c5bf1f13394d138 100644 GIT binary patch delta 1402 zcmXZbTS${(7{Kvolr^WbqNX;pmd@<p*3=!^>|`z!8+4$u#Gsjj6mzky19h>ykU*#? zyQritin@{t7eUcQLPQq@)lDItsPHPRhzKI8|Ks<KvETc>-+OqT=Y4mp<6_6{r_-`? zMa1tAnTIV{iF?qB{TRSOoP`fDAD?3Zj^kpSz&x}Ti%iENoP%>w=T)JO51_8wh=tf% zEZM0>8y$8!y6~$-B!!!K?kpAAgrm3;|6&7r=ZmnX^k6NXz%@9EI_?{4A}*)MMvUTY z97IiY7(IB~nKe4z(V?0Az#uxxL?ReNH=e{Q9Kj9v5;X(I0ye=4)XbW2C&o;#VT|YB zr~x#W8<p9GQwd~gl<?vN`tbs4gwId|`iOjG`a<J<1#0AN=*0ol!0w>V8%G`g6?L3_ zk+E?D>QTo~E4mlgVRnRujm8($2!ElToz-Pz=tPp0YSe|BP&18V1NNXE!41@szeXMR z2Q%m-3u2KosQcVRP52%%J}WP1Xo-I!$0p7SBO^cR#_gtuQ5QI8K3_)N@E*3}Thy~& zN~dPlXL=I@JWrxl%FSka#I0D#{?boFOLrY-;C<9ejG+#EiJDP9vt3w?#40Vwn$m+> z+S6wLRntc}ll}>;#!1w5ikBF7%a7&kFG;iG6zW1xa4Eh)ALcQ;27?&FeHg`I^Z5fV z=ed+gI<JYJ9_&TE1FujoTi!CGqGhNFhA>-1qlE_5lQz`LaSZkE_ZWw<nAO+fDDKDa z$XD7|7<XU*b^aI<i+n~6u#l*Aqpi3bJ5i7H0ow6(jkF1WX_=ryOZ^XZLXfa+*o;(7 zwxLcuidy>f=KH&-B^^WVt-MFQL;pNOm#is^4B7KPS&rwv=hs^-M{-pK6}DW9BVp;Z zZQ1PEmN?v<?oRf3qVY^3;;Qr21wFp7$5-#F^+f{V6+S+;SUjEaY){4e(!KFaG8J(} z6TOK{qRX`-ok+P>AL2LYt6$|0hx~z1D6sxuDw#?4c@HFV?vj|LP#5)vZBkY1P7gKK LIC5u8uQ>h#b}y|C delta 1376 zcmXZbTS(JU9Ki82o0n-`DmQPXYo=YCYcr>!X(C2si|D0Vs5oml(>c+VRu2=Yr=o&^ zQ4$0dL_{ScNUDcKLPB!(B2@I^gT5r)iSVKC&;K99&pBu3^85YH`QK`N+WKsET_js6 zRcTgAJcz~Ejwv{bZoGn-_y$clk7@V`x8f2eqRFC^5i@ZEW}~btMVViPvTqZv$3qqs zS!<kNAe{j}8WWTn#3S^FbD4xQxEr(cl&ZsK<V-b!E*!&poI#nFl&>XHh5PBB#4Nmv zlIR4M<J<g*Ht>r9$;4DZlvswf*ooWl4wm2)HsLZ#24$O-vSJNNW?npoow}3gp`W!y z3!n|9GJUv~K!ipP17lc;Pf#LUL<#6G@~83(wcl${A`hYi$58@%hqCS)%KXG4ZC)`- z=0|Z8cA`}D9PY!&6pd6G307@lI!c6vC>c7DWK})N#$J?6hp-MuP_Ez=%G+5%nYWSX z!{|g}QV&qhnMO%?4w)ZOOEjd!X*Q*}KIKHos0HQVe%-4m8$8teF_Z)6umyjj+;szk zlG!EQX>`*!a)|6(i*m&sSj_qACJiawOH9HzO34;bCN867RK#j4)*~^ic9Z}{P)d7W zAAh0y5tA8T#qDS;)%LNYysZ}LbH2K)58Ol9=nL+^ALzsaR@>2o)p#D8aYFC^!Cmwn zERuCz{++>5l(+H~<*^m))GF#iN$@a6GHA5Zpn58Z@;Gjwe14yC9PRAhfHN4t<TCBR zev~&bj<S9MiADWG3D8E=a!?m~aTw)FKcIZ>D|U5U@oB472Bg$x!j%OcOvN^&dMbdj z@ES_#AL+l(qFl`a7U6G{H)M81Z>J0xE=JQ$i-w`tcheq&LB&eaY^erY?10&C$O#Yl z`UZP_;qFl1U}KlB*B=i19saY?$Cf;kyWHurx+-d&(f5|z()d{XdHnVKb?e^#f$p9_ q=yXr$LcrhSt8ltpja@-scdz4AC>YDg@feca4p+6q73;{IH2((_RH(TC From 300e2c3518acfaea3ca9dccace5abdf6b949bc30 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski <johan@x-tnd.be> Date: Sat, 26 Sep 2026 14:58:18 +0200 Subject: [PATCH 19/20] Remove stray quote from English event removal title --- lang/en_US/LC_MESSAGES/events.mo | Bin 8942 -> 8941 bytes lang/events_en_US.po | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en_US/LC_MESSAGES/events.mo b/lang/en_US/LC_MESSAGES/events.mo index e78365dc3bc1c876aa52ba305062a2a305ca3a9f..3a20a04c6e65cbec0b917198512d3b0fc421050a 100644 GIT binary patch delta 160 zcmaFo`qp)W3`e~%0|Uc)MFs{DAbl7}D+B2pK$-(c{{hmbK$=yFfq@@LTLWoMAngsL zxq);jkOnGYNCeWXK)M-7vjFK%AT0=_`+zh^-vS^FQojL6gUsCnq<Mh!Q6&aGFyj(X TfEOrmfAdNXNy*KvqS}lAu1pl; delta 168 zcmaFs`p$KO3`e~P0|Uc)MFs{CeFR7=1L>PUngdAx1=6NKnoWs;fgecQ0BKGj?E|E_ zfpi#<1}b4l0@AENx&=tH0O>9uEeNFhfiy_pLLd!NzY$1-%-sv5d4TjWB?dk)<1$cy b7bx&x^GXg$NoFO6pveh5BAaK6iZTKKUGWxw diff --git a/lang/events_en_US.po b/lang/events_en_US.po index cfea5fd8..39c6d7b9 100644 --- a/lang/events_en_US.po +++ b/lang/events_en_US.po @@ -138,7 +138,7 @@ msgstr "An error occurred while storing the event." #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format msgid "Remove event '%1$s'" -msgstr "Remove event '%1$s'\"" +msgstr "Remove event '%1$s'" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 msgid "Bookings management" From 905a3781b38c59a5916ac57a2696443146df5934 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski <johan@x-tnd.be> Date: Sat, 26 Sep 2026 15:01:37 +0200 Subject: [PATCH 20/20] Document calendar rows as ArrayObject --- lib/GaletteEvents/Repository/Events.php | 3 ++- .../GaletteEvents/Repository/tests/units/Events.php | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/GaletteEvents/Repository/Events.php b/lib/GaletteEvents/Repository/Events.php index 8f3dcb52..af9e2a61 100644 --- a/lib/GaletteEvents/Repository/Events.php +++ b/lib/GaletteEvents/Repository/Events.php @@ -11,6 +11,7 @@ namespace GaletteEvents\Repository; use Analog\Analog; +use ArrayObject; use Galette\Entity\Adherent; use GaletteEvents\Booking; use Laminas\Db\ResultSet\ResultSet; @@ -71,7 +72,7 @@ public function __construct(Db $zdb, Login $login, ?EventsList $filters = null) * @param bool $fullcalendar get events for fullcalendar display (ie. end date +1 day) * @param bool $full get full list, not paginated * - * @return array<int|string, Event|array<string, mixed>> + * @return array<int|string, Event|ArrayObject<string, mixed>> */ public function getList(bool $bookable = false, bool $fullcalendar = false, bool $full = false): array { diff --git a/tests/GaletteEvents/Repository/tests/units/Events.php b/tests/GaletteEvents/Repository/tests/units/Events.php index b842b612..8094ea33 100644 --- a/tests/GaletteEvents/Repository/tests/units/Events.php +++ b/tests/GaletteEvents/Repository/tests/units/Events.php @@ -45,7 +45,11 @@ public function tearDown(): void private function getListed(bool $bookable = false): array { $events = new \GaletteEvents\Repository\Events($this->zdb, $this->login); - $names = array_map(fn(\GaletteEvents\Event $event): string => (string)$event->getName(), $events->getList($bookable)); + $names = []; + foreach ($events->getList($bookable) as $event) { + $this->assertInstanceOf(\GaletteEvents\Event::class, $event); + $names[] = (string)$event->getName(); + } sort($names); return $names; } @@ -62,7 +66,11 @@ private function getCalendar(): array $filters->start_date_filter = date(__('Y-m-d'), strtotime('-1 month')); $filters->end_date_filter = date(__('Y-m-d'), strtotime('+1 month')); $events = new \GaletteEvents\Repository\Events($this->zdb, $this->login, $filters); - $names = array_map(fn(\ArrayObject $event): string => $event['name'], $events->getList(false, true)); + $names = []; + foreach ($events->getList(false, true) as $event) { + $this->assertInstanceOf(\ArrayObject::class, $event); + $names[] = $event['name']; + } sort($names); return $names; }