",
"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
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/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/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/Controllers/Crud/tests/units/ActivitiesController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php
index 2aa8c058..6ef6cc08 100644
--- a/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php
+++ b/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php
@@ -86,4 +86,37 @@ 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();
+ }
+
+ /**
+ * 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'));
+ }
}
diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php
index 84faa654..613dfebf 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->plugin_events_bookings_filter = $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/Controllers/Crud/tests/units/EventsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php
index 9266ae23..6821bd44 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,173 @@ 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->plugin_events_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();
+ }
+
+ /**
+ * 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]);
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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);
+ }
}
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();
+ }
}
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);
+ }
+}
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));
+ }
}
diff --git a/tests/GaletteEvents/Repository/tests/units/Events.php b/tests/GaletteEvents/Repository/tests/units/Events.php
new file mode 100644
index 00000000..8094ea33
--- /dev/null
+++ b/tests/GaletteEvents/Repository/tests/units/Events.php
@@ -0,0 +1,133 @@
+
+ */
+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 = [];
+ foreach ($events->getList($bookable) as $event) {
+ $this->assertInstanceOf(\GaletteEvents\Event::class, $event);
+ $names[] = (string)$event->getName();
+ }
+ 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 = [];
+ foreach ($events->getList(false, true) as $event) {
+ $this->assertInstanceOf(\ArrayObject::class, $event);
+ $names[] = $event['name'];
+ }
+ 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());
+ }
+}
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..f2f7135c
--- /dev/null
+++ b/tests/GaletteEvents/tests/units/Booking.php
@@ -0,0 +1,135 @@
+
+ */
+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));
+ }
+
+ /**
+ * 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));
+ }
+
+ /**
+ * 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');
+ }
+}
diff --git a/tests/GaletteEvents/tests/units/Event.php b/tests/GaletteEvents/tests/units/Event.php
new file mode 100644
index 00000000..e0ad3206
--- /dev/null
+++ b/tests/GaletteEvents/tests/units/Event.php
@@ -0,0 +1,219 @@
+
+ */
+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());
+ }
+
+ /**
+ * 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());
+ }
+
+ /**
+ * 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());
+ }
+
+ /**
+ * 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));
+ }
+}