diff --git a/lang/events.pot b/lang/events.pot index abb9c88..c434563 100644 --- a/lang/events.pot +++ b/lang/events.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-27 08:45+0200\n" +"POT-Creation-Date: 2026-09-27 12:35+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,43 +18,43 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: ../lib/GaletteEvents/Event.php:182 +#: ../lib/GaletteEvents/Event.php:116 msgid "Begin date is mandatory" msgstr "" -#: ../lib/GaletteEvents/Event.php:206 ../templates/default/event.html.twig:40 +#: ../lib/GaletteEvents/Event.php:119 ../templates/default/event.html.twig:40 msgid "Begin date" msgstr "" -#: ../lib/GaletteEvents/Event.php:208 ../templates/default/event.html.twig:46 +#: ../lib/GaletteEvents/Event.php:120 ../templates/default/event.html.twig:46 msgid "End date" msgstr "" -#: ../lib/GaletteEvents/Event.php:226 +#: ../lib/GaletteEvents/Event.php:137 msgid "End date must be later or equal to begin date" msgstr "" -#: ../lib/GaletteEvents/Event.php:232 ../lib/GaletteEvents/Activity.php:155 +#: ../lib/GaletteEvents/Event.php:143 ../lib/GaletteEvents/Activity.php:92 msgid "Name is mandatory" msgstr "" -#: ../lib/GaletteEvents/Event.php:248 +#: ../lib/GaletteEvents/Event.php:159 msgid "Please select a group you own!" msgstr "" -#: ../lib/GaletteEvents/Event.php:255 +#: ../lib/GaletteEvents/Event.php:166 msgid "Town is mandatory" msgstr "" -#: ../lib/GaletteEvents/Event.php:380 -msgid "Event added" +#: ../lib/GaletteEvents/Event.php:277 +msgid "Fail to add new event." msgstr "" -#: ../lib/GaletteEvents/Event.php:384 -msgid "Fail to add new event." +#: ../lib/GaletteEvents/Event.php:286 +msgid "Event added" msgstr "" -#: ../lib/GaletteEvents/Event.php:403 +#: ../lib/GaletteEvents/Event.php:302 msgid "Event updated" msgstr "" @@ -70,286 +70,306 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: ../lib/GaletteEvents/Controllers/CsvController.php:86 -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:226 +#: ../lib/GaletteEvents/Controllers/CsvController.php:87 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:233 #: ../templates/default/booking.html.twig:33 ../templates/default/booking.html.twig:39 #: ../templates/default/bookings.html.twig:49 ../templates/default/bookings.html.twig:77 msgid "Event" msgstr "" -#: ../lib/GaletteEvents/Controllers/CsvController.php:90 +#: ../lib/GaletteEvents/Controllers/CsvController.php:91 #: ../templates/default/event.html.twig:86 msgid "Zip code" msgstr "" -#: ../lib/GaletteEvents/Controllers/CsvController.php:96 +#: ../lib/GaletteEvents/Controllers/CsvController.php:97 #: ../templates/default/booking.html.twig:63 msgid "Number of persons" msgstr "" -#: ../lib/GaletteEvents/Controllers/CsvController.php:111 +#. TRANS: %1$s is the event identifier +#: ../lib/GaletteEvents/Controllers/CsvController.php:110 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:389 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:121 +#, php-format +msgid "No event #%1$s." +msgstr "" + +#: ../lib/GaletteEvents/Controllers/CsvController.php:125 #: ../templates/default/booking.html.twig:115 msgid "Amount" msgstr "" -#: ../lib/GaletteEvents/Controllers/CsvController.php:113 +#: ../lib/GaletteEvents/Controllers/CsvController.php:127 #: ../templates/default/booking.html.twig:127 msgid "Bank name" msgstr "" #. TRANS: Bank check number -#: ../lib/GaletteEvents/Controllers/CsvController.php:115 +#: ../lib/GaletteEvents/Controllers/CsvController.php:129 #: ../templates/default/booking.html.twig:133 msgid "Check number" msgstr "" #. TRANS: this is a filename: all lowercase, no special character, no space. -#: ../lib/GaletteEvents/Controllers/CsvController.php:170 +#: ../lib/GaletteEvents/Controllers/CsvController.php:184 msgid "bookingslist" msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:102 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:103 msgid "Events management" msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:128 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:129 msgid "Javascript libraries has not been built!" msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:137 -#: ../lib/GaletteEvents/PluginGaletteEvents.php:136 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:138 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:138 msgid "Events calendar" msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:288 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:299 msgid "Activity has been attached to event." msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:289 -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:295 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:300 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:306 msgid "Do not forget to store the event" msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:291 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:302 #: ../templates/default/event.html.twig:179 msgid "Please choose an activity to add" msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:294 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:305 msgid "Activity has been detached from event." msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:303 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:315 msgid "New event has been successfully added." msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:305 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:317 msgid "Event has been modified." msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:308 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:324 msgid "An error occurred while storing the event." msgstr "" #. TRANS: %1$s is the event name -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:417 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:468 #, php-format msgid "Remove event '%1$s'" msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:134 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:150 msgid "Bookings management" msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:258 -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:322 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:274 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:338 msgid "No booking was selected, please check at least one." msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:317 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:333 msgid "No action was matching." msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:381 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:403 msgid "Booking" msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:510 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:530 msgid "New booking has been successfully added." msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:512 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:532 msgid "Booking has been modified." msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:516 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:539 msgid "An error occurred while storing the booking." msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:524 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:546 msgid "Do not forget to store the booking" msgstr "" +#. TRANS: %1$s is the booking identifier +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:617 +#, php-format +msgid "No booking #%1$s." +msgstr "" + #. TRANS: %1$s is the member name, %2$s the event name. -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:641 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:698 #, php-format msgid "Remove booking for %1$s on %2$s" msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:94 +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:96 msgid "Activities management" msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:156 +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:164 msgid "Activity" msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:212 +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:218 msgid "New activity has been successfully added." msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:214 +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:220 msgid "Activity has been modified." msgstr "" -#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:218 +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:227 msgid "An error occurred while storing the activity." msgstr "" +#. TRANS: %1$s is the activity identifier +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:282 +#, php-format +msgid "No activity #%1$s." +msgstr "" + #. TRANS %1$s is activity name -#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:297 +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:341 #, php-format msgid "Remove activity %1$s" msgstr "" -#: ../lib/GaletteEvents/Booking.php:177 +#: ../lib/GaletteEvents/Booking.php:118 msgid "Event is mandatory" msgstr "" -#: ../lib/GaletteEvents/Booking.php:183 +#: ../lib/GaletteEvents/Booking.php:127 msgid "This event cannot be booked." msgstr "" -#. TRANS: %1$s is activity name -#: ../lib/GaletteEvents/Booking.php:193 -#, php-format -msgid "%1$s is mandatory for this event!" -msgstr "" - -#: ../lib/GaletteEvents/Booking.php:231 +#: ../lib/GaletteEvents/Booking.php:151 msgid "Amount must be a number." msgstr "" -#: ../lib/GaletteEvents/Booking.php:236 +#: ../lib/GaletteEvents/Booking.php:156 msgid "Please specify amount if booking has been paid ;)" msgstr "" -#: ../lib/GaletteEvents/Booking.php:257 +#: ../lib/GaletteEvents/Booking.php:177 msgid "Member is mandatory" msgstr "" -#: ../lib/GaletteEvents/Booking.php:271 +#: ../lib/GaletteEvents/Booking.php:191 msgid "You can only book other members on events of groups you manage." msgstr "" -#: ../lib/GaletteEvents/Booking.php:281 +#: ../lib/GaletteEvents/Booking.php:201 msgid "There must be at least one person" msgstr "" -#: ../lib/GaletteEvents/Booking.php:290 +#: ../lib/GaletteEvents/Booking.php:210 msgid "Booking date is mandatory!" msgstr "" -#: ../lib/GaletteEvents/Booking.php:314 +#: ../lib/GaletteEvents/Booking.php:212 msgid "booking date" msgstr "" #. TRANS: first replacement is member name, second is event name -#: ../lib/GaletteEvents/Booking.php:336 +#: ../lib/GaletteEvents/Booking.php:235 #, php-format msgid "A booking already exists for %1$s in %2$s" msgstr "" -#: ../lib/GaletteEvents/Booking.php:402 -msgid "Booking added" +#. TRANS: %1$s is activity name +#: ../lib/GaletteEvents/Booking.php:274 +#, php-format +msgid "%1$s is mandatory for this event!" msgstr "" -#: ../lib/GaletteEvents/Booking.php:406 +#: ../lib/GaletteEvents/Booking.php:325 msgid "Fail to add new booking." msgstr "" -#: ../lib/GaletteEvents/Booking.php:425 -msgid "Booking updated" +#: ../lib/GaletteEvents/Booking.php:334 +msgid "Booking added" msgstr "" -#: ../lib/GaletteEvents/Activity.php:221 -msgid "Activity added" +#: ../lib/GaletteEvents/Booking.php:350 +msgid "Booking updated" msgstr "" -#: ../lib/GaletteEvents/Activity.php:226 +#: ../lib/GaletteEvents/Activity.php:145 msgid "Fail to add new activity." msgstr "" -#: ../lib/GaletteEvents/Activity.php:245 +#: ../lib/GaletteEvents/Activity.php:154 +msgid "Activity added" +msgstr "" + +#: ../lib/GaletteEvents/Activity.php:170 msgid "Activity updated" msgstr "" -#: ../lib/GaletteEvents/Repository/Events.php:170 +#: ../lib/GaletteEvents/Repository/Events.php:182 msgid "Event information" msgstr "" -#: ../lib/GaletteEvents/Repository/Events.php:174 +#: ../lib/GaletteEvents/Repository/Events.php:186 msgid "Start date:" msgstr "" -#: ../lib/GaletteEvents/Repository/Events.php:175 +#: ../lib/GaletteEvents/Repository/Events.php:187 msgid "End date:" msgstr "" -#: ../lib/GaletteEvents/Repository/Events.php:176 +#: ../lib/GaletteEvents/Repository/Events.php:188 msgid "Location:" msgstr "" -#: ../lib/GaletteEvents/Repository/Events.php:178 +#: ../lib/GaletteEvents/Repository/Events.php:190 msgid "Comment:" msgstr "" #. TRANS: %1$s is the number of paid attendees -#: ../lib/GaletteEvents/Repository/Events.php:195 +#: ../lib/GaletteEvents/Repository/Events.php:199 #, php-format msgid "%1$s paid" msgstr "" -#: ../lib/GaletteEvents/Repository/Events.php:200 +#: ../lib/GaletteEvents/Repository/Events.php:204 msgid "Attendees:" msgstr "" -#: ../lib/GaletteEvents/Repository/Events.php:208 -#: ../lib/GaletteEvents/PluginGaletteEvents.php:93 +#: ../lib/GaletteEvents/Repository/Events.php:212 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:95 #: ../templates/default/booking.html.twig:78 ../templates/default/event.html.twig:108 msgid "Activities" msgstr "" -#: ../lib/GaletteEvents/PluginGaletteEvents.php:58 -#: ../lib/GaletteEvents/PluginGaletteEvents.php:62 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:60 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:64 #: ../templates/default/activities.html.twig:41 ../templates/default/activities.html.twig:74 msgid "Events" msgstr "" -#: ../lib/GaletteEvents/PluginGaletteEvents.php:69 -#: ../lib/GaletteEvents/PluginGaletteEvents.php:135 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:71 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:137 msgid "Calendar" msgstr "" -#: ../lib/GaletteEvents/PluginGaletteEvents.php:75 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:77 msgid "Bookings" msgstr "" -#: ../lib/GaletteEvents/PluginGaletteEvents.php:156 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:158 msgid "New event booking" msgstr "" -#: ../lib/GaletteEvents/PluginGaletteEvents.php:212 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:214 msgid "Upcoming events" msgstr "" diff --git a/lang/events_en_US.po b/lang/events_en_US.po index 8dc055e..3da8665 100644 --- a/lang/events_en_US.po +++ b/lang/events_en_US.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-27 08:45+0200\n" -"PO-Revision-Date: 2026-09-27 08:45+0200\n" +"POT-Creation-Date: 2026-09-27 12:35+0200\n" +"PO-Revision-Date: 2026-09-27 12:35+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: en_US\n" @@ -17,43 +17,43 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: ../lib/GaletteEvents/Event.php:182 +#: ../lib/GaletteEvents/Event.php:116 msgid "Begin date is mandatory" msgstr "Begin date is mandatory" -#: ../lib/GaletteEvents/Event.php:206 ../templates/default/event.html.twig:40 +#: ../lib/GaletteEvents/Event.php:119 ../templates/default/event.html.twig:40 msgid "Begin date" msgstr "Begin date" -#: ../lib/GaletteEvents/Event.php:208 ../templates/default/event.html.twig:46 +#: ../lib/GaletteEvents/Event.php:120 ../templates/default/event.html.twig:46 msgid "End date" msgstr "End date" -#: ../lib/GaletteEvents/Event.php:226 +#: ../lib/GaletteEvents/Event.php:137 msgid "End date must be later or equal to begin date" msgstr "End date must be later or equal to begin date" -#: ../lib/GaletteEvents/Event.php:232 ../lib/GaletteEvents/Activity.php:155 +#: ../lib/GaletteEvents/Event.php:143 ../lib/GaletteEvents/Activity.php:92 msgid "Name is mandatory" msgstr "Name is mandatory" -#: ../lib/GaletteEvents/Event.php:248 +#: ../lib/GaletteEvents/Event.php:159 msgid "Please select a group you own!" msgstr "Please select a group you own!" -#: ../lib/GaletteEvents/Event.php:255 +#: ../lib/GaletteEvents/Event.php:166 msgid "Town is mandatory" msgstr "Town is mandatory" -#: ../lib/GaletteEvents/Event.php:380 -msgid "Event added" -msgstr "Event added" - -#: ../lib/GaletteEvents/Event.php:384 +#: ../lib/GaletteEvents/Event.php:277 msgid "Fail to add new event." msgstr "Fail to add new event." -#: ../lib/GaletteEvents/Event.php:403 +#: ../lib/GaletteEvents/Event.php:286 +msgid "Event added" +msgstr "Event added" + +#: ../lib/GaletteEvents/Event.php:302 msgid "Event updated" msgstr "Event updated" @@ -73,8 +73,8 @@ msgstr[1] "" "Group \"%1$s\" is used by %2$s events, it cannot be deleted. Remove the " "events or change their group first." -#: ../lib/GaletteEvents/Controllers/CsvController.php:86 -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:226 +#: ../lib/GaletteEvents/Controllers/CsvController.php:87 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:233 #: ../templates/default/booking.html.twig:33 #: ../templates/default/booking.html.twig:39 #: ../templates/default/bookings.html.twig:49 @@ -82,281 +82,301 @@ msgstr[1] "" msgid "Event" msgstr "Event" -#: ../lib/GaletteEvents/Controllers/CsvController.php:90 +#: ../lib/GaletteEvents/Controllers/CsvController.php:91 #: ../templates/default/event.html.twig:86 msgid "Zip code" msgstr "Zip code" -#: ../lib/GaletteEvents/Controllers/CsvController.php:96 +#: ../lib/GaletteEvents/Controllers/CsvController.php:97 #: ../templates/default/booking.html.twig:63 msgid "Number of persons" msgstr "Number of persons" -#: ../lib/GaletteEvents/Controllers/CsvController.php:111 +#. TRANS: %1$s is the event identifier +#: ../lib/GaletteEvents/Controllers/CsvController.php:110 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:389 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:121 +#, php-format +msgid "No event #%1$s." +msgstr "No event #%1$s." + +#: ../lib/GaletteEvents/Controllers/CsvController.php:125 #: ../templates/default/booking.html.twig:115 msgid "Amount" msgstr "Amount" -#: ../lib/GaletteEvents/Controllers/CsvController.php:113 +#: ../lib/GaletteEvents/Controllers/CsvController.php:127 #: ../templates/default/booking.html.twig:127 msgid "Bank name" msgstr "Bank name" #. TRANS: Bank check number -#: ../lib/GaletteEvents/Controllers/CsvController.php:115 +#: ../lib/GaletteEvents/Controllers/CsvController.php:129 #: ../templates/default/booking.html.twig:133 msgid "Check number" msgstr "Check number" #. TRANS: this is a filename: all lowercase, no special character, no space. -#: ../lib/GaletteEvents/Controllers/CsvController.php:170 +#: ../lib/GaletteEvents/Controllers/CsvController.php:184 msgid "bookingslist" msgstr "bookingslist" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:102 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:103 msgid "Events management" msgstr "Events management" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:128 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:129 msgid "Javascript libraries has not been built!" msgstr "Javascript libraries has not been built!" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:137 -#: ../lib/GaletteEvents/PluginGaletteEvents.php:136 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:138 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:138 msgid "Events calendar" msgstr "Events calendar" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:288 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:299 msgid "Activity has been attached to event." msgstr "Activity has been attached to event." -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:289 -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:295 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:300 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:306 msgid "Do not forget to store the event" msgstr "Do not forget to store the event" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:291 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:302 #: ../templates/default/event.html.twig:179 msgid "Please choose an activity to add" msgstr "Please choose an activity to add" -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:294 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:305 msgid "Activity has been detached from event." msgstr "Activity has been detached from event." -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:303 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:315 msgid "New event has been successfully added." msgstr "New event has been successfully added." -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:305 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:317 msgid "Event has been modified." msgstr "Event has been modified." -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:308 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:324 msgid "An error occurred while storing the event." msgstr "An error occurred while storing the event." #. TRANS: %1$s is the event name -#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:417 +#: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:468 #, php-format msgid "Remove event '%1$s'" msgstr "Remove event '%1$s'" -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:134 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:150 msgid "Bookings management" msgstr "Bookings management" -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:258 -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:322 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:274 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:338 msgid "No booking was selected, please check at least one." msgstr "No booking was selected, please check at least one." -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:317 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:333 msgid "No action was matching." msgstr "No action was matching." -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:381 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:403 msgid "Booking" msgstr "Booking" -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:510 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:530 msgid "New booking has been successfully added." msgstr "New booking has been successfully added." -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:512 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:532 msgid "Booking has been modified." msgstr "Booking has been modified." -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:516 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:539 msgid "An error occurred while storing the booking." msgstr "An error occurred while storing the booking." -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:524 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:546 msgid "Do not forget to store the booking" msgstr "Do not forget to store the booking" +#. TRANS: %1$s is the booking identifier +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:617 +#, php-format +msgid "No booking #%1$s." +msgstr "No booking #%1$s." + #. TRANS: %1$s is the member name, %2$s the event name. -#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:641 +#: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:698 #, php-format msgid "Remove booking for %1$s on %2$s" msgstr "Remove booking for %1$s on %2$s" -#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:94 +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:96 msgid "Activities management" msgstr "Activities management" -#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:156 +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:164 msgid "Activity" msgstr "Activity" -#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:212 +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:218 msgid "New activity has been successfully added." msgstr "New activity has been successfully added." -#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:214 +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:220 msgid "Activity has been modified." msgstr "Activity has been modified." -#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:218 +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:227 msgid "An error occurred while storing the activity." msgstr "An error occurred while storing the activity." +#. TRANS: %1$s is the activity identifier +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:282 +#, php-format +msgid "No activity #%1$s." +msgstr "No activity #%1$s." + #. TRANS %1$s is activity name -#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:297 +#: ../lib/GaletteEvents/Controllers/Crud/ActivitiesController.php:341 #, php-format msgid "Remove activity %1$s" msgstr "Remove activity %1$s" -#: ../lib/GaletteEvents/Booking.php:177 +#: ../lib/GaletteEvents/Booking.php:118 msgid "Event is mandatory" msgstr "Event is mandatory" -#: ../lib/GaletteEvents/Booking.php:183 +#: ../lib/GaletteEvents/Booking.php:127 msgid "This event cannot be booked." msgstr "This event cannot be booked." -#. TRANS: %1$s is activity name -#: ../lib/GaletteEvents/Booking.php:193 -#, php-format -msgid "%1$s is mandatory for this event!" -msgstr "%1$s is mandatory for this event!" - -#: ../lib/GaletteEvents/Booking.php:231 +#: ../lib/GaletteEvents/Booking.php:151 msgid "Amount must be a number." msgstr "Amount must be a number." -#: ../lib/GaletteEvents/Booking.php:236 +#: ../lib/GaletteEvents/Booking.php:156 msgid "Please specify amount if booking has been paid ;)" msgstr "Please specify amount if booking has been paid ;)" -#: ../lib/GaletteEvents/Booking.php:257 +#: ../lib/GaletteEvents/Booking.php:177 msgid "Member is mandatory" msgstr "Member is mandatory" -#: ../lib/GaletteEvents/Booking.php:271 +#: ../lib/GaletteEvents/Booking.php:191 msgid "You can only book other members on events of groups you manage." msgstr "You can only book other members on events of groups you manage." -#: ../lib/GaletteEvents/Booking.php:281 +#: ../lib/GaletteEvents/Booking.php:201 msgid "There must be at least one person" msgstr "There must be at least one person" -#: ../lib/GaletteEvents/Booking.php:290 +#: ../lib/GaletteEvents/Booking.php:210 msgid "Booking date is mandatory!" msgstr "Booking date is mandatory!" -#: ../lib/GaletteEvents/Booking.php:314 +#: ../lib/GaletteEvents/Booking.php:212 msgid "booking date" msgstr "booking date" #. TRANS: first replacement is member name, second is event name -#: ../lib/GaletteEvents/Booking.php:336 +#: ../lib/GaletteEvents/Booking.php:235 #, php-format msgid "A booking already exists for %1$s in %2$s" msgstr "A booking already exists for %1$s in %2$s" -#: ../lib/GaletteEvents/Booking.php:402 -msgid "Booking added" -msgstr "Booking added" +#. TRANS: %1$s is activity name +#: ../lib/GaletteEvents/Booking.php:274 +#, php-format +msgid "%1$s is mandatory for this event!" +msgstr "%1$s is mandatory for this event!" -#: ../lib/GaletteEvents/Booking.php:406 +#: ../lib/GaletteEvents/Booking.php:325 msgid "Fail to add new booking." msgstr "Fail to add new booking." -#: ../lib/GaletteEvents/Booking.php:425 +#: ../lib/GaletteEvents/Booking.php:334 +msgid "Booking added" +msgstr "Booking added" + +#: ../lib/GaletteEvents/Booking.php:350 msgid "Booking updated" msgstr "Booking updated" -#: ../lib/GaletteEvents/Activity.php:221 -msgid "Activity added" -msgstr "Activity added" - -#: ../lib/GaletteEvents/Activity.php:226 +#: ../lib/GaletteEvents/Activity.php:145 msgid "Fail to add new activity." msgstr "Fail to add new activity." -#: ../lib/GaletteEvents/Activity.php:245 +#: ../lib/GaletteEvents/Activity.php:154 +msgid "Activity added" +msgstr "Activity added" + +#: ../lib/GaletteEvents/Activity.php:170 msgid "Activity updated" msgstr "Activity updated" -#: ../lib/GaletteEvents/Repository/Events.php:170 +#: ../lib/GaletteEvents/Repository/Events.php:182 msgid "Event information" msgstr "Event information" -#: ../lib/GaletteEvents/Repository/Events.php:174 +#: ../lib/GaletteEvents/Repository/Events.php:186 msgid "Start date:" msgstr "Start date:" -#: ../lib/GaletteEvents/Repository/Events.php:175 +#: ../lib/GaletteEvents/Repository/Events.php:187 msgid "End date:" msgstr "End date:" -#: ../lib/GaletteEvents/Repository/Events.php:176 +#: ../lib/GaletteEvents/Repository/Events.php:188 msgid "Location:" msgstr "Location:" -#: ../lib/GaletteEvents/Repository/Events.php:178 +#: ../lib/GaletteEvents/Repository/Events.php:190 msgid "Comment:" msgstr "Comment:" #. TRANS: %1$s is the number of paid attendees -#: ../lib/GaletteEvents/Repository/Events.php:195 +#: ../lib/GaletteEvents/Repository/Events.php:199 #, php-format msgid "%1$s paid" msgstr "%1$s paid" -#: ../lib/GaletteEvents/Repository/Events.php:200 +#: ../lib/GaletteEvents/Repository/Events.php:204 msgid "Attendees:" msgstr "Attendees:" -#: ../lib/GaletteEvents/Repository/Events.php:208 -#: ../lib/GaletteEvents/PluginGaletteEvents.php:93 +#: ../lib/GaletteEvents/Repository/Events.php:212 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:95 #: ../templates/default/booking.html.twig:78 #: ../templates/default/event.html.twig:108 msgid "Activities" msgstr "Activities" -#: ../lib/GaletteEvents/PluginGaletteEvents.php:58 -#: ../lib/GaletteEvents/PluginGaletteEvents.php:62 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:60 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:64 #: ../templates/default/activities.html.twig:41 #: ../templates/default/activities.html.twig:74 msgid "Events" msgstr "Events" -#: ../lib/GaletteEvents/PluginGaletteEvents.php:69 -#: ../lib/GaletteEvents/PluginGaletteEvents.php:135 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:71 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:137 msgid "Calendar" msgstr "Calendar" -#: ../lib/GaletteEvents/PluginGaletteEvents.php:75 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:77 msgid "Bookings" msgstr "Bookings" -#: ../lib/GaletteEvents/PluginGaletteEvents.php:156 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:158 msgid "New event booking" msgstr "New event booking" -#: ../lib/GaletteEvents/PluginGaletteEvents.php:212 +#: ../lib/GaletteEvents/PluginGaletteEvents.php:214 msgid "Upcoming events" msgstr "Upcoming events" diff --git a/lib/GaletteEvents/Activity.php b/lib/GaletteEvents/Activity.php index 49e57f5..0d6369c 100644 --- a/lib/GaletteEvents/Activity.php +++ b/lib/GaletteEvents/Activity.php @@ -12,7 +12,7 @@ use ArrayObject; use Galette\Core\Db; -use Galette\Core\Login; +use Galette\Core\History; use Analog\Analog; use Laminas\Db\Sql\Expression; @@ -23,6 +23,8 @@ */ class Activity { + use EntityTrait; + public const string TABLE = 'activities'; public const string PK = 'id_activity'; @@ -31,66 +33,37 @@ class Activity public const int REQUIRED = 2; private Db $zdb; - private Login $login; + private History $history; /** @var array */ private array $errors = []; - private int $id; - private string $name; + private ?int $id = null; + private string $name = ''; private bool $active = false; - private string $creation_date; + private ?string $creation_date = null; private string $comment = ''; /** * Default constructor * - * @param Db $zdb Database instance - * @param Login $login Login instance - * @param null|int|ArrayObject $args Either a ResultSet row or its id for to load - * a specific activity, or null to just - * instanciate object + * @param Db $zdb Database instance + * @param History $history History instance + * @param null|int|ArrayObject $args Either a ResultSet row or its id for to load + * a specific activity, or null to just + * instanciate object */ - public function __construct(Db $zdb, Login $login, int|ArrayObject|null $args = null) + public function __construct(Db $zdb, History $history, int|ArrayObject|null $args = null) { $this->zdb = $zdb; - $this->login = $login; + $this->history = $history; - if (is_int($args) && $args > 0) { + if (is_int($args)) { $this->load($args); } elseif (is_object($args)) { $this->loadFromRS($args); } } - /** - * Loads an activity from its id - * - * @param int $id the identifiant for the activity to load - * - * @return bool true if query succeed, false otherwise - */ - public function load(int $id): bool - { - try { - $select = $this->zdb->select($this->getTableName()); - $select->where([self::PK => $id]); - $results = $this->zdb->execute($select); - - if ($results->count() > 0) { - $this->loadFromRS($results->current()); - return true; - } else { - return false; - } - } catch (\Exception $e) { - Analog::log( - 'Cannot load activity #`' . $id . '` | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; - } - } - /** * Populate object from a resultset row * @@ -105,42 +78,6 @@ private function loadFromRS(ArrayObject $r): void $this->comment = $r['comment'] ?? ''; } - /** - * Remove specified event - */ - public function remove(): bool - { - $transaction = false; - - try { - if (!$this->zdb->connection->inTransaction()) { - $this->zdb->connection->beginTransaction(); - $transaction = true; - } - - $delete = $this->zdb->delete($this->getTableName()); - $delete->where([self::PK => $this->id]); - $this->zdb->execute($delete); - - //commit all changes - if ($transaction) { - $this->zdb->connection->commit(); - } - - return true; - } catch (\Exception $e) { - if ($transaction) { - $this->zdb->connection->rollBack(); - } - Analog::log( - 'Unable to delete activity ' . $this->name - . ' (' . $this->id . ') |' . $e->getMessage(), - Analog::ERROR - ); - return false; - } - } - /** * Check posted values validity * @@ -186,11 +123,9 @@ public function check(array $values): bool /** * Store the activity */ - public function store(): bool + public function store(): void { - global $hist; - - try { + $this->transactional(function (): void { $values = [ 'name' => $this->name, 'is_active' => ($this->active ? $this->active @@ -198,39 +133,29 @@ public function store(): bool 'comment' => $this->comment ]; - if (empty($this->id)) { - //we're inserting a new event + if ($this->id === null) { + //we're inserting a new activity $this->creation_date = date("Y-m-d"); $values['creation_date'] = $this->creation_date; $insert = $this->zdb->insert($this->getTableName()); $insert->values($values); $add = $this->zdb->execute($insert); - if ($add->count() > 0) { - if ($this->zdb->isPostgres()) { - /** @phpstan-ignore-next-line */ - $this->id = (int)$this->zdb->driver->getLastGeneratedValue( - PREFIX_DB . $this->getTableName() . '_id_seq' - ); - } else { - $this->id = (int)$this->zdb->driver->getLastGeneratedValue(); - } - - // logging - $hist->add( - _T("Activity added", "events"), - $this->name - ); - return true; - } else { - $hist->add(_T("Fail to add new activity.", "events")); - throw new \Exception( + if ($add->count() === 0) { + $this->history->add(_T("Fail to add new activity.", "events")); + throw new \RuntimeException( 'An error occurred inserting new activity!' ); } + $this->id = $this->getLastInsertId(); + + // logging + $this->history->add( + _T("Activity added", "events"), + $this->name + ); } else { - //we're editing an existing event - $values[self::PK] = $this->id; + //we're editing an existing activity $update = $this->zdb->update($this->getTableName()); $update ->set($values) @@ -241,67 +166,37 @@ public function store(): bool //edit == 0 does not mean there were an error, but that there //were nothing to change if ($edit->count() > 0) { - $hist->add( + $this->history->add( _T("Activity updated", "events"), $this->name ); } - return true; } - } catch (\Exception $e) { - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); - throw $e; - } + }); } /** - * Get event id + * Get activity id */ public function getId(): ?int { - return $this->id ?? null; + return $this->id; } /** - * Get event name + * Get activity name */ public function getName(): string { - return $this->name ?? ''; + return $this->name; } /** - * Get date - * - * @param string $prop Property to use - * @param bool $formatted Return date formatted, raw if false + * Get creation date, as Y-m-d */ - private function getDate(string $prop, bool $formatted = true): string + public function getCreationDate(): string { - if (!isset($this->$prop)) { - return ''; - } - - if ($formatted === true) { - $date = new \DateTime($this->$prop); - return $date->format(__("Y-m-d")); - } else { - return $this->$prop; - } - } - - /** - * Get creation date - * - * @param bool $formatted Return date formatted, raw if false - */ - public function getCreationDate(bool $formatted = true): string - { - return $this->getDate('creation_date', $formatted); + return $this->creation_date ?? ''; } /** @@ -312,14 +207,6 @@ public function isActive(): bool return $this->active; } - /** - * Get table's name - */ - protected function getTableName(): string - { - return EVENTS_PREFIX . self::TABLE; - } - /** * Get comment */ @@ -333,7 +220,7 @@ public function getComment(): string */ public function countEvents(): int { - if (empty($this->id)) { + if ($this->id === null) { return 0; } diff --git a/lib/GaletteEvents/Booking.php b/lib/GaletteEvents/Booking.php index 01fe5b7..0f26242 100644 --- a/lib/GaletteEvents/Booking.php +++ b/lib/GaletteEvents/Booking.php @@ -12,6 +12,7 @@ use ArrayObject; use Galette\Core\Db; +use Galette\Core\History; use Galette\Core\Login; use Galette\Entity\Adherent; use Galette\Entity\PaymentType; @@ -25,17 +26,22 @@ */ class Booking { + use EntityTrait; + public const string TABLE = 'bookings'; public const string PK = 'id_booking'; private Db $zdb; private Login $login; + private History $history; /** @var array */ - private array $errors; + private array $errors = []; - private int $id; - private int $event; - private int $member; + private ?int $id = null; + private ?int $event = null; + private ?int $member = null; + private ?Event $event_entity = null; + private ?Adherent $member_entity = null; private string $date = ''; private bool $paid = false; private ?float $amount = null; @@ -49,57 +55,29 @@ class Booking private array $activities = []; /** @var array> */ private array $activities_removed = []; - private string $creation_date; + private ?string $creation_date = null; /** * Default constructor * - * @param Db $zdb Database instance - * @param Login $login Login instance - * @param null|int|ArrayObject $args Either a ResultSet row or its id for to load - * a specific event, or null to just - * instanciate object + * @param Db $zdb Database instance + * @param Login $login Login instance + * @param History $history History instance + * @param null|int|ArrayObject $args Either a ResultSet row or its id for to load + * a specific event, or null to just + * instanciate object */ - public function __construct(Db $zdb, Login $login, int|ArrayObject|null $args = null) + public function __construct(Db $zdb, Login $login, History $history, int|ArrayObject|null $args = null) { $this->zdb = $zdb; $this->login = $login; + $this->history = $history; if (is_int($args)) { $this->load($args); - } elseif (is_object($args)) { + } elseif ($args !== null) { $this->loadFromRS($args); - $this->loadActivities(); - } - } - - /** - * Loads an event from its id - * - * @param int $id the identifiant for the event to load - * - * @return bool true if query succeed, false otherwise - */ - public function load(int $id): bool - { - try { - $select = $this->zdb->select($this->getTableName()); - $select->where([self::PK => $id]); - - $results = $this->zdb->execute($select); - - if ($results->count() > 0) { - $this->loadFromRS($results->current()); - $this->loadActivities(); - return true; - } else { - return false; - } - } catch (\Exception $e) { - Analog::log( - 'Cannot load booking form id `' . $id . '` | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; + } else { + $this->date = date('Y-m-d'); } } @@ -122,42 +100,7 @@ private function loadFromRS(ArrayObject $r): void $this->number_people = (int)($r['number_people'] ?? 1); $this->comment = $r['comment'] ?? ''; $this->creation_date = $r['creation_date']; - } - - /** - * Remove specified event - */ - public function remove(): bool - { - $transaction = false; - - try { - if (!$this->zdb->connection->inTransaction()) { - $this->zdb->connection->beginTransaction(); - $transaction = true; - } - - $delete = $this->zdb->delete($this->getTableName()); - $delete->where([self::PK => $this->id]); - $this->zdb->execute($delete); - - //commit all changes - if ($transaction) { - $this->zdb->connection->commit(); - } - - return true; - } catch (\Exception $e) { - if ($transaction) { - $this->zdb->connection->rollBack(); - } - Analog::log( - 'Unable to delete booking ' - . ' (' . $this->id . ') |' . $e->getMessage(), - Analog::ERROR - ); - return false; - } + $this->loadActivities(); } /** @@ -165,10 +108,8 @@ public function remove(): bool * * @param array $values All values to check, basically the $_POST array * after sending the form - * - * @return true|array */ - public function check(array $values): array|bool + public function check(array $values): bool { $this->errors = []; @@ -177,38 +118,17 @@ public function check(array $values): array|bool $this->errors[] = _T('Event is mandatory', 'events'); } else { $event_changed = $this->getId() === null || $this->getEventId() !== (int)$values['event']; - $this->event = (int)$values['event']; - $event = $this->getEvent(); - if ($event_changed && !$this->canBook($event)) { - $this->errors[] = _T('This event cannot be booked.', 'events'); + try { + $event = new Event($this->zdb, $this->login, $this->history, (int)$values['event']); + } catch (NotFoundException) { + $event = null; } - $activities = $event->getActivities(); - foreach ($activities as $aid => $entry) { - if ( - $event->isActivityRequired($aid) - && (!isset($values['activities']) || !in_array($aid, $values['activities'])) - ) { - $this->errors[] = sprintf( - //TRANS: %1$s is activity name - _T('%1$s is mandatory for this event!', 'events'), - $entry['activity']->getName() - ); - } else { - $act = [ - 'activity' => $entry['activity'], - 'checked' => (isset($values['activities']) && in_array($aid, $values['activities'])) - ]; - $this->activities[$aid] = $act; - } + if ($event === null || ($event_changed && !$this->canBook($event))) { + $this->errors[] = _T('This event cannot be booked.', 'events'); } - 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]); - } + if ($event !== null) { + $this->useEvent($event); + $this->checkActivities($event, $values['activities'] ?? []); } } @@ -289,30 +209,9 @@ public function check(array $values): array|bool if (!isset($values['booking_date']) || empty($values['booking_date'])) { $this->errors[] = _T('Booking date is mandatory!', 'events'); } else { - $value = $values['booking_date']; - try { - $d = \DateTime::createFromFormat(__("Y-m-d"), $value); - if ($d === false) { - //try with non localized date - $d = \DateTime::createFromFormat("Y-m-d", $value); - if ($d === false) { - throw new \Exception('Incorrect format'); - } - } - $this->date = $d->format('Y-m-d'); - } catch (\Exception $e) { - Analog::log( - 'Wrong date format. field: booking_date' - . ', value: ' . $value . ', expected fmt: ' - . __("Y-m-d") . ' | ' . $e->getMessage(), - Analog::INFO - ); - $this->errors[] = sprintf( - //TRANS %1$s is the expected date format, %2$s is the field label - _T('- Wrong date format (%1$s) for %2$s!'), - __("Y-m-d"), - __('booking date', 'events') - ); + $date = $this->parseDate((string)$values['booking_date'], __('booking date', 'events')); + if ($date !== null) { + $this->date = $date; } } @@ -323,7 +222,7 @@ public function check(array $values): array|bool Event::PK => $this->event, Adherent::PK => $this->member ]); - if (isset($this->id)) { + if ($this->id !== null) { $select->where->notEqualTo( self::PK, $this->id @@ -346,7 +245,7 @@ public function check(array $values): array|bool . print_r($this->errors, true), Analog::ERROR ); - return $this->errors; + return false; } else { Analog::log( 'Event checked successfully.', @@ -357,14 +256,49 @@ public function check(array $values): array|bool } /** - * Store the booking + * Check activities of the booking against the ones of its event + * + * @param Event $event Booked event + * @param array $checked Checked activities identifiers */ - public function store(): bool + private function checkActivities(Event $event, array $checked): void { - global $hist; + $activities = $event->getActivities(); + foreach ($activities as $aid => $entry) { + if ( + $event->isActivityRequired($aid) + && !in_array($aid, $checked) + ) { + $this->errors[] = sprintf( + //TRANS: %1$s is activity name + _T('%1$s is mandatory for this event!', 'events'), + $entry['activity']->getName() + ); + } else { + $act = [ + 'activity' => $entry['activity'], + 'checked' => in_array($aid, $checked) + ]; + $this->activities[$aid] = $act; + } + } + 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]); + } + } + } - try { - $this->zdb->connection->beginTransaction(); + /** + * Store the booking + */ + public function store(): void + { + $this->transactional(function (): void { $values = [ Event::PK => $this->event, Adherent::PK => $this->member, @@ -379,38 +313,29 @@ public function store(): bool 'comment' => $this->comment ]; - if (empty($this->id)) { - //we're inserting a new event + if ($this->id === null) { + //we're inserting a new booking $this->creation_date = date("Y-m-d"); $values['creation_date'] = $this->creation_date; $insert = $this->zdb->insert($this->getTableName()); $insert->values($values); $add = $this->zdb->execute($insert); - if ($add->count() > 0) { - if ($this->zdb->isPostgres()) { - /** @phpstan-ignore-next-line */ - $this->id = (int)$this->zdb->driver->getLastGeneratedValue( - PREFIX_DB . EVENTS_PREFIX . Booking::TABLE . '_id_seq' - ); - } else { - $this->id = (int)$this->zdb->driver->getLastGeneratedValue(); - } - - // logging - $hist->add( - _T("Booking added", "events"), - $this->getEvent()->getName() - ); - } else { - $hist->add(_T("Fail to add new booking.", "events")); - throw new \Exception( + if ($add->count() === 0) { + $this->history->add(_T("Fail to add new booking.", "events")); + throw new \RuntimeException( 'An error occurred inserting new booking!' ); } + $this->id = $this->getLastInsertId(); + + // logging + $this->history->add( + _T("Booking added", "events"), + $this->getEvent()->getName() + ); } else { //we're editing an existing booking - $values[self::PK] = $this->id; $update = $this->zdb->update($this->getTableName()); $update ->set($values) @@ -421,7 +346,7 @@ public function store(): bool //edit == 0 does not mean there were an error, but that there //were nothing to change if ($edit->count() > 0) { - $hist->add( + $this->history->add( _T("Booking updated", "events") ); } @@ -537,18 +462,7 @@ public function store(): bool Analog::INFO ); } - - $this->zdb->connection->commit(); - return true; - } catch (\Exception $e) { - $this->zdb->connection->rollBack(); - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); - throw $e; - } + }); } /** @@ -556,7 +470,7 @@ public function store(): bool */ public function getId(): ?int { - return $this->id ?? null; + return $this->id; } /** @@ -564,7 +478,7 @@ public function getId(): ?int */ public function getEventId(): ?int { - return $this->event ?? null; + return $this->event; } /** @@ -572,10 +486,13 @@ public function getEventId(): ?int */ public function getEvent(): ?Event { - if (isset($this->event)) { - return new Event($this->zdb, $this->login, $this->event); + if ($this->event === null) { + return null; + } + if ($this->event_entity?->getId() !== $this->event) { + $this->event_entity = new Event($this->zdb, $this->login, $this->history, $this->event); } - return null; + return $this->event_entity; } /** @@ -583,30 +500,26 @@ public function getEvent(): ?Event */ public function getMemberId(): ?int { - return $this->member ?? null; + return $this->member; } /** - * Get member + * Get member, empty if booking has no member yet */ public function getMember(): Adherent { - return new Adherent($this->zdb, $this->member); + if ($this->member_entity === null || $this->member_entity->id !== $this->member) { + $this->member_entity = new Adherent($this->zdb, $this->member); + } + return $this->member_entity; } /** - * Get date - * - * @param bool $formatted Return date formatted, raw if false + * Get booking date, as Y-m-d */ - public function getDate(bool $formatted = true): string + public function getDate(): string { - if ($formatted === true) { - $date = new \DateTime($this->date); - return $date->format(__("Y-m-d")); - } else { - return $this->date; - } + return $this->date; } /** @@ -673,18 +586,11 @@ public function getNumberPeople(): int } /** - * Get creation date - * - * @param bool $formatted Return date formatted, raw if false + * Get creation date, as Y-m-d */ - public function getCreationDate(bool $formatted = true): string + public function getCreationDate(): string { - if ($formatted === true) { - $date = new \DateTime($this->creation_date); - return $date->format(__("Y-m-d")); - } else { - return $this->creation_date; - } + return $this->creation_date ?? ''; } /** @@ -699,22 +605,26 @@ public function setEvent(int $event): self } /** - * Set member + * Set event from an already loaded one * - * @param int $member Member id + * @param Event $event Event */ - public function setMember(int $member): self + public function useEvent(Event $event): self { - $this->member = $member; + $this->event = $event->getId(); + $this->event_entity = $event; return $this; } /** - * Get table's name + * Set member + * + * @param int $member Member id */ - protected function getTableName(): string + public function setMember(int $member): self { - return EVENTS_PREFIX . self::TABLE; + $this->member = $member; + return $this; } /** @@ -742,15 +652,17 @@ public function loadActivities(): void { $this->activities = []; $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesbookings', 'acb'); - $select->where([self::PK => $this->id]); + //activities are loaded along with their links + $select->join( + ['ac' => PREFIX_DB . EVENTS_PREFIX . Activity::TABLE], + 'acb.' . Activity::PK . ' = ac.' . Activity::PK, + ['name', 'is_active', 'creation_date', 'comment'] + ); + $select->where(['acb.' . self::PK => $this->id]); $results = $this->zdb->execute($select); foreach ($results as $result) { $this->activities[$result[Activity::PK]] = [ - 'activity' => new Activity( - $this->zdb, - $this->login, - (int)$result[Activity::PK] - ), + 'activity' => new Activity($this->zdb, $this->history, $result), 'checked' => $result['checked'] ]; } @@ -812,6 +724,16 @@ public function canEdit(Login $login): bool return $group !== null && $login->isGroupManager($group); } + /** + * Get errors + * + * @return array + */ + public function getErrors(): array + { + return $this->errors; + } + /** * Get row class related to current fee status * diff --git a/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php b/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php index 8ea3de7..fbbeec7 100644 --- a/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php +++ b/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php @@ -10,9 +10,11 @@ namespace GaletteEvents\Controllers\Crud; +use Analog\Analog; use Galette\Controllers\Crud\AbstractPluginController; use GaletteEvents\Filters\ActivitiesList; use GaletteEvents\Activity; +use GaletteEvents\NotFoundException; use GaletteEvents\Repository\Activities; use Slim\Psr7\Request; use Slim\Psr7\Response; @@ -78,7 +80,7 @@ public function list(Request $request, Response $response, ?string $option = nul } } - $activities = new Activities($this->zdb, $this->login, $this->preferences, $filters); + $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 @@ -141,15 +143,21 @@ public function filter(Request $request, Response $response): Response */ public function edit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response { - if ($this->session->plugin_events_activity !== null) { - $activity = $this->session->plugin_events_activity; - $this->session->plugin_events_activity = null; - } else { - $activity = new Activity($this->zdb, $this->login); + $activity = new Activity($this->zdb, $this->history); + + if ($id !== null) { + try { + $activity->load($id); + } catch (NotFoundException) { + return $this->redirectNotFound($response, $id); + } } - if ($id !== null && $activity->getId() != $id) { - $activity->load($id); + //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']); } // template variable declaration @@ -184,37 +192,38 @@ public function edit(Request $request, Response $response, ?int $id = null, stri public function doEdit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response { $post = $request->getParsedBody(); - $activity = new Activity($this->zdb, $this->login); + $activity = new Activity($this->zdb, $this->history); if (isset($post['id']) && !empty($post['id'])) { - $activity->load((int)$post['id']); + try { + $activity->load((int)$post['id']); + } catch (NotFoundException) { + return $this->redirectNotFound($response, (int)$post['id']); + } } $success_detected = []; $error_detected = []; // Validation - $valid = $activity->check($post); - if ($valid !== true) { + if (!$activity->check($post)) { $error_detected = array_merge($error_detected, $activity->getErrors()); } if (count($error_detected) == 0) { //all goes well, we can proceed - - $new = false; - if ($activity->getId() == '') { - $new = true; - } - $store = $activity->store(); - if ($store === true) { - //member has been stored :) + $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"); } - } else { - //something went wrong :'( + } 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"); } } @@ -240,8 +249,11 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st if (count($error_detected) == 0) { $redirect_url = $this->routeparser->urlFor('events_activities'); } else { - //store entity in session - $this->session->plugin_events_activity = $activity; + //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( @@ -258,6 +270,34 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st ->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( + response: $response, + errors: [$this->getNotFoundMessage($id)], + redirect_url: $this->routeparser->urlFor('events_activities') + ); + } + // /CRUD - Update // CRUD - Delete @@ -291,7 +331,11 @@ public function formUri(array $args): string */ public function confirmRemoveTitle(array $args): string { - $activity = new Activity($this->zdb, $this->login, (int)$args['id']); + try { + $activity = new Activity($this->zdb, $this->history, (int)$args['id']); + } catch (NotFoundException) { + return $this->getNotFoundMessage((int)$args['id']); + } return sprintf( //TRANS %1$s is activity name _T('Remove activity %1$s', 'events'), @@ -307,8 +351,9 @@ public function confirmRemoveTitle(array $args): string */ protected function doDelete(array $args, array $post): bool { - $activity = new Activity($this->zdb, $this->login, (int)$args['id']); - return $activity->remove(); + $activity = new Activity($this->zdb, $this->history, (int)$args['id']); + $activity->remove(); + return true; } // /CRUD - Delete diff --git a/lib/GaletteEvents/Controllers/Crud/BookingsController.php b/lib/GaletteEvents/Controllers/Crud/BookingsController.php index 7e5d60a..9ac9abf 100644 --- a/lib/GaletteEvents/Controllers/Crud/BookingsController.php +++ b/lib/GaletteEvents/Controllers/Crud/BookingsController.php @@ -19,6 +19,7 @@ use GaletteEvents\Filters\BookingsList; use GaletteEvents\Booking; use GaletteEvents\Event; +use GaletteEvents\NotFoundException; use GaletteEvents\Repository\Bookings; use GaletteEvents\Repository\Events; use Slim\Psr7\Request; @@ -86,7 +87,7 @@ public function listBookings(Response $response, string|int $event, ?string $opt $filters = $this->session->{$this->getFilterName('bookings')} ?? new BookingsList(); if ($event == 'guess') { - $linked_event = $filters->event_filter; + $linked_event = $filters->event_filter ?? 'all'; } else { $linked_event = $event; } @@ -107,17 +108,32 @@ public function listBookings(Response $response, string|int $event, ?string $opt $event = null; if ($linked_event !== 'all') { + try { + $event = new Event($this->zdb, $this->login, $this->history, (int)$linked_event); + } catch (NotFoundException) { + //event may have been removed since it has been filtered + $filters->event_filter = null; + $this->session->{$this->getFilterName('bookings')} = $filters; + return $this->redirectWithErrors( + response: $response, + errors: [sprintf( + //TRANS: %1$s is the event identifier + _T('No event #%1$s.', 'events'), + (int)$linked_event + )], + redirect_url: $this->routeparser->urlFor('events_bookings', ['event' => 'all']) + ); + } $filters->event_filter = (int)$linked_event; - $event = new Event($this->zdb, $this->login, (int)$linked_event); } //Groups $groups = new Groups($this->zdb, $this->login); $groups_list = $groups->getList(); - $bookings = new Bookings($this->zdb, $this->login, $filters); + $bookings = new Bookings($this->zdb, $this->login, $this->history, $filters); - $events = new Events($this->zdb, $this->login); + $events = new Events($this->zdb, $this->login, $this->history); $list = $bookings->getList(); $count = $bookings->getCount(); @@ -247,7 +263,7 @@ public function handleBatch(Request $request, Response $response): Response $filters->selected = $post['entries_sel']; //selection is restricted to bookings current logged-in user can list - $bookings = new Bookings($this->zdb, $this->login, $filters); + $bookings = new Bookings($this->zdb, $this->login, $this->history, $filters); $members = []; foreach ($bookings->getList() as $booking) { $members[] = $booking->getMemberId(); @@ -362,21 +378,27 @@ public function edit(Request $request, Response $response, ?int $id = null, stri $get = $request->getQueryParams(); $route_params = []; - if ($this->session->plugin_events_booking !== null) { - $booking = $this->session->plugin_events_booking; - $this->session->plugin_events_booking = null; - } else { - $booking = new Booking($this->zdb, $this->login); - } + $booking = new Booking($this->zdb, $this->login, $this->history); - if ($id !== null && $booking->getId() != $id) { - $booking->load($id); + if ($id !== null) { + try { + $booking->load($id); + } catch (NotFoundException) { + return $this->redirectNotFound($response, $id); + } } if ($booking->getId() !== null && !$booking->canEdit($this->login)) { return $this->redirectForbidden($response, $booking); } + //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']); + } + // template variable declaration $title = _T("Booking", "events"); if ($booking->getId() != '') { @@ -386,7 +408,7 @@ public function edit(Request $request, Response $response, ?int $id = null, stri } //Events - $events = new Events($this->zdb, $this->login); + $events = new Events($this->zdb, $this->login, $this->history); if ($action === 'add') { if (isset($get['event'])) { $booking->setEvent((int)$get['event']); @@ -466,9 +488,13 @@ public function edit(Request $request, Response $response, ?int $id = null, stri public function doEdit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response { $post = $request->getParsedBody(); - $booking = new Booking($this->zdb, $this->login); + $booking = new Booking($this->zdb, $this->login, $this->history); if (isset($post['id']) && !empty($post['id'])) { - $booking->load((int)$post['id']); + try { + $booking->load((int)$post['id']); + } catch (NotFoundException) { + return $this->redirectNotFound($response, (int)$post['id']); + } } if ($booking->getId() !== null && !$booking->canEdit($this->login)) { @@ -491,34 +517,30 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st $goto_list = true; // Validation - $valid = $booking->check($post); - if ($valid !== true) { - $error_detected = array_merge($error_detected, $valid); + if (!$booking->check($post)) { + $error_detected = array_merge($error_detected, $booking->getErrors()); } if (count($error_detected) == 0 && isset($post['save'])) { //all goes well, we can proceed - - $new = false; - if ($booking->getId() == '') { - $new = true; - } - $store = $booking->store(); - if ($store === true) { - //member has been stored :) + $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"); } - } else { - //something went wrong :'( + } 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"); } } if (!isset($post['save'])) { - $this->session->plugin_events_booking = $booking; $error_detected = []; $goto_list = false; $warning_detected[] = _T('Do not forget to store the booking', 'events'); @@ -556,8 +578,11 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st ['event' => (string)$booking->getEventId()] ); } else { - //store entity in session - $this->session->plugin_events_booking = $booking; + //keep posted values for the form + $this->session->plugin_events_booking_data = [ + 'id' => $booking->getId(), + 'values' => $post + ]; if ($booking->getId()) { $route = 'events_booking_edit'; @@ -580,6 +605,34 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st ->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 * @@ -633,7 +686,11 @@ public function formUri(array $args): string */ public function confirmRemoveTitle(array $args): string { - $booking = new Booking($this->zdb, $this->login, (int)$args['id']); + try { + $booking = new Booking($this->zdb, $this->login, $this->history, (int)$args['id']); + } catch (NotFoundException) { + return $this->getNotFoundMessage((int)$args['id']); + } $member = $booking->getMember(); $event = $booking->getEvent(); return sprintf( @@ -652,8 +709,9 @@ public function confirmRemoveTitle(array $args): string */ protected function doDelete(array $args, array $post): bool { - $booking = new Booking($this->zdb, $this->login, (int)$post['id']); - return $booking->remove(); + $booking = new Booking($this->zdb, $this->login, $this->history, (int)$post['id']); + $booking->remove(); + return true; } // /CRUD - Delete diff --git a/lib/GaletteEvents/Controllers/Crud/EventsController.php b/lib/GaletteEvents/Controllers/Crud/EventsController.php index 41c4ef5..4a8eeca 100644 --- a/lib/GaletteEvents/Controllers/Crud/EventsController.php +++ b/lib/GaletteEvents/Controllers/Crud/EventsController.php @@ -15,6 +15,7 @@ use Galette\Controllers\Crud\AbstractPluginController; use GaletteEvents\Filters\EventsList; use GaletteEvents\Event; +use GaletteEvents\NotFoundException; use GaletteEvents\Repository\Events; use Slim\Psr7\Request; use Slim\Psr7\Response; @@ -86,7 +87,7 @@ public function list(Request $request, Response $response, ?string $option = nul } } - $events = new Events($this->zdb, $this->login, $filters); + $events = new Events($this->zdb, $this->login, $this->history, $filters); $events_list = $events->getList(); //assign pagination variables to the template and add pagination links @@ -159,7 +160,7 @@ 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, $filters); + $events = new Events($this->zdb, $this->login, $this->history, $filters); return $this->withJson($response, $events->getList(false, true)); } @@ -204,16 +205,15 @@ public function filter(Request $request, Response $response): Response */ public function edit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response { - if ($this->session->plugin_events_event !== null) { - $event = $this->session->plugin_events_event; - $this->session->plugin_events_event = null; - } else { - $event = new Event($this->zdb, $this->login); - } + $event = new Event($this->zdb, $this->login, $this->history); $can = $event->canCreate($this->login); - if ($id !== null && $event->getId() != $id) { - $event->load($id); + if ($id !== null) { + try { + $event->load($id); + } catch (NotFoundException) { + return $this->redirectNotFound($response, $id); + } $can = $event->canEdit($this->login); } @@ -222,6 +222,13 @@ public function edit(Request $request, Response $response, ?int $id = null, stri return $this->redirectForbidden($response, $event); } + //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']); + } + // template variable declaration $title = _T("Event", "events"); if ($event->getId() != '') { @@ -260,10 +267,14 @@ public function edit(Request $request, Response $response, ?int $id = null, stri public function doEdit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response { $post = $request->getParsedBody(); - $event = new Event($this->zdb, $this->login); + $event = new Event($this->zdb, $this->login, $this->history); $can = $event->canCreate($this->login); if (isset($post['id']) && !empty($post['id'])) { - $event->load((int)$post['id']); + try { + $event->load((int)$post['id']); + } catch (NotFoundException) { + return $this->redirectNotFound($response, (int)$post['id']); + } $can = $event->canEdit($this->login); } @@ -294,17 +305,22 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st $success_detected[] = _T("Activity has been detached from event.", "events"); $warning_detected[] = _T('Do not forget to store the event', 'events'); } - } elseif ($valid !== true) { - $error_detected = array_merge($error_detected, $valid); + } elseif (!$valid) { + $error_detected = array_merge($error_detected, $event->getErrors()); } elseif (isset($post['save'])) { $new = $event->getId() === null; - if ($event->store() === true) { + try { + $event->store(); if ($new) { $success_detected[] = _T("New event has been successfully added.", "events"); } else { $success_detected[] = _T("Event has been modified.", "events"); } - } else { + } 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"); } } else { @@ -340,8 +356,11 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st if (count($error_detected) == 0 && $goto_list) { $redirect_url = $this->routeparser->urlFor('events_events'); } else { - //store entity in session - $this->session->plugin_events_event = $event; + //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( @@ -358,6 +377,34 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st ->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 * @@ -411,7 +458,11 @@ public function formUri(array $args): string */ public function confirmRemoveTitle(array $args): string { - $event = new Event($this->zdb, $this->login, (int)$args['id']); + try { + $event = new Event($this->zdb, $this->login, $this->history, (int)$args['id']); + } catch (NotFoundException) { + return $this->getNotFoundMessage((int)$args['id']); + } return sprintf( //TRANS: %1$s is the event name _T('Remove event \'%1$s\'', 'events'), @@ -427,8 +478,9 @@ public function confirmRemoveTitle(array $args): string */ protected function doDelete(array $args, array $post): bool { - $event = new Event($this->zdb, $this->login, (int)$post['id']); - return $event->remove(); + $event = new Event($this->zdb, $this->login, $this->history, (int)$post['id']); + $event->remove(); + return true; } // /CRUD - Delete diff --git a/lib/GaletteEvents/Controllers/CsvController.php b/lib/GaletteEvents/Controllers/CsvController.php index 4cf1c06..6930660 100644 --- a/lib/GaletteEvents/Controllers/CsvController.php +++ b/lib/GaletteEvents/Controllers/CsvController.php @@ -19,6 +19,7 @@ use Galette\IO\CsvOut; use GaletteEvents\Event; use GaletteEvents\Filters\BookingsList; +use GaletteEvents\NotFoundException; use GaletteEvents\Repository\Bookings; /** @@ -79,7 +80,7 @@ public function bookingsExport(Request $request, Response $response, ?int $id = $filters->event_filter = $id; } - $bookings = new Bookings($this->zdb, $this->login, $filters); + $bookings = new Bookings($this->zdb, $this->login, $this->history, $filters); $bookings_list = $bookings->getList(true); $labels = [ @@ -96,9 +97,22 @@ public function bookingsExport(Request $request, Response $response, ?int $id = _T('Number of persons', 'events'), ]; - //activities are onl:y available for one event - if ($filters->event_filter > 0) { - $event = new Event($this->zdb, $this->login, (int)$filters->event_filter); + //activities are only available for one event + $event = null; + if (is_numeric($filters->event_filter) && (int)$filters->event_filter > 0) { + try { + $event = new Event($this->zdb, $this->login, $this->history, (int)$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 + )], + redirect_url: $this->routeparser->urlFor('events_bookings', ['event' => 'all']) + ); + } $activities = $event->getActivities(); foreach ($activities as $activity) { $labels[] = $activity['activity']->getName(); @@ -146,7 +160,7 @@ public function bookingsExport(Request $request, Response $response, ?int $id = $booking->getNumberPeople() ]; - if ($filters->event_filter > 0) { + if ($event !== null) { $bactivities = $booking->getActivities(); foreach (array_keys($activities) as $aid) { $entry[] = isset($bactivities[$aid]) && $bactivities[$aid]['checked'] ? _T('Yes') : _T('No'); diff --git a/lib/GaletteEvents/EntityTrait.php b/lib/GaletteEvents/EntityTrait.php new file mode 100644 index 0000000..101334f --- /dev/null +++ b/lib/GaletteEvents/EntityTrait.php @@ -0,0 +1,139 @@ + + */ +trait EntityTrait +{ + /** + * Populate object from a resultset row + * + * @param ArrayObject $r the resultset row + */ + abstract private function loadFromRS(ArrayObject $r): void; + + /** + * Load entity from its id + * + * @param int $id Identifier + * + * @throws NotFoundException + */ + public function load(int $id): void + { + $select = $this->zdb->select($this->getTableName()); + $select->where([self::PK => $id]); + $results = $this->zdb->execute($select); + + if ($results->count() === 0) { + throw new NotFoundException(sprintf('%1$s #%2$s does not exist', self::class, $id)); + } + $this->loadFromRS($results->current()); + } + + /** + * Remove entity; database removes its links + */ + public function remove(): void + { + $delete = $this->zdb->delete($this->getTableName()); + $delete->where([self::PK => $this->id]); + $this->zdb->execute($delete); + } + + /** + * Run storage in a transaction, unless one is already running + * + * @param callable $store Storage + */ + private function transactional(callable $store): void + { + $new = $this->id === null; + $transaction = !$this->zdb->connection->inTransaction(); + if ($transaction) { + $this->zdb->connection->beginTransaction(); + } + + try { + $store(); + if ($transaction) { + $this->zdb->connection->commit(); + } + } catch (\Throwable $e) { + if ($transaction) { + $this->zdb->connection->rollBack(); + } + if ($new) { + //nothing has been stored + $this->id = null; + } + throw $e; + } + } + + /** + * Get identifier of the row that has just been inserted + */ + private function getLastInsertId(): int + { + if ($this->zdb->isPostgres()) { + /** @phpstan-ignore-next-line */ + return (int)$this->zdb->driver->getLastGeneratedValue( + PREFIX_DB . $this->getTableName() . '_id_seq' + ); + } + return (int)$this->zdb->driver->getLastGeneratedValue(); + } + + /** + * Parse a date typed in the localized format, or as Y-m-d + * + * @param string $value Typed date + * @param string $label Field label, for the error message + * + * @return ?string Date as Y-m-d, null when it cannot be parsed + */ + private function parseDate(string $value, string $label): ?string + { + $date = \DateTime::createFromFormat(__('Y-m-d'), $value) + ?: \DateTime::createFromFormat('Y-m-d', $value); + if ($date === false) { + Analog::log( + 'Wrong date format. field: ' . $label . ', value: ' . $value + . ', expected fmt: ' . __('Y-m-d'), + Analog::INFO + ); + $this->errors[] = sprintf( + //TRANS: %1$s is the expected date format, %2$s is the field label + _T('- Wrong date format (%1$s) for %2$s!'), + __('Y-m-d'), + $label + ); + return null; + } + return $date->format('Y-m-d'); + } + + /** + * Get table's name + */ + protected function getTableName(): string + { + return EVENTS_PREFIX . self::TABLE; + } +} diff --git a/lib/GaletteEvents/Event.php b/lib/GaletteEvents/Event.php index 3a0bba4..5c5ca75 100644 --- a/lib/GaletteEvents/Event.php +++ b/lib/GaletteEvents/Event.php @@ -12,10 +12,10 @@ use ArrayObject; use Galette\Core\Db; +use Galette\Core\History; use Galette\Core\Login; use Galette\Entity\Group; use Analog\Analog; -use Laminas\Db\ResultSet\ResultSet; use Laminas\Db\Sql\Expression; /** @@ -25,23 +25,26 @@ */ class Event { + use EntityTrait; + public const string TABLE = 'events'; public const string PK = 'id_event'; private Db $zdb; private Login $login; + private History $history; /** @var array */ - private array $errors; + private array $errors = []; - private int $id; - private string $name; + private ?int $id = null; + private string $name = ''; private string $address = ''; private string $zip = ''; private string $town = ''; private ?string $country = null; private string $begin_date; private string $end_date; - private string $creation_date; + private ?string $creation_date = null; private bool $open = true; private ?int $group = null; private string $comment = ''; @@ -53,58 +56,26 @@ class Event /** * Default constructor * - * @param Db $zdb Database instance - * @param Login $login Login instance - * @param null|int|ArrayObject $args Either a ResultSet row or its id for to load - * a specific event, or null to just - * instanciate object + * @param Db $zdb Database instance + * @param Login $login Login instance + * @param History $history History instance + * @param null|int|ArrayObject $args Either a ResultSet row or its id for to load + * a specific event, or null to just + * instanciate object */ - public function __construct(Db $zdb, Login $login, int|ArrayObject|null $args = null) + public function __construct(Db $zdb, Login $login, History $history, int|ArrayObject|null $args = null) { $this->zdb = $zdb; $this->login = $login; - if ($args == null || is_int($args)) { - if (is_int($args) && $args > 0) { - $this->load($args); - } else { - $now = date('Y-m-d'); - $this->begin_date = $now; - $this->end_date = $now; - } - } elseif (is_object($args)) { + $this->history = $history; + if (is_int($args)) { + $this->load($args); + } elseif ($args !== null) { $this->loadFromRS($args); - $this->loadActivities(); - } - } - - /** - * Loads an event from its id - * - * @param int $id the identifiant for the event to load - * - * @return bool true if query succeed, false otherwise - */ - public function load(int $id): bool - { - try { - $select = $this->zdb->select($this->getTableName()); - $select->where([self::PK => $id]); - - $results = $this->zdb->execute($select); - - if ($results->count() > 0) { - $this->loadFromRS($results->current()); - $this->loadActivities(); - return true; - } else { - return false; - } - } catch (\Exception $e) { - Analog::log( - 'Cannot load event form id `' . $id . '` | ' . $e->getMessage(), - Analog::WARNING - ); - throw $e; + } else { + $now = date('Y-m-d'); + $this->begin_date = $now; + $this->end_date = $now; } } @@ -128,42 +99,7 @@ private function loadFromRS(ArrayObject $r): void $this->group = $r['id_group'] === null ? null : (int)$r['id_group']; $this->comment = $r['comment'] ?? ''; $this->color = $r['color']; - } - - /** - * Remove specified event - */ - public function remove(): bool - { - $transaction = false; - - try { - if (!$this->zdb->connection->inTransaction()) { - $this->zdb->connection->beginTransaction(); - $transaction = true; - } - - $delete = $this->zdb->delete($this->getTableName()); - $delete->where([self::PK => $this->id]); - $this->zdb->execute($delete); - - //commit all changes - if ($transaction) { - $this->zdb->connection->commit(); - } - - return true; - } catch (\Exception $e) { - if ($transaction) { - $this->zdb->connection->rollBack(); - } - Analog::log( - 'Unable to delete event ' . $this->name - . ' (' . $this->id . ') |' . $e->getMessage(), - Analog::ERROR - ); - return false; - } + $this->loadActivities(); } /** @@ -171,48 +107,23 @@ public function remove(): bool * * @param array $values All values to check, basically the $_POST array * after sending the form - * - * @return true|array */ - public function check(array $values): bool|array + public function check(array $values): bool { $this->errors = []; if (empty($values['begin_date'])) { $this->errors[] = _T('Begin date is mandatory', 'events'); } else { - //handle dates - foreach (['begin_date', 'end_date'] as $datefield) { + $labels = [ + 'begin_date' => _T('Begin date', 'events'), + 'end_date' => _T('End date', 'events'), + ]; + foreach ($labels as $datefield => $label) { if (isset($values[$datefield])) { - $value = $values[$datefield]; - try { - $d = \DateTime::createFromFormat(__("Y-m-d"), $value); - if ($d === false) { - //try with non localized date - $d = \DateTime::createFromFormat("Y-m-d", $value); - if ($d === false) { - throw new \Exception('Incorrect format'); - } - } - $this->$datefield = $d->format('Y-m-d'); - } catch (\Exception $e) { - Analog::log( - 'Wrong date format. field: ' . $datefield - . ', value: ' . $value . ', expected fmt: ' - . __("Y-m-d") . ' | ' . $e->getMessage(), - Analog::INFO - ); - if ($datefield == 'begin_date') { - $label = _T('Begin date', 'events'); - } else { - $label = _T('End date', 'events'); - } - $this->errors[] = sprintf( - //TRANS %1$s is the expected date format, %2$s is the field label - _T('- Wrong date format (%1$s) for %2$s!'), - __("Y-m-d"), - $label - ); + $date = $this->parseDate((string)$values[$datefield], $label); + if ($date !== null) { + $this->$datefield = $date; } } } @@ -323,7 +234,7 @@ public function check(array $values): bool|array . print_r($this->errors, true), Analog::ERROR ); - return $this->errors; + return false; } else { Analog::log( 'Event checked successfully.', @@ -336,12 +247,9 @@ public function check(array $values): bool|array /** * Store the event */ - public function store(): bool + public function store(): void { - global $hist; - - try { - $this->zdb->connection->beginTransaction(); + $this->transactional(function (): void { $values = [ 'name' => $this->name, 'address' => $this->address, @@ -357,7 +265,7 @@ public function store(): bool 'color' => $this->color ]; - if (empty($this->id)) { + if ($this->id === null) { //we're inserting a new event $this->creation_date = date("Y-m-d"); $values['creation_date'] = $this->creation_date; @@ -365,29 +273,20 @@ public function store(): bool $insert = $this->zdb->insert($this->getTableName()); $insert->values($values); $add = $this->zdb->execute($insert); - if ($add->count() > 0) { - if ($this->zdb->isPostgres()) { - /** @phpstan-ignore-next-line */ - $this->id = (int)$this->zdb->driver->getLastGeneratedValue( - PREFIX_DB . EVENTS_PREFIX . Event::TABLE . '_id_seq' - ); - } else { - $this->id = (int)$this->zdb->driver->getLastGeneratedValue(); - } - - // logging - $hist->add( - _T("Event added", "events"), - $this->name - ); - } else { - $hist->add(_T("Fail to add new event.", "events")); - throw new \Exception( + if ($add->count() === 0) { + $this->history->add(_T("Fail to add new event.", "events")); + throw new \RuntimeException( 'An error occurred inserting new event!' ); } + $this->id = $this->getLastInsertId(); + + // logging + $this->history->add( + _T("Event added", "events"), + $this->name + ); } else { - $values['id_event'] = $this->id; //we're editing an existing event $update = $this->zdb->update($this->getTableName()); $update @@ -399,7 +298,7 @@ public function store(): bool //edit == 0 does not mean there were an error, but that there //were nothing to change if ($edit->count() > 0) { - $hist->add( + $this->history->add( _T("Event updated", "events"), $this->name ); @@ -407,18 +306,7 @@ public function store(): bool } $this->storeActivities(); - - $this->zdb->connection->commit(); - return true; - } catch (\Exception $e) { - $this->zdb->connection->rollBack(); - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); - throw $e; - } + }); } /** @@ -428,8 +316,12 @@ public function store(): bool */ private function getActiveActivity(int $id): ?Activity { - $activity = new Activity($this->zdb, $this->login, $id); - return $activity->getId() !== null && $activity->isActive() ? $activity : null; + try { + $activity = new Activity($this->zdb, $this->history, $id); + } catch (NotFoundException) { + return null; + } + return $activity->isActive() ? $activity : null; } /** @@ -496,15 +388,15 @@ private function storeActivities(): void */ public function getId(): ?int { - return $this->id ?? null; + return $this->id; } /** * Get event name */ - public function getName(): ?string + public function getName(): string { - return $this->name ?? null; + return $this->name; } /** @@ -536,7 +428,7 @@ public function getTown(): string */ public function getCountry(): ?string { - return $this->country ?? null; + return $this->country; } /** @@ -544,7 +436,7 @@ public function getCountry(): ?string */ public function getGroup(): ?int { - return $this->group ?? null; + return $this->group; } /** @@ -561,49 +453,27 @@ public function getGroupName(): string } /** - * Get date - * - * @param string $prop Property to use - * @param bool $formatted Return date formatted, raw if false + * Get creation date, as Y-m-d */ - private function getDate(string $prop, bool $formatted = true): string + public function getCreationDate(): string { - if ($formatted === true) { - $date = new \DateTime($this->$prop); - return $date->format(__("Y-m-d")); - } else { - return $this->$prop; - } + return $this->creation_date ?? ''; } /** - * Get creation date - * - * @param bool $formatted Return date formatted, raw if false + * Get begin date, as Y-m-d */ - public function getCreationDate(bool $formatted = true): string + public function getBeginDate(): string { - return $this->getDate('creation_date', $formatted); + return $this->begin_date; } /** - * Get begin date - * - * @param bool $formatted Return date formatted, raw if false - */ - public function getBeginDate(bool $formatted = true): string - { - return $this->getDate('begin_date', $formatted); - } - - /** - * Get end date - * - * @param bool $formatted Return date formatted, raw if false + * Get end date, as Y-m-d */ - public function getEndDate(bool $formatted = true): string + public function getEndDate(): string { - return $this->getDate('end_date', $formatted); + return $this->end_date; } /** @@ -655,14 +525,6 @@ public function isOpen(): bool return false; } - /** - * Get table's name - */ - protected function getTableName(): string - { - return EVENTS_PREFIX . self::TABLE; - } - /** * Get activities list * @@ -691,15 +553,17 @@ public function loadActivities(): void { $this->activities = []; $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesevents', 'ace'); - $select->where([self::PK => $this->id]); + //activities are loaded along with their links + $select->join( + ['ac' => PREFIX_DB . EVENTS_PREFIX . Activity::TABLE], + 'ace.' . Activity::PK . ' = ac.' . Activity::PK, + ['name', 'is_active', 'creation_date', 'comment'] + ); + $select->where(['ace.' . self::PK => $this->id]); $results = $this->zdb->execute($select); foreach ($results as $result) { $this->activities[$result[Activity::PK]] = [ - 'activity' => new Activity( - $this->zdb, - $this->login, - (int)$result[Activity::PK] - ), + 'activity' => new Activity($this->zdb, $this->history, $result), 'status' => $result['status'] ]; } @@ -732,29 +596,6 @@ public function getColor(): string return $this->color ?? ''; } - /** - * Count attendees per event - */ - public function countAttendees(): ResultSet - { - $select = $this->zdb->select(EVENTS_PREFIX . Booking::TABLE, 'b'); - $select->columns( - [ - 'count' => new Expression('SUM(b.number_people)'), - 'is_paid' - ] - ); - $select->where([ - self::PK => $this->id, - ]); - - $select->group('is_paid'); - - $results = $this->zdb->execute($select); - - return $results; - } - /** * Can member edit event * @@ -783,10 +624,20 @@ public function canCreate(Login $login): bool return ($login->isAdmin() || $login->isStaff() || $login->isGroupManager()); } + /** + * Get errors + * + * @return array + */ + public function getErrors(): array + { + return $this->errors; + } + /** * Get foreground contrasted color for current background color */ - public function getForegoundColor(): string + public function getForegroundColor(): string { $bgcolor = trim($this->color ?? '#ffffff', '#'); $r = hexdec(substr($bgcolor, 0, 2)); diff --git a/lib/GaletteEvents/NotFoundException.php b/lib/GaletteEvents/NotFoundException.php new file mode 100644 index 0000000..5587fc8 --- /dev/null +++ b/lib/GaletteEvents/NotFoundException.php @@ -0,0 +1,20 @@ + + */ +class NotFoundException extends \RuntimeException +{ +} diff --git a/lib/GaletteEvents/PluginGaletteEvents.php b/lib/GaletteEvents/PluginGaletteEvents.php index 7320afa..883c417 100644 --- a/lib/GaletteEvents/PluginGaletteEvents.php +++ b/lib/GaletteEvents/PluginGaletteEvents.php @@ -12,6 +12,7 @@ use DI\Attribute\Inject; use Galette\Core\Db; +use Galette\Core\History; use Galette\Core\Login; use Galette\Core\Plugins\DashboardProviderInterface; use Galette\Core\Plugins\InstallableInterface; @@ -42,6 +43,9 @@ class PluginGaletteEvents extends GalettePlugin implements InstallableInterface, #[Inject] protected Login $login; + #[Inject] + protected History $history; + /** * Extra menus entries * @@ -49,11 +53,9 @@ class PluginGaletteEvents extends GalettePlugin implements InstallableInterface, */ public function getMenus(): array { - /** @var Login $login */ - global $login; $menus = []; - if ($login->isLogged()) { + if ($this->login->isLogged()) { $menus['plugin_events'] = [ 'title' => _T("Events", "events"), 'icon' => 'calendar alternate', @@ -85,7 +87,7 @@ public function getMenus(): array ]; } - if ($login->isAdmin() || $login->isStaff()) { + if ($this->login->isAdmin() || $this->login->isStaff()) { $menus['plugin_events']['items'] = array_merge( $menus['plugin_events']['items'], [ @@ -193,7 +195,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, $filters); + $events = new Events($this->zdb, $this->login, $this->history, $filters); $posts = []; $list = $events->getList(); @@ -204,7 +206,7 @@ public function getNews(): ?Entry foreach ($list as $event) { $posts[] = new Post( title: $event->getName(), - date: $event->getBeginDate() + date: (new \DateTime($event->getBeginDate()))->format(__('Y-m-d')) ); } diff --git a/lib/GaletteEvents/Repository/Activities.php b/lib/GaletteEvents/Repository/Activities.php index 593242e..6114d8c 100644 --- a/lib/GaletteEvents/Repository/Activities.php +++ b/lib/GaletteEvents/Repository/Activities.php @@ -19,6 +19,7 @@ use Laminas\Db\Sql\Expression; use Galette\Core\Login; use Galette\Core\Db; +use Galette\Core\History; use Laminas\Db\Sql\Select; /** @@ -28,6 +29,7 @@ */ class Activities extends Repository { + private History $history; private int $count; public const int ORDERBY_DATE = 0; @@ -38,13 +40,15 @@ class Activities extends Repository * * @param Db $zdb Database instance * @param Login $login Login instance + * @param History $history History instance * @param Preferences $preferences Preferences instance * @param ?ActivitiesList $filters Filtering */ - public function __construct(Db $zdb, Login $login, Preferences $preferences, ?ActivitiesList $filters = null) + 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); @@ -75,7 +79,7 @@ public function getList(): array|ResultSet $activities = []; foreach ($results as $row) { - $activity = new Activity($this->zdb, $this->login, $row); + $activity = new Activity($this->zdb, $this->history, $row); $activities[] = $activity; } diff --git a/lib/GaletteEvents/Repository/Bookings.php b/lib/GaletteEvents/Repository/Bookings.php index 1dfebb9..eb2a6d4 100644 --- a/lib/GaletteEvents/Repository/Bookings.php +++ b/lib/GaletteEvents/Repository/Bookings.php @@ -16,6 +16,7 @@ use Laminas\Db\Sql\Predicate\PredicateSet; use Galette\Core\Login; use Galette\Core\Db; +use Galette\Core\History; use Galette\Entity\Adherent; use Galette\Entity\Group; use GaletteEvents\Event; @@ -32,6 +33,7 @@ class Bookings { private Db $zdb; private Login $login; + private History $history; private BookingsList $filters; private int $count; private float $sum; @@ -50,12 +52,14 @@ class Bookings * * @param Db $zdb Database instance * @param Login $login Login instance + * @param History $history History instance * @param ?BookingsList $filters Filtering */ - public function __construct(Db $zdb, Login $login, ?BookingsList $filters = null) + 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(); @@ -87,9 +91,10 @@ public function getList(bool $full = false): array $bookings = []; foreach ($results as $row) { - $booking = new Booking($this->zdb, $this->login, $row); + $booking = new Booking($this->zdb, $this->login, $this->history, $row); $bookings[] = $booking; } + $this->loadEvents($bookings); return $bookings; } catch (\Exception $e) { @@ -101,6 +106,30 @@ public function getList(bool $full = false): array } } + /** + * Load events of listed bookings, once each + * + * @param array $bookings Bookings + */ + private function loadEvents(array $bookings): void + { + $ids = array_unique(array_map(fn(Booking $booking): int => (int)$booking->getEventId(), $bookings)); + if (count($ids) === 0) { + return; + } + + $select = $this->zdb->select(EVENTS_PREFIX . Event::TABLE); + $select->where([Event::PK => array_values($ids)]); + $events = []; + foreach ($this->zdb->execute($select) as $row) { + $events[(int)$row[Event::PK]] = new Event($this->zdb, $this->login, $this->history, $row); + } + + foreach ($bookings as $booking) { + $booking->useEvent($events[$booking->getEventId()]); + } + } + /** * Builds the SELECT statement * diff --git a/lib/GaletteEvents/Repository/Events.php b/lib/GaletteEvents/Repository/Events.php index af9e2a6..4ec9c17 100644 --- a/lib/GaletteEvents/Repository/Events.php +++ b/lib/GaletteEvents/Repository/Events.php @@ -14,12 +14,12 @@ use ArrayObject; use Galette\Entity\Adherent; use GaletteEvents\Booking; -use Laminas\Db\ResultSet\ResultSet; use Laminas\Db\Sql\Expression; use Laminas\Db\Sql\Predicate; use Laminas\Db\Sql\Predicate\PredicateSet; use Galette\Core\Login; use Galette\Core\Db; +use Galette\Core\History; use Galette\Entity\Group; use Galette\Repository\Groups; use GaletteEvents\Event; @@ -35,6 +35,7 @@ class Events { private Db $zdb; private Login $login; + private History $history; private EventsList $filters; private int $count = 0; @@ -47,12 +48,14 @@ class Events * * @param Db $zdb Database instance * @param Login $login Login instance + * @param History $history History instance * @param ?EventsList $filters Filtering */ - public function __construct(Db $zdb, Login $login, ?EventsList $filters = null) + 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(); @@ -146,9 +149,18 @@ public function getList(bool $bookable = false, bool $fullcalendar = false, bool $results = $this->zdb->execute($select); $this->filters->query = $this->zdb->query_string; - $events = []; + $rows = []; foreach ($results as $row) { - $event = new Event($this->zdb, $this->login, $row); + $rows[] = $row; + } + $attendees = []; + if ($this->filters->calendar_filter) { + $attendees = $this->countAttendees(array_map(fn(ArrayObject $row): int => (int)$row[Event::PK], $rows)); + } + + $events = []; + foreach ($rows as $row) { + $event = new Event($this->zdb, $this->login, $this->history, $row); if (!$this->filters->calendar_filter) { $events[] = $event; } else { @@ -156,38 +168,30 @@ public function getList(bool $bookable = false, bool $fullcalendar = false, bool $row['title'] = $row['name']; $row['can_edit'] = $event->canEdit($this->login); $row['start'] = $row['begin_date']; - $end_date = new \DateTime($event->getEndDate(false)); + $end_date = new \DateTime($event->getEndDate()); if ($fullcalendar === true) { $end_date = $end_date->modify('+1 day'); - $row['textColor'] = $event->getForegoundColor(); + $row['textColor'] = $event->getForegroundColor(); } $row['end'] = $end_date->format('Y-m-d'); //extended description - $row['begin_date_fmt'] = $event->getBeginDate(); - $row['end_date_fmt'] = $event->getEndDate(); + $row['begin_date_fmt'] = $this->formatDate($event->getBeginDate()); + $row['end_date_fmt'] = $this->formatDate($event->getEndDate()); $description = '

