diff --git a/_routes.php b/_routes.php index c8cb327..81634e8 100644 --- a/_routes.php +++ b/_routes.php @@ -63,13 +63,13 @@ $app->get( '/bookings/{event:guess|all|\d+}[/{option:page|order|clear_filter}/{value:\d+}]', - [BookingsController::class, 'listBookings'] + [BookingsController::class, 'list'] )->setName('events_bookings')->add(Authenticate::class); //bookings list filtering $app->post( '/bookings/filter/{event:guess|all|\d+}', - [BookingsController::class, 'filterBookings'] + [BookingsController::class, 'filter'] )->setName('filter-bookingslist')->add(Authenticate::class); $app->get( diff --git a/lib/GaletteEvents/Activity.php b/lib/GaletteEvents/Activity.php index 0d6369c..eed7f4d 100644 --- a/lib/GaletteEvents/Activity.php +++ b/lib/GaletteEvents/Activity.php @@ -128,8 +128,7 @@ public function store(): void $this->transactional(function (): void { $values = [ 'name' => $this->name, - 'is_active' => ($this->active ? $this->active - : ($this->zdb->isPostgres() ? 'false' : 0)), + 'is_active' => (int)$this->active, 'comment' => $this->comment ]; diff --git a/lib/GaletteEvents/Booking.php b/lib/GaletteEvents/Booking.php index 0f26242..5d3f227 100644 --- a/lib/GaletteEvents/Booking.php +++ b/lib/GaletteEvents/Booking.php @@ -16,8 +16,8 @@ use Galette\Core\Login; use Galette\Entity\Adherent; use Galette\Entity\PaymentType; -use Galette\Repository\Groups; use Analog\Analog; +use GaletteEvents\Repository\Events; /** * Booking entity @@ -53,8 +53,6 @@ class Booking /** @var array> */ private array $activities = []; - /** @var array> */ - private array $activities_removed = []; private ?string $creation_date = null; /** @@ -284,10 +282,6 @@ private function checkActivities(Event $event, array $checked): void } foreach (array_keys($this->activities) as $aid) { if (!isset($activities[$aid])) { - $this->activities_removed[$aid] = [ - Activity::PK => $aid, - self::PK => $this->id - ]; unset($this->activities[$aid]); } } @@ -303,8 +297,7 @@ public function store(): void Event::PK => $this->event, Adherent::PK => $this->member, 'booking_date' => $this->date, - 'is_paid' => ($this->paid ? $this->paid - : ($this->zdb->isPostgres() ? 'false' : 0)), + 'is_paid' => (int)$this->paid, 'payment_method' => $this->payment_method, 'payment_amount' => $this->amount, 'bank_name' => $this->bank_name, @@ -352,117 +345,67 @@ public function store(): void } } - //store booking activities - $void = []; - $update = []; - $insert = []; - $delete = $this->activities_removed; - - foreach ($this->activities as $aid => $data) { - $activity = $data['activity']; - $checked = $data['checked']; - $key_values = [ - self::PK => $this->id, - $activity::PK => $activity->getId() - ]; + $this->storeActivities(); + }); + } - $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesbookings', 'acb'); - $select->where($key_values); - $results = $this->zdb->execute($select); - - foreach ($results as $result) { - if (!isset($this->activities[$result[Activity::PK]])) { - $delete[$result[Activity::PK]] = [ - Activity::PK => $result[Activity::PK], - self::PK => $this->id, - ]; - } elseif ($result['checked'] != $this->activities[$result[Activity::PK]]['checked']) { - $update[$result[Activity::PK]] = [ - 'checked' => ($checked ? $checked - : ($this->zdb->isPostgres() ? 'false' : 0)) - ]; - } else { - $void[$result[Activity::PK]] = true; - } - } + /** + * Store activities of the booking, compared to the stored ones + */ + private function storeActivities(): void + { + $table = EVENTS_PREFIX . 'activitiesbookings'; - if (!isset($void[$aid]) && !isset($update[$aid]) && !isset($delete[$aid])) { - $insert[$aid] = [ - Activity::PK => $aid, - self::PK => $this->id, - 'checked' => ($checked ? $checked - : ($this->zdb->isPostgres() ? 'false' : 0)) - ]; - } - } + $stored = []; + $select = $this->zdb->select($table); + $select->where([self::PK => $this->id]); + foreach ($this->zdb->execute($select) as $row) { + $stored[(int)$row[Activity::PK]] = (bool)$row['checked']; + } - if (count($delete)) { - $prepare = $this->zdb->delete(EVENTS_PREFIX . 'activitiesbookings'); - $prepare->where([ + $counts = ['added' => 0, 'updated' => 0, 'removed' => 0]; + foreach ($this->activities as $aid => $data) { + $checked = (bool)$data['checked']; + if (!isset($stored[$aid])) { + $insert = $this->zdb->insert($table); + $insert->values([ self::PK => $this->id, - Activity::PK => ':aid' + Activity::PK => $aid, + 'checked' => (int)$checked ]); - $stmt = $this->zdb->sql->prepareStatementForSqlObject($prepare); - - $count = 0; - foreach ($delete as $values) { - $stmt->execute([':aid' => $values[Activity::PK]]); - ++$count; - } - Analog::log( - sprintf('%1$s activities removed', $count), - Analog::INFO - ); + $this->zdb->execute($insert); + ++$counts['added']; + } elseif ($stored[$aid] !== $checked) { + $update = $this->zdb->update($table); + $update->set(['checked' => (int)$checked])->where([ + self::PK => $this->id, + Activity::PK => $aid + ]); + $this->zdb->execute($update); + ++$counts['updated']; } + } - if (count($update)) { - $prepare = $this->zdb->update(EVENTS_PREFIX . 'activitiesbookings'); - $prepare->set([ - 'checked' => ':checked' - ])->where([ + 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' + Activity::PK => $aid ]); - $stmt = $this->zdb->sql->prepareStatementForSqlObject($prepare); - $count = 0; - foreach ($update as $aid => $values) { - $params = [ - 'where2' => $aid, - ':checked' => $values['checked'] - ]; - $stmt->execute($params); - ++$count; - } - Analog::log( - sprintf('%1$s activities updated', $count), - Analog::INFO - ); + $this->zdb->execute($delete); + ++$counts['removed']; } + } - if (count($insert)) { - $prepare = $this->zdb->insert(EVENTS_PREFIX . 'activitiesbookings'); - $prepare->values([ - self::PK => ':id', - Activity::PK => ':aid', - 'checked' => ':checked' - ]); - $stmt = $this->zdb->sql->prepareStatementForSqlObject($prepare); - $count = 0; - foreach ($insert as $aid => $values) { - $params = [ - $this->id, - $aid, - $values['checked'] - ]; - $stmt->execute($params); - ++$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 ); } - }); + } } /** @@ -689,17 +632,10 @@ public function getActivities(): array private function canBook(Event $event): bool { if ($this->login->isAdmin() || $this->login->isStaff()) { - return $event->getId() !== null; - } - - if ($event->getId() === null || !$event->isOpen()) { - return false; + return true; } - $group = $event->getGroup(); - return $group === null - || $this->login->isGroupManager($group) - || in_array($group, array_map('intval', Groups::loadGroups($this->login->id, false, false)), true); + return $event->isOpen() && Events::isVisible($event->getGroup(), $this->login); } /** diff --git a/lib/GaletteEvents/Controllers/Crud/AbstractController.php b/lib/GaletteEvents/Controllers/Crud/AbstractController.php new file mode 100644 index 0000000..bc653bf --- /dev/null +++ b/lib/GaletteEvents/Controllers/Crud/AbstractController.php @@ -0,0 +1,274 @@ + + * + * @template TFilters of Pagination + */ +abstract class AbstractController extends AbstractPluginController +{ + /** + * @var array + */ + #[Inject("Plugin Galette Events")] + protected array $module_info; + + /** + * Entity name, for session keys and logs: event, booking or activity + */ + abstract protected function getEntityName(): string; + + /** + * List name, for filters session key: events, bookings or activities + */ + abstract protected function getListName(): string; + + /** + * Create empty list filters + * + * @return TFilters + */ + abstract protected function createFilters(): Pagination; + + /** + * Get the message for an entity that does not exist + * + * @param int $id Requested identifier + */ + abstract protected function getNotFoundMessage(int $id): string; + + /** + * Apply posted filters specific to the list + * + * @param TFilters $filters Filters + * @param array $post Posted values + */ + protected function applyPostedFilters(Pagination $filters, array $post): void + { + } + + /** + * Get list filters from session, with page or order of the list route + * + * @param string|null $option One of 'page', 'order' or 'clear_filter' + * @param string|int|null $value Value of the option + * + * @return TFilters + */ + protected function getFilters(?string $option = null, string|int|null $value = null): Pagination + { + $filters = $this->session->{$this->getFilterName($this->getListName())} ?? $this->createFilters(); + + switch ($option) { + case 'page': + $filters->current_page = (int)$value; + break; + case 'order': + $filters->orderby = $value; + break; + case 'clear_filter': + $filters->reinit(); + break; + } + + return $filters; + } + + /** + * Store list filters in session + * + * @param Pagination $filters Filters + */ + protected function storeFilters(Pagination $filters): void + { + $this->session->{$this->getFilterName($this->getListName())} = $filters; + } + + /** + * Render a list page, with its pagination + * + * @param string $template Template name + * @param Pagination $filters Filters, once the list has been counted + * @param array $params Template parameters + */ + protected function renderList(Response $response, string $template, Pagination $filters, array $params): Response + { + //assign pagination variables to the template and add pagination links + $filters->setViewPagination($this->routeparser, $this->view, false); + $this->storeFilters($filters); + + $this->view->render( + $response, + $this->getTemplate($template), + $params + [ + 'require_dialog' => true, + 'filters' => $filters + ] + ); + return $response; + } + + /** + * Filtering + */ + public function filter(Request $request, Response $response): Response + { + $this->updateFilters($request); + return $response + ->withStatus(301) + ->withHeader('Location', $this->redirectUri([])); + } + + /** + * Update list filters from posted values + * + * @return TFilters + */ + protected function updateFilters(Request $request): Pagination + { + $post = $request->getParsedBody(); + $filters = $this->getFilters(); + + if (isset($post['clear_filter'])) { + $filters->reinit(); + } else { + //number of rows to show + if (isset($post['nbshow'])) { + $filters->show = $post['nbshow']; + } + $this->applyPostedFilters($filters, $post); + } + + $this->storeFilters($filters); + return $filters; + } + + /** + * Keep posted values, to fill the form again after a redirection + * + * @param ?int $id Entity identifier, null for a new one + * @param array $post Posted values + */ + protected function keepPostedValues(?int $id, array $post): void + { + $this->session->{$this->getPostedValuesKey()} = [ + 'id' => $id, + 'values' => $post + ]; + } + + /** + * Get values posted on the form of an entity, once + * + * @param ?int $id Entity identifier, null for a new one + * + * @return ?array + */ + protected function getPostedValues(?int $id): ?array + { + $key = $this->getPostedValuesKey(); + $data = $this->session->$key ?? null; + unset($this->session->$key); + return is_array($data) && $data['id'] === $id ? $data['values'] : null; + } + + /** + * Session key of posted values + */ + private function getPostedValuesKey(): string + { + return 'plugin_events_' . $this->getEntityName() . '_data'; + } + + /** + * Store an entity, and report how it went + * + * @param Event|Booking|Activity $entity Entity + * @param string $added Message for a new entity + * @param string $modified Message for an existing entity + * @param string $failed Message when storage failed + * @param array $successes Success messages + * @param array $errors Error messages + */ + protected function storeEntity( + Event|Booking|Activity $entity, + string $added, + string $modified, + string $failed, + array &$successes, + array &$errors + ): void { + $new = $entity->getId() === null; + try { + $entity->store(); + $successes[] = $new ? $added : $modified; + } catch (\Throwable $e) { + Analog::log( + sprintf( + 'Unable to store %1$s #%2$s | %3$s', + $this->getEntityName(), + $entity->getId() ?? 'new', + $e->getMessage() + ), + Analog::ERROR + ); + $errors[] = $failed; + } + } + + /** + * Redirect when requested entity does not exist + * + * @param int $id Requested identifier + */ + protected function redirectNotFound(Response $response, int $id): Response + { + return $this->redirectWithErrors( + response: $response, + errors: [$this->getNotFoundMessage($id)], + redirect_url: $this->redirectUri([]) + ); + } + + /** + * Redirect when current logged-in user cannot edit an entity + * + * @param ?int $id Entity identifier + */ + protected function redirectForbidden(Response $response, ?int $id): Response + { + Analog::log( + 'Logged in member ' . $this->login->login + . ' has tried to edit ' . $this->getEntityName() . ' #' . $id + . ' 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->redirectUri([]) + ); + } +} diff --git a/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php b/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php index fbbeec7..465a605 100644 --- a/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php +++ b/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php @@ -10,29 +10,61 @@ namespace GaletteEvents\Controllers\Crud; -use Analog\Analog; -use Galette\Controllers\Crud\AbstractPluginController; +use Galette\Core\Pagination; use GaletteEvents\Filters\ActivitiesList; use GaletteEvents\Activity; use GaletteEvents\NotFoundException; use GaletteEvents\Repository\Activities; use Slim\Psr7\Request; use Slim\Psr7\Response; -use DI\Attribute\Inject; /** * Activities controller * * @author Johan Cwiklinski + * + * @extends AbstractController */ -class ActivitiesController extends AbstractPluginController +class ActivitiesController extends AbstractController { /** - * @var array + * Entity name, for session keys and logs + */ + protected function getEntityName(): string + { + return 'activity'; + } + + /** + * List name, for filters session key + */ + protected function getListName(): string + { + return 'activities'; + } + + /** + * Create empty list filters */ - #[Inject("Plugin Galette Events")] - protected array $module_info; + protected function createFilters(): Pagination + { + return new ActivitiesList(); + } + + /** + * Get the message for an activity that does not exist + * + * @param int $id Requested activity identifier + */ + protected function getNotFoundMessage(int $id): string + { + return sprintf( + //TRANS: %1$s is the activity identifier + _T('No activity #%1$s.', 'events'), + $id + ); + } // CRUD - Create @@ -63,73 +95,19 @@ 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->{$this->getFilterName('activities')})) { - $filters = $this->session->{$this->getFilterName('activities')}; - } else { - $filters = new ActivitiesList(); - } - - if ($option !== null) { - switch ($option) { - case 'page': - $filters->current_page = (int)$value; - break; - case 'order': - $filters->orderby = $value; - break; - } - } - + $filters = $this->getFilters($option, $value); $activities = new Activities($this->zdb, $this->login, $this->history, $this->preferences, $filters); - $list = $activities->getList(); - - //assign pagination variables to the template and add pagination links - $filters->setViewPagination($this->routeparser, $this->view, false); - $this->session->{$this->getFilterName('activities')} = $filters; - - // display page - $this->view->render( + return $this->renderList( $response, - $this->getTemplate('activities'), + 'activities', + $filters, [ 'page_title' => _T("Activities management", "events"), - 'require_dialog' => true, - 'activities' => $list, + 'activities' => $activities->getList(), 'nb_activities' => $activities->getCount(), - 'filters' => $filters ] ); - return $response; - } - - /** - * Filtering - */ - public function filter(Request $request, Response $response): Response - { - $post = $request->getParsedBody(); - if (isset($this->session->{$this->getFilterName('activities')})) { - $filters = $this->session->{$this->getFilterName('activities')}; - } else { - $filters = new ActivitiesList(); - } - - //reinitialize filters - if (isset($post['clear_filter'])) { - $filters->reinit(); - } else { - //number of rows to show - if (isset($post['nbshow'])) { - $filters->show = $post['nbshow']; - } - } - - $this->session->{$this->getFilterName('activities')} = $filters; - - return $response - ->withStatus(301) - ->withHeader('Location', $this->routeparser->urlFor('events_activities')); } // /CRUD - Read @@ -154,15 +132,14 @@ public function edit(Request $request, Response $response, ?int $id = null, stri } //values posted before an error - $data = $this->session->plugin_events_activity_data ?? null; - unset($this->session->plugin_events_activity_data); - if (is_array($data) && $data['id'] === $activity->getId()) { - $activity->check($data['values']); + $values = $this->getPostedValues($activity->getId()); + if ($values !== null) { + $activity->check($values); } // template variable declaration $title = _T("Activity", "events"); - if ($activity->getId() != '') { + if ($activity->getId() !== null) { $title .= ' (' . _T("modification") . ')'; } else { $title .= ' (' . _T("creation") . ')'; @@ -193,7 +170,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st { $post = $request->getParsedBody(); $activity = new Activity($this->zdb, $this->history); - if (isset($post['id']) && !empty($post['id'])) { + if (!empty($post['id'])) { try { $activity->load((int)$post['id']); } catch (NotFoundException) { @@ -201,100 +178,35 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st } } - $success_detected = []; - $error_detected = []; - - // Validation - if (!$activity->check($post)) { - $error_detected = array_merge($error_detected, $activity->getErrors()); - } - - if (count($error_detected) == 0) { - //all goes well, we can proceed - $new = $activity->getId() === null; - try { - $activity->store(); - if ($new) { - $success_detected[] = _T("New activity has been successfully added.", "events"); - } else { - $success_detected[] = _T("Activity has been modified.", "events"); - } - } catch (\Throwable $e) { - Analog::log( - 'Unable to store activity #' . ($activity->getId() ?? 'new') . ' | ' . $e->getMessage(), - Analog::ERROR - ); - $error_detected[] = _T("An error occurred while storing the activity.", "events"); - } - } - - if (count($error_detected) > 0) { - foreach ($error_detected as $error) { - $this->flash->addMessage( - 'error_detected', - $error - ); - } - } - - if (count($success_detected) > 0) { - foreach ($success_detected as $success) { - $this->flash->addMessage( - 'success_detected', - $success - ); - } + $successes = []; + $errors = []; + if ($activity->check($post)) { + $this->storeEntity( + $activity, + _T("New activity has been successfully added.", "events"), + _T("Activity has been modified.", "events"), + _T("An error occurred while storing the activity.", "events"), + $successes, + $errors + ); + } else { + $errors = $activity->getErrors(); } - if (count($error_detected) == 0) { + if (count($errors) === 0) { $redirect_url = $this->routeparser->urlFor('events_activities'); } else { - //keep posted values for the form - $this->session->plugin_events_activity_data = [ - 'id' => $activity->getId(), - 'values' => $post - ]; - - if ($activity->getId()) { - $redirect_url = $this->routeparser->urlFor( - 'events_activity_edit', - ['id' => (string)$activity->getId()] - ); - } else { - $redirect_url = $this->routeparser->urlFor('events_activity_add'); - } + $this->keepPostedValues($activity->getId(), $post); + $redirect_url = $activity->getId() !== null + ? $this->routeparser->urlFor('events_activity_edit', ['id' => (string)$activity->getId()]) + : $this->routeparser->urlFor('events_activity_add'); } - return $response - ->withStatus(301) - ->withHeader('Location', $redirect_url); - } - - /** - * Get the message for an activity that does not exist - * - * @param int $id Requested activity identifier - */ - private function getNotFoundMessage(int $id): string - { - return sprintf( - //TRANS: %1$s is the activity identifier - _T('No activity #%1$s.', 'events'), - $id - ); - } - - /** - * Redirect when requested activity does not exist - * - * @param int $id Requested activity identifier - */ - private function redirectNotFound(Response $response, int $id): Response - { - return $this->redirectWithErrors( + return $this->redirect( response: $response, - errors: [$this->getNotFoundMessage($id)], - redirect_url: $this->routeparser->urlFor('events_activities') + redirect_url: $redirect_url, + successes: $successes, + errors: $errors ); } diff --git a/lib/GaletteEvents/Controllers/Crud/BookingsController.php b/lib/GaletteEvents/Controllers/Crud/BookingsController.php index 9ac9abf..ae78f9b 100644 --- a/lib/GaletteEvents/Controllers/Crud/BookingsController.php +++ b/lib/GaletteEvents/Controllers/Crud/BookingsController.php @@ -14,7 +14,7 @@ use Galette\Entity\Adherent; use Galette\Repository\Groups; use Galette\Repository\Members; -use Galette\Controllers\Crud\AbstractPluginController; +use Galette\Core\Pagination; use Galette\Filters\MembersList; use GaletteEvents\Filters\BookingsList; use GaletteEvents\Booking; @@ -24,21 +24,54 @@ use GaletteEvents\Repository\Events; use Slim\Psr7\Request; use Slim\Psr7\Response; -use DI\Attribute\Inject; /** * Bookings controller * * @author Johan Cwiklinski + * + * @extends AbstractController */ -class BookingsController extends AbstractPluginController +class BookingsController extends AbstractController { /** - * @var array + * Entity name, for session keys and logs + */ + protected function getEntityName(): string + { + return 'booking'; + } + + /** + * List name, for filters session key + */ + protected function getListName(): string + { + return 'bookings'; + } + + /** + * Create empty list filters + */ + protected function createFilters(): Pagination + { + return new BookingsList(); + } + + /** + * Get the message for a booking that does not exist + * + * @param int $id Requested booking identifier */ - #[Inject("Plugin Galette Events")] - protected array $module_info; + protected function getNotFoundMessage(int $id): string + { + return sprintf( + //TRANS: %1$s is the booking identifier + _T('No booking #%1$s.', 'events'), + $id + ); + } // CRUD - Create @@ -66,45 +99,19 @@ public function doAdd(Request $request, Response $response): Response /** * List page * - * @param string|null $option One of 'page' or 'order' + * @param string|null $option One of 'page', 'order' or 'clear_filter' * @param string|int|null $value Value of the option - */ - public function list(Request $request, Response $response, ?string $option = null, string|int|null $value = null): Response - { - //just for inheritance. see listBookings which signature changes. - return $response; - } - - /** - * List page - * * @param string|int $event Linked event. May be an event ID, 'all' or 'guess'. - * @param string|null $option One of 'page' or 'order' - * @param string|int|null $value Value of the option */ - public function listBookings(Response $response, string|int $event, ?string $option = null, string|int|null $value = null): Response - { - $filters = $this->session->{$this->getFilterName('bookings')} ?? new BookingsList(); - - if ($event == 'guess') { - $linked_event = $filters->event_filter ?? 'all'; - } else { - $linked_event = $event; - } - - if ($option !== null) { - switch ($option) { - case 'page': - $filters->current_page = (int)$value; - break; - case 'order': - $filters->orderby = $value; - break; - case 'clear_filter': - $filters->reinit(); - break; - } - } + public function list( + Request $request, + Response $response, + ?string $option = null, + string|int|null $value = null, + string|int $event = 'all' + ): Response { + $filters = $this->getFilters($option, $value); + $linked_event = $event == 'guess' ? ($filters->event_filter ?? 'all') : $event; $event = null; if ($linked_event !== 'all') { @@ -113,7 +120,7 @@ public function listBookings(Response $response, string|int $event, ?string $opt } catch (NotFoundException) { //event may have been removed since it has been filtered $filters->event_filter = null; - $this->session->{$this->getFilterName('bookings')} = $filters; + $this->storeFilters($filters); return $this->redirectWithErrors( response: $response, errors: [sprintf( @@ -125,109 +132,62 @@ public function listBookings(Response $response, string|int $event, ?string $opt ); } $filters->event_filter = (int)$linked_event; + } else { + $filters->event_filter = null; } //Groups $groups = new Groups($this->zdb, $this->login); $groups_list = $groups->getList(); - $bookings = new Bookings($this->zdb, $this->login, $this->history, $filters); - - $events = new Events($this->zdb, $this->login, $this->history); + $bookings = new Bookings($this->zdb, $this->login, $this->history, $this->preferences, $filters); $list = $bookings->getList(); - $count = $bookings->getCount(); - - //assign pagination variables to the template and add pagination links - $filters->setViewPagination($this->routeparser, $this->view, false); + $events = new Events($this->zdb, $this->login, $this->history, $this->preferences); - $this->session->{$this->getFilterName('bookings')} = $filters; - - // display page - $this->view->render( + return $this->renderList( $response, - $this->getTemplate('bookings'), + 'bookings', + $filters, [ 'page_title' => _T("Bookings management", "events"), 'bookings' => $bookings, 'bookings_list' => $list, - 'nb_bookings' => $count, + 'nb_bookings' => $bookings->getCount(), 'event' => $event, 'eventid' => $linked_event, - 'require_dialog' => true, - 'filters' => $filters, 'events' => $events->getList(full: true), 'groups' => $groups_list ] ); - return $response; } /** - * Filtering + * Filtering; list shows the filtered event */ public function filter(Request $request, Response $response): Response { - //just for inheritance. see filterBookings which signature changes. - return $response; + $filters = $this->updateFilters($request); + return $response + ->withStatus(301) + ->withHeader( + 'Location', + $this->routeparser->urlFor('events_bookings', ['event' => (string)($filters->event_filter ?? 'all')]) + ); } /** - * Filtering + * Apply posted bookings filters * - * @param string|int $event Linked event. May be an event ID, 'all' or 'guess'. + * @param BookingsList $filters Filters + * @param array $post Posted values */ - public function filterBookings(Request $request, Response $response, string|int $event): Response + protected function applyPostedFilters(Pagination $filters, array $post): void { - $post = $request->getParsedBody(); - if (isset($this->session->{$this->getFilterName('bookings')})) { - $filters = $this->session->{$this->getFilterName('bookings')}; - } else { - $filters = new BookingsList(); - } - - //reintialize filters - if (isset($post['clear_filter'])) { - $filters->reinit(); - $event = 'all'; - } else { - //number of rows to show - if (isset($post['nbshow'])) { - $filters->show = $post['nbshow']; - } - - if (isset($post['paid_filter'])) { - if (is_numeric($post['paid_filter'])) { - $filters->paid_filter = $post['paid_filter']; - } - } - - if (isset($post['payment_type_filter'])) { - if (is_numeric($post['payment_type_filter'])) { - $filters->payment_type_filter = $post['payment_type_filter']; - } - } - - if (isset($post['event_filter'])) { - if (is_numeric($post['event_filter'])) { - $filters->event_filter = $post['event_filter']; - } - } - - if (isset($post['group_filter'])) { - if (is_numeric($post['group_filter'])) { - $filters->group_filter = $post['group_filter']; - } + foreach (['paid_filter', 'payment_type_filter', 'event_filter', 'group_filter'] as $name) { + if (isset($post[$name])) { + $filters->$name = $post[$name]; } } - - $this->session->{$this->getFilterName('bookings')} = $filters; - - return $response - ->withStatus(301) - ->withHeader( - 'Location', - $this->routeparser->urlFor('events_bookings', ['event' => $event]) - ); } /** @@ -254,28 +214,22 @@ public function handleBatch(Request $request, Response $response): Response } if (isset($post['entries_sel'])) { - if (isset($this->session->{$this->getFilterName('bookings')})) { - $filters = clone $this->session->{$this->getFilterName('bookings')}; - } else { - $filters = new BookingsList(); - } + $filters = clone $this->getFilters(); $filters->selected = $post['entries_sel']; //selection is restricted to bookings current logged-in user can list - $bookings = new Bookings($this->zdb, $this->login, $this->history, $filters); + $bookings = new Bookings($this->zdb, $this->login, $this->history, $this->preferences, $filters); $members = []; foreach ($bookings->getList() as $booking) { $members[] = $booking->getMemberId(); } if (count($members) === 0) { - $this->flash->addMessage( - 'error_detected', - _T("No booking was selected, please check at least one.", "events") + return $this->redirectWithErrors( + response: $response, + errors: [_T("No booking was selected, please check at least one.", "events")], + redirect_url: $this->routeparser->urlFor('events_events') ); - return $response - ->withStatus(301) - ->withHeader('Location', $this->routeparser->urlFor('events_events')); } $mfilter = new MembersList(); $mfilter->selected = $members; @@ -285,9 +239,7 @@ public function handleBatch(Request $request, Response $response): Response $this->session->redirect_mailing = $this->routeparser->urlFor( 'events_bookings', [ - 'event' => $filters->event_filter == null - ? 'all' - : $filters->event_filter + 'event' => $filters->event_filter ?? 'all' ] ); return $response @@ -328,20 +280,16 @@ public function handleBatch(Request $request, Response $response): Response ); } - $this->flash->addMessage( - 'error_detected', - _T("No action was matching.", "events") - ); + $error = _T("No action was matching.", "events"); } else { - $this->flash->addMessage( - 'error_detected', - _T("No booking was selected, please check at least one.", "events") - ); + $error = _T("No booking was selected, please check at least one.", "events"); } - return $response - ->withStatus(301) - ->withHeader('Location', $this->routeparser->urlFor('events_events')); + return $this->redirectWithErrors( + response: $response, + errors: [$error], + redirect_url: $this->routeparser->urlFor('events_events') + ); } /** @@ -389,26 +337,25 @@ public function edit(Request $request, Response $response, ?int $id = null, stri } if ($booking->getId() !== null && !$booking->canEdit($this->login)) { - return $this->redirectForbidden($response, $booking); + return $this->redirectForbidden($response, $booking->getId()); } //values posted before an error, or before the event has been changed - $data = $this->session->plugin_events_booking_data ?? null; - unset($this->session->plugin_events_booking_data); - if (is_array($data) && $data['id'] === $booking->getId()) { - $booking->check($data['values']); + $values = $this->getPostedValues($booking->getId()); + if ($values !== null) { + $booking->check($values); } // template variable declaration $title = _T("Booking", "events"); - if ($booking->getId() != '') { + if ($booking->getId() !== null) { $title .= ' (' . _T("modification") . ')'; } else { $title .= ' (' . _T("creation") . ')'; } //Events - $events = new Events($this->zdb, $this->login, $this->history); + $events = new Events($this->zdb, $this->login, $this->history, $this->preferences); if ($action === 'add') { if (isset($get['event'])) { $booking->setEvent((int)$get['event']); @@ -489,7 +436,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st { $post = $request->getParsedBody(); $booking = new Booking($this->zdb, $this->login, $this->history); - if (isset($post['id']) && !empty($post['id'])) { + if (!empty($post['id'])) { try { $booking->load((int)$post['id']); } catch (NotFoundException) { @@ -498,7 +445,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st } if ($booking->getId() !== null && !$booking->canEdit($this->login)) { - return $this->redirectForbidden($response, $booking); + return $this->redirectForbidden($response, $booking->getId()); } if (isset($post['cancel'])) { @@ -522,22 +469,14 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st } if (count($error_detected) == 0 && isset($post['save'])) { - //all goes well, we can proceed - $new = $booking->getId() === null; - try { - $booking->store(); - if ($new) { - $success_detected[] = _T("New booking has been successfully added.", "events"); - } else { - $success_detected[] = _T("Booking has been modified.", "events"); - } - } catch (\Throwable $e) { - Analog::log( - 'Unable to store booking #' . ($booking->getId() ?? 'new') . ' | ' . $e->getMessage(), - Analog::ERROR - ); - $error_detected[] = _T("An error occurred while storing the booking.", "events"); - } + $this->storeEntity( + $booking, + _T("New booking has been successfully added.", "events"), + _T("Booking has been modified.", "events"), + _T("An error occurred while storing the booking.", "events"), + $success_detected, + $error_detected + ); } if (!isset($post['save'])) { @@ -546,110 +485,24 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st $warning_detected[] = _T('Do not forget to store the booking', 'events'); } - if (count($error_detected) > 0) { - foreach ($error_detected as $error) { - $this->flash->addMessage( - 'error_detected', - $error - ); - } - } - - if (count($warning_detected) > 0) { - foreach ($warning_detected as $warning) { - $this->flash->addMessage( - 'warning_detected', - $warning - ); - } - } - if (count($success_detected) > 0) { - foreach ($success_detected as $success) { - $this->flash->addMessage( - 'success_detected', - $success - ); - } - } - if (count($error_detected) == 0 && $goto_list) { $redirect_url = $this->routeparser->urlFor( 'events_bookings', ['event' => (string)$booking->getEventId()] ); } else { - //keep posted values for the form - $this->session->plugin_events_booking_data = [ - 'id' => $booking->getId(), - 'values' => $post - ]; - - if ($booking->getId()) { - $route = 'events_booking_edit'; - $rparams = [ - 'id' => $booking->getId(), - 'action' => 'edit' - ]; - } else { - $route = 'events_booking_add'; - $rparams = ['action' => 'add']; - } - $redirect_url = $this->routeparser->urlFor( - $route, - $rparams - ); + $this->keepPostedValues($booking->getId(), $post); + $redirect_url = $booking->getId() !== null + ? $this->routeparser->urlFor('events_booking_edit', ['id' => (string)$booking->getId(), 'action' => 'edit']) + : $this->routeparser->urlFor('events_booking_add', ['action' => 'add']); } - return $response - ->withStatus(301) - ->withHeader('Location', $redirect_url); - } - - /** - * Get the message for a booking that does not exist - * - * @param int $id Requested booking identifier - */ - private function getNotFoundMessage(int $id): string - { - return sprintf( - //TRANS: %1$s is the booking identifier - _T('No booking #%1$s.', 'events'), - $id - ); - } - - /** - * Redirect when requested booking does not exist - * - * @param int $id Requested booking identifier - */ - private function redirectNotFound(Response $response, int $id): Response - { - return $this->redirectWithErrors( - response: $response, - errors: [$this->getNotFoundMessage($id)], - redirect_url: $this->routeparser->urlFor('events_bookings', ['event' => 'all']) - ); - } - - /** - * Redirect when current logged-in user cannot edit a booking - * - * @param Booking $booking Booking - */ - private function redirectForbidden(Response $response, Booking $booking): Response - { - Analog::log( - 'Logged in member ' . $this->login->login - . ' has tried to edit booking #' . $booking->getId() - . ' without the right to do so.', - Analog::WARNING - ); - return $this->redirectWithErrors( + return $this->redirect( response: $response, - errors: [_T("You do not have permission for requested URL.")], - redirect_url: $this->routeparser->urlFor('events_bookings', ['event' => 'all']) + redirect_url: $redirect_url, + successes: $success_detected, + warnings: $warning_detected, + errors: $error_detected ); } diff --git a/lib/GaletteEvents/Controllers/Crud/EventsController.php b/lib/GaletteEvents/Controllers/Crud/EventsController.php index 4a8eeca..cbba10e 100644 --- a/lib/GaletteEvents/Controllers/Crud/EventsController.php +++ b/lib/GaletteEvents/Controllers/Crud/EventsController.php @@ -10,16 +10,14 @@ namespace GaletteEvents\Controllers\Crud; -use Analog\Analog; use Galette\Repository\Groups; -use Galette\Controllers\Crud\AbstractPluginController; +use Galette\Core\Pagination; use GaletteEvents\Filters\EventsList; use GaletteEvents\Event; use GaletteEvents\NotFoundException; use GaletteEvents\Repository\Events; use Slim\Psr7\Request; use Slim\Psr7\Response; -use DI\Attribute\Inject; /** * Events controller @@ -31,15 +29,49 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version * @link https://galette.eu * @since 2021-05-09 + * + * @extends AbstractController */ -class EventsController extends AbstractPluginController +class EventsController extends AbstractController { /** - * @var array + * Entity name, for session keys and logs + */ + protected function getEntityName(): string + { + return 'event'; + } + + /** + * List name, for filters session key + */ + protected function getListName(): string + { + return 'events'; + } + + /** + * Create empty list filters + */ + protected function createFilters(): Pagination + { + return new EventsList(); + } + + /** + * Get the message for an event that does not exist + * + * @param int $id Requested event identifier */ - #[Inject("Plugin Galette Events")] - protected array $module_info; + protected function getNotFoundMessage(int $id): string + { + return sprintf( + //TRANS: %1$s is the event identifier + _T('No event #%1$s.', 'events'), + $id + ); + } // CRUD - Create @@ -70,44 +102,19 @@ 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->{$this->getFilterName('events')})) { - $filters = $this->session->{$this->getFilterName('events')}; - } else { - $filters = new EventsList(); - } - - 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, $this->history, $filters); - $events_list = $events->getList(); - - //assign pagination variables to the template and add pagination links - $filters->setViewPagination($this->routeparser, $this->view, false); + $filters = $this->getFilters($option, $value); + $events = new Events($this->zdb, $this->login, $this->history, $this->preferences, $filters); - $this->session->{$this->getFilterName('events')} = $filters; - - // display page - $this->view->render( + return $this->renderList( $response, - $this->getTemplate('events'), + 'events', + $filters, [ 'page_title' => _T("Events management", "events"), - 'require_dialog' => true, - 'events' => $events_list, + 'events' => $events->getList(), 'nb_events' => $events->getCount(), - 'filters' => $filters ] ); - return $response; } /** @@ -160,40 +167,11 @@ public function ajaxCalendar(Request $request, Response $response): Response $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, $this->history, $filters); + $events = new Events($this->zdb, $this->login, $this->history, $this->preferences, $filters); return $this->withJson($response, $events->getList(false, true)); } - /** - * Filtering - */ - public function filter(Request $request, Response $response): Response - { - $post = $request->getParsedBody(); - if (isset($this->session->{$this->getFilterName('events')})) { - $filters = $this->session->{$this->getFilterName('events')}; - } else { - $filters = new EventsList(); - } - - //reintialize filters - if (isset($post['clear_filter'])) { - $filters->reinit(); - } else { - //number of rows to show - if (isset($post['nbshow'])) { - $filters->show = $post['nbshow']; - } - } - - $this->session->{$this->getFilterName('events')} = $filters; - - return $response - ->withStatus(301) - ->withHeader('Location', $this->routeparser->urlFor('events_events')); - } - // /CRUD - Read // CRUD - Update @@ -219,19 +197,18 @@ public function edit(Request $request, Response $response, ?int $id = null, stri //check if logged-in user can edit event if (!$can) { - return $this->redirectForbidden($response, $event); + return $this->redirectForbidden($response, $event->getId()); } //values posted before an error, or before activities have been changed - $data = $this->session->plugin_events_event_data ?? null; - unset($this->session->plugin_events_event_data); - if (is_array($data) && $data['id'] === $event->getId()) { - $event->check($data['values']); + $values = $this->getPostedValues($event->getId()); + if ($values !== null) { + $event->check($values); } // template variable declaration $title = _T("Event", "events"); - if ($event->getId() != '') { + if ($event->getId() !== null) { $title .= ' (' . _T("modification") . ')'; } else { $title .= ' (' . _T("creation") . ')'; @@ -269,7 +246,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st $post = $request->getParsedBody(); $event = new Event($this->zdb, $this->login, $this->history); $can = $event->canCreate($this->login); - if (isset($post['id']) && !empty($post['id'])) { + if (!empty($post['id'])) { try { $event->load((int)$post['id']); } catch (NotFoundException) { @@ -280,7 +257,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st //check if logged-in user can edit event if (!$can) { - return $this->redirectForbidden($response, $event); + return $this->redirectForbidden($response, $event->getId()); } $success_detected = []; @@ -308,120 +285,33 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st } elseif (!$valid) { $error_detected = array_merge($error_detected, $event->getErrors()); } elseif (isset($post['save'])) { - $new = $event->getId() === null; - try { - $event->store(); - if ($new) { - $success_detected[] = _T("New event has been successfully added.", "events"); - } else { - $success_detected[] = _T("Event has been modified.", "events"); - } - } catch (\Throwable $e) { - Analog::log( - 'Unable to store event #' . ($event->getId() ?? 'new') . ' | ' . $e->getMessage(), - Analog::ERROR - ); - $error_detected[] = _T("An error occurred while storing the event.", "events"); - } + $this->storeEntity( + $event, + _T("New event has been successfully added.", "events"), + _T("Event has been modified.", "events"), + _T("An error occurred while storing the event.", "events"), + $success_detected, + $error_detected + ); } else { $goto_list = false; } - if (count($error_detected) > 0) { - foreach ($error_detected as $error) { - $this->flash->addMessage( - 'error_detected', - $error - ); - } - } - - if (count($warning_detected) > 0) { - foreach ($warning_detected as $warning) { - $this->flash->addMessage( - 'warning_detected', - $warning - ); - } - } - if (count($success_detected) > 0) { - foreach ($success_detected as $success) { - $this->flash->addMessage( - 'success_detected', - $success - ); - } - } - if (count($error_detected) == 0 && $goto_list) { $redirect_url = $this->routeparser->urlFor('events_events'); } else { - //keep posted values for the form - $this->session->plugin_events_event_data = [ - 'id' => $event->getId(), - 'values' => $post - ]; - - if ($event->getId()) { - $redirect_url = $this->routeparser->urlFor( - 'events_event_edit', - ['id' => (string)$event->getId()] - ); - } else { - $redirect_url = $this->routeparser->urlFor('events_event_add'); - } + $this->keepPostedValues($event->getId(), $post); + $redirect_url = $event->getId() !== null + ? $this->routeparser->urlFor('events_event_edit', ['id' => (string)$event->getId()]) + : $this->routeparser->urlFor('events_event_add'); } - return $response - ->withStatus(301) - ->withHeader('Location', $redirect_url); - } - - /** - * Get the message for an event that does not exist - * - * @param int $id Requested event identifier - */ - private function getNotFoundMessage(int $id): string - { - return sprintf( - //TRANS: %1$s is the event identifier - _T('No event #%1$s.', 'events'), - $id - ); - } - - /** - * Redirect when requested event does not exist - * - * @param int $id Requested event identifier - */ - private function redirectNotFound(Response $response, int $id): Response - { - return $this->redirectWithErrors( - response: $response, - errors: [$this->getNotFoundMessage($id)], - redirect_url: $this->routeparser->urlFor('events_events') - ); - } - - /** - * 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( + return $this->redirect( response: $response, - errors: [_T("You do not have permission for requested URL.")], - redirect_url: $this->routeparser->urlFor('events_events') + redirect_url: $redirect_url, + successes: $success_detected, + warnings: $warning_detected, + errors: $error_detected ); } diff --git a/lib/GaletteEvents/Controllers/CsvController.php b/lib/GaletteEvents/Controllers/CsvController.php index 6930660..0939c1c 100644 --- a/lib/GaletteEvents/Controllers/CsvController.php +++ b/lib/GaletteEvents/Controllers/CsvController.php @@ -80,7 +80,7 @@ public function bookingsExport(Request $request, Response $response, ?int $id = $filters->event_filter = $id; } - $bookings = new Bookings($this->zdb, $this->login, $this->history, $filters); + $bookings = new Bookings($this->zdb, $this->login, $this->history, $this->preferences, $filters); $bookings_list = $bookings->getList(true); $labels = [ @@ -99,16 +99,16 @@ public function bookingsExport(Request $request, Response $response, ?int $id = //activities are only available for one event $event = null; - if (is_numeric($filters->event_filter) && (int)$filters->event_filter > 0) { + if ($filters->event_filter !== null) { try { - $event = new Event($this->zdb, $this->login, $this->history, (int)$filters->event_filter); + $event = new Event($this->zdb, $this->login, $this->history, $filters->event_filter); } catch (NotFoundException) { return $this->redirectWithErrors( response: $response, errors: [sprintf( //TRANS: %1$s is the event identifier _T('No event #%1$s.', 'events'), - (int)$filters->event_filter + $filters->event_filter )], redirect_url: $this->routeparser->urlFor('events_bookings', ['event' => 'all']) ); diff --git a/lib/GaletteEvents/Event.php b/lib/GaletteEvents/Event.php index 5c5ca75..d9bb6cc 100644 --- a/lib/GaletteEvents/Event.php +++ b/lib/GaletteEvents/Event.php @@ -258,8 +258,7 @@ public function store(): void 'country' => ($this->country ?: new Expression('NULL')), 'begin_date' => $this->begin_date, 'end_date' => $this->end_date, - 'is_open' => ($this->open - ?: ($this->zdb->isPostgres() ? 'false' : 0)), + 'is_open' => (int)$this->open, Group::PK => ($this->group ?: new Expression('NULL')), 'comment' => $this->comment, 'color' => $this->color @@ -533,7 +532,7 @@ public function isOpen(): bool public function availableActivities(): array { $select = $this->zdb->select(EVENTS_PREFIX . Activity::TABLE, 'ac'); - $select->where->equalTo('is_active', true); + $select->where->equalTo('is_active', 1); $results = $this->zdb->execute($select); $activities = []; diff --git a/lib/GaletteEvents/Filters/ActivitiesList.php b/lib/GaletteEvents/Filters/ActivitiesList.php index eab1713..85ef774 100644 --- a/lib/GaletteEvents/Filters/ActivitiesList.php +++ b/lib/GaletteEvents/Filters/ActivitiesList.php @@ -10,31 +10,18 @@ namespace GaletteEvents\Filters; -use Analog\Analog; use Galette\Core\Pagination; use Galette\Enums\SQLOrder; use GaletteEvents\Repository\Activities; /** - * Events lists filters and paginator + * Activities lists paginator * - * @author sJohan Cwiklinski - * - * @property string $query + * @author Johan Cwiklinski */ - class ActivitiesList extends Pagination { - //filters - private ?string $name_filter = null; - private ?bool $active_filter = null; - private string $query; - - /** @var array */ - protected array $list_fields = [ - 'name_filter', - 'active_filter' - ]; + use FiltersTrait; /** * Default constructor @@ -63,58 +50,23 @@ protected function getDefaultDirection(): SQLOrder } /** - * Reinit default parameters - */ - public function reinit(): void - { - parent::reinit(); - $this->name_filter = null; - $this->active_filter = null; - } - - /** - * Global getter method - * - * @param string $name name of the property we want to retrieve + * Activities lists have no filter, only pagination * - * @return mixed the called property + * @return array */ - public function __get(string $name): mixed + protected function getFilterNames(): array { - if (in_array($name, $this->pagination_fields)) { - return parent::__get($name); - } else { - if (in_array($name, $this->list_fields)) { - return $this->$name; - } - } - - throw new \RuntimeException( - sprintf( - 'Unable to get property "%s::%s"!', - __CLASS__, - $name - ) - ); + return []; } /** - * Global setter method + * Activities lists have no filter, only pagination * - * @param string $name name of the property we want to assign a value to - * @param mixed $value a relevant value for the property + * @param string $name Property name + * @param mixed $value Value */ - public function __set(string $name, mixed $value): void + protected function setFilter(string $name, mixed $value): bool { - if (in_array($name, $this->pagination_fields)) { - parent::__set($name, $value); - } else { - Analog::log( - '[ActivitiesList] Setting property `' . $name . '`', - Analog::DEBUG - ); - - $this->$name = $value; - } + return false; } } diff --git a/lib/GaletteEvents/Filters/BookingsList.php b/lib/GaletteEvents/Filters/BookingsList.php index 7b872d7..a1d16f9 100644 --- a/lib/GaletteEvents/Filters/BookingsList.php +++ b/lib/GaletteEvents/Filters/BookingsList.php @@ -10,7 +10,6 @@ namespace GaletteEvents\Filters; -use Analog\Analog; use Galette\Core\Pagination; use Galette\Enums\SQLOrder; use GaletteEvents\Repository\Bookings; @@ -20,34 +19,28 @@ * * @author Johan Cwiklinski * - * @property string $query - * @property string|int|null $event_filter - * @property int|string $paid_filter - * @property int $payment_type_filter - * @property array $selected - * @property string|int|null $group_filter + * @property-read ?int $event_filter + * @property-read int $paid_filter + * @property-read int $payment_type_filter + * @property-read array $selected + * @property-read ?int $group_filter + * @property-write mixed $event_filter + * @property-write mixed $paid_filter + * @property-write mixed $payment_type_filter + * @property-write mixed $selected + * @property-write mixed $group_filter */ - class BookingsList extends Pagination { - //filters - private string|int|null $event_filter; - private int|string $paid_filter; - private int $payment_type_filter; - private string|int|null $group_filter; + use FiltersTrait; + //filters + private ?int $event_filter = null; + private int $paid_filter = Bookings::FILTER_DC_PAID; + private int $payment_type_filter = -1; + private ?int $group_filter = null; /** @var array */ - private array $selected; - private string $query; - - /** @var array */ - protected array $list_fields = [ - 'event_filter', - 'paid_filter', - 'payment_type_filter', - 'selected', - 'group_filter' - ]; + private array $selected = []; /** * Default constructor @@ -81,7 +74,7 @@ protected function getDefaultDirection(): SQLOrder public function reinit(): void { parent::reinit(); - $this->event_filter = 'all'; + $this->event_filter = null; $this->paid_filter = Bookings::FILTER_DC_PAID; $this->payment_type_filter = -1; $this->selected = []; @@ -89,67 +82,54 @@ public function reinit(): void } /** - * Global getter method - * - * @param string $name name of the property we want to retrieve + * Names of the filtering properties * - * @return mixed the called property + * @return array */ - public function __get(string $name): mixed + protected function getFilterNames(): array { - if (in_array($name, $this->pagination_fields)) { - return parent::__get($name); - } else { - if (in_array($name, $this->list_fields)) { - return $this->$name; - } - } - - throw new \RuntimeException( - sprintf( - 'Unable to get property "%s::%s"!', - __CLASS__, - $name - ) - ); + return ['event_filter', 'paid_filter', 'payment_type_filter', 'selected', 'group_filter']; } /** - * Global setter method + * Set a filtering property * - * @param string $name name of the property we want to assign a value to - * @param mixed $value a relevant value for the property + * @param string $name Property name + * @param mixed $value Value */ - public function __set(string $name, mixed $value): void + protected function setFilter(string $name, mixed $value): bool { - if (in_array($name, $this->pagination_fields)) { - parent::__set($name, $value); - } else { - Analog::log( - '[BookingsList] Setting property `' . $name . '`', - Analog::DEBUG - ); - - switch ($name) { - case 'selected': - if (is_array($value)) { - $this->$name = $value; - } elseif ($value !== null) { - Analog::log( - '[BookingsList] Value for property `' . $name - . '` should be an array (' . gettype($value) . ' given)', - Analog::WARNING - ); - } - break; - case 'payment_type_filter': - $this->$name = (int)$value; - break; - default: - $this->$name = $value; - break; - } + switch ($name) { + case 'event_filter': + case 'group_filter': + $id = $this->toId($name, $value); + if ($id !== false) { + $this->$name = $id; + } + return true; + case 'paid_filter': + $this->paid_filter = $this->toChoice( + $name, + $value, + [Bookings::FILTER_DC_PAID, Bookings::FILTER_PAID, Bookings::FILTER_NOT_PAID] + ) ?? $this->paid_filter; + return true; + case 'payment_type_filter': + if (is_numeric($value)) { + $this->payment_type_filter = (int)$value; + } else { + $this->logInvalid($name, $value); + } + return true; + case 'selected': + if (is_array($value)) { + $this->selected = array_values(array_map('intval', $value)); + } else { + $this->logInvalid($name, $value); + } + return true; } + return false; } /** @@ -165,7 +145,6 @@ protected function getHref(int $page): string 'value' => (string)$page, 'event' => $this->event_filter === null ? 'all' : (string)$this->event_filter ]; - $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 828a77f..681f55d 100644 --- a/lib/GaletteEvents/Filters/EventsList.php +++ b/lib/GaletteEvents/Filters/EventsList.php @@ -20,30 +20,24 @@ * * @author Johan Cwiklinski * - * @property string $query - * @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 + * @property-read bool $calendar_filter + * @property-read ?string $start_date_filter + * @property-read ?string $raw_start_date_filter + * @property-read ?string $end_date_filter + * @property-read ?string $raw_end_date_filter + * @property-write mixed $calendar_filter + * @property-write mixed $start_date_filter + * @property-write mixed $end_date_filter */ class EventsList extends Pagination { + use FiltersTrait; + //filters private ?string $start_date_filter = null; private ?string $end_date_filter = null; private bool $calendar_filter = false; - private string $query; - - /** @var array */ - protected array $list_fields = [ - 'start_date_filter', - 'raw_start_date_filter', - 'end_date_filter', - 'raw_end_date_filter', - 'calendar_filter' - ]; /** * Default constructor @@ -83,152 +77,106 @@ public function reinit(): void } /** - * Global getter method + * Names of the filtering properties; raw dates are read only * - * @param string $name name of the property we want to retrieve + * @return array + */ + protected function getFilterNames(): array + { + return [ + 'start_date_filter', + 'raw_start_date_filter', + 'end_date_filter', + 'raw_end_date_filter', + 'calendar_filter' + ]; + } + + /** + * Get a filtering property; dates are localized, raw ones are not * - * @return mixed the called property + * @param string $name Property name */ - public function __get(string $name): mixed + protected function getFilter(string $name): mixed { - if (in_array($name, $this->pagination_fields)) { - return parent::__get($name); - } else { - if (in_array($name, $this->list_fields)) { - switch ($name) { - case 'raw_start_date_filter': - return $this->start_date_filter; - case 'raw_end_date_filter': - return $this->end_date_filter; - case 'start_date_filter': - case 'end_date_filter': - try { - if ($this->$name !== null) { - $d = new \DateTime($this->$name); - return $d->format(__("Y-m-d")); - } - } catch (\Exception $e) { - //oops, we've got a bad date :/ - Analog::log( - 'Bad date (' . $this->$name . ') | ' - . $e->getMessage(), - Analog::INFO - ); - return $this->$name; - } - break; - default: - return $this->$name; + switch ($name) { + case 'raw_start_date_filter': + return $this->start_date_filter; + case 'raw_end_date_filter': + return $this->end_date_filter; + case 'start_date_filter': + case 'end_date_filter': + if ($this->$name === null) { + return null; } - } + return (new \DateTime($this->$name))->format(__("Y-m-d")); + default: + return $this->$name; } + } - throw new \RuntimeException( - sprintf( - 'Unable to get property "%s::%s"!', - __CLASS__, - $name - ) - ); + /** + * Set a filtering property + * + * @param string $name Property name + * @param mixed $value Value + */ + protected function setFilter(string $name, mixed $value): bool + { + switch ($name) { + case 'start_date_filter': + case 'end_date_filter': + $this->$name = $value === '' || $value === null + ? null + : $this->parseDate($name, (string)$value); + return true; + case 'calendar_filter': + $this->calendar_filter = (bool)$value; + return true; + } + return false; } /** - * Global setter method + * Parse a date filter typed as a year, a month or a day + * + * A year or a month starts on its first day, or ends on its last one for the end date. + * + * @param string $name Property name + * @param string $value Typed value * - * @param string $name name of the property we want to assign a value to - * @param mixed $value a relevant value for the property + * @return string Date, as Y-m-d */ - public function __set(string $name, mixed $value): void + private function parseDate(string $name, string $value): string { - if (in_array($name, $this->pagination_fields)) { - parent::__set($name, $value); - } else { - Analog::log( - '[EventsList] Setting property `' . $name . '`', - Analog::DEBUG - ); - - switch ($name) { - case 'start_date_filter': - case 'end_date_filter': - try { - if ($value !== '') { - $y = \DateTime::createFromFormat(__("Y"), $value); - if ($y !== false) { - $month = 1; - $day = 1; - if ($name === 'end_date_filter') { - $month = 12; - $day = 31; - } - $y->setDate( - (int)$y->format('Y'), - $month, - $day - ); - $this->$name = $y->format('Y-m-d'); - } - - $ym = \DateTime::createFromFormat(__("Y-m"), $value); - if ($y === false && $ym !== false) { - $day = 1; - if ($name === 'end_date_filter') { - $day = (int)$ym->format('t'); - } - $ym->setDate( - (int)$ym->format('Y'), - (int)$ym->format('m'), - $day - ); - $this->$name = $ym->format('Y-m-d'); - } - - $d = \DateTime::createFromFormat(__("Y-m-d"), $value); - if ($y === false && $ym === false && $d !== false) { - $this->$name = $d->format('Y-m-d'); - } - - if ($y === false && $ym === false && $d === false) { - $formats = [ - __("Y"), - __("Y-m"), - __("Y-m-d"), - ]; - - $field = null; - if ($name === 'start_date_filter') { - $field = _T("start date filter"); - } - if ($name === 'end_date_filter') { - $field = _T("end date filter"); - } - - throw new \Exception( - sprintf( - //TRANS: %1$s is field label, %2$s is list of known date formats - _T('Unknown date format for %1$s.
Know formats are: %2$s'), - $field, - implode(', ', $formats) - ) - ); - } - } else { - $this->$name = null; - } - } catch (\Exception $e) { - Analog::log( - 'Wrong date format. field: ' . $name - . ', value: ' . $value . ', expected fmt: ' - . __("Y-m-d") . ' | ' . $e->getMessage(), - Analog::INFO - ); - throw $e; - } - break; - default: - $this->$name = $value; - break; - } + $end = $name === 'end_date_filter'; + + $date = \DateTime::createFromFormat('!' . __("Y"), $value); + if ($date !== false) { + return ($end ? $date->setDate((int)$date->format('Y'), 12, 31) : $date)->format('Y-m-d'); + } + + $date = \DateTime::createFromFormat('!' . __("Y-m"), $value); + if ($date !== false) { + return ($end ? $date->modify('last day of this month') : $date)->format('Y-m-d'); } + + $date = \DateTime::createFromFormat(__("Y-m-d"), $value); + if ($date !== false) { + return $date->format('Y-m-d'); + } + + Analog::log( + 'Wrong date format. field: ' . $name . ', value: ' . $value . ', expected fmt: ' . __("Y-m-d"), + Analog::INFO + ); + throw new \RuntimeException( + sprintf( + //TRANS: %1$s is field label, %2$s is list of known date formats + _T('Unknown date format for %1$s.
Know formats are: %2$s'), + $end ? _T("end date filter") : _T("start date filter"), + implode(', ', [__("Y"), __("Y-m"), __("Y-m-d")]) + ) + ); } } diff --git a/lib/GaletteEvents/Filters/FiltersTrait.php b/lib/GaletteEvents/Filters/FiltersTrait.php new file mode 100644 index 0000000..14894aa --- /dev/null +++ b/lib/GaletteEvents/Filters/FiltersTrait.php @@ -0,0 +1,166 @@ + + */ +trait FiltersTrait +{ + /** + * Names of the filtering properties that can be read + * + * @return array + */ + abstract protected function getFilterNames(): array; + + /** + * Set a filtering property + * + * @param string $name Property name + * @param mixed $value Value + * + * @return bool false if the property cannot be set + */ + abstract protected function setFilter(string $name, mixed $value): bool; + + /** + * Get a filtering property + * + * @param string $name Property name + */ + protected function getFilter(string $name): mixed + { + return $this->$name; + } + + /** + * Global isset method + * + * @param string $name Property name + */ + public function __isset(string $name): bool + { + return in_array($name, $this->pagination_fields) || in_array($name, $this->getFilterNames()); + } + + /** + * Global getter method + * + * @param string $name name of the property we want to retrieve + * + * @return mixed the called property + */ + public function __get(string $name): mixed + { + if (in_array($name, $this->pagination_fields)) { + return parent::__get($name); + } + if (in_array($name, $this->getFilterNames())) { + return $this->getFilter($name); + } + + throw new \RuntimeException( + sprintf( + 'Unable to get property "%s::%s"!', + static::class, + $name + ) + ); + } + + /** + * Global setter method + * + * @param string $name name of the property we want to assign a value to + * @param mixed $value a relevant value for the property + */ + public function __set(string $name, mixed $value): void + { + if (in_array($name, $this->pagination_fields)) { + parent::__set($name, $value); + return; + } + + if (!$this->setFilter($name, $value)) { + throw new \RuntimeException( + sprintf( + 'Unable to set property "%s::%s"!', + static::class, + $name + ) + ); + } + } + + /** + * Get an identifier from a value, null for none or all + * + * @param string $name Property name + * @param mixed $value Value + * + * @return ?int Identifier, null for none; false if the value is not valid + */ + protected function toId(string $name, mixed $value): int|false|null + { + if ($value === null || $value === '' || $value === 'all' || $value === 0 || $value === '0') { + return null; + } + if (is_numeric($value) && (int)$value > 0) { + return (int)$value; + } + $this->logInvalid($name, $value); + return false; + } + + /** + * Check a value is one of the allowed choices + * + * @param string $name Property name + * @param mixed $value Value + * @param array $choices Allowed values + * + * @return ?int Value, null if it is not allowed + */ + protected function toChoice(string $name, mixed $value, array $choices): ?int + { + if (is_numeric($value) && in_array((int)$value, $choices, true)) { + return (int)$value; + } + $this->logInvalid($name, $value); + return null; + } + + /** + * Log an invalid value, that is ignored + * + * @param string $name Property name + * @param mixed $value Value + */ + private function logInvalid(string $name, mixed $value): void + { + Analog::log( + sprintf( + '[%1$s] Invalid value for %2$s: %3$s', + static::class, + $name, + var_export($value, true) + ), + Analog::WARNING + ); + } +} diff --git a/lib/GaletteEvents/PluginGaletteEvents.php b/lib/GaletteEvents/PluginGaletteEvents.php index 883c417..9dd9799 100644 --- a/lib/GaletteEvents/PluginGaletteEvents.php +++ b/lib/GaletteEvents/PluginGaletteEvents.php @@ -14,6 +14,7 @@ use Galette\Core\Db; use Galette\Core\History; use Galette\Core\Login; +use Galette\Core\Preferences; use Galette\Core\Plugins\DashboardProviderInterface; use Galette\Core\Plugins\InstallableInterface; use Galette\Core\Plugins\MemberActionProviderInterface; @@ -46,6 +47,9 @@ class PluginGaletteEvents extends GalettePlugin implements InstallableInterface, #[Inject] protected History $history; + #[Inject] + protected Preferences $preferences; + /** * Extra menus entries * @@ -195,7 +199,7 @@ public function getNews(): ?Entry $filters = new EventsList(); $now = new \DateTime(); $filters->start_date_filter = $now->format(__('Y-m-d')); - $events = new Events($this->zdb, $this->login, $this->history, $filters); + $events = new Events($this->zdb, $this->login, $this->history, $this->preferences, $filters); $posts = []; $list = $events->getList(); diff --git a/lib/GaletteEvents/Repository/AbstractRepository.php b/lib/GaletteEvents/Repository/AbstractRepository.php new file mode 100644 index 0000000..6e81c06 --- /dev/null +++ b/lib/GaletteEvents/Repository/AbstractRepository.php @@ -0,0 +1,100 @@ + + */ +abstract class AbstractRepository extends Repository +{ + /** Primary key of listed entities */ + protected const string PK = ''; + /** Table alias used in queries */ + protected const string ALIAS = ''; + + private int $count = 0; + + /** + * Constructor + * + * @param Db $zdb Database instance + * @param Login $login Login instance + * @param History $history History instance, for listed entities + * @param Preferences $preferences Preferences instance + * @param string $entity Entity class name, relative to the plugin namespace + * @param Pagination $filters Filtering + */ + public function __construct( + Db $zdb, + Login $login, + protected History $history, + Preferences $preferences, + string $entity, + Pagination $filters + ) { + parent::__construct($zdb, $preferences, $login, $entity, 'GaletteEvents', EVENTS_PREFIX); + $this->filters = $filters; + } + + /** + * Count rows matching the query + * + * Counting on a subquery keeps grouping right. + * + * @param Select $select Original select + */ + protected function proceedCount(Select $select): void + { + $counted = clone $select; + $counted->reset(Select::COLUMNS); + $counted->reset(Select::ORDER); + $counted->reset(Select::JOINS); + $counted->columns(['id' => new Expression(static::ALIAS . '.' . static::PK)]); + foreach ($select->joins as $join) { + $counted->join($join['name'], $join['on'], [], $join['type']); + } + + $count_select = new Select(['counted' => $counted]); + $count_select->columns(['count' => new Expression('COUNT(DISTINCT id)')]); + + $this->count = (int)$this->zdb->execute($count_select)->current()['count']; + $this->filters->setCounter($this->count); + } + + /** + * Get count for current query + */ + public function getCount(): int + { + return $this->count; + } + + /** + * Nothing to initialize + * + * @param bool $check_first Check first if it seems initialized + */ + public function installInit(bool $check_first = true): bool + { + return true; + } +} diff --git a/lib/GaletteEvents/Repository/Activities.php b/lib/GaletteEvents/Repository/Activities.php index 6114d8c..10b1726 100644 --- a/lib/GaletteEvents/Repository/Activities.php +++ b/lib/GaletteEvents/Repository/Activities.php @@ -11,26 +11,25 @@ namespace GaletteEvents\Repository; use Analog\Analog; -use Galette\Repository\Repository; -use GaletteEvents\Activity; -use Galette\Core\Preferences; -use GaletteEvents\Filters\ActivitiesList; -use Laminas\Db\ResultSet\ResultSet; -use Laminas\Db\Sql\Expression; -use Galette\Core\Login; use Galette\Core\Db; use Galette\Core\History; -use Laminas\Db\Sql\Select; +use Galette\Core\Login; +use Galette\Core\Preferences; +use GaletteEvents\Activity; +use GaletteEvents\Filters\ActivitiesList; /** - * Events + * Activities * * @author Johan Cwiklinski */ -class Activities extends Repository +class Activities extends AbstractRepository { - private History $history; - private int $count; + protected const string PK = Activity::PK; + protected const string ALIAS = 'ac'; + + /** @var ActivitiesList */ + protected \Galette\Core\Pagination $filters; public const int ORDERBY_DATE = 0; public const int ORDERBY_NAME = 1; @@ -44,45 +43,33 @@ class Activities extends Repository * @param Preferences $preferences Preferences instance * @param ?ActivitiesList $filters Filtering */ - public function __construct(Db $zdb, Login $login, History $history, Preferences $preferences, ?ActivitiesList $filters = null) - { - $this->zdb = $zdb; - $this->login = $login; - $this->history = $history; - - parent::__construct($zdb, $preferences, $login, 'Activity', 'GaletteEvents', EVENTS_PREFIX); - - if ($filters === null) { - $this->filters = new ActivitiesList(); - } else { - $this->filters = $filters; - } + public function __construct( + Db $zdb, + Login $login, + History $history, + Preferences $preferences, + ?ActivitiesList $filters = null + ) { + parent::__construct($zdb, $login, $history, $preferences, 'Activity', $filters ?? new ActivitiesList()); } /** * Get activities list * - * @return array|ResultSet + * @return array */ - public function getList(): array|ResultSet + public function getList(): array { try { $select = $this->zdb->select(EVENTS_PREFIX . Activity::TABLE, 'ac'); - $select->order($this->buildOrderClause()); - $this->proceedCount($select); - + $select->order($this->buildOrderClause()); $this->filters->setLimits($select); - $results = $this->zdb->execute($select); - //@phpstan-ignore-next-line - $this->filters->query = $this->zdb->query_string; $activities = []; - foreach ($results as $row) { - $activity = new Activity($this->zdb, $this->history, $row); - $activities[] = $activity; + foreach ($this->zdb->execute($select) as $row) { + $activities[] = new Activity($this->zdb, $this->history, $row); } - return $activities; } catch (\Exception $e) { Analog::log( @@ -96,99 +83,14 @@ public function getList(): array|ResultSet /** * Builds the order clause * - * @param ?array $fields Fields list to ensure ORDER clause - * references selected fields. Optional. - * * @return array SQL ORDER clauses */ - private function buildOrderClause(?array $fields = null): array - { - $order = []; - - switch ($this->filters->orderby) { - case self::ORDERBY_DATE: - if ($this->canOrderBy('creation_date', $fields)) { - $order[] = 'creation_date ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_NAME: - if ($this->canOrderBy('name', $fields)) { - $order[] = 'name ' . $this->filters->getDirection(); - } - break; - } - - return $order; - } - - /** - * Count events from the query - * - * @param Select $select Original select - */ - private function proceedCount(Select $select): void - { - try { - $countSelect = clone $select; - $countSelect->reset($countSelect::COLUMNS); - $countSelect->reset($countSelect::ORDER); - $countSelect->reset($countSelect::HAVING); - $joins = $countSelect->joins; - $countSelect->reset($countSelect::JOINS); - foreach ($joins as $join) { - $countSelect->join( - $join['name'], - $join['on'], - [], - $join['type'] - ); - unset($join['columns']); - } - - $countSelect->columns( - [ - 'count' => new Expression('count(DISTINCT ac.' . Activity::PK . ')') - ] - ); - - $have = $select->having; - if ($have->count() > 0) { - foreach ($have->getPredicates() as $h) { - $countSelect->where($h); - } - } - - $results = $this->zdb->execute($countSelect); - - $this->count = (int)$results->current()->count; - if (isset($this->filters) && $this->count > 0) { - $this->filters->setCounter($this->count); - } - } catch (\Exception $e) { - Analog::log( - 'Cannot count activities | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; - } - } - - /** - * Get count for current query - */ - public function getCount(): int - { - return $this->count; - } - - /** - * Add default activities in database - * - * @param bool $check_first Check first if it seems initialized - */ - public function installInit(bool $check_first = true): bool + private function buildOrderClause(): array { - //to satisfy inheritance - return true; + $column = match ($this->filters->orderby) { + self::ORDERBY_NAME => 'ac.name', + default => 'ac.creation_date', + }; + return [$column . ' ' . $this->filters->getDirection()]; } } diff --git a/lib/GaletteEvents/Repository/Bookings.php b/lib/GaletteEvents/Repository/Bookings.php index eb2a6d4..c51790c 100644 --- a/lib/GaletteEvents/Repository/Bookings.php +++ b/lib/GaletteEvents/Repository/Bookings.php @@ -17,6 +17,7 @@ use Galette\Core\Login; use Galette\Core\Db; use Galette\Core\History; +use Galette\Core\Preferences; use Galette\Entity\Adherent; use Galette\Entity\Group; use GaletteEvents\Event; @@ -29,14 +30,15 @@ * * @author Johan Cwiklinski */ -class Bookings +class Bookings extends AbstractRepository { - private Db $zdb; - private Login $login; - private History $history; - private BookingsList $filters; - private int $count; - private float $sum; + protected const string PK = Booking::PK; + protected const string ALIAS = 'b'; + + /** @var BookingsList */ + protected \Galette\Core\Pagination $filters; + + private float $sum = 0; public const int ORDERBY_EVENT = 0; public const int ORDERBY_MEMBER = 1; @@ -50,22 +52,20 @@ class Bookings /** * Constructor * - * @param Db $zdb Database instance - * @param Login $login Login instance - * @param History $history History instance - * @param ?BookingsList $filters Filtering + * @param Db $zdb Database instance + * @param Login $login Login instance + * @param History $history History instance + * @param Preferences $preferences Preferences instance + * @param ?BookingsList $filters Filtering */ - public function __construct(Db $zdb, Login $login, History $history, ?BookingsList $filters = null) - { - $this->zdb = $zdb; - $this->login = $login; - $this->history = $history; - - if ($filters === null) { - $this->filters = new BookingsList(); - } else { - $this->filters = $filters; - } + public function __construct( + Db $zdb, + Login $login, + History $history, + Preferences $preferences, + ?BookingsList $filters = null + ) { + parent::__construct($zdb, $login, $history, $preferences, 'Booking', $filters ?? new BookingsList()); } /** @@ -78,16 +78,15 @@ public function __construct(Db $zdb, Login $login, History $history, ?BookingsLi public function getList(bool $full = false): array { try { - $select = $this->buildSelect(null); - $select->order($this->buildOrderClause()); - + $select = $this->buildSelect(); + $this->calculateSum($select); $this->proceedCount($select); + $select->order($this->buildOrderClause()); if ($full !== true) { $this->filters->setLimits($select); } $results = $this->zdb->execute($select); - $this->filters->query = $this->zdb->query_string; $bookings = []; foreach ($results as $row) { @@ -131,53 +130,26 @@ private function loadEvents(array $bookings): void } /** - * Builds the SELECT statement - * - * @param ?array $fields fields list to retrieve - * @param bool $count true if we want to count members - * (not applicable from static calls), defaults to false - * - * @return Select SELECT statement + * Builds the SELECT statement, filtered but neither ordered nor limited */ - private function buildSelect(?array $fields, bool $count = false): Select + private function buildSelect(): Select { - try { - $fieldsList = ['*']; - if (is_array($fields) && count($fields)) { - $fieldsList = $fields; - } - - $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, - [] - ); - $select->join( - ['e' => PREFIX_DB . EVENTS_PREFIX . Event::TABLE], - 'b.' . Event::PK . '= e.' . Event::PK, - [] - ); - - $this->buildWhereClause($select); - - $this->calculateSum($select); - - if ($count) { - $this->proceedCount($select); - } - - return $select; - } catch (\Exception $e) { - Analog::log( - 'Cannot build SELECT clause for bookings | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; - } + $select = $this->zdb->select(EVENTS_PREFIX . Booking::TABLE, 'b'); + + //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, + [] + ); + $select->join( + ['e' => PREFIX_DB . EVENTS_PREFIX . Event::TABLE], + 'b.' . Event::PK . '= e.' . Event::PK, + [] + ); + + $this->buildWhereClause($select); + return $select; } /** @@ -187,39 +159,9 @@ private function buildSelect(?array $fields, bool $count = false): Select */ private function calculateSum(Select $select): void { - try { - $sumSelect = clone $select; - $sumSelect->reset($sumSelect::COLUMNS); - $joins = $sumSelect->joins; - $sumSelect->reset($sumSelect::JOINS); - foreach ($joins as $join) { - $sumSelect->join( - $join['name'], - $join['on'], - [], - $join['type'] - ); - unset($join['columns']); - } - - $sumSelect->reset($sumSelect::ORDER); - $sumSelect->columns( - [ - 'sum' => new Expression('SUM(payment_amount)') - ] - ); - - $results = $this->zdb->execute($sumSelect); - $result = $results->current(); - - $this->sum = round((float)$result->sum, 2); - } catch (\Exception $e) { - Analog::log( - 'Cannot calculate bookings sum | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; - } + $sum_select = clone $select; + $sum_select->columns(['sum' => new Expression('SUM(b.payment_amount)')]); + $this->sum = round((float)$this->zdb->execute($sum_select)->current()['sum'], 2); } /** @@ -232,20 +174,17 @@ private function buildWhereClause(Select $select): void try { switch ($this->filters->paid_filter) { case self::FILTER_PAID: - $select->where('is_paid = true'); + $select->where(['b.is_paid' => 1]); break; case self::FILTER_NOT_PAID: - $select->where('is_paid = false'); + $select->where(['b.is_paid' => 0]); break; case self::FILTER_DC_PAID: //nothing to do here. break; } - if ( - $this->filters->event_filter !== null - && $this->filters->event_filter != 'all' - ) { + if ($this->filters->event_filter !== null) { $select->where(['b.' . Event::PK => $this->filters->event_filter]); } @@ -256,11 +195,7 @@ private function buildWhereClause(Select $select): void ); } - if ( - $this->filters->group_filter !== null - && $this->filters->group_filter != 'all' - && $this->filters->group_filter != 0 - ) { + if ($this->filters->group_filter !== null) { $select->where(['e.' . Group::PK => $this->filters->group_filter]); } @@ -301,126 +236,21 @@ private function buildWhereClause(Select $select): void } } - /** - * Is field allowed to order? it shoulsd be present in - * provided fields list (those that are SELECT'ed). - * - * @param string $field_name Field name to order by - * @param ?array $fields SELECTE'ed fields - */ - private function canOrderBy(string $field_name, ?array $fields): bool - { - if (!is_array($fields)) { - return true; - } elseif (in_array($field_name, $fields)) { - return true; - } else { - Analog::log( - 'Trying to order by ' . $field_name . ' while it is not in ' - . 'selected fields.', - Analog::WARNING - ); - return false; - } - } - /** * Builds the order clause * - * @param array $fields Fields list to ensure ORDER clause - * references selected fields. Optional. - * * @return array SQL ORDER clauses */ - private function buildOrderClause(?array $fields = null): array - { - $order = []; - - switch ($this->filters->orderby) { - case self::ORDERBY_EVENT: - if ($this->canOrderBy(Event::PK, $fields)) { - $order[] = 'e.name ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_MEMBER: - if ($this->canOrderBy(Adherent::PK, $fields)) { - $order[] = 'a.nom_adh ' . $this->filters->getDirection(); - $order[] = 'a.prenom_adh ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_BOOKDATE: - if ($this->canOrderBy('booking_date', $fields)) { - $order[] = 'booking_date ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_PAID: - if ($this->canOrderBy('is_paid', $fields)) { - $order[] = 'is_paid ' . $this->filters->getDirection(); - } - break; - } - - return $order; - } - - /** - * Count events from the query - * - * @param Select $select Original select - */ - private function proceedCount(Select $select): void - { - try { - $countSelect = clone $select; - $countSelect->reset($countSelect::COLUMNS); - $countSelect->reset($countSelect::ORDER); - $countSelect->reset($countSelect::HAVING); - $joins = $countSelect->joins; - $countSelect->reset($countSelect::JOINS); - foreach ($joins as $join) { - $countSelect->join( - $join['name'], - $join['on'], - [], - $join['type'] - ); - unset($join['columns']); - } - - $countSelect->columns( - [ - 'count' => new Expression('count(DISTINCT b.' . Booking::PK . ')') - ] - ); - - $have = $select->having; - if ($have->count() > 0) { - foreach ($have->getPredicates() as $h) { - $countSelect->where($h); - } - } - - $results = $this->zdb->execute($countSelect); - - $this->count = (int)$results->current()->count; - if ($this->count > 0) { - $this->filters->setCounter($this->count); - } - } catch (\Exception $e) { - Analog::log( - 'Cannot count bookings | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; - } - } - - /** - * Get count for current query - */ - public function getCount(): int + private function buildOrderClause(): array { - return $this->count; + $columns = match ($this->filters->orderby) { + self::ORDERBY_EVENT => ['e.name'], + self::ORDERBY_MEMBER => ['a.nom_adh', 'a.prenom_adh'], + self::ORDERBY_PAID => ['b.is_paid'], + default => ['b.booking_date'], + }; + $direction = $this->filters->getDirection(); + return array_map(fn(string $column): string => $column . ' ' . $direction, $columns); } /** diff --git a/lib/GaletteEvents/Repository/Events.php b/lib/GaletteEvents/Repository/Events.php index 4ec9c17..efb7822 100644 --- a/lib/GaletteEvents/Repository/Events.php +++ b/lib/GaletteEvents/Repository/Events.php @@ -20,24 +20,24 @@ use Galette\Core\Login; use Galette\Core\Db; use Galette\Core\History; +use Galette\Core\Preferences; use Galette\Entity\Group; use Galette\Repository\Groups; use GaletteEvents\Event; use GaletteEvents\Filters\EventsList; -use Laminas\Db\Sql\Select; /** * Events * * @author Johan Cwiklinski */ -class Events +class Events extends AbstractRepository { - private Db $zdb; - private Login $login; - private History $history; - private EventsList $filters; - private int $count = 0; + protected const string PK = Event::PK; + protected const string ALIAS = 'e'; + + /** @var EventsList */ + protected \Galette\Core\Pagination $filters; public const int ORDERBY_DATE = 0; public const int ORDERBY_NAME = 1; @@ -46,22 +46,20 @@ class Events /** * Constructor * - * @param Db $zdb Database instance - * @param Login $login Login instance - * @param History $history History instance - * @param ?EventsList $filters Filtering + * @param Db $zdb Database instance + * @param Login $login Login instance + * @param History $history History instance + * @param Preferences $preferences Preferences instance + * @param ?EventsList $filters Filtering */ - public function __construct(Db $zdb, Login $login, History $history, ?EventsList $filters = null) - { - $this->zdb = $zdb; - $this->login = $login; - $this->history = $history; - - if ($filters === null) { - $this->filters = new EventsList(); - } else { - $this->filters = $filters; - } + public function __construct( + Db $zdb, + Login $login, + History $history, + Preferences $preferences, + ?EventsList $filters = null + ) { + parent::__construct($zdb, $login, $history, $preferences, 'Event', $filters ?? new EventsList()); } /** @@ -91,10 +89,7 @@ public function getList(bool $bookable = false, bool $fullcalendar = false, bool if (!$this->login->isAdmin() && !$this->login->isStaff()) { $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 - )); + $groups = self::getVisibleGroups($this->login); $visible = [new Predicate\IsNull('e.' . Group::PK)]; if (count($groups)) { @@ -109,7 +104,7 @@ public function getList(bool $bookable = false, bool $fullcalendar = false, bool } else { $set = [new PredicateSet( [ - new Predicate\Operator('e.is_open', '=', true), + new Predicate\Operator('e.is_open', '=', 1), new Predicate\Operator('e.begin_date', '>=', date('Y-m-d')), $visible ] @@ -147,7 +142,6 @@ public function getList(bool $bookable = false, bool $fullcalendar = false, bool $this->filters->setLimits($select); } $results = $this->zdb->execute($select); - $this->filters->query = $this->zdb->query_string; $rows = []; foreach ($results as $row) { @@ -233,6 +227,37 @@ public function getList(bool $bookable = false, bool $fullcalendar = false, bool } } + /** + * Groups whose events a member can see and book: the ones they belong to or manage + * + * Events without group are visible to every member. + * + * @param Login $login Logged-in member + * + * @return array + */ + public static function getVisibleGroups(Login $login): array + { + return array_values(array_unique(array_merge( + array_map('intval', Groups::loadGroups((int)$login->id, false, false)), + array_map('intval', $login->managed_groups) + ))); + } + + /** + * Is an event of this group visible to a member + * + * @param ?int $group Event group, if any + * @param Login $login Logged-in member + */ + public static function isVisible(?int $group, Login $login): bool + { + return $login->isAdmin() + || $login->isStaff() + || $group === null + || in_array($group, self::getVisibleGroups($login), true); + } + /** * Count attendees of events, and the paid ones * @@ -287,122 +312,18 @@ private function escape(string $value): string return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } - /** - * Is field allowed to order? it should be present in - * provided fields list (those that are SELECT'ed). - * - * @param string $field_name Field name to order by - * @param ?array $fields SELECTE'ed fields - */ - private function canOrderBy(string $field_name, ?array $fields = null): bool - { - if (!is_array($fields)) { - return true; - } elseif (in_array($field_name, $fields)) { - return true; - } else { - Analog::log( - 'Trying to order by ' . $field_name . ' while it is not in ' - . 'selected fields.', - Analog::WARNING - ); - return false; - } - } - /** * Builds the order clause * - * @param array $fields Fields list to ensure ORDER clause - * references selected fields. Optional. - * * @return array SQL ORDER clauses */ - private function buildOrderClause(?array $fields = null): array - { - $order = []; - - switch ($this->filters->orderby) { - case self::ORDERBY_DATE: - if ($this->canOrderBy('begin_date', $fields)) { - $order[] = 'begin_date ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_NAME: - if ($this->canOrderBy('name', $fields)) { - $order[] = 'name ' . $this->filters->getDirection(); - } - break; - case self::ORDERBY_TOWN: - if ($this->canOrderBy('town', $fields)) { - $order[] = 'town ' . $this->filters->getDirection(); - } - break; - } - - return $order; - } - - /** - * Count events from the query - * - * @param Select $select Original select - */ - private function proceedCount(Select $select): void - { - try { - $countSelect = clone $select; - $countSelect->reset($countSelect::COLUMNS); - $countSelect->reset($countSelect::ORDER); - $countSelect->reset($countSelect::HAVING); - $countSelect->reset($countSelect::GROUP); - $joins = $countSelect->joins; - $countSelect->reset($countSelect::JOINS); - foreach ($joins as $join) { - $countSelect->join( - $join['name'], - $join['on'], - [], - $join['type'] - ); - unset($join['columns']); - } - - $countSelect->columns( - [ - 'count' => new Expression('count(DISTINCT e.' . Event::PK . ')') - ] - ); - - $have = $select->having; - if ($have->count() > 0) { - foreach ($have->getPredicates() as $h) { - $countSelect->where($h); - } - } - - $results = $this->zdb->execute($countSelect); - - if ($result = $results->current()) { - $this->count = (int)$result->count; - if ($this->count > 0) { - $this->filters->setCounter($this->count); - } - } - } catch (\Exception $e) { - Analog::log( - 'Cannot count events | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; - } - } - - /** - * Get count for current query - */ - public function getCount(): int + private function buildOrderClause(): array { - return $this->count; + $column = match ($this->filters->orderby) { + self::ORDERBY_NAME => 'e.name', + self::ORDERBY_TOWN => 'e.town', + default => 'e.begin_date', + }; + return [$column . ' ' . $this->filters->getDirection()]; } } diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php index 8a3888a..18d9311 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php @@ -611,6 +611,7 @@ public function testFilter(): void $test_response->getHeaders() ); + $this->expectLogEntry(Analog::WARNING, 'Invalid value for group_filter'); $filters = $this->session->plugin_events_bookings_filter; $this->assertSame(20, $filters->show); $this->assertEquals(\GaletteEvents\Repository\Bookings::FILTER_PAID, $filters->paid_filter); @@ -625,6 +626,38 @@ public function testFilter(): void ['Location' => [$this->routeparser->urlFor('events_bookings', ['event' => 'all'])]], $test_response->getHeaders() ); - $this->assertSame('all', $this->session->plugin_events_bookings_filter->event_filter); + $this->assertNull($this->session->plugin_events_bookings_filter->event_filter); + } + + /** + * Filtering on an event shows its bookings, and all bookings drop the event filter + */ + public function testFilterOnEvent(): void + { + $member_one = $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $event = $this->insertEvent('Event'); + $other = $this->insertEvent('Other event'); + $this->insertBooking($event, $member_one->id); + $this->insertBooking($other, $member_two->id); + $this->logSuperAdmin(); + + $request = $this->createRequest('filter-bookingslist', ['event' => 'all'], 'POST') + ->withParsedBody(['event_filter' => (string)$event]); + $test_response = $this->app->handle($request); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('events_bookings', ['event' => (string)$event])]], + $test_response->getHeaders() + ); + + $body = (string)$this->app->handle($this->createRequest('events_bookings', ['event' => 'guess']))->getBody(); + $this->assertStringContainsString($member_one->sfullname, $body); + $this->assertStringNotContainsString($member_two->sfullname, $body); + + $body = (string)$this->app->handle($this->createRequest('events_bookings', ['event' => 'all']))->getBody(); + $this->assertStringContainsString($member_one->sfullname, $body); + $this->assertStringContainsString($member_two->sfullname, $body); + $this->assertNull($this->session->plugin_events_bookings_filter->event_filter); + $this->expectNoLogEntry(); } } diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php index 03015cc..9e510ed 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php @@ -141,8 +141,8 @@ public function testDetachActivityWaitsForStore(): void $test_response->getHeaders() ); $this->expectFlashData([ - 'warning_detected' => ['Do not forget to store the event'], 'success_detected' => ['Activity has been detached from event.'], + 'warning_detected' => ['Do not forget to store the event'], ]); $this->assertSame([$dinner => 1, $lodging => 1], $this->getEventActivities($event)); @@ -173,8 +173,8 @@ public function testAttachActivityOnIncompleteEvent(): void $test_response->getHeaders() ); $this->expectFlashData([ - 'warning_detected' => ['Do not forget to store the event'], 'success_detected' => ['Activity has been attached to event.'], + 'warning_detected' => ['Do not forget to store the event'], ]); $this->expectLogEntry(Analog::ERROR, 'Some errors has been threw attempting to edit/store an event'); //form shows posted values, with the attached activity diff --git a/tests/GaletteEvents/Filters/tests/units/Filters.php b/tests/GaletteEvents/Filters/tests/units/Filters.php new file mode 100644 index 0000000..b10aa79 --- /dev/null +++ b/tests/GaletteEvents/Filters/tests/units/Filters.php @@ -0,0 +1,118 @@ + + */ +class Filters extends GaletteTestCase +{ + protected bool $db_transactions = false; + + /** + * Only known properties are read and written + */ + public function testWhitelist(): void + { + $filters = new BookingsList(); + $this->assertTrue($filters->__isset('event_filter')); + $this->assertTrue($filters->__isset('show')); + $this->assertFalse($filters->__isset('query')); + + foreach ([new BookingsList(), new EventsList(), new ActivitiesList()] as $filters) { + try { + $filters->__set('list_fields', []); + $this->fail('Unknown property must not be set on ' . $filters::class); + } catch (\RuntimeException $e) { + $this->assertStringContainsString('Unable to set property', $e->getMessage()); + } + } + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unable to get property'); + (new EventsList())->__get('query'); + } + + /** + * Bookings filters are typed, invalid values are ignored + */ + public function testBookingsValues(): void + { + $filters = new BookingsList(); + $this->assertNull($filters->event_filter); + + $filters->event_filter = '12'; + $this->assertSame(12, $filters->event_filter); + $filters->event_filter = 'all'; + $this->assertNull($filters->event_filter); + $filters->group_filter = '3'; + $this->assertSame(3, $filters->group_filter); + $filters->group_filter = '0'; + $this->assertNull($filters->group_filter); + + $filters->paid_filter = (string)Bookings::FILTER_PAID; + $this->assertSame(Bookings::FILTER_PAID, $filters->paid_filter); + $filters->payment_type_filter = '2'; + $this->assertSame(2, $filters->payment_type_filter); + $filters->selected = ['4', '5']; + $this->assertSame([4, 5], $filters->selected); + + $filters->event_filter = 'not a number'; + $this->assertNull($filters->event_filter); + $filters->paid_filter = 42; + $this->assertSame(Bookings::FILTER_PAID, $filters->paid_filter); + $this->expectLogEntry(Analog::WARNING, 'Invalid value for event_filter'); + $this->expectLogEntry(Analog::WARNING, 'Invalid value for paid_filter'); + + $filters->reinit(); + $this->assertNull($filters->event_filter); + $this->assertSame([], $filters->selected); + } + + /** + * Events dates filters take a year, a month or a day + */ + public function testEventsDates(): void + { + $filters = new 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-03-15'; + $this->assertSame('2026-03-15', $filters->raw_start_date_filter); + $this->assertSame('2026-03-15', $filters->start_date_filter); + $filters->end_date_filter = ''; + $this->assertNull($filters->raw_end_date_filter); + + $filters->calendar_filter = 1; + $this->assertTrue($filters->calendar_filter); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unknown date format for start date filter'); + $filters->start_date_filter = 'yesterday'; + } +} diff --git a/tests/GaletteEvents/Repository/tests/units/Bookings.php b/tests/GaletteEvents/Repository/tests/units/Bookings.php index c76086c..0bd76a9 100644 --- a/tests/GaletteEvents/Repository/tests/units/Bookings.php +++ b/tests/GaletteEvents/Repository/tests/units/Bookings.php @@ -42,7 +42,7 @@ public function tearDown(): void */ private function getVisibleBookings(): array { - $bookings = new \GaletteEvents\Repository\Bookings($this->zdb, $this->login, $this->history); + $bookings = new \GaletteEvents\Repository\Bookings($this->zdb, $this->login, $this->history, $this->preferences); $ids = array_map(fn(Booking $booking): ?int => $booking->getId(), $bookings->getList(true)); sort($ids); return ['ids' => $ids, 'sum' => $bookings->getSum()]; @@ -100,7 +100,7 @@ public function testListKeepsBookingValues(): void $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, $this->history))->getList(); + $list = (new \GaletteEvents\Repository\Bookings($this->zdb, $this->login, $this->history, $this->preferences))->getList(); $this->assertCount(1, $list); $this->assertSame('Booking comment', $list[0]->getComment()); $this->assertSame('2026-02-01', $list[0]->getCreationDate()); @@ -120,7 +120,7 @@ public function testListLoadsEventsOnce(): void $this->insertBooking($this->insertEvent('Other event'), $member_one->id); $this->logSuperAdmin(); - $list = (new \GaletteEvents\Repository\Bookings($this->zdb, $this->login, $this->history))->getList(); + $list = (new \GaletteEvents\Repository\Bookings($this->zdb, $this->login, $this->history, $this->preferences))->getList(); $this->assertCount(3, $list); $events = []; foreach ($list as $booking) { @@ -161,7 +161,7 @@ public function testFilters(): void foreach ($filters as $name => $value) { $bookings_filters->$name = $value; } - $bookings = new \GaletteEvents\Repository\Bookings($this->zdb, $this->login, $this->history, $bookings_filters); + $bookings = new \GaletteEvents\Repository\Bookings($this->zdb, $this->login, $this->history, $this->preferences, $bookings_filters); $ids = array_map(fn(Booking $booking): ?int => $booking->getId(), $bookings->getList()); return ['ids' => $ids, 'count' => $bookings->getCount(), 'sum' => $bookings->getSum()]; }; diff --git a/tests/GaletteEvents/Repository/tests/units/Events.php b/tests/GaletteEvents/Repository/tests/units/Events.php index 5238bea..c5dd157 100644 --- a/tests/GaletteEvents/Repository/tests/units/Events.php +++ b/tests/GaletteEvents/Repository/tests/units/Events.php @@ -44,7 +44,7 @@ public function tearDown(): void */ private function getListed(bool $bookable = false): array { - $events = new \GaletteEvents\Repository\Events($this->zdb, $this->login, $this->history); + $events = new \GaletteEvents\Repository\Events($this->zdb, $this->login, $this->history, $this->preferences); $names = []; foreach ($events->getList($bookable) as $event) { $this->assertInstanceOf(\GaletteEvents\Event::class, $event); @@ -65,7 +65,7 @@ private function getCalendar(): array $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, $this->history, $filters); + $events = new \GaletteEvents\Repository\Events($this->zdb, $this->login, $this->history, $this->preferences, $filters); $names = []; foreach ($events->getList(false, true) as $event) { $this->assertInstanceOf(\ArrayObject::class, $event); @@ -130,4 +130,33 @@ public function testVisibility(): void $this->assertSame($all, $this->getListed(true)); $this->assertSame(array_values(array_diff($all, ['public far'])), $this->getCalendar()); } + + /** + * Events are counted once whatever their bookings, ordered and paginated + */ + public function testCountOrderAndPages(): void + { + $member_one = $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $booked = $this->insertEvent('Booked', ['town' => 'Paris']); + $this->insertBooking($booked, $member_one->id); + $this->insertBooking($booked, $member_two->id); + $this->insertEvent('Another', ['town' => 'Rennes']); + $this->insertEvent('Third', ['town' => 'Arras']); + $this->logSuperAdmin(); + + $filters = new EventsList(); + $filters->show = 2; + $filters->orderby = \GaletteEvents\Repository\Events::ORDERBY_TOWN; + $events = new \GaletteEvents\Repository\Events($this->zdb, $this->login, $this->history, $this->preferences, $filters); + $names = array_map(fn(\GaletteEvents\Event $event): string => $event->getName(), $events->getList()); + $this->assertSame(3, $events->getCount()); + //events list is ordered descending by default + $this->assertSame(['Another', 'Booked'], $names); + + $filters->current_page = 2; + $names = array_map(fn(\GaletteEvents\Event $event): string => $event->getName(), $events->getList()); + $this->assertSame(['Third'], $names); + $this->assertSame(3, $events->getCount()); + } } diff --git a/tests/GaletteEvents/tests/units/Event.php b/tests/GaletteEvents/tests/units/Event.php index 4be0cc4..dff27e9 100644 --- a/tests/GaletteEvents/tests/units/Event.php +++ b/tests/GaletteEvents/tests/units/Event.php @@ -280,6 +280,24 @@ public function testRemove(): void (new \GaletteEvents\Event($this->zdb, $this->login, $this->history))->load($id); } + /** + * Closed events are stored closed, and reopened + */ + public function testStoreOpenFlag(): void + { + $this->logSuperAdmin(); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history); + $this->assertTrue($event->check($this->getFormValues(['open' => null]))); + $event->store(); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history, (int)$event->getId()); + $this->assertFalse($event->isOpenFlag()); + + $this->assertTrue($event->check($this->getFormValues())); + $event->store(); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history, (int)$event->getId()); + $this->assertTrue($event->isOpenFlag()); + } + /** * Events are stored in the running transaction, and nothing is kept when storage fails */