diff --git a/.composer-require-checker.config.json b/.composer-require-checker.config.json deleted file mode 100644 index f03dabfc..00000000 --- a/.composer-require-checker.config.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "php-core-extensions": [ - "core", "standard", - - "ctype", "date", "fileinfo", "filter", "iconv", "json", "pcre", "reflection", "session", "simplexml", "spl", "xml", - - "curl", "gd", "intl", "mbstring", "mysqli", "zlib", "pdo", "pgsql", "gettext", - - "exif", "pcntl" - ], - "symbol-whitelist": [ - "// PHP symbols", - "null", - "false", "true", - "static", "self", "parent", - "array", "bool", "callable", "float", "int", "iterable", "object", "string", "void", - - "// random native PHP functions", - "// these are moved in random native extension in PHP 8.2, but this extension does not exists in previous PHP versions", - "mt_rand", - "rand", - "random_int", - - "// Galette constants (not detected as they are dynamically declared)", - "GALETTE_ATTACHMENTS_PATH", - "GALETTE_ADAPTATIVE_CARDS", - "GALETTE_BASE_PATH", - "GALETTE_CACHE_DIR", - "GALETTE_CARD_COLS", - "GALETTE_CARD_HEIGHT", - "GALETTE_CARD_ROWS", - "GALETTE_CARD_WIDTH", - "GALETTE_COMPAT_VERSION", - "GALETTE_CONFIG_PATH", - "GALETTE_SYSCONFIG_PATH", - "GALETTE_DATA_PATH", - "GALETTE_DB_VERSION", - "GALETTE_DOCUMENTS_PATH", - "GALETTE_DOWNLOADS_URI", - "GALETTE_EXPORTS_PATH", - "GALETTE_FILES_PATH", - "GALETTE_HOSTED", - "GALETTE_IMPORTS_PATH", - "GALETTE_LOGS_PATH", - "GALETTE_MODE", - "GALETTE_DEBUG", - "GALETTE_NIGHTLY", - "GALETTE_PHOTOS_PATH", - "GALETTE_PLUGINS_PATH", - "GALETTE_PLUGINS_DATA_PATH", - "GALETTE_ROOT", - "GALETTE_TELEMETRY_URI", - "GALETTE_TEMPIMAGES_PATH", - "GALETTE_THEME", - "GALETTE_THEMES_PATH", - "GALETTE_VERSION", - "GALETTE_X_FORWARDED_FOR_INDEX", - "_CURRENT_THEME_PATH", - "GALETTE_URI", - "GALETTE_MYSQL_MIN", - "GALETTE_MARIADB_MIN", - "GALETTE_PGSQL_MIN", - "GALETTE_TESTS", - "GALETTE_DISPLAY_VERSION", - "GALETTE_PHP_MIN", - - "// Galette db constants (not detected as they are dynamically declared)", - "HOST_DB", - "NAME_DB", - "PORT_DB", - "PREFIX_DB", - "PWD_DB", - "TYPE_DB", - "USER_DB", - - "//XHProf constants", - - "//know but not detected Galette function,", - "remove_remarks", - "isValidWebUrl", - "remove_remarks", - "split_sql_file", - "_T", - "__", - "_Tn", - "_Tx", - "_Tnx", - - "// XHProf PECL extension", - "XHProfRuns_Default", - "xhprof_disable", - "xhprof_enable", - "GALETTE_XHPROF_PATH", - "XHPROF_PATH", - "XHPROF_URL", - "XHPROF_FLAGS_CPU", - "XHPROF_FLAGS_MEMORY", - "XHPROF_FLAGS_NO_BUILTINS", - "XHPROF_HOST", - - "// DI functions", - "DI\\autowire", - "DI\\Container", - "DI\\ContainerBuilder", - "DI\\Bridge\\Slim\\App", - "DI\\Attribute\\Inject", - - "// Not detected, do not know why.", - "Psr\\Http\\Server\\RequestHandlerInterface", - "Install" - ], - "scan-files": [ - "*.php", - "lib/**/.php", - "scripts/*.php", - "scripts/**/*.php", - "webroot/*.php" - ] -} diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 08e47309..00000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,173 +0,0 @@ -#tools: -# external_code_coverage: -# timeout: 600 # Timeout in seconds. - -build: - dependencies: - override: - - composer self-update - - git clone --depth=1 https://github.com/galette/galette -b develop galette && pushd galette - - nodes: - - analysis: - environment: - php: - version: 8.2 - - project_setup: - override: - - 'true' - tests: - override: - - php-scrutinizer-run - - js-scrutinizer-run - - cache: - directories: - - vendor # Cache for already installed composer package -> speed up composer install - - galette/vendor # Same ^ - - bin # As vendor directory is cached, bin directory must be also cached (as some dependency will not installed if they are already present and so, related binary will not be linked) - - ~/.composer # Composer home directory (avoid fetching already fetched packages) -filter: - excluded_paths: - - 'galette/*' - - 'tests/*' - - 'bin/*' - - '*.min.js' - dependency_paths: - - "galette/vendor/" - - "vendor/" - -checks: - php: - fix_php_opening_tag: false - remove_php_closing_tag: false - one_class_per_file: false - side_effects_or_types: false - no_mixed_inline_html: false - require_braces_around_control_structures: false - php5_style_constructor: false - no_global_keyword: false - avoid_usage_of_logical_operators: false - psr2_class_declaration: false - no_underscore_prefix_in_properties: false - no_underscore_prefix_in_methods: false - blank_line_after_namespace_declaration: false - single_namespace_per_use: false - psr2_switch_declaration: false - psr2_control_structure_declaration: false - avoid_superglobals: false - security_vulnerabilities: false - no_exit: false - fix_use_statements: - remove_unused: true - preserve_multiple: false - preserve_blanklines: true - order_alphabetically: false - javascript: true - -coding_style: - php: - indentation: - general: - use_tabs: false - size: 4 - switch: - indent_case: true - spaces: - general: - linefeed_character: newline - before_parentheses: - function_declaration: false - closure_definition: true - function_call: false - if: true - for: true - while: true - switch: true - catch: true - array_initializer: false - around_operators: - assignment: true - logical: true - equality: true - relational: true - bitwise: true - additive: true - multiplicative: true - shift: true - unary_additive: false - concatenation: true - negation: false - before_left_brace: - class: true - function: true - if: true - else: true - for: true - while: true - do: true - switch: true - try: true - catch: true - finally: true - before_keywords: - else: true - while: true - catch: true - finally: true - within: - brackets: false - array_initializer: false - grouping: false - function_call: false - function_declaration: false - if: false - for: false - while: false - switch: false - catch: false - type_cast: false - ternary_operator: - before_condition: true - after_condition: true - before_alternative: true - after_alternative: true - in_short_version: false - other: - before_comma: false - after_comma: true - before_semicolon: false - after_semicolon: true - after_type_cast: false - braces: - classes_functions: - class: undefined - function: undefined - closure: undefined - if: - opening: undefined - always: true - else_on_new_line: false - for: - opening: undefined - always: true - while: - opening: undefined - always: true - do_while: - opening: undefined - always: true - while_on_new_line: false - switch: - opening: undefined - try: - opening: undefined - catch_on_new_line: false - finally_on_new_line: false - upper_lower_casing: - keywords: - general: undefined - constants: - true_false_null: undefined diff --git a/_routes.php b/_routes.php index b171427d..c8cb3275 100644 --- a/_routes.php +++ b/_routes.php @@ -9,17 +9,6 @@ declare(strict_types=1); use Galette\Middleware\Authenticate; -use Galette\Repository\Groups; -use GaletteEvents\Filters\BookingsList; -use GaletteEvents\Filters\ActivitiesList; -use GaletteEvents\Event; -use GaletteEvents\Booking; -use GaletteEvents\Activity; -use GaletteEvents\Repository\Bookings; -use GaletteEvents\Repository\Activities; -use Galette\Repository\Members; -use Galette\Filters\MembersList; -use Galette\Entity\Adherent; use GaletteEvents\Controllers\Crud\EventsController; use GaletteEvents\Controllers\Crud\ActivitiesController; use GaletteEvents\Controllers\Crud\BookingsController; diff --git a/bin/release b/bin/release index c442bcbf..02e0202b 100755 --- a/bin/release +++ b/bin/release @@ -409,14 +409,11 @@ def add_libs(rel_name, galette_archive): # development only, nothing a Galette installation needs todrop = [ - '.composer-require-checker.config.json', '.docheader', '.gitignore', '.php-cs-fixer.dist.php', '.phpcs.xml', - '.scrutinizer.yml', 'calendar.js', - 'composer.json.checker', 'package-lock.json', 'package.json', 'phpstan.neon', diff --git a/bin/twig-cache b/bin/twig-cache deleted file mode 100755 index 6074b018..00000000 --- a/bin/twig-cache +++ /dev/null @@ -1,114 +0,0 @@ -#!/bin/php -isDir() && !$fpath->isLink()) ? rmdir($fpath->getPathname()) : unlink($fpath->getPathname()); - } - - rmdir($path); - return true; - } - return false; -} - -/** - * Return a custom Twig cache handler. - * This handler is useful to be able to preserve filenames of compiled files. - * - * @param string $directory - * - * @return CacheInterface - */ -function getTwigCacheHandler(string $directory): CacheInterface -{ - return new class($directory) extends FilesystemCache { - - private string $directory; - - public function __construct(string $directory, int $options = 0) - { - $this->directory = rtrim($directory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - parent::__construct($directory, $options); - } - - public function generateKey(string $name, string $className): string - { - return $this->directory . $name; - } - }; -} - -$directory = sprintf('%s/../templates/default', __DIR__); -$cache_dir = sprintf('%s/../tempcache', __DIR__); -$cache = getTwigCacheHandler($cache_dir); -if (file_exists($cache_dir)) { - rmdir_recursive($cache_dir); -} -mkdir($cache_dir); - -$iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($directory), - RecursiveIteratorIterator::LEAVES_ONLY -); - -$loader = new FilesystemLoader($directory); -$twig = new Environment( - $loader, - [ - 'cache' => $cache, - 'auto_reload' => true, - ] -); - -$twig_functions = [ - '__', - '_T', - '_Tn', - '_Tx', - '_Tnx', - 'url_for', - 'memberName', - 'callstatic', - 'is_current_url', - 'get_class', - 'base_path', - 'file_exists' -]; - -foreach ($twig_functions as $function) { - $twig->addFunction(new TwigFunction($function, $function)); -} - -/** @var SplFileInfo $file */ -foreach ($iterator as $file) { - if ($file->isFile()) { - $twig->load(str_replace($directory . '/', '', $file)); - } -} diff --git a/calendar.js b/calendar.js index 744a59e8..8ebce7e4 100644 --- a/calendar.js +++ b/calendar.js @@ -35,14 +35,16 @@ $(function() { var _infos = JSON.parse(JSON.stringify(info.event.extendedProps)); _infos.url = _calendar_event_url.replace('PLACEBO', _infos.id_event); _infos.booking = _calendar_booking_url.replace('PLACEBO', _infos.id_event); - _booking_action = function() { - window.location.href = _infos.booking; - }; - if (_modal_actions.length == 2) { - _modal_actions[0].click = _booking_action; - } else { - _modal_actions[1].click = _booking_action; + var _actions = []; + if (_infos.can_edit) { + _actions.push(_edit_action); } + _actions.push(Object.assign({}, _booking_action, { + click: function() { + window.location.href = _infos.booking; + } + })); + _actions.push(_close_action); //description is built and escaped server side, other values must be displayed as text var _elt = $(''); _elt.find('.header').text(_infos.name + ' (' + _infos.begin_date_fmt + ' - ' + _infos.end_date_fmt + ')'); @@ -52,7 +54,7 @@ $(function() { onApprove: function() { window.location.href = _infos.url; }, - actions: _modal_actions + actions: _actions }).modal('show'); }, eventMouseEnter: function(info) { diff --git a/lang/ar.utf8/LC_MESSAGES/events.mo b/lang/ar.utf8/LC_MESSAGES/events.mo index b4010e6c..4027058a 100644 Binary files a/lang/ar.utf8/LC_MESSAGES/events.mo and b/lang/ar.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/br.utf8/LC_MESSAGES/events.mo b/lang/br.utf8/LC_MESSAGES/events.mo index 37604cdc..a6f1255d 100644 Binary files a/lang/br.utf8/LC_MESSAGES/events.mo and b/lang/br.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/ca.utf8/LC_MESSAGES/events.mo b/lang/ca.utf8/LC_MESSAGES/events.mo index c5f2e0f3..91fd9522 100644 Binary files a/lang/ca.utf8/LC_MESSAGES/events.mo and b/lang/ca.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/de_DE.utf8/LC_MESSAGES/events.mo b/lang/de_DE.utf8/LC_MESSAGES/events.mo index d5c1569b..f030e0ff 100644 Binary files a/lang/de_DE.utf8/LC_MESSAGES/events.mo and b/lang/de_DE.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/en_US/LC_MESSAGES/events.mo b/lang/en_US/LC_MESSAGES/events.mo index 0b01990f..3a20a04c 100644 Binary files a/lang/en_US/LC_MESSAGES/events.mo and b/lang/en_US/LC_MESSAGES/events.mo differ diff --git a/lang/es.utf8/LC_MESSAGES/events.mo b/lang/es.utf8/LC_MESSAGES/events.mo index 2a28cb54..97a4bb92 100644 Binary files a/lang/es.utf8/LC_MESSAGES/events.mo and b/lang/es.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/events.pot b/lang/events.pot index a89b599c..9c786da6 100644 --- a/lang/events.pot +++ b/lang/events.pot @@ -136,7 +136,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_ar.utf8.po b/lang/events_ar.utf8.po index 1a68933e..6b405b55 100644 --- a/lang/events_ar.utf8.po +++ b/lang/events_ar.utf8.po @@ -138,7 +138,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_br.utf8.po b/lang/events_br.utf8.po index 7cff5fbb..9728af63 100644 --- a/lang/events_br.utf8.po +++ b/lang/events_br.utf8.po @@ -138,7 +138,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_ca.utf8.po b/lang/events_ca.utf8.po index 6fb83280..7f0f2a68 100644 --- a/lang/events_ca.utf8.po +++ b/lang/events_ca.utf8.po @@ -137,7 +137,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_de_DE.utf8.po b/lang/events_de_DE.utf8.po index 11693b23..480d85cc 100644 --- a/lang/events_de_DE.utf8.po +++ b/lang/events_de_DE.utf8.po @@ -144,7 +144,7 @@ msgstr "Ein Fehler ist beim Speichern des Events aufgetreten." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Lösche Ereignis '%1$s'\"" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_en_US.po b/lang/events_en_US.po index c9d20ad7..39c6d7b9 100644 --- a/lang/events_en_US.po +++ b/lang/events_en_US.po @@ -137,8 +137,8 @@ msgstr "An error occurred while storing the event." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" -msgstr "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" +msgstr "Remove event '%1$s'" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 msgid "Bookings management" diff --git a/lang/events_es.utf8.po b/lang/events_es.utf8.po index 6acb5f5f..bdfcc97a 100644 --- a/lang/events_es.utf8.po +++ b/lang/events_es.utf8.po @@ -148,7 +148,7 @@ msgstr "Ha ocurrido un error al almacenar el evento." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Eliminar evento «%1$s»" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_fr_FR.utf8.po b/lang/events_fr_FR.utf8.po index 298bee0d..86cdf2d5 100644 --- a/lang/events_fr_FR.utf8.po +++ b/lang/events_fr_FR.utf8.po @@ -140,7 +140,7 @@ msgstr "Une erreur est survenue à l'enregistrement de l'évènement." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Supprimer l'évènement «%1$s»" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_it_IT.utf8.po b/lang/events_it_IT.utf8.po index ce388fd8..db4771f6 100644 --- a/lang/events_it_IT.utf8.po +++ b/lang/events_it_IT.utf8.po @@ -138,7 +138,7 @@ msgstr "Si è verificato un errore durante la memorizzazione dell'evento." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Rimuovi evento '%1$s'\"" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_nb_NO.utf8.po b/lang/events_nb_NO.utf8.po index 92447fce..621e65a4 100644 --- a/lang/events_nb_NO.utf8.po +++ b/lang/events_nb_NO.utf8.po @@ -134,7 +134,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_oc.utf8.po b/lang/events_oc.utf8.po index 2c7d5b3a..379a0b4f 100644 --- a/lang/events_oc.utf8.po +++ b/lang/events_oc.utf8.po @@ -138,7 +138,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_ota.utf8.po b/lang/events_ota.utf8.po index ef6d0394..294895ea 100644 --- a/lang/events_ota.utf8.po +++ b/lang/events_ota.utf8.po @@ -134,7 +134,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_pt.utf8.po b/lang/events_pt.utf8.po index 279d81da..5f43559b 100644 --- a/lang/events_pt.utf8.po +++ b/lang/events_pt.utf8.po @@ -143,7 +143,7 @@ msgstr "Ocorreu um erro ao armazenar o evento." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Remover evento '%1$s'\"" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_pt_BR.utf8.po b/lang/events_pt_BR.utf8.po index 344ae6d5..9626a832 100644 --- a/lang/events_pt_BR.utf8.po +++ b/lang/events_pt_BR.utf8.po @@ -140,7 +140,7 @@ msgstr "Ocorreu um erro ao armazenar o evento." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Remover evento '%1$s'\"" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_ru.utf8.po b/lang/events_ru.utf8.po index 80381e9b..1360377b 100644 --- a/lang/events_ru.utf8.po +++ b/lang/events_ru.utf8.po @@ -145,7 +145,7 @@ msgstr "При сохранении мероприятия произошла о #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Удалите мероприятие '%1$s'\"" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_si.utf8.po b/lang/events_si.utf8.po index 3d31376c..e9b83cda 100644 --- a/lang/events_si.utf8.po +++ b/lang/events_si.utf8.po @@ -134,7 +134,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_sl.utf8.po b/lang/events_sl.utf8.po index 701a317a..21cb5e7a 100644 --- a/lang/events_sl.utf8.po +++ b/lang/events_sl.utf8.po @@ -140,7 +140,7 @@ msgstr "Pri shranjevanju dogodka je prišlo do napake." #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Odstrani dogodek '%1$s'" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_ta.utf8.po b/lang/events_ta.utf8.po index f3f73177..0fc19489 100644 --- a/lang/events_ta.utf8.po +++ b/lang/events_ta.utf8.po @@ -141,7 +141,7 @@ msgstr "நிகழ்வை சேமிக்கும் போது பி #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "நிகழ்வை '%1$s' '" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_tr.utf8.po b/lang/events_tr.utf8.po index bb879997..1f52cb36 100644 --- a/lang/events_tr.utf8.po +++ b/lang/events_tr.utf8.po @@ -137,7 +137,7 @@ msgstr "" #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/events_uk.utf8.po b/lang/events_uk.utf8.po index 1871ff5e..58efeb45 100644 --- a/lang/events_uk.utf8.po +++ b/lang/events_uk.utf8.po @@ -146,7 +146,7 @@ msgstr "Під час зберігання події сталася помил #. TRANS: %1$s is the event name #: ../lib/GaletteEvents/Controllers/Crud/EventsController.php:448 #, php-format -msgid "Remove event '%1$s'\"" +msgid "Remove event '%1$s'" msgstr "Вилучити подію '%1$s'" #: ../lib/GaletteEvents/Controllers/Crud/BookingsController.php:133 diff --git a/lang/fr_FR.utf8/LC_MESSAGES/events.mo b/lang/fr_FR.utf8/LC_MESSAGES/events.mo index 7aa5b894..48d86ce2 100644 Binary files a/lang/fr_FR.utf8/LC_MESSAGES/events.mo and b/lang/fr_FR.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/it_IT.utf8/LC_MESSAGES/events.mo b/lang/it_IT.utf8/LC_MESSAGES/events.mo index af35225f..04f6de9b 100644 Binary files a/lang/it_IT.utf8/LC_MESSAGES/events.mo and b/lang/it_IT.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/nb_NO.utf8/LC_MESSAGES/events.mo b/lang/nb_NO.utf8/LC_MESSAGES/events.mo index 76fda090..69937efa 100644 Binary files a/lang/nb_NO.utf8/LC_MESSAGES/events.mo and b/lang/nb_NO.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/oc.utf8/LC_MESSAGES/events.mo b/lang/oc.utf8/LC_MESSAGES/events.mo index cc5d75b6..14357a85 100644 Binary files a/lang/oc.utf8/LC_MESSAGES/events.mo and b/lang/oc.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/ota.utf8/LC_MESSAGES/events.mo b/lang/ota.utf8/LC_MESSAGES/events.mo index d827dd04..5228cf99 100644 Binary files a/lang/ota.utf8/LC_MESSAGES/events.mo and b/lang/ota.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/pt.utf8/LC_MESSAGES/events.mo b/lang/pt.utf8/LC_MESSAGES/events.mo index b2d9727d..a42a9191 100644 Binary files a/lang/pt.utf8/LC_MESSAGES/events.mo and b/lang/pt.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/pt_BR.utf8/LC_MESSAGES/events.mo b/lang/pt_BR.utf8/LC_MESSAGES/events.mo index f17c87f5..8b6d3b74 100644 Binary files a/lang/pt_BR.utf8/LC_MESSAGES/events.mo and b/lang/pt_BR.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/ru.utf8/LC_MESSAGES/events.mo b/lang/ru.utf8/LC_MESSAGES/events.mo index cb97dde3..d718a6e4 100644 Binary files a/lang/ru.utf8/LC_MESSAGES/events.mo and b/lang/ru.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/si.utf8/LC_MESSAGES/events.mo b/lang/si.utf8/LC_MESSAGES/events.mo index 94286e0e..c834683e 100644 Binary files a/lang/si.utf8/LC_MESSAGES/events.mo and b/lang/si.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/sl.utf8/LC_MESSAGES/events.mo b/lang/sl.utf8/LC_MESSAGES/events.mo index aa520aad..775b9b39 100644 Binary files a/lang/sl.utf8/LC_MESSAGES/events.mo and b/lang/sl.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/ta.utf8/LC_MESSAGES/events.mo b/lang/ta.utf8/LC_MESSAGES/events.mo index dd5a9c6e..98c1b2f0 100644 Binary files a/lang/ta.utf8/LC_MESSAGES/events.mo and b/lang/ta.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/tr.utf8/LC_MESSAGES/events.mo b/lang/tr.utf8/LC_MESSAGES/events.mo index d8acd338..da762800 100644 Binary files a/lang/tr.utf8/LC_MESSAGES/events.mo and b/lang/tr.utf8/LC_MESSAGES/events.mo differ diff --git a/lang/uk.utf8/LC_MESSAGES/events.mo b/lang/uk.utf8/LC_MESSAGES/events.mo index cb34bfb5..0c314ff5 100644 Binary files a/lang/uk.utf8/LC_MESSAGES/events.mo and b/lang/uk.utf8/LC_MESSAGES/events.mo differ diff --git a/lib/GaletteEvents/Activity.php b/lib/GaletteEvents/Activity.php index 1fae14c7..afdac6ea 100644 --- a/lib/GaletteEvents/Activity.php +++ b/lib/GaletteEvents/Activity.php @@ -39,7 +39,7 @@ class Activity private string $name; private bool $active = false; private string $creation_date; - private string $comment; + private string $comment = ''; /** * Default constructor @@ -102,7 +102,7 @@ private function loadFromRS(ArrayObject $r): void $this->name = $r->name; $this->active = (bool)$r->is_active; $this->creation_date = $r->creation_date; - $this->comment = $r->comment; + $this->comment = $r->comment ?? ''; } /** @@ -325,7 +325,7 @@ protected function getTableName(): string */ public function getComment(): string { - return $this->comment ?? ''; + return $this->comment; } /** diff --git a/lib/GaletteEvents/Booking.php b/lib/GaletteEvents/Booking.php index d557e540..8250c1c1 100644 --- a/lib/GaletteEvents/Booking.php +++ b/lib/GaletteEvents/Booking.php @@ -115,12 +115,13 @@ private function loadFromRS(ArrayObject $r): void $this->member = (int)$r->id_adh; $this->date = $r->booking_date; $this->paid = (bool)$r->is_paid; - $this->amount = (float)$r->payment_amount; + $this->amount = $r->payment_amount === null ? null : (float)$r->payment_amount; $this->payment_method = (int)$r->payment_method; $this->bank_name = $r->bank_name; $this->check_number = $r->check_number; - $this->number_people = (int)$r->number_people; - $this->comment = $r->comment; + $this->number_people = (int)($r->number_people ?? 1); + $this->comment = $r->comment ?? ''; + $this->creation_date = $r->creation_date; } /** @@ -219,11 +220,19 @@ public function check(array $values): array|bool $this->paid = false; } - if (isset($values['amount']) && !empty($values['amount'])) { - $this->amount = (float)$values['amount']; + if (isset($values['amount'])) { + //accept comma as decimal separator + $amount = strtr(trim((string)$values['amount']), ',', '.'); + if ($amount === '') { + $this->amount = null; + } elseif (is_numeric($amount)) { + $this->amount = (float)$amount; + } else { + $this->errors[] = _T('Amount must be a number.', 'events'); + } } - if ($this->paid && !$this->amount) { + if ($this->paid && $this->amount === null) { $this->errors[] = _T('Please specify amount if booking has been paid ;)', 'events'); } @@ -255,7 +264,7 @@ public function check(array $values): array|bool && $member !== $this->getMemberId() ) { //group managers book for members of the groups they manage, on events of those groups - $group = $this->getEvent()?->getGroup() ?: null; + $group = $this->getEvent()?->getGroup(); if (!(new Adherent($this->zdb, $member))->canShow($this->login)) { $this->errors[] = _T("- Please select a member from a group you manage."); } elseif ($group === null || !$this->login->isGroupManager($group)) { @@ -629,8 +638,14 @@ public function getPaymentMethod(): int */ public function getPaymentMethodName(): string { - $pt = new PaymentType($this->zdb, (int)$this->payment_method); - return $pt->getname(); + //payment method may be missing: 0 is the column default, and types can be removed + $select = $this->zdb->select(PaymentType::TABLE); + $select->where([PaymentType::PK => $this->payment_method]); + if ($this->zdb->execute($select)->count() === 0) { + return ''; + } + $pt = new PaymentType($this->zdb, $this->payment_method); + return $pt->getName(); } /** @@ -725,6 +740,7 @@ public function has(int $activity): bool */ public function loadActivities(): void { + $this->activities = []; $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesbookings', 'acb'); $select->where([self::PK => $this->id]); $results = $this->zdb->execute($select); @@ -768,8 +784,7 @@ private function canBook(Event $event): bool return false; } - //public events have no group, loaded as 0 - $group = $event->getGroup() ?: null; + $group = $event->getGroup(); return $group === null || $this->login->isGroupManager($group) || in_array($group, array_map('intval', Groups::loadGroups($this->login->id, false, false)), true); @@ -793,8 +808,7 @@ public function canEdit(Login $login): bool return true; } - //public events have no group, loaded as 0 - $group = $this->getEvent()?->getGroup() ?: null; + $group = $this->getEvent()?->getGroup(); return $group !== null && $login->isGroupManager($group); } diff --git a/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php b/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php index eea220bb..8ea3de7e 100644 --- a/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php +++ b/lib/GaletteEvents/Controllers/Crud/ActivitiesController.php @@ -61,8 +61,8 @@ public function doAdd(Request $request, Response $response): Response */ public function list(Request $request, Response $response, ?string $option = null, string|int|null $value = null): Response { - if (isset($this->session->filter_activities)) { - $filters = $this->session->filter_activities; + if (isset($this->session->{$this->getFilterName('activities')})) { + $filters = $this->session->{$this->getFilterName('activities')}; } else { $filters = new ActivitiesList(); } @@ -84,7 +84,7 @@ public function list(Request $request, Response $response, ?string $option = nul //assign pagination variables to the template and add pagination links $filters->setViewPagination($this->routeparser, $this->view, false); - $this->session->filter_activities = $filters; + $this->session->{$this->getFilterName('activities')} = $filters; // display page $this->view->render( @@ -107,8 +107,8 @@ public function list(Request $request, Response $response, ?string $option = nul public function filter(Request $request, Response $response): Response { $post = $request->getParsedBody(); - if (isset($this->session->filter_activities)) { - $filters = $this->session->filter_activities; + if (isset($this->session->{$this->getFilterName('activities')})) { + $filters = $this->session->{$this->getFilterName('activities')}; } else { $filters = new ActivitiesList(); } @@ -123,7 +123,7 @@ public function filter(Request $request, Response $response): Response } } - $this->session->filter_activities = $filters; + $this->session->{$this->getFilterName('activities')} = $filters; return $response ->withStatus(301) @@ -141,9 +141,9 @@ public function filter(Request $request, Response $response): Response */ public function edit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response { - if ($this->session->activity !== null) { - $activity = $this->session->activity; - $this->session->activity = null; + if ($this->session->plugin_events_activity !== null) { + $activity = $this->session->plugin_events_activity; + $this->session->plugin_events_activity = null; } else { $activity = new Activity($this->zdb, $this->login); } @@ -241,7 +241,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st $redirect_url = $this->routeparser->urlFor('events_activities'); } else { //store entity in session - $this->session->activity = $activity; + $this->session->plugin_events_activity = $activity; if ($activity->getId()) { $redirect_url = $this->routeparser->urlFor( diff --git a/lib/GaletteEvents/Controllers/Crud/BookingsController.php b/lib/GaletteEvents/Controllers/Crud/BookingsController.php index 41711f40..7e5d60a2 100644 --- a/lib/GaletteEvents/Controllers/Crud/BookingsController.php +++ b/lib/GaletteEvents/Controllers/Crud/BookingsController.php @@ -83,7 +83,7 @@ public function list(Request $request, Response $response, ?string $option = nul */ public function listBookings(Response $response, string|int $event, ?string $option = null, string|int|null $value = null): Response { - $filters = $this->session->filter_bookings ?? new BookingsList(); + $filters = $this->session->{$this->getFilterName('bookings')} ?? new BookingsList(); if ($event == 'guess') { $linked_event = $filters->event_filter; @@ -124,7 +124,7 @@ public function listBookings(Response $response, string|int $event, ?string $opt //assign pagination variables to the template and add pagination links $filters->setViewPagination($this->routeparser, $this->view, false); - $this->session->filter_bookings = $filters; + $this->session->{$this->getFilterName('bookings')} = $filters; // display page $this->view->render( @@ -139,7 +139,7 @@ public function listBookings(Response $response, string|int $event, ?string $opt 'eventid' => $linked_event, 'require_dialog' => true, 'filters' => $filters, - 'events' => $events->getList(), + 'events' => $events->getList(full: true), 'groups' => $groups_list ] ); @@ -163,8 +163,8 @@ public function filter(Request $request, Response $response): Response public function filterBookings(Request $request, Response $response, string|int $event): Response { $post = $request->getParsedBody(); - if (isset($this->session->filter_bookings)) { - $filters = $this->session->filter_bookings; + if (isset($this->session->{$this->getFilterName('bookings')})) { + $filters = $this->session->{$this->getFilterName('bookings')}; } else { $filters = new BookingsList(); } @@ -204,7 +204,7 @@ public function filterBookings(Request $request, Response $response, string|int } } - $this->session->filter_bookings = $filters; + $this->session->{$this->getFilterName('bookings')} = $filters; return $response ->withStatus(301) @@ -238,13 +238,12 @@ public function handleBatch(Request $request, Response $response): Response } if (isset($post['entries_sel'])) { - if (isset($this->session->filter_bookings)) { - $filters = clone $this->session->filter_bookings; + if (isset($this->session->{$this->getFilterName('bookings')})) { + $filters = clone $this->session->{$this->getFilterName('bookings')}; } else { $filters = new BookingsList(); } - //$this->session->filter_bookings = $filters; $filters->selected = $post['entries_sel']; //selection is restricted to bookings current logged-in user can list @@ -363,9 +362,9 @@ public function edit(Request $request, Response $response, ?int $id = null, stri $get = $request->getQueryParams(); $route_params = []; - if ($this->session->booking !== null) { - $booking = $this->session->booking; - $this->session->booking = null; + if ($this->session->plugin_events_booking !== null) { + $booking = $this->session->plugin_events_booking; + $this->session->plugin_events_booking = null; } else { $booking = new Booking($this->zdb, $this->login); } @@ -519,7 +518,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st } if (!isset($post['save'])) { - $this->session->booking = $booking; + $this->session->plugin_events_booking = $booking; $error_detected = []; $goto_list = false; $warning_detected[] = _T('Do not forget to store the booking', 'events'); @@ -558,7 +557,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st ); } else { //store entity in session - $this->session->booking = $booking; + $this->session->plugin_events_booking = $booking; if ($booking->getId()) { $route = 'events_booking_edit'; diff --git a/lib/GaletteEvents/Controllers/Crud/EventsController.php b/lib/GaletteEvents/Controllers/Crud/EventsController.php index c4baceb7..41c4ef5a 100644 --- a/lib/GaletteEvents/Controllers/Crud/EventsController.php +++ b/lib/GaletteEvents/Controllers/Crud/EventsController.php @@ -69,8 +69,8 @@ public function doAdd(Request $request, Response $response): Response */ public function list(Request $request, Response $response, ?string $option = null, string|int|null $value = null): Response { - if (isset($this->session->filter_events)) { - $filters = $this->session->filter_events; + if (isset($this->session->{$this->getFilterName('events')})) { + $filters = $this->session->{$this->getFilterName('events')}; } else { $filters = new EventsList(); } @@ -92,7 +92,7 @@ public function list(Request $request, Response $response, ?string $option = nul //assign pagination variables to the template and add pagination links $filters->setViewPagination($this->routeparser, $this->view, false); - $this->session->filter_events = $filters; + $this->session->{$this->getFilterName('events')} = $filters; // display page $this->view->render( @@ -121,31 +121,6 @@ public function calendar( ?string $option = null, string|int|null $value = null ): Response { - if (isset($this->session->filter_events_calendar)) { - $filters = $this->session->filter_events_calendar; - } else { - $filters = new EventsList(); - } - $filters->calendar_filter = true; - - if ($option !== null) { - switch ($option) { - case 'page': - $filters->current_page = (int)$value; - break; - case 'order': - $filters->orderby = $value; - break; - } - } - - $events = new Events($this->zdb, $this->login, $filters); - - //assign pagination variables to the template and add pagination links - $filters->setViewPagination($this->routeparser, $this->view, false); - - $this->session->filter_events_calendar = $filters; - //check if JS has been generated if (!file_exists(__DIR__ . '/../../../../webroot/js/calendar.bundle.js')) { $this->flash->addMessageNow( @@ -161,9 +136,6 @@ public function calendar( [ 'page_title' => _T("Events calendar", "events"), 'require_dialog' => true, - 'events' => $events->getList(), - 'nb_events' => $events->getCount(), - 'filters' => $filters, 'module_id' => $this->getModuleId() ] ); @@ -176,10 +148,16 @@ public function calendar( public function ajaxCalendar(Request $request, Response $response): Response { $get = $request->getQueryParams(); - $filters = $this->session->filter_events_calendar ?? new EventsList(); + $start = strtotime((string)($get['start'] ?? '')); + $end = strtotime((string)($get['end'] ?? '')); + if ($start === false || $end === false) { + return $this->withJson($response, [], 400); + } + + $filters = new EventsList(); $filters->calendar_filter = true; - $filters->start_date_filter = date(__("Y-m-d"), strtotime($get['start'])); - $filters->end_date_filter = date(__("Y-m-d"), strtotime($get['end'])); + $filters->start_date_filter = date(__("Y-m-d"), $start); + $filters->end_date_filter = date(__("Y-m-d"), $end); $events = new Events($this->zdb, $this->login, $filters); @@ -192,8 +170,8 @@ public function ajaxCalendar(Request $request, Response $response): Response public function filter(Request $request, Response $response): Response { $post = $request->getParsedBody(); - if (isset($this->session->filter_events)) { - $filters = $this->session->filter_events; + if (isset($this->session->{$this->getFilterName('events')})) { + $filters = $this->session->{$this->getFilterName('events')}; } else { $filters = new EventsList(); } @@ -208,7 +186,7 @@ public function filter(Request $request, Response $response): Response } } - $this->session->filter_events = $filters; + $this->session->{$this->getFilterName('events')} = $filters; return $response ->withStatus(301) @@ -226,9 +204,9 @@ public function filter(Request $request, Response $response): Response */ public function edit(Request $request, Response $response, ?int $id = null, string $action = 'edit'): Response { - if ($this->session->event !== null) { - $event = $this->session->event; - $this->session->event = null; + if ($this->session->plugin_events_event !== null) { + $event = $this->session->plugin_events_event; + $this->session->plugin_events_event = null; } else { $event = new Event($this->zdb, $this->login); } @@ -241,16 +219,7 @@ public function edit(Request $request, Response $response, ?int $id = null, stri //check if logged-in user can edit event if (!$can) { - $redirect_url = $this->routeparser->urlFor('events_events'); - Analog::log( - sprintf( - 'Member %1$s cannot edit event %2$s', - $this->login->id, - $event->getId() - ) - ); - return $response - ->withHeader('Location', $redirect_url); + return $this->redirectForbidden($response, $event); } // template variable declaration @@ -300,16 +269,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st //check if logged-in user can edit event if (!$can) { - $redirect_url = $this->routeparser->urlFor('events_events'); - Analog::log( - sprintf( - 'Member %1$s cannot edit event %2$s', - $this->login->id, - $event->getId() - ) - ); - return $response - ->withHeader('Location', $redirect_url); + return $this->redirectForbidden($response, $event); } $success_detected = []; @@ -319,46 +279,35 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st // Validation $valid = $event->check($post); - if ($valid !== true) { - $error_detected = array_merge($error_detected, $valid); - } - - if (count($error_detected) == 0) { - //all goes well, we can proceed - $new = false; - if ($event->getId() == '') { - $new = true; - } - if (isset($post['add_activity']) || isset($post['remove_activity'])) { - $this->session->event = $event; - if (isset($post['add_activity'])) { + if (isset($post['add_activity']) || isset($post['remove_activity'])) { + //activities are changed on a form that may not be complete yet, event is stored later + $goto_list = false; + if (isset($post['add_activity'])) { + if (isset($event->getActivities()[(int)($post['attach_activity'] ?? 0)])) { $success_detected[] = _T("Activity has been attached to event.", "events"); $warning_detected[] = _T('Do not forget to store the event', 'events'); } else { - $success_detected[] = _T("Activity has been detached from event.", "events"); + $error_detected[] = _T("Please choose an activity to add", "events"); } - $goto_list = false; + } else { + $success_detected[] = _T("Activity has been detached from event.", "events"); + $warning_detected[] = _T('Do not forget to store the event', 'events'); } - if (isset($post['save']) || isset($post['remove_activity'])) { - $store = $event->store(); - if ($store === true) { - //member has been stored :) - if ($new) { - $success_detected[] = _T("New event has been successfully added.", "events"); - } else { - $success_detected[] = _T("Event has been modified.", "events"); - } + } elseif ($valid !== true) { + $error_detected = array_merge($error_detected, $valid); + } elseif (isset($post['save'])) { + $new = $event->getId() === null; + if ($event->store() === true) { + if ($new) { + $success_detected[] = _T("New event has been successfully added.", "events"); } else { - //something went wrong :'( - $error_detected[] = _T("An error occurred while storing the event.", "events"); + $success_detected[] = _T("Event has been modified.", "events"); } + } else { + $error_detected[] = _T("An error occurred while storing the event.", "events"); } - } - - if (!isset($post['save'])) { - $this->session->event = $event; - $error_detected = []; + } else { $goto_list = false; } @@ -392,7 +341,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st $redirect_url = $this->routeparser->urlFor('events_events'); } else { //store entity in session - $this->session->event = $event; + $this->session->plugin_events_event = $event; if ($event->getId()) { $redirect_url = $this->routeparser->urlFor( @@ -409,6 +358,26 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st ->withHeader('Location', $redirect_url); } + /** + * Redirect when current logged-in user cannot edit an event + * + * @param Event $event Event + */ + private function redirectForbidden(Response $response, Event $event): Response + { + Analog::log( + 'Logged in member ' . $this->login->login + . ' has tried to edit event #' . $event->getId() + . ' without the right to do so.', + Analog::WARNING + ); + return $this->redirectWithErrors( + response: $response, + errors: [_T("You do not have permission for requested URL.")], + redirect_url: $this->routeparser->urlFor('events_events') + ); + } + // /CRUD - Update // CRUD - Delete @@ -445,7 +414,7 @@ public function confirmRemoveTitle(array $args): string $event = new Event($this->zdb, $this->login, (int)$args['id']); return sprintf( //TRANS: %1$s is the event name - _T('Remove event \'%1$s\'"', 'events'), + _T('Remove event \'%1$s\'', 'events'), $event->getName() ); } diff --git a/lib/GaletteEvents/Controllers/CsvController.php b/lib/GaletteEvents/Controllers/CsvController.php index 389c35ab..4cf1c06b 100644 --- a/lib/GaletteEvents/Controllers/CsvController.php +++ b/lib/GaletteEvents/Controllers/CsvController.php @@ -11,6 +11,8 @@ namespace GaletteEvents\Controllers; use Analog\Analog; +use DI\Attribute\Inject; +use Galette\Core\PluginControllerTrait; use Slim\Psr7\Request; use Slim\Psr7\Response; use Galette\IO\Csv; @@ -26,6 +28,14 @@ */ class CsvController extends \Galette\Controllers\CsvController { + use PluginControllerTrait; + + /** + * @var array + */ + #[Inject("Plugin Galette Events")] + protected array $module_info; + /** * Bookings CSV exports * @@ -55,9 +65,9 @@ public function bookingsExport(Request $request, Response $response, ?int $id = $csv = new CsvOut(); //filters come from bookings list, or from its batch actions - $session_var = $post['session_var'] ?? $get['session_var'] ?? 'filter_bookings'; - if (!in_array($session_var, ['filter_bookings', 'plugin-events-bookings'], true)) { - $session_var = 'filter_bookings'; + $session_var = $post['session_var'] ?? $get['session_var'] ?? $this->getFilterName('bookings'); + if (!in_array($session_var, [$this->getFilterName('bookings'), 'plugin-events-bookings'], true)) { + $session_var = $this->getFilterName('bookings'); } if ($id === null && ($this->session->$session_var ?? null) instanceof BookingsList) { $filters = $this->session->$session_var; diff --git a/lib/GaletteEvents/Event.php b/lib/GaletteEvents/Event.php index 0fce7485..f2e299a7 100644 --- a/lib/GaletteEvents/Event.php +++ b/lib/GaletteEvents/Event.php @@ -28,10 +28,6 @@ class Event public const string TABLE = 'events'; public const string PK = 'id_event'; - public const int ACTIVITY_NO = 0; - public const int ACTIVITY_YES = 1; - public const int ACTIVITY_REQUIRED = 2; - private Db $zdb; private Login $login; /** @var array */ @@ -39,22 +35,20 @@ class Event private int $id; private string $name; - private string $address; - private string $zip; - private string $town; - private ?string $country; + private string $address = ''; + private string $zip = ''; + private string $town = ''; + private ?string $country = null; private string $begin_date; private string $end_date; private string $creation_date; private bool $open = true; - private ?int $group; + private ?int $group = null; private string $comment = ''; - private ?string $color; + private ?string $color = null; /** @var array> */ private array $activities = []; - /** @var array> */ - private array $activities_removed = []; /** * Default constructor @@ -131,8 +125,8 @@ private function loadFromRS(ArrayObject $r): void $this->end_date = $r->end_date; $this->creation_date = $r->creation_date; $this->open = (bool)$r->is_open; - $this->group = (int)$r->id_group; - $this->comment = $r->comment; + $this->group = $r->id_group === null ? null : (int)$r->id_group; + $this->comment = $r->comment ?? ''; $this->color = $r->color; } @@ -249,11 +243,11 @@ public function check(array $values): bool|array } else { if ( empty($values['group']) - || !in_array($values['group'], $this->login->managed_groups) + || !in_array((int)$values['group'], array_map('intval', $this->login->managed_groups), true) ) { $this->errors[] = _T('Please select a group you own!', 'events'); } else { - $this->group = $values['group']; + $this->group = (int)$values['group']; } } @@ -276,48 +270,46 @@ public function check(array $values): bool|array } } - if ( - isset($values['add_activity']) - && !empty($values['attach_activity']) - ) { - $this->activities[$values['attach_activity']] = [ - 'activity' => new Activity( - $this->zdb, - $this->login, - (int)$values['attach_activity'] - ), - 'status' => Activity::YES - ]; - } - + //the form posts every linked activity: posted list replaces the current one + $detached = null; if ( isset($values['remove_activity']) && !empty($values['detach_activity']) ) { - unset($this->activities[$values['detach_activity']]); - $this->activities_removed[$values['detach_activity']] = [ - self::PK => $this->id, - Activity::PK => $values['detach_activity'] - ]; + $detached = (int)$values['detach_activity']; + } - if (count($values['activities_ids'])) { - unset($values['activities_ids'][array_search($values['detach_activity'], $values['activities_ids'])]); + $activities = []; + foreach ($values['activities_ids'] ?? [] as $row => $activity_id) { + $activity_id = (int)$activity_id; + $status = (int)($values['activities_status'][$row] ?? Activity::YES); + if ($activity_id === $detached || !in_array($status, [Activity::NO, Activity::YES, Activity::REQUIRED], true)) { + continue; + } + //already linked activities stay, even if they have been deactivated since + $activity = $this->activities[$activity_id]['activity'] ?? $this->getActiveActivity($activity_id); + if ($activity !== null) { + $activities[$activity_id] = [ + 'activity' => $activity, + 'status' => $status + ]; } } - if (isset($values['activities_ids'])) { - foreach ($values['activities_ids'] as $row => $activity_id) { - if (isset($this->activities[$activity_id])) { - $this->activities[$activity_id]['status'] = $values['activities_status'][$row]; - } else { - $activity = new Activity($this->zdb, $this->login, (int)$activity_id); - $this->activities[$activity_id] = [ - 'activity' => $activity, - 'status' => $values['activities_status'][$row] - ]; - } + if ( + isset($values['add_activity']) + && !empty($values['attach_activity']) + && !isset($activities[(int)$values['attach_activity']]) + ) { + $activity = $this->getActiveActivity((int)$values['attach_activity']); + if ($activity !== null) { + $activities[(int)$values['attach_activity']] = [ + 'activity' => $activity, + 'status' => Activity::YES + ]; } } + $this->activities = $activities; if (isset($values['open'])) { $this->open = true; @@ -414,102 +406,88 @@ public function store(): bool } } - $void = []; - $update = []; - $insert = []; - $key_values = []; - $delete = $this->activities_removed; + $this->storeActivities(); - foreach ($this->activities as $aid => $data) { - $activity = $data['activity']; - $status = $data['status']; - $key_values = [ - self::PK => $this->id, - $activity::PK => $activity->getId() - ]; + $this->zdb->connection->commit(); + return true; + } catch (\Exception $e) { + $this->zdb->connection->rollBack(); + Analog::log( + 'Something went wrong :\'( | ' . $e->getMessage() . "\n" + . $e->getTraceAsString(), + Analog::ERROR + ); + throw $e; + } + } - $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesevents', 'ace'); - $select->where($key_values); - $results = $this->zdb->execute($select); - - foreach ($results as $result) { - $values = [ - Activity::PK => $result[Activity::PK], - self::PK => $this->id, - 'status' => $status - ]; - if (!isset($this->activities[$result[Activity::PK]])) { - $delete[$result[Activity::PK]] = $values; - } elseif ($result['status'] != $this->activities[$result[Activity::PK]]['status']) { - $update[$result[Activity::PK]] = $values; - } else { - $void[$result[Activity::PK]] = $values; - } - } + /** + * Get an activity that can be attached to the event + * + * @param int $id Activity ID + */ + private function getActiveActivity(int $id): ?Activity + { + $activity = new Activity($this->zdb, $this->login, $id); + return $activity->getId() !== null && $activity->isActive() ? $activity : null; + } - if (!isset($void[$aid]) && !isset($update[$aid]) && !isset($delete[$aid])) { - $insert[$aid] = [ - Activity::PK => $aid, - self::PK => $this->id, - 'status' => $status - ]; - } - } + /** + * Store activities linked to the event, compared to the stored ones + */ + private function storeActivities(): void + { + $table = EVENTS_PREFIX . 'activitiesevents'; - if (count($delete)) { - $stmt = $this->zdb->delete(EVENTS_PREFIX . 'activitiesevents'); - $count = 0; - foreach ($delete as $values) { - $stmt->where($values); - $this->zdb->execute($stmt); - ++$count; - } - Analog::log( - sprintf('%1$s activities removed', $count), - Analog::INFO - ); + $stored = []; + $select = $this->zdb->select($table); + $select->where([self::PK => $this->id]); + foreach ($this->zdb->execute($select) as $row) { + $stored[(int)$row[Activity::PK]] = (int)$row['status']; + } + + $counts = ['added' => 0, 'updated' => 0, 'removed' => 0]; + foreach ($this->activities as $aid => $data) { + $status = (int)$data['status']; + if (!isset($stored[$aid])) { + $insert = $this->zdb->insert($table); + $insert->values([ + self::PK => $this->id, + Activity::PK => $aid, + 'status' => $status + ]); + $this->zdb->execute($insert); + ++$counts['added']; + } elseif ($stored[$aid] !== $status) { + $update = $this->zdb->update($table); + $update->set(['status' => $status])->where([ + self::PK => $this->id, + Activity::PK => $aid + ]); + $this->zdb->execute($update); + ++$counts['updated']; } + } - if (count($update)) { - $stmt = $this->zdb->update(EVENTS_PREFIX . 'activitiesevents'); - $count = 0; - foreach ($update as $values) { - $stmt - ->set($values) - ->where($key_values); - $this->zdb->execute($stmt); - ++$count; - } - Analog::log( - sprintf('%1$s activities updated', $count), - Analog::INFO - ); + foreach (array_keys($stored) as $aid) { + if (!isset($this->activities[$aid])) { + $delete = $this->zdb->delete($table); + $delete->where([ + self::PK => $this->id, + Activity::PK => $aid + ]); + $this->zdb->execute($delete); + ++$counts['removed']; } + } - if (count($insert)) { - $stmt = $this->zdb->insert(EVENTS_PREFIX . 'activitiesevents'); - $count = 0; - foreach ($insert as $values) { - $stmt->values(array_merge($key_values, $values)); - $this->zdb->execute($stmt); - ++$count; - } + foreach ($counts as $action => $count) { + if ($count > 0) { Analog::log( - sprintf('%1$s activities added', $count), + sprintf('%1$s activities %2$s', $count, $action), Analog::INFO ); } - - $this->zdb->connection->commit(); - return true; - } catch (\Exception $e) { - $this->zdb->connection->rollBack(); - Analog::log( - 'Something went wrong :\'( | ' . $e->getMessage() . "\n" - . $e->getTraceAsString(), - Analog::ERROR - ); - throw $e; } } @@ -532,25 +510,25 @@ public function getName(): ?string /** * Get event address */ - public function getAddress(): ?string + public function getAddress(): string { - return $this->address ?? null; + return $this->address; } /** * Get event zip */ - public function getZip(): ?string + public function getZip(): string { - return $this->zip ?? null; + return $this->zip; } /** * Get event town */ - public function getTown(): ?string + public function getTown(): string { - return $this->town ?? null; + return $this->town; } /** @@ -648,6 +626,15 @@ public function hasActivity(int $activity): bool return $this->activities[$activity]['status'] != Activity::NO; } + /** + * Has event been flagged as open? + * Unlike isOpen(), whatever its dates + */ + public function isOpenFlag(): bool + { + return $this->open; + } + /** * Is event open? * Will return false once the begin date has been exceeded @@ -668,16 +655,6 @@ public function isOpen(): bool return false; } - /** - * Set name - * - * @param string $name Event name - */ - public function setName(string $name): void - { - $this->name = $name; - } - /** * Get table's name */ @@ -694,6 +671,7 @@ protected function getTableName(): string public function availableActivities(): array { $select = $this->zdb->select(EVENTS_PREFIX . Activity::TABLE, 'ac'); + $select->where->equalTo('is_active', true); $results = $this->zdb->execute($select); $activities = []; @@ -711,6 +689,7 @@ public function availableActivities(): array */ public function loadActivities(): void { + $this->activities = []; $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesevents', 'ace'); $select->where([self::PK => $this->id]); $results = $this->zdb->execute($select); @@ -791,12 +770,7 @@ public function canEdit(Login $login): bool return false; } - if ($this->group) { - $groups = $this->login->getManagedGroups(); - return (in_array($this->group, $groups)); - } - - return false; + return $this->group !== null && $login->isGroupManager($this->group); } /** diff --git a/lib/GaletteEvents/Filters/BookingsList.php b/lib/GaletteEvents/Filters/BookingsList.php index ea9f9621..7b872d79 100644 --- a/lib/GaletteEvents/Filters/BookingsList.php +++ b/lib/GaletteEvents/Filters/BookingsList.php @@ -154,7 +154,7 @@ public function __set(string $name, mixed $value): void /** * Build href - * Override to add "event" parameter + * Override to keep "event" parameter * * @param int $page Page */ @@ -163,13 +163,9 @@ protected function getHref(int $page): string $args = [ 'option' => 'page', 'value' => (string)$page, - 'event' => 'all' + 'event' => $this->event_filter === null ? 'all' : (string)$this->event_filter ]; - if ($this->view->getEnvironment()->getGlobals()['cur_subroute']) { - $args['type'] = $this->view->getEnvironment()->getGlobals()['cur_subroute']; - } - $href = $this->routeparser->urlFor( $this->view->getEnvironment()->getGlobals()['cur_route'], $args diff --git a/lib/GaletteEvents/Filters/EventsList.php b/lib/GaletteEvents/Filters/EventsList.php index d079909a..828a77f7 100644 --- a/lib/GaletteEvents/Filters/EventsList.php +++ b/lib/GaletteEvents/Filters/EventsList.php @@ -24,32 +24,24 @@ * @property bool $calendar_filter * @property ?string $start_date_filter * @property ?string $raw_start_date_filter + * @property ?string $end_date_filter + * @property ?string $raw_end_date_filter */ class EventsList extends Pagination { //filters - private ?string $name_filter = null; private ?string $start_date_filter = null; private ?string $end_date_filter = null; - private int $group_filter = 0; - private ?string $meal_filter = null; - private ?string $lodging_filter = null; - private ?string $open_filter = null; private bool $calendar_filter = false; private string $query; /** @var array */ protected array $list_fields = [ - 'name_filter', 'start_date_filter', 'raw_start_date_filter', 'end_date_filter', 'raw_end_date_filter', - 'group_filter', - 'meal_filter', - 'lodging_filter', - 'open_filter', 'calendar_filter' ]; @@ -85,13 +77,8 @@ protected function getDefaultDirection(): SQLOrder public function reinit(): void { parent::reinit(); - $this->name_filter = null; $this->start_date_filter = null; $this->end_date_filter = null; - $this->group_filter = 0; - $this->meal_filter = null; - $this->lodging_filter = null; - $this->open_filter = null; $this->calendar_filter = false; } @@ -186,7 +173,7 @@ public function __set(string $name, mixed $value): void if ($y === false && $ym !== false) { $day = 1; if ($name === 'end_date_filter') { - $day = $ym->format('t'); + $day = (int)$ym->format('t'); } $ym->setDate( (int)$ym->format('Y'), diff --git a/lib/GaletteEvents/PluginGaletteEvents.php b/lib/GaletteEvents/PluginGaletteEvents.php index 99fd8700..9cefa598 100644 --- a/lib/GaletteEvents/PluginGaletteEvents.php +++ b/lib/GaletteEvents/PluginGaletteEvents.php @@ -189,7 +189,7 @@ public function getNews(): ?Entry { $filters = new EventsList(); $now = new \DateTime(); - $filters->start_date_filter = $now->format(_T('Y-m-d')); + $filters->start_date_filter = $now->format(__('Y-m-d')); $events = new Events($this->zdb, $this->login, $filters); $posts = []; diff --git a/lib/GaletteEvents/Repository/Activities.php b/lib/GaletteEvents/Repository/Activities.php index adf482d6..593242e7 100644 --- a/lib/GaletteEvents/Repository/Activities.php +++ b/lib/GaletteEvents/Repository/Activities.php @@ -19,7 +19,6 @@ use Laminas\Db\Sql\Expression; use Galette\Core\Login; use Galette\Core\Db; -use GaletteEvents\Filters\EventsList; use Laminas\Db\Sql\Select; /** @@ -50,7 +49,7 @@ public function __construct(Db $zdb, Login $login, Preferences $preferences, ?Ac parent::__construct($zdb, $preferences, $login, 'Activity', 'GaletteEvents', EVENTS_PREFIX); if ($filters === null) { - $this->filters = new EventsList(); + $this->filters = new ActivitiesList(); } else { $this->filters = $filters; } diff --git a/lib/GaletteEvents/Repository/Bookings.php b/lib/GaletteEvents/Repository/Bookings.php index 753fc435..1dfebb94 100644 --- a/lib/GaletteEvents/Repository/Bookings.php +++ b/lib/GaletteEvents/Repository/Bookings.php @@ -121,17 +121,19 @@ private function buildSelect(?array $fields, bool $count = false): Select $select = $this->zdb->select(EVENTS_PREFIX . Booking::TABLE, 'b'); $select->columns($fieldsList); + //joined tables are used for filtering and ordering only, their columns would override bookings ones $select->join( ['a' => PREFIX_DB . Adherent::TABLE], - 'b.' . Adherent::PK . '= a.' . Adherent::PK + 'b.' . Adherent::PK . '= a.' . Adherent::PK, + [] ); $select->join( ['e' => PREFIX_DB . EVENTS_PREFIX . Event::TABLE], - 'b.' . Event::PK . '= e.' . Event::PK + 'b.' . Event::PK . '= e.' . Event::PK, + [] ); $this->buildWhereClause($select); - $select->order(self::buildOrderClause()); $this->calculateSum($select); @@ -142,7 +144,7 @@ private function buildSelect(?array $fields, bool $count = false): Select return $select; } catch (\Exception $e) { Analog::log( - 'Cannot build SELECT clause for contributions | ' . $e->getMessage(), + 'Cannot build SELECT clause for bookings | ' . $e->getMessage(), Analog::WARNING ); throw $e; @@ -150,7 +152,7 @@ private function buildSelect(?array $fields, bool $count = false): Select } /** - * Calculate sum of all selected contributions + * Calculate sum of all selected bookings * * @param Select $select Original select */ @@ -323,7 +325,7 @@ private function buildOrderClause(?array $fields = null): array } break; case self::ORDERBY_PAID: - if ($this->canOrderBy('id_paid', $fields)) { + if ($this->canOrderBy('is_paid', $fields)) { $order[] = 'is_paid ' . $this->filters->getDirection(); } break; diff --git a/lib/GaletteEvents/Repository/Events.php b/lib/GaletteEvents/Repository/Events.php index 132678a9..af9e2a61 100644 --- a/lib/GaletteEvents/Repository/Events.php +++ b/lib/GaletteEvents/Repository/Events.php @@ -11,6 +11,7 @@ namespace GaletteEvents\Repository; use Analog\Analog; +use ArrayObject; use Galette\Entity\Adherent; use GaletteEvents\Booking; use Laminas\Db\ResultSet\ResultSet; @@ -63,12 +64,17 @@ public function __construct(Db $zdb, Login $login, ?EventsList $filters = null) /** * Get events list * - * @param bool $onlyevents get events member has booking on + * Members get open and upcoming events that are public or restricted to their groups, + * and the ones they have booked; group managers also get events of the groups they manage. + * In calendar, members get all those events, even past or closed ones, in requested dates. + * + * @param bool $bookable get only events current logged-in user can book, not paginated * @param bool $fullcalendar get events for fullcalendar display (ie. end date +1 day) + * @param bool $full get full list, not paginated * - * @return array> + * @return array> */ - public function getList(bool $onlyevents = false, bool $fullcalendar = false): array + public function getList(bool $bookable = false, bool $fullcalendar = false, bool $full = false): array { try { $select = $this->zdb->select(EVENTS_PREFIX . Event::TABLE, 'e'); @@ -80,94 +86,61 @@ public function getList(bool $onlyevents = false, bool $fullcalendar = false): a $select::JOIN_LEFT ); - $groups = null; if (!$this->login->isAdmin() && !$this->login->isStaff()) { - if ($this->login->isGroupManager()) { - $groups = $this->login->managed_groups; + $managed = array_map('intval', $this->login->managed_groups); + $groups = array_unique(array_merge( + array_map('intval', Groups::loadGroups((int)$this->login->id, false, false)), + $managed + )); + + $visible = [new Predicate\IsNull('e.' . Group::PK)]; + if (count($groups)) { + $visible[] = new Predicate\In('e.' . Group::PK, $groups); + } + $visible = new PredicateSet($visible, PredicateSet::OP_OR); + + $booked = new Predicate\Operator('b.' . Adherent::PK, '=', $this->login->id); + + if ($this->filters->calendar_filter) { + $set = [$visible, $booked]; + } else { $set = [new PredicateSet( [ - new Predicate\IsNull(Group::PK), - new Predicate\Operator( - 'is_open', - '=', - true - ), - new Predicate\Operator( - 'begin_date', - '>=', - date('Y-m-d') - ) + new Predicate\Operator('e.is_open', '=', true), + new Predicate\Operator('e.begin_date', '>=', date('Y-m-d')), + $visible ] )]; - - if (count($groups)) { - $set[] = new Predicate\In( - Group::PK, - $groups - ); + if (!$bookable) { + if (count($managed)) { + //managers get events of their groups, whatever their state + $set[] = new Predicate\In('e.' . Group::PK, $managed); + } + $set[] = $booked; } + } - if ($onlyevents === false) { - //get events member has booking on - $set[] = new Predicate\Operator( - 'b.' . Adherent::PK, - '=', - $this->login->id - ); - } + $select->where(new PredicateSet($set, PredicateSet::OP_OR)); + } - $select->where( - new PredicateSet( - $set, - PredicateSet::OP_OR - ) - ); + if ($this->filters->raw_start_date_filter !== null) { + if ($this->filters->calendar_filter) { + //events overlapping requested dates + $select->where->greaterThanOrEqualTo('e.end_date', $this->filters->raw_start_date_filter); } else { - $select->where( - 'is_open', - //@phpstan-ignore-next-line - new Expression('true') - ); - $select->where->greaterThanOrEqualTo('begin_date', date('Y-m-d')); - - $set = [new Predicate\IsNull(Group::PK)]; - $groups = Groups::loadGroups($this->login->id, false, false); - if (count($groups)) { - $set[] = new Predicate\In( - Group::PK, - $groups - ); - } - - if ($onlyevents === false) { - //get events member has booking on - - $set[] = new Predicate\Operator( - 'b.' . Adherent::PK, - '=', - $this->login->id - ); - } - - $select->where( - new PredicateSet( - $set, - PredicateSet::OP_OR - ) - ); - } - } else { - if (isset($this->filters->start_date_filter) && $this->filters->raw_start_date_filter !== null) { - $select->where->greaterThanOrEqualTo('begin_date', $this->filters->raw_start_date_filter); + $select->where->greaterThanOrEqualTo('e.begin_date', $this->filters->raw_start_date_filter); } } + if ($this->filters->raw_end_date_filter !== null) { + $select->where->lessThanOrEqualTo('e.begin_date', $this->filters->raw_end_date_filter); + } $select->group(['e.' . Event::PK]); $select->order($this->buildOrderClause()); $this->proceedCount($select); - if (!$this->filters->calendar_filter) { + if (!$this->filters->calendar_filter && !$bookable && !$full) { $this->filters->setLimits($select); } $results = $this->zdb->execute($select); @@ -181,6 +154,7 @@ public function getList(bool $onlyevents = false, bool $fullcalendar = false): a } else { //required entries for fullcalendar $row['title'] = $row['name']; + $row['can_edit'] = $event->canEdit($this->login); $row['start'] = $row['begin_date']; $end_date = new \DateTime($event->getEndDate(false)); if ($fullcalendar === true) { diff --git a/package-lock.json b/package-lock.json index a1b89d00..bd2f51b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "plugin-events", - "version": "1.0.0", + "version": "2.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "plugin-events", - "version": "1.0.0", + "version": "2.2.1", "license": "GPL-3.0-or-later", "dependencies": { "fullcalendar": "^7.1.0", diff --git a/package.json b/package.json index d98c3137..9bc4b543 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,8 @@ { "name": "plugin-events", - "version": "1.0.0", + "version": "2.2.1", "description": "A [Galette](https://galette.eu) plugin to manage events and bookings", "main": "index.js", - "directories": { - "lib": "lib" - }, "scripts": { "build": "webpack", "build-dist": "webpack --mode=production", @@ -14,7 +11,7 @@ }, "repository": { "type": "git", - "url": "ssh://git.tuxfamily.org/gitroot/galette/plugin-events.git" + "url": "https://github.com/galette-plugins/plugin-events.git" }, "author": "Johan Cwiklinski ", "license": "GPL-3.0-or-later", diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index af389325..00000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -gitpython -urlgrabber diff --git a/templates/default/calendar.html.twig b/templates/default/calendar.html.twig index f895e50c..c95d1a48 100644 --- a/templates/default/calendar.html.twig +++ b/templates/default/calendar.html.twig @@ -23,15 +23,12 @@ today: { buttonText: '{{ _T("Today", "events")|e('js') }}' } } var _fullcalendar_locale = '{{ i18n.getWebID() }}'; - var _modal_actions = []; - {% if login.isAdmin() or login.isStaff() or login.isGroupManager() %} - var _edit_action = { - text : '{{ _T("Edit")|e('js') }}', - icon : 'edit', - class : 'icon labeled blue approve' - }; - _modal_actions.push(_edit_action); - {% endif %} + //edit action is added for events current user can edit + var _edit_action = { + text : '{{ _T("Edit")|e('js') }}', + icon : 'edit', + class : 'icon labeled blue approve' + }; var _booking_action = { text : '{{ _T("New booking", "events")|e('js') }}', icon : 'plus circle', @@ -42,8 +39,6 @@ icon : 'times', class : 'icon labeled cancel' }; - _modal_actions.push(_booking_action); - _modal_actions.push(_close_action); {% endblock %} diff --git a/templates/default/event.html.twig b/templates/default/event.html.twig index e0ae0962..f790e8ae 100644 --- a/templates/default/event.html.twig +++ b/templates/default/event.html.twig @@ -16,9 +16,9 @@
{% include "components/forms/checkbox.html.twig" with { id: 'open', - value: event.isOpen(), + value: 1, label: _T("Is open", "events"), - checked: event.IsOpen() + checked: event.isOpenFlag() } %} {% include "components/forms/text.html.twig" with { diff --git a/tests/EventsFixtures.php b/tests/EventsFixtures.php index 0afe2fd9..ca839145 100644 --- a/tests/EventsFixtures.php +++ b/tests/EventsFixtures.php @@ -189,6 +189,42 @@ protected function countBookings(int $event): int return $this->zdb->execute($select)->count(); } + /** + * Get activities linked to an event, and their status + * + * @param int $event Event ID + * + * @return array + */ + protected function getEventActivities(int $event): array + { + $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesevents'); + $select->where([Event::PK => $event])->order(Activity::PK); + $activities = []; + foreach ($this->zdb->execute($select) as $row) { + $activities[(int)$row[Activity::PK]] = (int)$row['status']; + } + return $activities; + } + + /** + * Get activities of a booking, and whether they are checked + * + * @param int $booking Booking ID + * + * @return array + */ + protected function getBookingActivities(int $booking): array + { + $select = $this->zdb->select(EVENTS_PREFIX . 'activitiesbookings'); + $select->where([Booking::PK => $booking])->order(Activity::PK); + $activities = []; + foreach ($this->zdb->execute($select) as $row) { + $activities[(int)$row[Activity::PK]] = (bool)$row['checked']; + } + return $activities; + } + /** * Get an ID from a name * diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php index 2aa8c058..6ef6cc08 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/ActivitiesController.php @@ -86,4 +86,37 @@ public function testMemberCannotStoreActivity(): void $this->expectAuthMiddlewareRefused($this->postActivity(['name' => 'Created by a member', 'active' => '1', 'comment' => ''])); $this->assertSame(0, $this->countActivities('Created by a member')); } + + /** + * Session keys do not collide with other plugins ones + */ + public function testSessionKeysArePrefixed(): void + { + $this->logSuperAdmin(); + //plugin-activities stores its own entity under this key + $this->session->activity = new \stdClass(); + + $test_response = $this->app->handle($this->createRequest('events_activity_add')); + $this->assertSame(200, $test_response->getStatusCode()); + $this->expectNoLogEntry(); + } + + /** + * Activities are removed from the posted id + */ + public function testRemoveActivity(): void + { + $this->logSuperAdmin(); + $id = $this->insertActivity('Dinner'); + + $request = $this->createRequest('events_do_remove_activity', [], 'POST') + ->withParsedBody(['id' => (string)$id, 'confirm' => '1']); + $test_response = $this->app->handle($request); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('events_activities')]], + $test_response->getHeaders() + ); + $this->expectFlashData(['success_detected' => ['Successfully deleted!']]); + $this->assertSame(0, $this->countActivities('Dinner')); + } } diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php index 84faa654..613dfebf 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/BookingsController.php @@ -466,4 +466,31 @@ public function testManagerBatchAsCoreAllows(): void ); $this->assertSame(307, $batch('csv')->getStatusCode()); } + + /** + * Bookings list of an event keeps the event in its pagination + */ + public function testListPaginationKeepsEvent(): void + { + $member_one = $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $event = $this->insertEvent('Event'); + $this->insertBooking($event, $member_one->id); + $this->insertBooking($event, $member_two->id); + + $this->logSuperAdmin(); + $filters = new \GaletteEvents\Filters\BookingsList(); + $filters->show = 1; + $this->session->plugin_events_bookings_filter = $filters; + + $test_response = $this->app->handle($this->createRequest('events_bookings', ['event' => (string)$event])); + $this->assertSame(200, $test_response->getStatusCode()); + $this->assertStringContainsString( + 'href="' . $this->routeparser->urlFor( + 'events_bookings', + ['event' => (string)$event, 'option' => 'page', 'value' => '2'] + ) . '"', + (string)$test_response->getBody() + ); + } } diff --git a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php index 9266ae23..6821bd44 100644 --- a/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php +++ b/tests/GaletteEvents/Controllers/Crud/tests/units/EventsController.php @@ -10,6 +10,7 @@ namespace GaletteEvents\Controllers\Crud\tests\units; +use Analog\Analog; use Galette\Tests\GaletteRoutingTestCase; use GaletteEvents\tests\EventsFixtures; @@ -35,6 +36,46 @@ public function tearDown(): void parent::tearDown(); } + /** + * Post an event + * + * @param ?int $id Event ID, null to add a new one + * @param array $data Posted data + */ + private function postEvent(?int $id, array $data): \Psr\Http\Message\ResponseInterface + { + if ($id === null) { + $request = $this->createRequest('events_storeevent_add', [], 'POST'); + } else { + $request = $this->createRequest('events_storeevent_edit', ['id' => (string)$id], 'POST'); + $data += ['id' => (string)$id]; + } + return $this->app->handle($request->withParsedBody($data)); + } + + /** + * Get values posted from event form + * + * @param array $values Values to override + * + * @return array + */ + private function getFormValues(array $values = []): array + { + return $values + [ + 'name' => 'Event', + 'address' => '', + 'zip' => '', + 'town' => 'Lille', + 'country' => '', + 'comment' => '', + 'color' => '', + 'begin_date' => date('Y-m-d', strtotime('+10 days')), + 'end_date' => date('Y-m-d', strtotime('+11 days')), + 'open' => '1', + ]; + } + /** * Calendar event description is HTML: values typed by users must be escaped */ @@ -76,4 +117,173 @@ public function testCalendarDescriptionIsEscaped(): void $this->assertSame('Party name', $events[0]['name']); $this->assertSame('Party name', $events[0]['title']); } + + /** + * Detaching an activity waits for the event to be stored, as attaching does + */ + public function testDetachActivityWaitsForStore(): void + { + $this->logSuperAdmin(); + $dinner = $this->insertActivity('Dinner'); + $lodging = $this->insertActivity('Lodging'); + $event = $this->insertEvent('Event'); + $this->linkActivity($event, $dinner); + $this->linkActivity($event, $lodging); + + $test_response = $this->postEvent($event, $this->getFormValues([ + 'remove_activity' => '1', + 'detach_activity' => (string)$dinner, + 'activities_ids' => [(string)$dinner, (string)$lodging], + 'activities_status' => ['1', '1'], + ])); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('events_event_edit', ['id' => (string)$event])]], + $test_response->getHeaders() + ); + $this->expectFlashData([ + 'warning_detected' => ['Do not forget to store the event'], + 'success_detected' => ['Activity has been detached from event.'], + ]); + $this->assertSame([$dinner => 1, $lodging => 1], $this->getEventActivities($event)); + + $this->postEvent($event, $this->getFormValues([ + 'save' => '1', + 'activities_ids' => [(string)$lodging], + 'activities_status' => ['1'], + ])); + $this->expectFlashData(['success_detected' => ['Event has been modified.']]); + $this->assertSame([$lodging => 1], $this->getEventActivities($event)); + } + + /** + * Activities are attached to an event whose form is not complete yet + */ + public function testAttachActivityOnIncompleteEvent(): void + { + $this->logSuperAdmin(); + $dinner = $this->insertActivity('Dinner'); + + $test_response = $this->postEvent(null, $this->getFormValues([ + 'name' => '', + 'add_activity' => '1', + 'attach_activity' => (string)$dinner, + ])); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('events_event_add')]], + $test_response->getHeaders() + ); + $this->expectFlashData([ + 'warning_detected' => ['Do not forget to store the event'], + 'success_detected' => ['Activity has been attached to event.'], + ]); + $this->expectLogEntry(Analog::ERROR, 'Some errors has been threw attempting to edit/store an event'); + $this->assertSame([$dinner], array_keys($this->session->plugin_events_event->getActivities())); + + //an unknown activity is not attached + $this->postEvent(null, $this->getFormValues([ + 'add_activity' => '1', + 'attach_activity' => (string)($dinner + 1000), + ])); + $this->expectFlashData(['error_detected' => ['Please choose an activity to add']]); + } + + /** + * Group managers cannot store events of groups they do not manage + */ + public function testManagerCannotStoreOtherEvent(): void + { + $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $this->createGroup('Managed group', [$member_two]); + $event = $this->insertEvent('Public event'); + $this->logMember($this->dataAdherentTwo()); + + $test_response = $this->postEvent($event, $this->getFormValues(['name' => 'Changed', 'save' => '1'])); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('events_events')]], + $test_response->getHeaders() + ); + $this->assertSame(301, $test_response->getStatusCode()); + //message comes in the language of the logged-in member + $this->expectFlashData(['error_detected' => [_T('You do not have permission for requested URL.')]]); + $this->expectLogEntry(Analog::WARNING, 'has tried to edit event #' . $event); + $this->expectNoLogEntry(); + } + + /** + * Past events stay open in their form, so storing them does not close them + */ + public function testPastEventFormKeepsOpenFlag(): void + { + $this->logSuperAdmin(); + $event = $this->insertEvent( + 'Past event', + ['begin_date' => date('Y-m-d', strtotime('-2 days')), 'end_date' => date('Y-m-d', strtotime('-1 day'))] + ); + + $test_response = $this->app->handle($this->createRequest('events_event_edit', ['id' => (string)$event])); + $this->assertSame(200, $test_response->getStatusCode()); + $this->assertSame( + 1, + preg_match('@]*id="open"[^>]*>@s', (string)$test_response->getBody(), $matches) + ); + $this->assertStringContainsString(' checked', $matches[0]); + $this->assertStringContainsString('value="1"', $matches[0]); + } + + /** + * Calendar requires dates + */ + public function testCalendarRequiresDates(): void + { + $this->getMemberOne(); + $this->logMember($this->dataAdherentOne()); + + foreach ([[], ['start' => 'soon', 'end' => 'later']] as $query) { + $test_response = $this->app->handle($this->createRequest('ajax-events_calendar', query_params: $query)); + $this->assertSame(400, $test_response->getStatusCode()); + $this->assertSame('[]', (string)$test_response->getBody()); + } + $this->expectNoLogEntry(); + } + + /** + * Calendar tells which events can be edited + */ + public function testCalendarCanEdit(): void + { + $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $managed = $this->createGroup('Managed group', [$member_two]); + $this->insertEvent('Managed event', ['id_group' => $managed->getId()]); + $this->insertEvent('Public event'); + $this->logMember($this->dataAdherentTwo()); + + $request = $this->createRequest( + 'ajax-events_calendar', + query_params: [ + 'start' => date('Y-m-d'), + 'end' => date('Y-m-d', strtotime('+1 month')), + ] + ); + $events = json_decode((string)$this->app->handle($request)->getBody(), true); + $this->assertIsArray($events); + $can_edit = array_column($events, 'can_edit', 'name'); + ksort($can_edit); + $this->assertSame(['Managed event' => true, 'Public event' => false], $can_edit); + } + + /** + * Removal confirmation page names the event + */ + public function testRemovalTitle(): void + { + $this->logSuperAdmin(); + $event = $this->insertEvent('Party'); + + $test_response = $this->app->handle($this->createRequest('events_remove_event', ['id' => (string)$event])); + $this->assertSame(200, $test_response->getStatusCode()); + $body = html_entity_decode((string)$test_response->getBody(), ENT_QUOTES); + $this->assertStringContainsString("Remove event 'Party' - ", $body); + } } diff --git a/tests/GaletteEvents/Controllers/tests/units/CsvController.php b/tests/GaletteEvents/Controllers/tests/units/CsvController.php index 5a84ac68..cc4b1a12 100644 --- a/tests/GaletteEvents/Controllers/tests/units/CsvController.php +++ b/tests/GaletteEvents/Controllers/tests/units/CsvController.php @@ -123,4 +123,18 @@ public function testExportReadsBookingsFiltersOnly(): void $this->assertSame(200, $test_response->getStatusCode()); $this->assertSame(['text/csv'], $test_response->getHeader('Content-Type')); } + + /** + * Bookings without a known payment method are exported + */ + public function testExportUnknownPaymentMethod(): void + { + $member_one = $this->getMemberOne(); + $event = $this->insertEvent('Event'); + $this->insertBooking($event, $member_one->id, ['payment_method' => 0]); + + $this->logSuperAdmin(); + $this->assertStringContainsString($member_one->email, $this->exportEvent($event)); + $this->expectNoLogEntry(); + } } diff --git a/tests/GaletteEvents/Filters/tests/units/EventsList.php b/tests/GaletteEvents/Filters/tests/units/EventsList.php new file mode 100644 index 00000000..54751501 --- /dev/null +++ b/tests/GaletteEvents/Filters/tests/units/EventsList.php @@ -0,0 +1,47 @@ +<?php + +/** + * This file is part of Galette Events plugin (https://galette.eu). + * SPDX-FileCopyrightText: Copyright © 2018-2026 The Galette Team + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace GaletteEvents\Filters\tests\units; + +use Galette\Tests\GaletteTestCase; + +/** + * Events list filters tests + * + * @author Johan Cwiklinski <johan@x-tnd.be> + */ +class EventsList extends GaletteTestCase +{ + protected int $seed = 20260926151512; + + /** + * Date filters accept a year, a month or a day + */ + public function testDateFilters(): void + { + $filters = new \GaletteEvents\Filters\EventsList(); + + $filters->start_date_filter = '2026'; + $filters->end_date_filter = '2026'; + $this->assertSame('2026-01-01', $filters->raw_start_date_filter); + $this->assertSame('2026-12-31', $filters->raw_end_date_filter); + + $filters->start_date_filter = '2026-02'; + $filters->end_date_filter = '2026-02'; + $this->assertSame('2026-02-01', $filters->raw_start_date_filter); + $this->assertSame('2026-02-28', $filters->raw_end_date_filter); + + $filters->start_date_filter = '2026-02-10'; + $this->assertSame('2026-02-10', $filters->raw_start_date_filter); + + $filters->end_date_filter = ''; + $this->assertNull($filters->raw_end_date_filter); + } +} diff --git a/tests/GaletteEvents/Repository/tests/units/Bookings.php b/tests/GaletteEvents/Repository/tests/units/Bookings.php index f1b6e56d..a5b7d60c 100644 --- a/tests/GaletteEvents/Repository/tests/units/Bookings.php +++ b/tests/GaletteEvents/Repository/tests/units/Bookings.php @@ -89,4 +89,20 @@ public function testListScope(): void $this->getVisibleBookings() ); } + + /** + * Listed bookings keep their own values, not the ones of their event + */ + public function testListKeepsBookingValues(): void + { + $member_one = $this->getMemberOne(); + $event = $this->insertEvent('Event', ['comment' => 'Event comment', 'creation_date' => '2026-01-01']); + $this->insertBooking($event, $member_one->id, ['comment' => 'Booking comment', 'creation_date' => '2026-02-01']); + + $this->logSuperAdmin(); + $list = (new \GaletteEvents\Repository\Bookings($this->zdb, $this->login))->getList(); + $this->assertCount(1, $list); + $this->assertSame('Booking comment', $list[0]->getComment()); + $this->assertSame('2026-02-01', $list[0]->getCreationDate(false)); + } } diff --git a/tests/GaletteEvents/Repository/tests/units/Events.php b/tests/GaletteEvents/Repository/tests/units/Events.php new file mode 100644 index 00000000..8094ea33 --- /dev/null +++ b/tests/GaletteEvents/Repository/tests/units/Events.php @@ -0,0 +1,133 @@ +<?php + +/** + * This file is part of Galette Events plugin (https://galette.eu). + * SPDX-FileCopyrightText: Copyright © 2018-2026 The Galette Team + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace GaletteEvents\Repository\tests\units; + +use Galette\Tests\GaletteTestCase; +use GaletteEvents\Filters\EventsList; +use GaletteEvents\tests\EventsFixtures; + +/** + * Events repository tests + * + * @author Johan Cwiklinski <johan@x-tnd.be> + */ +class Events extends GaletteTestCase +{ + use EventsFixtures; + + protected int $seed = 20260926151512; + + /** + * Cleanup after each test method + */ + public function tearDown(): void + { + $this->login->logout(); + $this->cleanEvents(); + parent::tearDown(); + } + + /** + * Get names of listed events + * + * @param bool $bookable Bookable events only + * + * @return array<string> + */ + private function getListed(bool $bookable = false): array + { + $events = new \GaletteEvents\Repository\Events($this->zdb, $this->login); + $names = []; + foreach ($events->getList($bookable) as $event) { + $this->assertInstanceOf(\GaletteEvents\Event::class, $event); + $names[] = (string)$event->getName(); + } + sort($names); + return $names; + } + + /** + * Get names of events displayed in calendar, from last month to next one + * + * @return array<string> + */ + private function getCalendar(): array + { + $filters = new EventsList(); + $filters->calendar_filter = true; + $filters->start_date_filter = date(__('Y-m-d'), strtotime('-1 month')); + $filters->end_date_filter = date(__('Y-m-d'), strtotime('+1 month')); + $events = new \GaletteEvents\Repository\Events($this->zdb, $this->login, $filters); + $names = []; + foreach ($events->getList(false, true) as $event) { + $this->assertInstanceOf(\ArrayObject::class, $event); + $names[] = $event['name']; + } + sort($names); + return $names; + } + + /** + * Events listed, bookable and displayed in calendar, depending on who is logged in + */ + public function testVisibility(): void + { + $member_one = $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $own = $this->createGroup('Own group', [], [$member_one]); + $other = $this->createGroup('Other group', [], [$member_two]); + $managed = $this->createGroup('Managed group', [$member_two]); + $past = [ + 'begin_date' => date('Y-m-d', strtotime('-10 days')), + 'end_date' => date('Y-m-d', strtotime('-9 days')), + ]; + + $this->insertEvent('public'); + $this->insertEvent('public closed', ['is_open' => false]); + $this->insertBooking($this->insertEvent('public past', $past), $member_one->id); + $this->insertEvent( + 'public far', + ['begin_date' => date('Y-m-d', strtotime('+3 months')), 'end_date' => date('Y-m-d', strtotime('+3 months'))] + ); + $this->insertEvent('own group', ['id_group' => $own->getId()]); + $this->insertEvent('other group', ['id_group' => $other->getId()]); + $this->insertEvent('managed', ['id_group' => $managed->getId()]); + $this->insertEvent('managed past closed', ['id_group' => $managed->getId(), 'is_open' => false] + $past); + + $this->logMember($this->dataAdherentOne()); + $this->assertSame(['own group', 'public', 'public far', 'public past'], $this->getListed()); + $this->assertSame(['own group', 'public', 'public far'], $this->getListed(true)); + $this->assertSame(['own group', 'public', 'public closed', 'public past'], $this->getCalendar()); + $this->login->logout(); + + //member two manages a group, and belongs to another one + $this->logMember($this->dataAdherentTwo()); + $this->assertSame( + ['managed', 'managed past closed', 'other group', 'public', 'public far'], + $this->getListed() + ); + $this->assertSame(['managed', 'other group', 'public', 'public far'], $this->getListed(true)); + $this->assertSame( + ['managed', 'managed past closed', 'other group', 'public', 'public closed', 'public past'], + $this->getCalendar() + ); + $this->login->logout(); + + $this->logSuperAdmin(); + $all = [ + 'managed', 'managed past closed', 'other group', 'own group', + 'public', 'public closed', 'public far', 'public past' + ]; + $this->assertSame($all, $this->getListed()); + $this->assertSame($all, $this->getListed(true)); + $this->assertSame(array_values(array_diff($all, ['public far'])), $this->getCalendar()); + } +} diff --git a/tests/GaletteEvents/tests/units/Activity.php b/tests/GaletteEvents/tests/units/Activity.php index 51ca743a..43be19ae 100644 --- a/tests/GaletteEvents/tests/units/Activity.php +++ b/tests/GaletteEvents/tests/units/Activity.php @@ -148,4 +148,22 @@ public function testLoadError(): void $activity = new \GaletteEvents\Activity($this->zdb, $this->login); $this->assertFalse($activity->load(999)); } + + /** + * Activities are stored without comment, and loaded with a NULL one + */ + public function testNoComment(): void + { + $activity = new \GaletteEvents\Activity($this->zdb, $this->login); + $this->assertTrue($activity->check(['name' => 'Dinner', 'active' => '1'])); + $this->assertTrue($activity->store()); + + $update = $this->zdb->update(EVENTS_PREFIX . \GaletteEvents\Activity::TABLE); + $update->set(['comment' => null])->where([\GaletteEvents\Activity::PK => $activity->getId()]); + $this->zdb->execute($update); + + $activity = new \GaletteEvents\Activity($this->zdb, $this->login, (int)$activity->getId()); + $this->assertSame('Dinner', $activity->getName()); + $this->assertSame('', $activity->getComment()); + } } diff --git a/tests/GaletteEvents/tests/units/Booking.php b/tests/GaletteEvents/tests/units/Booking.php new file mode 100644 index 00000000..f2f7135c --- /dev/null +++ b/tests/GaletteEvents/tests/units/Booking.php @@ -0,0 +1,135 @@ +<?php + +/** + * This file is part of Galette Events plugin (https://galette.eu). + * SPDX-FileCopyrightText: Copyright © 2018-2026 The Galette Team + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace GaletteEvents\tests\units; + +use Galette\Tests\GaletteTestCase; +use GaletteEvents\tests\EventsFixtures; + +/** + * Booking entity tests + * + * @author Johan Cwiklinski <johan@x-tnd.be> + */ +class Booking extends GaletteTestCase +{ + use EventsFixtures; + + protected int $seed = 20260926151512; + + /** + * Cleanup after each test method + */ + public function tearDown(): void + { + $this->login->logout(); + $this->cleanEvents(); + parent::tearDown(); + } + + /** + * Optional values may be NULL in database + */ + public function testLoadNullValues(): void + { + $member_one = $this->getMemberOne(); + $id = $this->insertBooking( + $this->insertEvent('Event'), + $member_one->id, + ['comment' => null, 'payment_amount' => null, 'number_people' => null, 'creation_date' => '2026-09-01'] + ); + + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $this->assertSame('', $booking->getComment()); + $this->assertNull($booking->getAmount()); + $this->assertSame(1, $booking->getNumberPeople()); + $this->assertSame('2026-09-01', $booking->getCreationDate(false)); + } + + /** + * Activities chosen on a booking are stored and changed + */ + public function testActivitiesSync(): void + { + $this->logSuperAdmin(); + $member_one = $this->getMemberOne(); + $event = $this->insertEvent('Event'); + $dinner = $this->insertActivity('Dinner'); + $lodging = $this->insertActivity('Lodging'); + $this->linkActivity($event, $dinner); + $this->linkActivity($event, $lodging); + + $values = [ + 'event' => (string)$event, + 'member' => (string)$member_one->id, + 'booking_date' => date('Y-m-d'), + 'number_people' => '1', + ]; + $booking = new \GaletteEvents\Booking($this->zdb, $this->login); + $this->assertTrue($booking->check($values + ['activities' => [(string)$dinner]])); + $this->assertTrue($booking->store()); + $id = (int)$booking->getId(); + $this->assertSame([$dinner => true, $lodging => false], $this->getBookingActivities($id)); + + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $this->assertTrue($booking->check($values + ['activities' => [(string)$lodging]])); + $this->assertTrue($booking->store()); + $this->assertSame([$dinner => false, $lodging => true], $this->getBookingActivities($id)); + + //activity removed from event is removed from booking + $delete = $this->zdb->delete(EVENTS_PREFIX . 'activitiesevents'); + $delete->where([\GaletteEvents\Event::PK => $event, \GaletteEvents\Activity::PK => $dinner]); + $this->zdb->execute($delete); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $this->assertTrue($booking->check($values + ['activities' => [(string)$lodging]])); + $this->assertTrue($booking->store()); + $this->assertSame([$lodging => true], $this->getBookingActivities($id)); + } + + /** + * Amounts are cleared, use comma as decimal separator, and may be zero once paid + */ + public function testAmount(): void + { + $this->logSuperAdmin(); + $member_one = $this->getMemberOne(); + $values = [ + 'event' => (string)$this->insertEvent('Event'), + 'member' => (string)$member_one->id, + 'booking_date' => date('Y-m-d'), + 'number_people' => '1', + ]; + + $booking = new \GaletteEvents\Booking($this->zdb, $this->login); + $this->assertTrue($booking->check($values + ['amount' => '12,50'])); + $this->assertSame(12.5, $booking->getAmount()); + $this->assertTrue($booking->store()); + $id = (int)$booking->getId(); + + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $this->assertTrue($booking->check($values + ['amount' => ''])); + $this->assertTrue($booking->store()); + $booking = new \GaletteEvents\Booking($this->zdb, $this->login, $id); + $this->assertNull($booking->getAmount()); + + $this->assertTrue($booking->check($values + ['amount' => '0', 'paid' => '1'])); + $this->assertSame(0.0, $booking->getAmount()); + + $this->assertSame( + [_T('Please specify amount if booking has been paid ;)', 'events')], + $booking->check($values + ['amount' => '', 'paid' => '1']) + ); + $this->assertSame( + [_T('Amount must be a number.', 'events')], + $booking->check($values + ['amount' => 'ten']) + ); + $this->expectLogEntry(\Analog\Analog::ERROR, 'Some errors has been threw attempting to edit/store a booking'); + } +} diff --git a/tests/GaletteEvents/tests/units/Event.php b/tests/GaletteEvents/tests/units/Event.php new file mode 100644 index 00000000..e0ad3206 --- /dev/null +++ b/tests/GaletteEvents/tests/units/Event.php @@ -0,0 +1,219 @@ +<?php + +/** + * This file is part of Galette Events plugin (https://galette.eu). + * SPDX-FileCopyrightText: Copyright © 2018-2026 The Galette Team + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace GaletteEvents\tests\units; + +use Galette\Tests\GaletteTestCase; +use GaletteEvents\tests\EventsFixtures; + +/** + * Event entity tests + * + * @author Johan Cwiklinski <johan@x-tnd.be> + */ +class Event extends GaletteTestCase +{ + use EventsFixtures; + + protected int $seed = 20260926151512; + + /** + * Cleanup after each test method + */ + public function tearDown(): void + { + $this->login->logout(); + $this->cleanEvents(); + parent::tearDown(); + } + + /** + * Get values posted from event form + * + * @param array<string,mixed> $values Values to override + * + * @return array<string,mixed> + */ + private function getFormValues(array $values = []): array + { + return $values + [ + 'name' => 'Event', + 'address' => '', + 'zip' => '', + 'town' => 'Lille', + 'country' => '', + 'comment' => '', + 'color' => '', + 'begin_date' => date('Y-m-d', strtotime('+10 days')), + 'end_date' => date('Y-m-d', strtotime('+11 days')), + 'open' => '1', + 'save' => '1', + ]; + } + + /** + * Group managers create events of the groups they manage + */ + public function testManagerCreatesEvent(): void + { + $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $managed = $this->createGroup('Managed group', [$member_two]); + $other = $this->createGroup('Other group', [], [$member_two]); + $this->logMember($this->dataAdherentTwo()); + + $event = new \GaletteEvents\Event($this->zdb, $this->login); + $this->assertSame( + [_T('Please select a group you own!', 'events')], + $event->check($this->getFormValues(['group' => (string)$other->getId()])) + ); + $this->expectLogEntry(\Analog\Analog::ERROR, 'Some errors has been threw attempting to edit/store an event'); + + $event = new \GaletteEvents\Event($this->zdb, $this->login); + $this->assertTrue($event->check($this->getFormValues(['group' => (string)$managed->getId()]))); + $this->assertTrue($event->store()); + + $event = new \GaletteEvents\Event($this->zdb, $this->login, (int)$event->getId()); + $this->assertSame($managed->getId(), $event->getGroup()); + } + + /** + * Events are stored with mandatory values only + */ + public function testStoreMandatoryValuesOnly(): void + { + $this->logSuperAdmin(); + $event = new \GaletteEvents\Event($this->zdb, $this->login); + $this->assertTrue($event->check([ + 'name' => 'Event', + 'town' => 'Lille', + 'begin_date' => date('Y-m-d', strtotime('+10 days')), + ])); + $this->assertTrue($event->store()); + + $event = new \GaletteEvents\Event($this->zdb, $this->login, (int)$event->getId()); + $this->assertSame('Event', $event->getName()); + $this->assertSame('', $event->getAddress()); + $this->assertNull($event->getGroup()); + } + + /** + * Optional values may be NULL in database + */ + public function testLoadNullValues(): void + { + $id = $this->insertEvent('Event', ['comment' => null, 'country' => null]); + + $event = new \GaletteEvents\Event($this->zdb, $this->login, $id); + $this->assertSame('', $event->getComment()); + $this->assertNull($event->getGroup()); + $this->assertSame('', $event->getColor()); + } + + /** + * Activities linked to an event are added, changed and removed + */ + public function testActivitiesSync(): void + { + $this->logSuperAdmin(); + $dinner = $this->insertActivity('Dinner'); + $lodging = $this->insertActivity('Lodging'); + $visit = $this->insertActivity('Visit'); + $ids = array_map('strval', [$dinner, $lodging, $visit]); + + $event = new \GaletteEvents\Event($this->zdb, $this->login); + $this->assertTrue($event->check($this->getFormValues([ + 'activities_ids' => $ids, + 'activities_status' => ['1', '1', '2'], + ]))); + $this->assertTrue($event->store()); + $id = (int)$event->getId(); + $this->assertSame([$dinner => 1, $lodging => 1, $visit => 2], $this->getEventActivities($id)); + + //change status of activities that are not the last one + $event = new \GaletteEvents\Event($this->zdb, $this->login, $id); + $this->assertTrue($event->check($this->getFormValues([ + 'activities_ids' => $ids, + 'activities_status' => ['2', '0', '2'], + ]))); + $this->assertTrue($event->store()); + $this->assertSame([$dinner => 2, $lodging => 0, $visit => 2], $this->getEventActivities($id)); + + //remove two activities before storing + $event = new \GaletteEvents\Event($this->zdb, $this->login, $id); + $this->assertTrue($event->check($this->getFormValues([ + 'remove_activity' => '1', + 'detach_activity' => (string)$dinner, + 'activities_ids' => $ids, + 'activities_status' => ['2', '0', '2'], + ]))); + $this->assertTrue($event->check($this->getFormValues([ + 'remove_activity' => '1', + 'detach_activity' => (string)$lodging, + 'activities_ids' => [(string)$lodging, (string)$visit], + 'activities_status' => ['0', '2'], + ]))); + $this->assertTrue($event->store()); + $this->assertSame([$visit => 2], $this->getEventActivities($id)); + + //reloading does not keep activities of the previous event + $other = (int)$this->insertEvent('Other event'); + $this->assertTrue($event->load($other)); + $this->assertSame([], $event->getActivities()); + } + + /** + * Only active activities can be attached to events + */ + public function testInactiveActivities(): void + { + $this->logSuperAdmin(); + $dinner = $this->insertActivity('Dinner'); + $lodging = $this->insertActivity('Lodging'); + $update = $this->zdb->update(EVENTS_PREFIX . \GaletteEvents\Activity::TABLE); + $update->set(['is_active' => $this->zdb->isPostgres() ? 'false' : 0]) + ->where([\GaletteEvents\Activity::PK => $lodging]); + $this->zdb->execute($update); + + $event = new \GaletteEvents\Event($this->zdb, $this->login); + $this->assertSame( + [$dinner], + array_map(fn($row): int => (int)$row[\GaletteEvents\Activity::PK], $event->availableActivities()) + ); + + $this->assertTrue($event->check($this->getFormValues([ + 'add_activity' => '1', + 'attach_activity' => (string)$lodging, + ]))); + $this->assertSame([], $event->getActivities()); + } + + /** + * Edition rights are checked for given login, not for the one the event has been loaded with + */ + public function testCanEditChecksGivenLogin(): void + { + $member_one = $this->getMemberOne(); + $member_two = $this->getMemberTwo(); + $managed = $this->createGroup('Managed group', [$member_two]); + $other = $this->createGroup('Other group', [$member_one]); + $managed_event = $this->insertEvent('Managed event', ['id_group' => $managed->getId()]); + $other_event = $this->insertEvent('Other event', ['id_group' => $other->getId()]); + + //events are loaded with member one logged in + $this->logMember($this->dataAdherentOne()); + $manager = new \Galette\Core\Login($this->zdb, $this->i18n); + $this->assertTrue($manager->login($this->dataAdherentTwo()['login_adh'], $this->dataAdherentTwo()['mdp_adh'])); + + $this->assertTrue((new \GaletteEvents\Event($this->zdb, $this->login, $managed_event))->canEdit($manager)); + $this->assertFalse((new \GaletteEvents\Event($this->zdb, $this->login, $other_event))->canEdit($manager)); + $this->assertFalse((new \GaletteEvents\Event($this->zdb, $this->login, $this->insertEvent('Public event')))->canEdit($manager)); + } +}