'; $description .= _T('Event information', 'events'); $description .= '

'; $description .= '
    '; $pattern = '
  • %1$s %2$s
  • '; - $description .= sprintf($pattern, _T("Start date:", "events"), $event->getBeginDate()); - $description .= sprintf($pattern, _T("End date:", "events"), $event->getEndDate()); + $description .= sprintf($pattern, _T("Start date:", "events"), $row['begin_date_fmt']); + $description .= sprintf($pattern, _T("End date:", "events"), $row['end_date_fmt']); $description .= sprintf($pattern, _T("Location:", "events"), $this->escape($event->getTown())); if ($comment = $event->getComment()) { $description .= sprintf($pattern, _T("Comment:", "events"), $this->escape($comment)); } - /** @var ResultSet $attendees */ - $attendees = $event->countAttendees(); - $total_attendees = 0; - $paid_attendees = 0; - foreach ($attendees as $attendee) { - $total_attendees += $attendee['count']; - if ($attendee['is_paid']) { - $paid_attendees += $attendee['count']; - } - } + $total_attendees = $attendees[$event->getId()]['total'] ?? 0; + $paid_attendees = $attendees[$event->getId()]['paid'] ?? 0; $attendees_str = $total_attendees; if ($total_attendees) { @@ -229,6 +233,50 @@ public function getList(bool $bookable = false, bool $fullcalendar = false, bool } } + /** + * Count attendees of events, and the paid ones + * + * @param array $ids Events identifiers + * + * @return array + */ + private function countAttendees(array $ids): array + { + if (count($ids) === 0) { + return []; + } + + $select = $this->zdb->select(EVENTS_PREFIX . Booking::TABLE, 'b'); + $select->columns([ + Event::PK, + 'is_paid', + 'count' => new Expression('SUM(b.number_people)') + ]); + $select->where([Event::PK => $ids]); + $select->group([Event::PK, 'is_paid']); + + $attendees = []; + foreach ($this->zdb->execute($select) as $row) { + $id = (int)$row[Event::PK]; + $attendees[$id] ??= ['total' => 0, 'paid' => 0]; + $attendees[$id]['total'] += (int)$row['count']; + if ($row['is_paid']) { + $attendees[$id]['paid'] += (int)$row['count']; + } + } + return $attendees; + } + + /** + * Format a date for the calendar + * + * @param string $date Date, as Y-m-d + */ + private function formatDate(string $date): string + { + return (new \DateTime($date))->format(__('Y-m-d')); + } + /** * Escape a value typed by users for the calendar HTML description * diff --git a/templates/default/activities.html.twig b/templates/default/activities.html.twig index 228c900..b8723c4 100644 --- a/templates/default/activities.html.twig +++ b/templates/default/activities.html.twig @@ -70,7 +70,7 @@ {% set aid = activity.getId() %} {{ activity.getName() }} - {{ activity.getCreationDate() }} + {{ activity.getCreationDate()|date(_T("Y-m-d")) }} {{ activity.countEvents() }} diff --git a/templates/default/booking.html.twig b/templates/default/booking.html.twig index fb19710..b249b93 100644 --- a/templates/default/booking.html.twig +++ b/templates/default/booking.html.twig @@ -16,7 +16,7 @@
    {% include "components/forms/date.html.twig" with { id: 'booking_date', - value: booking.getDate(), + value: booking.getDate()|date(_T("Y-m-d")), label: _T("Booking date", "events"), required: true } %} diff --git a/templates/default/bookings.html.twig b/templates/default/bookings.html.twig index 7dc544d..d373d74 100644 --- a/templates/default/bookings.html.twig +++ b/templates/default/bookings.html.twig @@ -173,7 +173,7 @@ {{ booking.getMember().sfullname }} - {{ booking.getDate() }} + {{ booking.getDate()|date(_T("Y-m-d")) }} diff --git a/templates/default/event.html.twig b/templates/default/event.html.twig index f790e8a..fd08bca 100644 --- a/templates/default/event.html.twig +++ b/templates/default/event.html.twig @@ -37,13 +37,13 @@ {% include "components/forms/date.html.twig" with { id: 'begin_date', - value: event.getBeginDate(), + value: event.getBeginDate()|date(_T("Y-m-d")), label: _T("Begin date", "events"), } %} {% include "components/forms/date.html.twig" with { id: 'end_date', - value: event.getEndDate(), + value: event.getEndDate()|date(_T("Y-m-d")), label: _T("End date", "events"), } %} diff --git a/templates/default/events.html.twig b/templates/default/events.html.twig index e0262e4..60f6dd1 100644 --- a/templates/default/events.html.twig +++ b/templates/default/events.html.twig @@ -74,7 +74,7 @@ {{ event.getName() }} {% endif %} - {{ event.getBeginDate() }} + {{ event.getBeginDate()|date(_T("Y-m-d")) }} {{ event.getTown() }} {{ event.getGroupName() }} diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php index 0fd4a56..f98ac8e 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php @@ -141,4 +141,19 @@ public function testStaffManagesActivities(): void $this->assertSame(1, $this->countActivities('Lodging')); $this->expectNoLogEntry(); } + + /** + * Unknown activities are reported, instead of showing an empty form + */ + public function testUnknownActivity(): void + { + $this->logSuperAdmin(); + $id = $this->insertActivity('Dinner'); + $this->cleanEvents(); + + $test_response = $this->app->handle($this->createRequest('events_activity_edit', ['id' => (string)$id])); + $this->assertSame(['Location' => [$this->routeparser->urlFor('events_activities')]], $test_response->getHeaders()); + $this->expectFlashData(['error_detected' => ['No activity #' . $id . '.']]); + $this->expectNoLogEntry(); + } } diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php index d30bb02..8a3888a 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php @@ -520,6 +520,77 @@ public function testList(): void $this->assertStringContainsString($member_two->sfullname, $body); } + /** + * Changing the event of a booking shows its activities, before the booking is stored + */ + public function testChangeEventShowsItsActivities(): void + { + $this->getMemberOne(); + $event = $this->insertEvent('Event'); + $dinner = $this->insertActivity('Dinner'); + $this->linkActivity($event, $dinner); + $this->logMember($this->dataAdherentOne()); + + $data = ['event' => (string)$event, 'booking_date' => date('Y-m-d'), 'number_people' => '1', 'comment' => '']; + $test_response = $this->app->handle( + $this->createRequest('events_storebooking_add', [], 'POST')->withParsedBody($data) + ); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('events_booking_add', ['action' => 'add'])]], + $test_response->getHeaders() + ); + $this->expectFlashData(['warning_detected' => ['Do not forget to store the booking']]); + $this->assertSame(0, $this->countBookings($event)); + + $test_response = $this->app->handle($this->createRequest('events_booking_add')); + $this->assertSame(200, $test_response->getStatusCode()); + $this->assertStringContainsString('id="activity_' . $dinner . '"', (string)$test_response->getBody()); + $this->expectNoLogEntry(); + } + + /** + * New bookings are dated from today + */ + public function testNewBookingIsDatedToday(): void + { + $this->logSuperAdmin(); + $test_response = $this->app->handle($this->createRequest('events_booking_add')); + $this->assertSame(200, $test_response->getStatusCode()); + $this->assertMatchesRegularExpression( + '/id="booking_date"[^>]* value="' . date('Y-m-d') . '"/', + (string)$test_response->getBody() + ); + } + + /** + * Unknown bookings are reported, as well as events removed since they have been filtered + */ + public function testUnknownBooking(): void + { + $member_one = $this->getMemberOne(); + $event = $this->insertEvent('Event'); + $id = $this->insertBooking($event, $member_one->id); + $this->logSuperAdmin(); + + //event is remembered in list filters + $test_response = $this->app->handle($this->createRequest('events_bookings', ['event' => (string)$event])); + $this->assertSame(200, $test_response->getStatusCode()); + $this->cleanEvents(); + + $expected = ['Location' => [$this->routeparser->urlFor('events_bookings', ['event' => 'all'])]]; + $this->assertSame($expected, $this->getBookingForm($id)->getHeaders()); + $this->expectFlashData(['error_detected' => ['No booking #' . $id . '.']]); + + $test_response = $this->app->handle($this->createRequest('events_bookings', ['event' => 'guess'])); + $this->assertSame($expected, $test_response->getHeaders()); + $this->expectFlashData(['error_detected' => ['No event #' . $event . '.']]); + $this->assertNull($this->session->plugin_events_bookings_filter->event_filter); + + $test_response = $this->app->handle($this->createRequest('events_bookings', ['event' => 'guess'])); + $this->assertSame(200, $test_response->getStatusCode()); + $this->expectNoLogEntry(); + } + /** * Bookings list filters are stored in session */ diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php index 583d736..03015cc 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php @@ -177,7 +177,14 @@ public function testAttachActivityOnIncompleteEvent(): void 'success_detected' => ['Activity has been attached to event.'], ]); $this->expectLogEntry(Analog::ERROR, 'Some errors has been threw attempting to edit/store an event'); - $this->assertSame([$dinner], array_keys($this->session->plugin_events_event->getActivities())); + //form shows posted values, with the attached activity + $test_response = $this->app->handle($this->createRequest('events_event_add')); + $this->assertSame(200, $test_response->getStatusCode()); + $body = (string)$test_response->getBody(); + $this->assertStringContainsString('name="activities_ids[]" value="' . $dinner . '"', $body); + $this->assertStringContainsString('value="Lille"', $body); + $this->expectLogEntry(Analog::ERROR, 'Some errors has been threw attempting to edit/store an event'); + $this->assertNull($this->session->plugin_events_event_data ?? null); //an unknown activity is not attached $this->postEvent(null, $this->getFormValues([ @@ -187,6 +194,25 @@ public function testAttachActivityOnIncompleteEvent(): void $this->expectFlashData(['error_detected' => ['Please choose an activity to add']]); } + /** + * Values posted on an event are not shown on another one + */ + public function testPostedValuesStayOnTheirEvent(): void + { + $this->logSuperAdmin(); + $first = $this->insertEvent('First event'); + $second = $this->insertEvent('Second event'); + + $this->postEvent($first, $this->getFormValues(['name' => 'Renamed', 'town' => '', 'save' => '1'])); + $this->expectFlashData(['error_detected' => ['Town is mandatory']]); + $this->expectLogEntry(Analog::ERROR, 'Some errors has been threw attempting to edit/store an event'); + + $body = (string)$this->app->handle($this->createRequest('events_event_edit', ['id' => (string)$second]))->getBody(); + $this->assertStringContainsString('value="Second event"', $body); + $this->assertStringNotContainsString('Renamed', $body); + $this->expectNoLogEntry(); + } + /** * Group managers cannot store events of groups they do not manage */ @@ -210,6 +236,27 @@ public function testManagerCannotStoreOtherEvent(): void $this->expectNoLogEntry(); } + /** + * Storage errors are logged, and reported on the form + */ + public function testStoreError(): void + { + $this->logSuperAdmin(); + //leave the test transaction: storage opens its own one, as it does outside of tests + $this->zdb->connection->rollBack(); + + //an unknown group breaks the foreign key + $test_response = $this->postEvent(null, $this->getFormValues(['group' => '999999', 'save' => '1'])); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('events_event_add')]], + $test_response->getHeaders() + ); + $this->expectFlashData(['error_detected' => ['An error occurred while storing the event.']]); + $this->expectLogEntry(Analog::ERROR, 'Query error'); + $this->expectLogEntry(Analog::ERROR, 'Unable to store event #new | '); + $this->expectNoLogEntry(); + } + /** * Past events stay open in their form, so storing them does not close them */ @@ -287,6 +334,58 @@ public function testRemovalTitle(): void $this->assertStringContainsString("Remove event 'Party' - ", $body); } + /** + * Calendar counts attendees of each event + */ + public function testCalendarAttendees(): void + { + $member_one = $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $paid = $this->zdb->isPostgres() ? 'true' : 1; + $first = $this->insertEvent('First event'); + $this->insertBooking($first, $member_one->id, ['number_people' => 3, 'is_paid' => $paid]); + $this->insertBooking($first, $member_two->id, ['number_people' => 2]); + $this->insertEvent('Second event'); + $this->logSuperAdmin(); + + $request = $this->createRequest( + 'ajax-events_calendar', + query_params: [ + 'start' => date('Y-m-d'), + 'end' => date('Y-m-d', strtotime('+1 month')), + ] + ); + $events = json_decode((string)$this->app->handle($request)->getBody(), true); + $this->assertIsArray($events); + $descriptions = array_column($events, 'description', 'name'); + $this->assertStringContainsString('<strong>Attendees:</strong> 5 (3 paid)', $descriptions['First event']); + $this->assertStringContainsString('<strong>Attendees:</strong> 0</li>', $descriptions['Second event']); + } + + /** + * Unknown events are reported, instead of showing an empty form + */ + public function testUnknownEvent(): void + { + $this->logSuperAdmin(); + $id = $this->insertEvent('Removed event'); + $this->cleanEvents(); + $expected = ['Location' => [$this->routeparser->urlFor('events_events')]]; + + $test_response = $this->app->handle($this->createRequest('events_event_edit', ['id' => (string)$id])); + $this->assertSame($expected, $test_response->getHeaders()); + $this->expectFlashData(['error_detected' => ['No event #' . $id . '.']]); + + $test_response = $this->postEvent($id, $this->getFormValues(['save' => '1'])); + $this->assertSame($expected, $test_response->getHeaders()); + $this->expectFlashData(['error_detected' => ['No event #' . $id . '.']]); + + $test_response = $this->app->handle($this->createRequest('events_remove_event', ['id' => (string)$id])); + $this->assertSame(200, $test_response->getStatusCode()); + $this->assertStringContainsString('<title>No event #' . $id . '. - ', (string)$test_response->getBody()); + $this->expectNoLogEntry(); + } + /** * Events list shows events current user can see */ diff --git a/tests/GaletteEvents/Controllers/tests/units/CsvController.php b/tests/GaletteEvents/Controllers/tests/units/CsvController.php index cc4b1a1..816023d 100644 --- a/tests/GaletteEvents/Controllers/tests/units/CsvController.php +++ b/tests/GaletteEvents/Controllers/tests/units/CsvController.php @@ -124,6 +124,24 @@ public function testExportReadsBookingsFiltersOnly(): void $this->assertSame(['text/csv'], $test_response->getHeader('Content-Type')); } + /** + * Export of an unknown event is reported + */ + public function testExportUnknownEvent(): void + { + $this->logSuperAdmin(); + $event = $this->insertEvent('Removed event'); + $this->cleanEvents(); + + $test_response = $this->app->handle($this->createRequest('event_bookings_export', ['id' => (string)$event])); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('events_bookings', ['event' => 'all'])]], + $test_response->getHeaders() + ); + $this->expectFlashData(['error_detected' => ['No event #' . $event . '.']]); + $this->expectNoLogEntry(); + } + /** * Bookings without a known payment method are exported */ diff --git a/tests/GaletteEvents/Repository/tests/units/Bookings.php b/tests/GaletteEvents/Repository/tests/units/Bookings.php index be44b2c..c76086c 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); + $bookings = new \GaletteEvents\Repository\Bookings($this->zdb, $this->login, $this->history); $ids = array_map(fn(Booking $booking): ?int => $booking->getId(), $bookings->getList(true)); sort($ids); return ['ids' => $ids, 'sum' => $bookings->getSum()]; @@ -100,10 +100,35 @@ 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))->getList(); + $list = (new \GaletteEvents\Repository\Bookings($this->zdb, $this->login, $this->history))->getList(); $this->assertCount(1, $list); $this->assertSame('Booking comment', $list[0]->getComment()); - $this->assertSame('2026-02-01', $list[0]->getCreationDate(false)); + $this->assertSame('2026-02-01', $list[0]->getCreationDate()); + $this->assertSame('Event comment', $list[0]->getEvent()?->getComment()); + } + + /** + * Bookings of an event share its instance, loaded once + */ + public function testListLoadsEventsOnce(): void + { + $member_one = $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $event = $this->insertEvent('Event'); + $this->insertBooking($event, $member_one->id); + $this->insertBooking($event, $member_two->id); + $this->insertBooking($this->insertEvent('Other event'), $member_one->id); + + $this->logSuperAdmin(); + $list = (new \GaletteEvents\Repository\Bookings($this->zdb, $this->login, $this->history))->getList(); + $this->assertCount(3, $list); + $events = []; + foreach ($list as $booking) { + $events[$booking->getEventId()][] = $booking->getEvent(); + } + $this->assertCount(2, $events); + $this->assertCount(2, $events[$event]); + $this->assertSame($events[$event][0], $events[$event][1]); } /** @@ -136,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, $bookings_filters); + $bookings = new \GaletteEvents\Repository\Bookings($this->zdb, $this->login, $this->history, $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 8094ea3..5238bea 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); + $events = new \GaletteEvents\Repository\Events($this->zdb, $this->login, $this->history); $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, $filters); + $events = new \GaletteEvents\Repository\Events($this->zdb, $this->login, $this->history, $filters); $names = []; foreach ($events->getList(false, true) as $event) { $this->assertInstanceOf(\ArrayObject::class, $event); diff --git a/tests/GaletteEvents/tests/units/Activity.php b/tests/GaletteEvents/tests/units/Activity.php index 668aa59..d9f4ea5 100644 --- a/tests/GaletteEvents/tests/units/Activity.php +++ b/tests/GaletteEvents/tests/units/Activity.php @@ -38,7 +38,7 @@ public function tearDown(): void */ public function testEmpty(): void { - $activity = new \GaletteEvents\Activity($this->zdb, $this->login); + $activity = new \GaletteEvents\Activity($this->zdb, $this->history); $this->assertNull($activity->getId()); $this->assertSame('', $activity->getName()); @@ -53,8 +53,8 @@ public function testEmpty(): void */ public function testCrud(): void { - $activity = new \GaletteEvents\Activity($this->zdb, $this->login); - $activities = new \GaletteEvents\Repository\Activities($this->zdb, $this->login, $this->preferences); + $activity = new \GaletteEvents\Activity($this->zdb, $this->history); + $activities = new \GaletteEvents\Repository\Activities($this->zdb, $this->login, $this->history, $this->preferences); //ensure the table is empty $this->assertCount(0, $activities->getList()); @@ -76,19 +76,19 @@ public function testCrud(): void 'comment' => 'Test comment', ]; $this->assertTrue($activity->check($data)); - $this->assertTrue($activity->store()); + $activity->store(); $first_id = $activity->getId(); $this->assertGreaterThan(0, $first_id); //creation date column holds no time - $this->assertSame(date('Y-m-d'), $activity->getCreationDate(false)); + $this->assertSame(date('Y-m-d'), $activity->getCreationDate()); - $this->assertTrue($activity->load($first_id)); + $activity->load($first_id); $this->assertSame('Test activity', $activity->getName()); $this->assertSame('Test comment', $activity->getComment()); $this->assertFalse($activity->isActive()); $this->assertSame(0, $activity->countEvents()); $this->assertSame(date('Y-m-d'), $activity->getCreationDate()); - $this->assertSame(date('Y-m-d'), $activity->getCreationDate(false)); + $this->assertSame(date('Y-m-d'), $activity->getCreationDate()); $activities_list = $activities->getList(); $this->assertCount(1, $activities_list); @@ -101,8 +101,8 @@ public function testCrud(): void $data['active'] = true; $data['name'] = 'Test activity edited'; $this->assertTrue($activity->check($data)); - $this->assertTrue($activity->store()); - $this->assertTrue($activity->load($first_id)); + $activity->store(); + $activity->load($first_id); $this->assertSame('Test activity edited', $activity->getName()); $this->assertTrue($activity->isActive()); @@ -113,8 +113,9 @@ public function testCrud(): void */ public function testLoadError(): void { - $activity = new \GaletteEvents\Activity($this->zdb, $this->login); - $this->assertFalse($activity->load(999)); + $activity = new \GaletteEvents\Activity($this->zdb, $this->history); + $this->expectException(\GaletteEvents\NotFoundException::class); + $activity->load(999); } /** @@ -122,15 +123,15 @@ public function testLoadError(): void */ public function testNoComment(): void { - $activity = new \GaletteEvents\Activity($this->zdb, $this->login); + $activity = new \GaletteEvents\Activity($this->zdb, $this->history); $this->assertTrue($activity->check(['name' => 'Dinner', 'active' => '1'])); - $this->assertTrue($activity->store()); + $activity->store(); $update = $this->zdb->update(EVENTS_PREFIX . \GaletteEvents\Activity::TABLE); $update->set(['comment' => null])->where([\GaletteEvents\Activity::PK => $activity->getId()]); $this->zdb->execute($update); - $activity = new \GaletteEvents\Activity($this->zdb, $this->login, (int)$activity->getId()); + $activity = new \GaletteEvents\Activity($this->zdb, $this->history, (int)$activity->getId()); $this->assertSame('Dinner', $activity->getName()); $this->assertSame('', $activity->getComment()); } @@ -144,10 +145,11 @@ public function testCountAndRemove(): void $this->linkActivity($this->insertEvent('First event'), $id); $this->linkActivity($this->insertEvent('Second event'), $id); - $activity = new \GaletteEvents\Activity($this->zdb, $this->login, $id); + $activity = new \GaletteEvents\Activity($this->zdb, $this->history, $id); $this->assertSame(2, $activity->countEvents()); - $this->assertTrue($activity->remove()); - $this->assertFalse((new \GaletteEvents\Activity($this->zdb, $this->login))->load($id)); + $activity->remove(); + $this->expectException(\GaletteEvents\NotFoundException::class); + (new \GaletteEvents\Activity($this->zdb, $this->history))->load($id); $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesevents'); $select->where([\GaletteEvents\Activity::PK => $id]); diff --git a/tests/GaletteEvents/tests/units/Booking.php b/tests/GaletteEvents/tests/units/Booking.php index f58cf62..d0f5d2a 100644 --- a/tests/GaletteEvents/tests/units/Booking.php +++ b/tests/GaletteEvents/tests/units/Booking.php @@ -34,6 +34,23 @@ public function tearDown(): void parent::tearDown(); } + /** + * New bookings are fully initialized + */ + public function testEmpty(): void + { + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $this->history); + $this->assertNull($booking->getId()); + $this->assertNull($booking->getEventId()); + $this->assertNull($booking->getEvent()); + $this->assertNull($booking->getMemberId()); + $this->assertNull($booking->getMember()->id); + $this->assertSame(date('Y-m-d'), $booking->getDate()); + $this->assertSame('', $booking->getCreationDate()); + $this->assertSame([], $booking->getActivities()); + $this->assertSame([], $booking->getErrors()); + } + /** * Optional values may be NULL in database */ @@ -46,11 +63,11 @@ public function testLoadNullValues(): void ['comment' => null, 'payment_amount' => null, 'number_people' => null, 'creation_date' => '2026-09-01'] ); - $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $this->history, $id); $this->assertSame('', $booking->getComment()); $this->assertNull($booking->getAmount()); $this->assertSame(1, $booking->getNumberPeople()); - $this->assertSame('2026-09-01', $booking->getCreationDate(false)); + $this->assertSame('2026-09-01', $booking->getCreationDate()); } /** @@ -72,24 +89,24 @@ public function testActivitiesSync(): void 'booking_date' => date('Y-m-d'), 'number_people' => '1', ]; - $booking = new \GaletteEvents\Booking($this->zdb, $this->login); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $this->history); $this->assertTrue($booking->check($values + ['activities' => [(string)$dinner]])); - $this->assertTrue($booking->store()); + $booking->store(); $id = (int)$booking->getId(); $this->assertSame([$dinner => true, $lodging => false], $this->getBookingActivities($id)); - $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $this->history, $id); $this->assertTrue($booking->check($values + ['activities' => [(string)$lodging]])); - $this->assertTrue($booking->store()); + $booking->store(); $this->assertSame([$dinner => false, $lodging => true], $this->getBookingActivities($id)); //activity removed from event is removed from booking $delete = $this->zdb->delete(EVENTS_PREFIX . 'activitiesevents'); $delete->where([\GaletteEvents\Event::PK => $event, \GaletteEvents\Activity::PK => $dinner]); $this->zdb->execute($delete); - $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $this->history, $id); $this->assertTrue($booking->check($values + ['activities' => [(string)$lodging]])); - $this->assertTrue($booking->store()); + $booking->store(); $this->assertSame([$lodging => true], $this->getBookingActivities($id)); } @@ -107,29 +124,25 @@ public function testAmount(): void 'number_people' => '1', ]; - $booking = new \GaletteEvents\Booking($this->zdb, $this->login); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $this->history); $this->assertTrue($booking->check($values + ['amount' => '12,50'])); $this->assertSame(12.5, $booking->getAmount()); - $this->assertTrue($booking->store()); + $booking->store(); $id = (int)$booking->getId(); - $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $this->history, $id); $this->assertTrue($booking->check($values + ['amount' => ''])); - $this->assertTrue($booking->store()); - $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $booking->store(); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $this->history, $id); $this->assertNull($booking->getAmount()); $this->assertTrue($booking->check($values + ['amount' => '0', 'paid' => '1'])); $this->assertSame(0.0, $booking->getAmount()); - $this->assertSame( - [_T('Please specify amount if booking has been paid ;)', 'events')], - $booking->check($values + ['amount' => '', 'paid' => '1']) - ); - $this->assertSame( - [_T('Amount must be a number.', 'events')], - $booking->check($values + ['amount' => 'ten']) - ); + $this->assertFalse($booking->check($values + ['amount' => '', 'paid' => '1'])); + $this->assertSame([_T('Please specify amount if booking has been paid ;)', 'events')], $booking->getErrors()); + $this->assertFalse($booking->check($values + ['amount' => 'ten'])); + $this->assertSame([_T('Amount must be a number.', 'events')], $booking->getErrors()); $this->expectLogEntry(\Analog\Analog::ERROR, 'Some errors has been threw attempting to edit/store a booking'); } @@ -159,24 +172,18 @@ public function testCheck(): void $event = $this->insertEvent('Event'); $this->insertBooking($event, $member_one->id); - $booking = new \GaletteEvents\Booking($this->zdb, $this->login); - $this->assertSame( - ['Event is mandatory', 'Member is mandatory', 'Booking date is mandatory!'], - $booking->check(['number_people' => '1']) - ); - $this->assertSame( - ['There must be at least one person', '- Wrong date format (Y-m-d) for booking date!'], - $booking->check($this->getFormValues([ - 'event' => (string)$event, - 'member' => (string)$member_one->id, - 'number_people' => '0', - 'booking_date' => 'today', - ])) - ); - $this->assertSame( - [sprintf('A booking already exists for %1$s in %2$s', $member_one->sfullname, 'Event')], - $booking->check($this->getFormValues(['event' => (string)$event, 'member' => (string)$member_one->id])) - ); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $this->history); + $this->assertFalse($booking->check(['number_people' => '1'])); + $this->assertSame(['Event is mandatory', 'Member is mandatory', 'Booking date is mandatory!'], $booking->getErrors()); + $this->assertFalse($booking->check($this->getFormValues([ + 'event' => (string)$event, + 'member' => (string)$member_one->id, + 'number_people' => '0', + 'booking_date' => 'today', + ]))); + $this->assertSame(['There must be at least one person', '- Wrong date format (Y-m-d) for booking date!'], $booking->getErrors()); + $this->assertFalse($booking->check($this->getFormValues(['event' => (string)$event, 'member' => (string)$member_one->id]))); + $this->assertSame([sprintf('A booking already exists for %1$s in %2$s', $member_one->sfullname, 'Event')], $booking->getErrors()); $this->expectLogEntry(\Analog\Analog::ERROR, 'Some errors has been threw attempting to edit/store a booking'); } @@ -189,7 +196,7 @@ public function testMemberFinancialValues(): void $event = $this->insertEvent('Event'); $this->logMember($this->dataAdherentOne()); - $booking = new \GaletteEvents\Booking($this->zdb, $this->login); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $this->history); $this->assertTrue($booking->check($this->getFormValues([ 'event' => (string)$event, 'paid' => '1', @@ -218,8 +225,8 @@ public function testRemove(): void $insert->values([\GaletteEvents\Activity::PK => $dinner, \GaletteEvents\Booking::PK => $id]); $this->zdb->execute($insert); - $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); - $this->assertTrue($booking->remove()); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $this->history, $id); + $booking->remove(); $this->assertSame(0, $this->countBookings($event)); $this->assertSame([], $this->getBookingActivities($id)); } diff --git a/tests/GaletteEvents/tests/units/Event.php b/tests/GaletteEvents/tests/units/Event.php index d8270eb..4be0cc4 100644 --- a/tests/GaletteEvents/tests/units/Event.php +++ b/tests/GaletteEvents/tests/units/Event.php @@ -58,6 +58,22 @@ private function getFormValues(array $values = []): array ]; } + /** + * New events are fully initialized + */ + public function testEmpty(): void + { + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history); + $this->assertNull($event->getId()); + $this->assertSame('', $event->getName()); + $this->assertSame('', $event->getCreationDate()); + $this->assertSame(date('Y-m-d'), $event->getBeginDate()); + $this->assertSame(date('Y-m-d'), $event->getEndDate()); + $this->assertNull($event->getGroup()); + $this->assertSame([], $event->getActivities()); + $this->assertSame([], $event->getErrors()); + } + /** * Group managers create events of the groups they manage */ @@ -69,18 +85,16 @@ public function testManagerCreatesEvent(): void $other = $this->createGroup('Other group', [], [$member_two]); $this->logMember($this->dataAdherentTwo()); - $event = new \GaletteEvents\Event($this->zdb, $this->login); - $this->assertSame( - [_T('Please select a group you own!', 'events')], - $event->check($this->getFormValues(['group' => (string)$other->getId()])) - ); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history); + $this->assertFalse($event->check($this->getFormValues(['group' => (string)$other->getId()]))); + $this->assertSame([_T('Please select a group you own!', 'events')], $event->getErrors()); $this->expectLogEntry(\Analog\Analog::ERROR, 'Some errors has been threw attempting to edit/store an event'); - $event = new \GaletteEvents\Event($this->zdb, $this->login); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history); $this->assertTrue($event->check($this->getFormValues(['group' => (string)$managed->getId()]))); - $this->assertTrue($event->store()); + $event->store(); - $event = new \GaletteEvents\Event($this->zdb, $this->login, (int)$event->getId()); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history, (int)$event->getId()); $this->assertSame($managed->getId(), $event->getGroup()); } @@ -90,15 +104,15 @@ public function testManagerCreatesEvent(): void public function testStoreMandatoryValuesOnly(): void { $this->logSuperAdmin(); - $event = new \GaletteEvents\Event($this->zdb, $this->login); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history); $this->assertTrue($event->check([ 'name' => 'Event', 'town' => 'Lille', 'begin_date' => date('Y-m-d', strtotime('+10 days')), ])); - $this->assertTrue($event->store()); + $event->store(); - $event = new \GaletteEvents\Event($this->zdb, $this->login, (int)$event->getId()); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history, (int)$event->getId()); $this->assertSame('Event', $event->getName()); $this->assertSame('', $event->getAddress()); $this->assertNull($event->getGroup()); @@ -111,7 +125,7 @@ public function testLoadNullValues(): void { $id = $this->insertEvent('Event', ['comment' => null, 'country' => null]); - $event = new \GaletteEvents\Event($this->zdb, $this->login, $id); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history, $id); $this->assertSame('', $event->getComment()); $this->assertNull($event->getGroup()); $this->assertSame('', $event->getColor()); @@ -128,26 +142,26 @@ public function testActivitiesSync(): void $visit = $this->insertActivity('Visit'); $ids = array_map('strval', [$dinner, $lodging, $visit]); - $event = new \GaletteEvents\Event($this->zdb, $this->login); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history); $this->assertTrue($event->check($this->getFormValues([ 'activities_ids' => $ids, 'activities_status' => ['1', '1', '2'], ]))); - $this->assertTrue($event->store()); + $event->store(); $id = (int)$event->getId(); $this->assertSame([$dinner => 1, $lodging => 1, $visit => 2], $this->getEventActivities($id)); //change status of activities that are not the last one - $event = new \GaletteEvents\Event($this->zdb, $this->login, $id); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history, $id); $this->assertTrue($event->check($this->getFormValues([ 'activities_ids' => $ids, 'activities_status' => ['2', '0', '2'], ]))); - $this->assertTrue($event->store()); + $event->store(); $this->assertSame([$dinner => 2, $lodging => 0, $visit => 2], $this->getEventActivities($id)); //remove two activities before storing - $event = new \GaletteEvents\Event($this->zdb, $this->login, $id); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history, $id); $this->assertTrue($event->check($this->getFormValues([ 'remove_activity' => '1', 'detach_activity' => (string)$dinner, @@ -160,12 +174,12 @@ public function testActivitiesSync(): void 'activities_ids' => [(string)$lodging, (string)$visit], 'activities_status' => ['0', '2'], ]))); - $this->assertTrue($event->store()); + $event->store(); $this->assertSame([$visit => 2], $this->getEventActivities($id)); //reloading does not keep activities of the previous event $other = (int)$this->insertEvent('Other event'); - $this->assertTrue($event->load($other)); + $event->load($other); $this->assertSame([], $event->getActivities()); } @@ -182,7 +196,7 @@ public function testInactiveActivities(): void ->where([\GaletteEvents\Activity::PK => $lodging]); $this->zdb->execute($update); - $event = new \GaletteEvents\Event($this->zdb, $this->login); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history); $this->assertSame( [$dinner], array_map(fn($row): int => (int)$row[\GaletteEvents\Activity::PK], $event->availableActivities()) @@ -212,9 +226,9 @@ public function testCanEditChecksGivenLogin(): void $manager = new \Galette\Core\Login($this->zdb, $this->i18n); $this->assertTrue($manager->login($this->dataAdherentTwo()['login_adh'], $this->dataAdherentTwo()['mdp_adh'])); - $this->assertTrue((new \GaletteEvents\Event($this->zdb, $this->login, $managed_event))->canEdit($manager)); - $this->assertFalse((new \GaletteEvents\Event($this->zdb, $this->login, $other_event))->canEdit($manager)); - $this->assertFalse((new \GaletteEvents\Event($this->zdb, $this->login, $this->insertEvent('Public event')))->canEdit($manager)); + $this->assertTrue((new \GaletteEvents\Event($this->zdb, $this->login, $this->history, $managed_event))->canEdit($manager)); + $this->assertFalse((new \GaletteEvents\Event($this->zdb, $this->login, $this->history, $other_event))->canEdit($manager)); + $this->assertFalse((new \GaletteEvents\Event($this->zdb, $this->login, $this->history, $this->insertEvent('Public event')))->canEdit($manager)); } /** @@ -223,27 +237,21 @@ public function testCanEditChecksGivenLogin(): void public function testCheck(): void { $this->logSuperAdmin(); - $event = new \GaletteEvents\Event($this->zdb, $this->login); - - $this->assertSame( - ['Begin date is mandatory', 'Name is mandatory', 'Town is mandatory'], - $event->check(['begin_date' => '']) - ); - $this->assertSame( - ['- Wrong date format (Y-m-d) for Begin date!'], - $event->check($this->getFormValues(['begin_date' => 'tomorrow'])) - ); - $this->assertSame( - ['End date must be later or equal to begin date'], - $event->check($this->getFormValues(['begin_date' => '2026-10-10', 'end_date' => '2026-10-09'])) - ); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history); + + $this->assertFalse($event->check(['begin_date' => ''])); + $this->assertSame(['Begin date is mandatory', 'Name is mandatory', 'Town is mandatory'], $event->getErrors()); + $this->assertFalse($event->check($this->getFormValues(['begin_date' => 'tomorrow']))); + $this->assertSame(['- Wrong date format (Y-m-d) for Begin date!'], $event->getErrors()); + $this->assertFalse($event->check($this->getFormValues(['begin_date' => '2026-10-10', 'end_date' => '2026-10-09']))); + $this->assertSame(['End date must be later or equal to begin date'], $event->getErrors()); $this->expectLogEntry(\Analog\Analog::ERROR, 'Some errors has been threw attempting to edit/store an event'); //end date defaults to begin date $values = $this->getFormValues(['begin_date' => '2026-10-10']); unset($values['end_date']); $this->assertTrue($event->check($values)); - $this->assertSame('2026-10-10', $event->getEndDate(false)); + $this->assertSame('2026-10-10', $event->getEndDate()); $this->assertTrue($event->isOpenFlag()); $this->assertTrue($event->check($this->getFormValues(['open' => null]))); @@ -264,16 +272,44 @@ public function testRemove(): void $this->linkActivity($id, $this->insertActivity('Dinner')); $this->insertBooking($id, $member_one->id, ['number_people' => 3, 'is_paid' => $this->zdb->isPostgres() ? 'true' : 1]); - $event = new \GaletteEvents\Event($this->zdb, $this->login, $id); - $attendees = []; - foreach ($event->countAttendees() as $row) { - $attendees[(int)(bool)$row['is_paid']] = (int)$row['count']; - } - $this->assertSame([1 => 3], $attendees); - - $this->assertTrue($event->remove()); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history, $id); + $event->remove(); $this->assertSame(0, $this->countBookings($id)); $this->assertSame([], $this->getEventActivities($id)); - $this->assertFalse((new \GaletteEvents\Event($this->zdb, $this->login))->load($id)); + $this->expectException(\GaletteEvents\NotFoundException::class); + (new \GaletteEvents\Event($this->zdb, $this->login, $this->history))->load($id); + } + + /** + * Events are stored in the running transaction, and nothing is kept when storage fails + */ + public function testStoreTransactions(): void + { + $this->logSuperAdmin(); + //leave the test transaction: storage opens its own one, as it does outside of tests + $this->zdb->connection->rollBack(); + + //an unknown group breaks the foreign key + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history); + $this->assertTrue($event->check($this->getFormValues(['group' => '999999']))); + try { + $event->store(); + $this->fail('An event of an unknown group must not be stored'); + } catch (\PDOException) { + //expected + } + $this->expectLogEntry(\Analog\Analog::ERROR, 'Query error'); + $this->assertNull($event->getId()); + $this->assertFalse($this->zdb->connection->inTransaction()); + $this->assertSame(0, $this->zdb->execute($this->zdb->select(EVENTS_PREFIX . \GaletteEvents\Event::TABLE))->count()); + + //the transaction opened by the caller is left to it + $this->zdb->connection->beginTransaction(); + $event = new \GaletteEvents\Event($this->zdb, $this->login, $this->history); + $this->assertTrue($event->check($this->getFormValues())); + $event->store(); + $this->assertTrue($this->zdb->connection->inTransaction()); + $this->zdb->connection->rollBack(); + $this->assertSame(0, $this->zdb->execute($this->zdb->select(EVENTS_PREFIX . \GaletteEvents\Event::TABLE))->count()); } }