diff --git a/.composer-require-checker.config.json b/.composer-require-checker.config.json deleted file mode 100644 index f03dabf..0000000 --- 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 31e1e5c..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,174 +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 - - ./bin/install_deps && popd - - nodes: - - analysis: - environment: - php: - version: 8.1 - - 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 6a9298c..3eafe92 100644 --- a/_routes.php +++ b/_routes.php @@ -20,6 +20,11 @@ [ActivitiesController::class, 'list'] )->setName('activities_activities')->add(Authenticate::class); +$app->post( + '/activities/filter', + [ActivitiesController::class, 'filter'] +)->setName('activities_filter-activitieslist')->add(Authenticate::class); + $app->get( '/activity/add', [ActivitiesController::class, 'add'] diff --git a/bin/release b/bin/release index 98da7f1..797cc4a 100755 --- a/bin/release +++ b/bin/release @@ -403,12 +403,10 @@ 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', 'composer.json.checker', 'phpstan.neon' ] diff --git a/lang/activities.pot b/lang/activities.pot index 2189f73..c229ec3 100644 --- a/lang/activities.pot +++ b/lang/activities.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-25 04:14+0200\n" +"POT-Creation-Date: 2026-09-27 07:07+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,81 +18,97 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: ../lib/GaletteActivities/Entity/Subscription.php:166 +#: ../lib/GaletteActivities/Entity/Subscription.php:172 msgid "Activity is mandatory" msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:195 +#. TRANS: %1$s is the activity ID +#: ../lib/GaletteActivities/Entity/Subscription.php:178 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:160 +#, php-format +msgid "No activity #%1$s." +msgstr "" + +#: ../lib/GaletteActivities/Entity/Subscription.php:200 +msgid "Amount must be a number." +msgstr "" + +#: ../lib/GaletteActivities/Entity/Subscription.php:220 msgid "Member is mandatory" msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:205 +#: ../lib/GaletteActivities/Entity/Subscription.php:236 msgid "Subscription date is mandatory" msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:211 +#: ../lib/GaletteActivities/Entity/Subscription.php:242 msgid "End date is mandatory" msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:267 +#: ../lib/GaletteActivities/Entity/Subscription.php:251 +msgid "End date must not be before subscription date." +msgstr "" + +#: ../lib/GaletteActivities/Entity/Subscription.php:287 +#: ../lib/GaletteActivities/Entity/Subscription.php:370 +msgid "Subscription already exists for this member and activity" +msgstr "" + +#: ../lib/GaletteActivities/Entity/Subscription.php:327 msgid "Subscription added" msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:277 +#: ../lib/GaletteActivities/Entity/Subscription.php:331 msgid "Fail to add new subscription." msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:296 +#: ../lib/GaletteActivities/Entity/Subscription.php:350 msgid "Subscription updated" msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:305 -msgid "Subscription already exists for this member and activity" -msgstr "" - -#: ../lib/GaletteActivities/Entity/Subscription.php:504 -#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:159 +#: ../lib/GaletteActivities/Entity/Subscription.php:606 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:170 #: ../templates/default/subscriptions.html.twig:39 #: ../templates/default/subscriptions.html.twig:76 #: ../templates/default/subscription.html.twig:46 msgid "Activity" msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:508 +#: ../lib/GaletteActivities/Entity/Subscription.php:610 msgid "Member" msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:512 +#: ../lib/GaletteActivities/Entity/Subscription.php:614 msgid "Is paid" msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:516 +#: ../lib/GaletteActivities/Entity/Subscription.php:618 #: ../templates/default/subscriptions.html.twig:39 -#: ../templates/default/subscriptions.html.twig:205 +#: ../templates/default/subscriptions.html.twig:201 #: ../templates/default/subscription.html.twig:75 msgid "Amount" msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:520 +#: ../lib/GaletteActivities/Entity/Subscription.php:622 msgid "Payment method" msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:524 -#: ../lib/GaletteActivities/Entity/Activity.php:386 -#: ../templates/default/subscription.html.twig:19 ../templates/default/activities.html.twig:36 -#: ../templates/default/activities.html.twig:65 +#: ../lib/GaletteActivities/Entity/Subscription.php:626 +#: ../lib/GaletteActivities/Entity/Activity.php:392 +#: ../templates/default/subscription.html.twig:19 ../templates/default/activities.html.twig:39 +#: ../templates/default/activities.html.twig:68 msgid "Creation date" msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:528 +#: ../lib/GaletteActivities/Entity/Subscription.php:630 #: ../templates/default/subscriptions.html.twig:39 -#: ../templates/default/subscriptions.html.twig:203 +#: ../templates/default/subscriptions.html.twig:199 #: ../templates/default/subscription.html.twig:26 msgid "Subscription date" msgstr "" -#: ../lib/GaletteActivities/Entity/Subscription.php:536 -#: ../lib/GaletteActivities/Entity/Activity.php:390 +#: ../lib/GaletteActivities/Entity/Subscription.php:638 +#: ../lib/GaletteActivities/Entity/Activity.php:396 #: ../templates/default/subscription.html.twig:61 msgid "Comment" msgstr "" @@ -101,95 +117,117 @@ msgstr "" msgid "Name is mandatory" msgstr "" -#: ../lib/GaletteActivities/Entity/Activity.php:167 +#: ../lib/GaletteActivities/Entity/Activity.php:162 +msgid "Name is too long" +msgstr "" + +#: ../lib/GaletteActivities/Entity/Activity.php:169 msgid "Type is too long" msgstr "" -#: ../lib/GaletteActivities/Entity/Activity.php:243 +#: ../lib/GaletteActivities/Entity/Activity.php:184 +msgid "Price must be a number." +msgstr "" + +#: ../lib/GaletteActivities/Entity/Activity.php:249 msgid "Activity added" msgstr "" -#: ../lib/GaletteActivities/Entity/Activity.php:248 +#: ../lib/GaletteActivities/Entity/Activity.php:254 msgid "Fail to add new activity." msgstr "" -#: ../lib/GaletteActivities/Entity/Activity.php:267 +#: ../lib/GaletteActivities/Entity/Activity.php:273 msgid "Activity updated" msgstr "" -#: ../lib/GaletteActivities/Entity/Activity.php:370 -#: ../templates/default/activities.html.twig:36 +#: ../lib/GaletteActivities/Entity/Activity.php:376 +#: ../templates/default/activities.html.twig:39 msgid "Name" msgstr "" -#: ../lib/GaletteActivities/Entity/Activity.php:374 -#: ../templates/default/activities.html.twig:36 ../templates/default/activities.html.twig:63 +#: ../lib/GaletteActivities/Entity/Activity.php:380 +#: ../templates/default/activities.html.twig:39 ../templates/default/activities.html.twig:66 msgid "Type" msgstr "" -#: ../lib/GaletteActivities/Entity/Activity.php:378 -#: ../templates/default/activities.html.twig:36 ../templates/default/activities.html.twig:64 +#: ../lib/GaletteActivities/Entity/Activity.php:384 +#: ../templates/default/activities.html.twig:39 ../templates/default/activities.html.twig:67 msgid "Price" msgstr "" -#: ../lib/GaletteActivities/Entity/Activity.php:382 -#: ../templates/default/activities.html.twig:36 ../templates/default/activities.html.twig:66 +#: ../lib/GaletteActivities/Entity/Activity.php:388 +#: ../templates/default/activities.html.twig:39 ../templates/default/activities.html.twig:69 msgid "Group" msgstr "" -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:115 +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:112 msgid "Subscriptions management" msgstr "" -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:231 -#: ../templates/default/subscriptions.html.twig:110 +#. TRANS: %1$s is the subscription ID +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:227 +#, php-format +msgid "No subscription #%1$s." +msgstr "" + +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:240 +#: ../templates/default/subscriptions.html.twig:109 msgid "Subscription" msgstr "" -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:329 +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:343 msgid "New subscription has been successfully added." msgstr "" -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:331 +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:345 msgid "Subscription has been modified." msgstr "" -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:339 +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:353 msgid "An error occurred while storing the subscription." msgstr "" -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:348 +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:362 msgid "Do not forget to store the subscription" msgstr "" #. TRANS: %1$s is the member name, %2$s the activity name. -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:445 +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:461 #, php-format msgid "Remove subscription for %1$s on %2$s" msgstr "" -#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:96 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:97 msgid "Activities management" msgstr "" -#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:220 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:236 msgid "New activity has been successfully added." msgstr "" -#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:222 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:238 msgid "Activity has been modified." msgstr "" -#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:226 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:242 msgid "An error occurred while storing the activity." msgstr "" #. TRANS %1$s is activity name -#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:305 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:321 #, php-format msgid "Remove activity %1$s" msgstr "" +#. TRANS: %1$s is the number of subscriptions +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:342 +#, php-format +msgid "%1$s subscription to this activity will be removed as well." +msgid_plural "%1$s subscriptions to this activity will be removed as well." +msgstr[0] "" +msgstr[1] "" + #: ../lib/GaletteActivities/PluginGaletteActivities.php:47 #: ../lib/GaletteActivities/PluginGaletteActivities.php:51 msgid "Activities" @@ -216,16 +254,16 @@ msgid "New subscription" msgstr "" #: ../templates/default/subscriptions.html.twig:39 -#: ../templates/default/subscriptions.html.twig:204 +#: ../templates/default/subscriptions.html.twig:200 #: ../templates/default/subscription.html.twig:33 msgid "End date" msgstr "" #: ../templates/default/subscriptions.html.twig:39 -#: ../templates/default/subscriptions.html.twig:147 +#: ../templates/default/subscriptions.html.twig:146 +#: ../templates/default/subscriptions.html.twig:202 +#: ../templates/default/subscriptions.html.twig:203 #: ../templates/default/subscriptions.html.twig:206 -#: ../templates/default/subscriptions.html.twig:207 -#: ../templates/default/subscriptions.html.twig:210 #: ../templates/default/subscription.html.twig:87 msgid "Paid" msgstr "" @@ -234,46 +272,46 @@ msgstr "" msgid "All activities" msgstr "" -#: ../templates/default/subscriptions.html.twig:90 +#: ../templates/default/subscriptions.html.twig:89 msgid "Search or pick a member" msgstr "" -#: ../templates/default/subscriptions.html.twig:98 +#: ../templates/default/subscriptions.html.twig:97 msgid "All payment types" msgstr "" -#: ../templates/default/subscriptions.html.twig:106 +#: ../templates/default/subscriptions.html.twig:105 msgid "Date type" msgstr "" -#: ../templates/default/subscriptions.html.twig:108 +#: ../templates/default/subscriptions.html.twig:107 msgid "Creation" msgstr "" -#: ../templates/default/subscriptions.html.twig:137 +#: ../templates/default/subscriptions.html.twig:136 msgid "Paid subscription:" msgstr "" -#: ../templates/default/subscriptions.html.twig:153 -#: ../templates/default/subscriptions.html.twig:207 -#: ../templates/default/subscriptions.html.twig:212 +#: ../templates/default/subscriptions.html.twig:152 +#: ../templates/default/subscriptions.html.twig:203 +#: ../templates/default/subscriptions.html.twig:208 msgid "Not paid" msgstr "" -#: ../templates/default/subscriptions.html.twig:178 +#: ../templates/default/subscriptions.html.twig:177 #, php-format msgid "Found subscriptions total %f" msgstr "" -#: ../templates/default/subscriptions.html.twig:219 +#: ../templates/default/subscriptions.html.twig:215 msgid "Edit subscription" msgstr "" -#: ../templates/default/subscriptions.html.twig:233 +#: ../templates/default/subscriptions.html.twig:229 msgid "Remove from database" msgstr "" -#: ../templates/default/subscriptions.html.twig:252 +#: ../templates/default/subscriptions.html.twig:248 msgid "No subscription has been found" msgstr "" @@ -308,20 +346,20 @@ msgid_plural "%1$s activities" msgstr[0] "" msgstr[1] "" -#: ../templates/default/activities.html.twig:31 +#: ../templates/default/activities.html.twig:34 msgid "New activity" msgstr "" -#: ../templates/default/activities.html.twig:68 +#: ../templates/default/activities.html.twig:71 #, php-format msgid "%1$s: edit information" msgstr "" -#: ../templates/default/activities.html.twig:73 +#: ../templates/default/activities.html.twig:76 #, php-format msgid "%1$s: remove from database" msgstr "" -#: ../templates/default/activities.html.twig:95 +#: ../templates/default/activities.html.twig:98 msgid "No activity has been found" msgstr "" diff --git a/lang/activities_en_US.po b/lang/activities_en_US.po index 7d5ba3d..94679cb 100644 --- a/lang/activities_en_US.po +++ b/lang/activities_en_US.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-09-25 04:14+0200\n" -"PO-Revision-Date: 2026-09-25 04:14+0200\n" +"POT-Creation-Date: 2026-09-27 07:07+0200\n" +"PO-Revision-Date: 2026-09-27 07:07+0200\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: en_US\n" @@ -17,82 +17,98 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: ../lib/GaletteActivities/Entity/Subscription.php:166 +#: ../lib/GaletteActivities/Entity/Subscription.php:172 msgid "Activity is mandatory" msgstr "Activity is mandatory" -#: ../lib/GaletteActivities/Entity/Subscription.php:195 +#. TRANS: %1$s is the activity ID +#: ../lib/GaletteActivities/Entity/Subscription.php:178 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:160 +#, php-format +msgid "No activity #%1$s." +msgstr "No activity #%1$s." + +#: ../lib/GaletteActivities/Entity/Subscription.php:200 +msgid "Amount must be a number." +msgstr "Amount must be a number." + +#: ../lib/GaletteActivities/Entity/Subscription.php:220 msgid "Member is mandatory" msgstr "Member is mandatory" -#: ../lib/GaletteActivities/Entity/Subscription.php:205 +#: ../lib/GaletteActivities/Entity/Subscription.php:236 msgid "Subscription date is mandatory" msgstr "Subscription date is mandatory" -#: ../lib/GaletteActivities/Entity/Subscription.php:211 +#: ../lib/GaletteActivities/Entity/Subscription.php:242 msgid "End date is mandatory" msgstr "End date is mandatory" -#: ../lib/GaletteActivities/Entity/Subscription.php:267 +#: ../lib/GaletteActivities/Entity/Subscription.php:251 +msgid "End date must not be before subscription date." +msgstr "End date must not be before subscription date." + +#: ../lib/GaletteActivities/Entity/Subscription.php:287 +#: ../lib/GaletteActivities/Entity/Subscription.php:370 +msgid "Subscription already exists for this member and activity" +msgstr "Subscription already exists for this member and activity" + +#: ../lib/GaletteActivities/Entity/Subscription.php:327 msgid "Subscription added" msgstr "Subscription added" -#: ../lib/GaletteActivities/Entity/Subscription.php:277 +#: ../lib/GaletteActivities/Entity/Subscription.php:331 msgid "Fail to add new subscription." msgstr "Fail to add new subscription." -#: ../lib/GaletteActivities/Entity/Subscription.php:296 +#: ../lib/GaletteActivities/Entity/Subscription.php:350 msgid "Subscription updated" msgstr "Subscription updated" -#: ../lib/GaletteActivities/Entity/Subscription.php:305 -msgid "Subscription already exists for this member and activity" -msgstr "Subscription already exists for this member and activity" - -#: ../lib/GaletteActivities/Entity/Subscription.php:504 -#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:159 +#: ../lib/GaletteActivities/Entity/Subscription.php:606 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:170 #: ../templates/default/subscriptions.html.twig:39 #: ../templates/default/subscriptions.html.twig:76 #: ../templates/default/subscription.html.twig:46 msgid "Activity" msgstr "Activity" -#: ../lib/GaletteActivities/Entity/Subscription.php:508 +#: ../lib/GaletteActivities/Entity/Subscription.php:610 msgid "Member" msgstr "Member" -#: ../lib/GaletteActivities/Entity/Subscription.php:512 +#: ../lib/GaletteActivities/Entity/Subscription.php:614 msgid "Is paid" msgstr "Is paid" -#: ../lib/GaletteActivities/Entity/Subscription.php:516 +#: ../lib/GaletteActivities/Entity/Subscription.php:618 #: ../templates/default/subscriptions.html.twig:39 -#: ../templates/default/subscriptions.html.twig:205 +#: ../templates/default/subscriptions.html.twig:201 #: ../templates/default/subscription.html.twig:75 msgid "Amount" msgstr "Amount" -#: ../lib/GaletteActivities/Entity/Subscription.php:520 +#: ../lib/GaletteActivities/Entity/Subscription.php:622 msgid "Payment method" msgstr "Payment method" -#: ../lib/GaletteActivities/Entity/Subscription.php:524 -#: ../lib/GaletteActivities/Entity/Activity.php:386 +#: ../lib/GaletteActivities/Entity/Subscription.php:626 +#: ../lib/GaletteActivities/Entity/Activity.php:392 #: ../templates/default/subscription.html.twig:19 -#: ../templates/default/activities.html.twig:36 -#: ../templates/default/activities.html.twig:65 +#: ../templates/default/activities.html.twig:39 +#: ../templates/default/activities.html.twig:68 msgid "Creation date" msgstr "Creation date" -#: ../lib/GaletteActivities/Entity/Subscription.php:528 +#: ../lib/GaletteActivities/Entity/Subscription.php:630 #: ../templates/default/subscriptions.html.twig:39 -#: ../templates/default/subscriptions.html.twig:203 +#: ../templates/default/subscriptions.html.twig:199 #: ../templates/default/subscription.html.twig:26 msgid "Subscription date" msgstr "Subscription date" -#: ../lib/GaletteActivities/Entity/Subscription.php:536 -#: ../lib/GaletteActivities/Entity/Activity.php:390 +#: ../lib/GaletteActivities/Entity/Subscription.php:638 +#: ../lib/GaletteActivities/Entity/Activity.php:396 #: ../templates/default/subscription.html.twig:61 msgid "Comment" msgstr "Comment" @@ -101,98 +117,120 @@ msgstr "Comment" msgid "Name is mandatory" msgstr "Name is mandatory" -#: ../lib/GaletteActivities/Entity/Activity.php:167 +#: ../lib/GaletteActivities/Entity/Activity.php:162 +msgid "Name is too long" +msgstr "Name is too long" + +#: ../lib/GaletteActivities/Entity/Activity.php:169 msgid "Type is too long" msgstr "Type is too long" -#: ../lib/GaletteActivities/Entity/Activity.php:243 +#: ../lib/GaletteActivities/Entity/Activity.php:184 +msgid "Price must be a number." +msgstr "Price must be a number." + +#: ../lib/GaletteActivities/Entity/Activity.php:249 msgid "Activity added" msgstr "Activity added" -#: ../lib/GaletteActivities/Entity/Activity.php:248 +#: ../lib/GaletteActivities/Entity/Activity.php:254 msgid "Fail to add new activity." msgstr "Fail to add new activity." -#: ../lib/GaletteActivities/Entity/Activity.php:267 +#: ../lib/GaletteActivities/Entity/Activity.php:273 msgid "Activity updated" msgstr "Activity updated" -#: ../lib/GaletteActivities/Entity/Activity.php:370 -#: ../templates/default/activities.html.twig:36 +#: ../lib/GaletteActivities/Entity/Activity.php:376 +#: ../templates/default/activities.html.twig:39 msgid "Name" msgstr "Name" -#: ../lib/GaletteActivities/Entity/Activity.php:374 -#: ../templates/default/activities.html.twig:36 -#: ../templates/default/activities.html.twig:63 +#: ../lib/GaletteActivities/Entity/Activity.php:380 +#: ../templates/default/activities.html.twig:39 +#: ../templates/default/activities.html.twig:66 msgid "Type" msgstr "Type" -#: ../lib/GaletteActivities/Entity/Activity.php:378 -#: ../templates/default/activities.html.twig:36 -#: ../templates/default/activities.html.twig:64 +#: ../lib/GaletteActivities/Entity/Activity.php:384 +#: ../templates/default/activities.html.twig:39 +#: ../templates/default/activities.html.twig:67 msgid "Price" msgstr "Price" -#: ../lib/GaletteActivities/Entity/Activity.php:382 -#: ../templates/default/activities.html.twig:36 -#: ../templates/default/activities.html.twig:66 +#: ../lib/GaletteActivities/Entity/Activity.php:388 +#: ../templates/default/activities.html.twig:39 +#: ../templates/default/activities.html.twig:69 msgid "Group" msgstr "Group" -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:115 +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:112 msgid "Subscriptions management" msgstr "Subscriptions management" -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:231 -#: ../templates/default/subscriptions.html.twig:110 +#. TRANS: %1$s is the subscription ID +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:227 +#, php-format +msgid "No subscription #%1$s." +msgstr "No subscription #%1$s." + +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:240 +#: ../templates/default/subscriptions.html.twig:109 msgid "Subscription" msgstr "Subscription" -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:329 +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:343 msgid "New subscription has been successfully added." msgstr "New subscription has been successfully added." -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:331 +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:345 msgid "Subscription has been modified." msgstr "Subscription has been modified." -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:339 +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:353 msgid "An error occurred while storing the subscription." msgstr "An error occurred while storing the subscription." -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:348 +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:362 msgid "Do not forget to store the subscription" msgstr "Do not forget to store the subscription" #. TRANS: %1$s is the member name, %2$s the activity name. -#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:445 +#: ../lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php:461 #, php-format msgid "Remove subscription for %1$s on %2$s" msgstr "Remove subscription for %1$s on %2$s" -#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:96 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:97 msgid "Activities management" msgstr "Activities management" -#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:220 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:236 msgid "New activity has been successfully added." msgstr "New activity has been successfully added." -#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:222 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:238 msgid "Activity has been modified." msgstr "Activity has been modified." -#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:226 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:242 msgid "An error occurred while storing the activity." msgstr "An error occurred while storing the activity." #. TRANS %1$s is activity name -#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:305 +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:321 #, php-format msgid "Remove activity %1$s" msgstr "Remove activity %1$s" +#. TRANS: %1$s is the number of subscriptions +#: ../lib/GaletteActivities/Controllers/Crud/ActivitiesController.php:342 +#, php-format +msgid "%1$s subscription to this activity will be removed as well." +msgid_plural "%1$s subscriptions to this activity will be removed as well." +msgstr[0] "%1$s subscription to this activity will be removed as well." +msgstr[1] "%1$s subscriptions to this activity will be removed as well." + #: ../lib/GaletteActivities/PluginGaletteActivities.php:47 #: ../lib/GaletteActivities/PluginGaletteActivities.php:51 msgid "Activities" @@ -219,16 +257,16 @@ msgid "New subscription" msgstr "New subscription" #: ../templates/default/subscriptions.html.twig:39 -#: ../templates/default/subscriptions.html.twig:204 +#: ../templates/default/subscriptions.html.twig:200 #: ../templates/default/subscription.html.twig:33 msgid "End date" msgstr "End date" #: ../templates/default/subscriptions.html.twig:39 -#: ../templates/default/subscriptions.html.twig:147 +#: ../templates/default/subscriptions.html.twig:146 +#: ../templates/default/subscriptions.html.twig:202 +#: ../templates/default/subscriptions.html.twig:203 #: ../templates/default/subscriptions.html.twig:206 -#: ../templates/default/subscriptions.html.twig:207 -#: ../templates/default/subscriptions.html.twig:210 #: ../templates/default/subscription.html.twig:87 msgid "Paid" msgstr "Paid" @@ -237,46 +275,46 @@ msgstr "Paid" msgid "All activities" msgstr "All activities" -#: ../templates/default/subscriptions.html.twig:90 +#: ../templates/default/subscriptions.html.twig:89 msgid "Search or pick a member" msgstr "Search or pick a member" -#: ../templates/default/subscriptions.html.twig:98 +#: ../templates/default/subscriptions.html.twig:97 msgid "All payment types" msgstr "All payment types" -#: ../templates/default/subscriptions.html.twig:106 +#: ../templates/default/subscriptions.html.twig:105 msgid "Date type" msgstr "Date type" -#: ../templates/default/subscriptions.html.twig:108 +#: ../templates/default/subscriptions.html.twig:107 msgid "Creation" msgstr "Creation" -#: ../templates/default/subscriptions.html.twig:137 +#: ../templates/default/subscriptions.html.twig:136 msgid "Paid subscription:" msgstr "Paid subscription:" -#: ../templates/default/subscriptions.html.twig:153 -#: ../templates/default/subscriptions.html.twig:207 -#: ../templates/default/subscriptions.html.twig:212 +#: ../templates/default/subscriptions.html.twig:152 +#: ../templates/default/subscriptions.html.twig:203 +#: ../templates/default/subscriptions.html.twig:208 msgid "Not paid" msgstr "Not paid" -#: ../templates/default/subscriptions.html.twig:178 +#: ../templates/default/subscriptions.html.twig:177 #, php-format msgid "Found subscriptions total %f" msgstr "Found subscriptions total %f" -#: ../templates/default/subscriptions.html.twig:219 +#: ../templates/default/subscriptions.html.twig:215 msgid "Edit subscription" msgstr "Edit subscription" -#: ../templates/default/subscriptions.html.twig:233 +#: ../templates/default/subscriptions.html.twig:229 msgid "Remove from database" msgstr "Remove from database" -#: ../templates/default/subscriptions.html.twig:252 +#: ../templates/default/subscriptions.html.twig:248 msgid "No subscription has been found" msgstr "No subscription has been found" @@ -311,20 +349,20 @@ msgid_plural "%1$s activities" msgstr[0] "%1$s activity" msgstr[1] "%1$s activities" -#: ../templates/default/activities.html.twig:31 +#: ../templates/default/activities.html.twig:34 msgid "New activity" msgstr "New activity" -#: ../templates/default/activities.html.twig:68 +#: ../templates/default/activities.html.twig:71 #, php-format msgid "%1$s: edit information" msgstr "%1$s: edit information" -#: ../templates/default/activities.html.twig:73 +#: ../templates/default/activities.html.twig:76 #, php-format msgid "%1$s: remove from database" msgstr "%1$s: remove from database" -#: ../templates/default/activities.html.twig:95 +#: ../templates/default/activities.html.twig:98 msgid "No activity has been found" msgstr "No activity has been found" diff --git a/lib/GaletteActivities/Controllers/Crud/ActivitiesController.php b/lib/GaletteActivities/Controllers/Crud/ActivitiesController.php index 6487dc1..d6be5a1 100644 --- a/lib/GaletteActivities/Controllers/Crud/ActivitiesController.php +++ b/lib/GaletteActivities/Controllers/Crud/ActivitiesController.php @@ -14,6 +14,7 @@ use Galette\Repository\Groups; use GaletteActivities\Filters\ActivitiesList; use GaletteActivities\Entity\Activity; +use GaletteActivities\Entity\Subscription; use GaletteActivities\Repository\Activities; use Slim\Psr7\Request; use Slim\Psr7\Response; @@ -129,7 +130,7 @@ public function filter(Request $request, Response $response): Response $this->session->$filter_name = $filters; return $response - ->withStatus(301) + ->withStatus(303) ->withHeader('Location', $this->routeparser->urlFor('activities_activities')); } @@ -144,15 +145,25 @@ 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; + $activity = $this->session->plugin_activities_activity ?? null; + if ($activity !== null) { + unset($this->session->plugin_activities_activity); } else { $activity = new Activity($this->zdb); } - if ($id !== null && $activity->getId() != $id) { - $activity->load($id); + if ($id !== null && $activity->getId() != $id && !$activity->load($id)) { + $this->flash->addMessage( + 'error_detected', + sprintf( + //TRANS: %1$s is the activity ID + _T('No activity #%1$s.', 'activities'), + $id + ) + ); + return $response + ->withStatus(302) + ->withHeader('Location', $this->routeparser->urlFor('activities_activities')); } // template variable declaration @@ -213,7 +224,12 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st if ($activity->getId() == '') { $new = true; } - $store = $activity->store(); + try { + $store = $activity->store(); + } catch (\Throwable) { + //already logged by the entity + $store = false; + } if ($store === true) { //member has been stored :) if ($new) { @@ -249,7 +265,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st $redirect_url = $this->routeparser->urlFor('activities_activities'); } else { //store entity in session - $this->session->activity = $activity; + $this->session->plugin_activities_activity = $activity; if ($activity->getId()) { $redirect_url = $this->routeparser->urlFor( @@ -262,7 +278,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st } return $response - ->withStatus(301) + ->withStatus(303) ->withHeader('Location', $redirect_url); } @@ -307,6 +323,34 @@ public function confirmRemoveTitle(array $args): string ); } + /** + * Removal confirmation parameters: subscriptions are removed with the activity + * + * @return array + */ + protected function getconfirmDeleteParams(Request $request): array + { + $params = parent::getconfirmDeleteParams($request); + + $select = $this->zdb->select(ACTIVITIES_PREFIX . Subscription::TABLE); + $select->where([Activity::PK => (int)$params['data']['id']]); + $count = $this->zdb->execute($select)->count(); + if ($count > 0) { + $params['message'] = sprintf( + _Tn( + //TRANS: %1$s is the number of subscriptions + '%1$s subscription to this activity will be removed as well.', + '%1$s subscriptions to this activity will be removed as well.', + $count, + 'activities' + ), + $count + ); + } + + return $params; + } + /** * Remove object * @@ -327,6 +371,6 @@ protected function doDelete(array $args, array $post): bool */ public static function getDefaultFilterName(): string { - return 'activities'; + return 'plugin_activities_activities'; } } diff --git a/lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php b/lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php index caf83d7..6dfcb72 100644 --- a/lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php +++ b/lib/GaletteActivities/Controllers/Crud/SubscriptionsController.php @@ -77,9 +77,6 @@ public function list(Request $request, Response $response, ?string $option = nul case 'order': $filters->orderby = $value; break; - case 'clear_filter': - $filters->reinit(); - break; } } @@ -170,7 +167,7 @@ public function filter(Request $request, Response $response): Response } if (isset($post['member_filter'])) { - if (is_numeric($post['member_filter'])) { + if ($post['member_filter'] === '' || is_numeric($post['member_filter'])) { $filters->member_filter = $post['member_filter']; } } @@ -193,7 +190,7 @@ public function filter(Request $request, Response $response): Response $this->session->{$this->getFilterName($this->getDefaultFilterName())} = $filters; return $response - ->withStatus(301) + ->withStatus(303) ->withHeader( 'Location', $this->routeparser->urlFor('activities_subscriptions') @@ -214,15 +211,27 @@ public function edit(Request $request, Response $response, ?int $id = null, stri { $route_params = []; - if ($this->session->subscription !== null) { - $subscription = $this->session->subscription; - $this->session->subscription = null; + $subscription = $this->session->plugin_activities_subscription ?? null; + if ($subscription !== null) { + unset($this->session->plugin_activities_subscription); } else { $subscription = new Subscription($this->zdb); } if ($id !== null && $subscription->getId() != $id) { - $subscription->load($id); + if (!$subscription->load($id)) { + $this->flash->addMessage( + 'error_detected', + sprintf( + //TRANS: %1$s is the subscription ID + _T('No subscription #%1$s.', 'activities'), + $id + ) + ); + return $response + ->withStatus(302) + ->withHeader('Location', $this->routeparser->urlFor('activities_subscriptions')); + } } elseif ($id_adh !== null) { $subscription->setMember($id_adh); } @@ -300,7 +309,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st 'activities_subscriptions' ); return $response - ->withStatus(301) + ->withStatus(303) ->withHeader('Location', $redirect_url); } @@ -322,7 +331,12 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st if ($subscription->getId() == '') { $new = true; } - $store = $subscription->store(); + try { + $store = $subscription->store(); + } catch (\Throwable) { + //already logged by the entity + $store = false; + } if ($store === true) { //member has been stored :) if ($new) { @@ -330,7 +344,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st } else { $success_detected[] = _T("Subscription has been modified.", "activities"); } - } elseif ($store === false) { + } else { //something went wrong :'( $errors = $subscription->getErrors(); if (count($errors)) { @@ -342,7 +356,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st } if (!isset($post['save'])) { - $this->session->subscription = $subscription; + $this->session->plugin_activities_subscription = $subscription; $error_detected = []; $goto_list = false; $warning_detected[] = _T('Do not forget to store the subscription', 'activities'); @@ -375,13 +389,15 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st } if (count($error_detected) == 0 && $goto_list) { - $redirect_url = $this->routeparser->urlFor( - 'activities_subscriptions', - ['activity' => (string)$subscription->getActivityId()] - ); + //show subscriptions of the stored activity + $filter_name = $this->getFilterName($this->getDefaultFilterName()); + $filters = $this->session->$filter_name ?? new SubscriptionsList(); + $filters->activity_filter = $subscription->getActivityId(); + $this->session->$filter_name = $filters; + $redirect_url = $this->routeparser->urlFor('activities_subscriptions'); } else { //store entity in session - $this->session->subscription = $subscription; + $this->session->plugin_activities_subscription = $subscription; if ($subscription->getId()) { $route = 'activities_subscription_edit'; @@ -400,7 +416,7 @@ public function doEdit(Request $request, Response $response, ?int $id = null, st } return $response - ->withStatus(301) + ->withStatus(303) ->withHeader('Location', $redirect_url); } @@ -456,7 +472,7 @@ public function confirmRemoveTitle(array $args): string */ protected function doDelete(array $args, array $post): bool { - $subscription = new Subscription($this->zdb, (int)$post['id']); + $subscription = new Subscription($this->zdb, (int)$args['id']); return $subscription->remove(); } @@ -468,6 +484,6 @@ protected function doDelete(array $args, array $post): bool */ public static function getDefaultFilterName(): string { - return 'subscriptions'; + return 'plugin_activities_subscriptions'; } } diff --git a/lib/GaletteActivities/Entity/Activity.php b/lib/GaletteActivities/Entity/Activity.php index c943e40..5d472e5 100644 --- a/lib/GaletteActivities/Entity/Activity.php +++ b/lib/GaletteActivities/Entity/Activity.php @@ -158,12 +158,14 @@ public function check(array $values): bool if (empty($values['name'])) { $this->errors[] = _T('Name is mandatory', 'activities'); + } elseif (mb_strlen($values['name']) > 150) { + $this->errors[] = _T('Name is too long', 'activities'); } else { $this->name = $values['name']; } if (isset($values['type']) && !empty($values['type'])) { - if (strlen($values['type']) > 3) { + if (mb_strlen($values['type']) > 3) { $this->errors[] = _T('Type is too long', 'activities'); } else { $this->type = $values['type']; @@ -172,10 +174,14 @@ public function check(array $values): bool $this->type = ''; } - if (isset($values['price']) && !empty($values['price'])) { - $this->price = (float)$values['price']; - } else { + //accept comma as decimal separator + $price = strtr(trim((string)($values['price'] ?? '')), ',', '.'); + if ($price === '') { $this->price = null; + } elseif (is_numeric($price)) { + $this->price = (float)$price; + } else { + $this->errors[] = _T('Price must be a number.', 'activities'); } if (isset($values['id_group']) && !empty($values['id_group'])) { diff --git a/lib/GaletteActivities/Entity/Subscription.php b/lib/GaletteActivities/Entity/Subscription.php index ea4afcf..6a91139 100644 --- a/lib/GaletteActivities/Entity/Subscription.php +++ b/lib/GaletteActivities/Entity/Subscription.php @@ -13,6 +13,7 @@ use ArrayObject; use Galette\Core\Db; use Galette\Entity\Adherent; +use Galette\Entity\Group; use Galette\Entity\PaymentType; use Analog\Analog; use Galette\Helpers\EntityHelper; @@ -38,6 +39,9 @@ class Subscription private ?Activity $activity = null; private int $id_member; private ?Adherent $member = null; + //activity and member as stored in database + private ?int $stored_activity = null; + private ?int $stored_member = null; private bool $paid = true; private ?float $payment_amount = null; private int $payment_method = PaymentType::OTHER; @@ -105,6 +109,8 @@ private function loadFromRS(ArrayObject $r): void $this->id = (int)$r->id_subscription; $this->setActivity((int)$r->{Activity::PK}); $this->setMember((int)$r->{Adherent::PK}); + $this->stored_activity = $this->id_activity; + $this->stored_member = $this->id_member; $this->paid = (bool)$r->is_paid; if ($r->payment_amount !== null) { $this->payment_amount = (float)$r->payment_amount; @@ -166,6 +172,13 @@ public function check(array $values): bool $this->errors[] = _T('Activity is mandatory', 'activities'); } else { $this->setActivity((int)$values['activity']); + if ($this->activity?->getId() === null) { + $this->errors[] = sprintf( + //TRANS: %1$s is the activity ID + _T('No activity #%1$s.', 'activities'), + (int)$values['activity'] + ); + } } //financial information @@ -175,12 +188,24 @@ public function check(array $values): bool $this->paid = false; } - if (isset($values['payment_amount']) && !empty($values['payment_amount'])) { - $this->payment_amount = (float)$values['payment_amount']; - } else { - if ($this->getActivity() && isset($values['save'])) { - $this->payment_amount = $this->getActivity()->getPrice(); + $amount = null; + if (isset($values['payment_amount'])) { + //accept comma as decimal separator + $amount = strtr(trim((string)$values['payment_amount']), ',', '.'); + } + if ($amount !== null && $amount !== '') { + if (is_numeric($amount)) { + $this->payment_amount = (float)$amount; + } else { + $this->errors[] = _T('Amount must be a number.', 'activities'); } + } elseif ($amount === null || empty($this->id)) { + //new subscriptions default to activity price; existing ones can be cleared + if (isset($values['save']) && $this->activity !== null) { + $this->payment_amount = $this->activity->getPrice(); + } + } else { + $this->payment_amount = null; } if (isset($values['creation_date']) && !empty($values['creation_date'])) { @@ -193,6 +218,11 @@ public function check(array $values): bool if (!isset($values['member']) || empty($values['member'])) { $this->errors[] = _T('Member is mandatory', 'activities'); + } elseif (!$this->memberExists((int)$values['member'])) { + $this->errors[] = sprintf( + _T('No member #%1$s.'), + (int)$values['member'] + ); } else { $this->setMember((int)$values['member']); } @@ -201,6 +231,7 @@ public function check(array $values): bool $this->comment = $values['comment']; } + $errors_count = count($this->errors); if (!isset($values['subscription_date']) || empty($values['subscription_date'])) { $this->errors[] = _T('Subscription date is mandatory', 'activities'); } else { @@ -213,6 +244,13 @@ public function check(array $values): bool $this->setDate('end_date', $values['end_date']); } + if ( + count($this->errors) === $errors_count + && $this->end_date < $this->subscription_date + ) { + $this->errors[] = _T('End date must not be before subscription date.', 'activities'); + } + if (count($this->errors) > 0) { Analog::log( 'Some errors has been threw attempting to edit/store a subscription' . "\n" @@ -225,6 +263,18 @@ public function check(array $values): bool return true; } + /** + * Does member exist? + * + * @param int $id Member ID + */ + private function memberExists(int $id): bool + { + $select = $this->zdb->select(Adherent::TABLE); + $select->columns([Adherent::PK])->where([Adherent::PK => $id]); + return $this->zdb->execute($select)->count() > 0; + } + /** * Store the subscription */ @@ -232,8 +282,18 @@ public function store(): bool { global $hist; + if ($this->isDuplicate()) { + //checked before any query: on PostgreSQL, a failing query aborts the whole transaction + $this->errors[] = _T('Subscription already exists for this member and activity', 'activities'); + return false; + } + + $transaction = false; try { - $this->zdb->connection->beginTransaction(); + if (!$this->zdb->connection->inTransaction()) { + $this->zdb->connection->beginTransaction(); + $transaction = true; + } $values = [ Activity::PK => $this->id_activity, Adherent::PK => $this->id_member, @@ -267,12 +327,6 @@ public function store(): bool _T("Subscription added", "activities"), $this->getActivity()->getName() ); - - //link member to activity group, if any - $group = $this->activity->getGroup(); - if ($group !== null) { - $group->addMember($this->member); - } } else { $hist->add(_T("Fail to add new subscription.", "activities")); throw new \Exception( @@ -298,14 +352,27 @@ public function store(): bool } } - $this->zdb->connection->commit(); + //members join the activity group when they subscribe, or change activity; they never leave it + if ($this->id_activity !== $this->stored_activity || $this->id_member !== $this->stored_member) { + $this->joinActivityGroup(); + } + + if ($transaction) { + $this->zdb->connection->commit(); + } + $this->stored_activity = $this->id_activity; + $this->stored_member = $this->id_member; return true; } catch (\OverflowException $e) { - $this->zdb->connection->rollBack(); + if ($transaction) { + $this->zdb->connection->rollBack(); + } $this->errors[] = _T('Subscription already exists for this member and activity', 'activities'); return false; } catch (\Exception $e) { - $this->zdb->connection->rollBack(); + if ($transaction) { + $this->zdb->connection->rollBack(); + } Analog::log( 'Something went wrong :\'( | ' . $e->getMessage() . "\n" . $e->getTraceAsString(), @@ -315,6 +382,43 @@ public function store(): bool } } + /** + * Does another subscription exist for the same member and activity? + */ + private function isDuplicate(): bool + { + $select = $this->zdb->select($this->getTableName()); + $select->where([ + Activity::PK => $this->id_activity, + Adherent::PK => $this->id_member + ]); + if (!empty($this->id)) { + $select->where->notEqualTo(self::PK, $this->id); + } + return $this->zdb->execute($select)->count() > 0; + } + + /** + * Add member to the activity group, if any and if not already in + */ + private function joinActivityGroup(): void + { + $group = $this->getActivity()?->getGroup(); + if ($group === null) { + return; + } + + $select = $this->zdb->select(Group::GROUPSUSERS_TABLE); + $select->where([ + Group::PK => $group->getId(), + Adherent::PK => $this->id_member + ]); + //checked before inserting: on PostgreSQL, a duplicate entry aborts the whole transaction + if ($this->zdb->execute($select)->count() === 0) { + $group->addMember($this->getMember()); + } + } + /** * Get activity id */ @@ -479,11 +583,9 @@ public function getComment(): string /** * Get row class related to current subscription status * - * @param bool $public we want the class for public pages - * * @return string the class to apply */ - public function getRowClass(bool $public = false): string + public function getRowClass(): string { $strclass = 'subscription-' . ($this->isPaid() ? 'paid' : 'notpaid'); diff --git a/lib/GaletteActivities/Filters/SubscriptionsList.php b/lib/GaletteActivities/Filters/SubscriptionsList.php index efeaf31..8bd38d6 100644 --- a/lib/GaletteActivities/Filters/SubscriptionsList.php +++ b/lib/GaletteActivities/Filters/SubscriptionsList.php @@ -183,9 +183,12 @@ public function __set(string $name, mixed $value): void ); } break; - case 'payment_type_filter': case 'activity_filter': case 'member_filter': + //empty means no filter + $this->$name = ($value === null || $value === '') ? null : (int)$value; + break; + case 'payment_type_filter': case 'date_field': $this->$name = (int)$value; break; diff --git a/lib/GaletteActivities/Repository/Subscriptions.php b/lib/GaletteActivities/Repository/Subscriptions.php index d8522f6..04eb15b 100644 --- a/lib/GaletteActivities/Repository/Subscriptions.php +++ b/lib/GaletteActivities/Repository/Subscriptions.php @@ -35,8 +35,8 @@ class Subscriptions public const int ORDERBY_MEMBER = 1; public const int ORDERBY_SUBSCRIPTIONDATE = 2; public const int ORDERBY_ENDDATE = 3; - public const int ORDERBY_PAID = 3; public const int ORDERBY_AMOUNT = 4; + public const int ORDERBY_PAID = 5; public const int FILTER_DC_PAID = 0; public const int FILTER_PAID = 1; @@ -106,7 +106,8 @@ public function getList(bool $full = false): array private function buildSelect(?array $fields, bool $count = false): Select { try { - $fieldsList = ['*']; + $fieldsList = [Subscription::PK, Activity::PK, Adherent::PK, 'is_paid', 'payment_amount', + 'payment_method', 'creation_date', 'subscription_date', 'end_date', 'comment']; if (is_array($fields) && count($fields)) { $fieldsList = $fields; } @@ -114,13 +115,16 @@ private function buildSelect(?array $fields, bool $count = false): Select $select = $this->zdb->select(ACTIVITIES_PREFIX . Subscription::TABLE, 's'); $select->columns($fieldsList); + //joined tables are used for filtering and ordering only, their columns would override subscriptions ones $select->join( ['a' => PREFIX_DB . Adherent::TABLE], - 's.' . Adherent::PK . '= a.' . Adherent::PK + 's.' . Adherent::PK . '= a.' . Adherent::PK, + [] ); $select->join( ['ac' => PREFIX_DB . ACTIVITIES_PREFIX . Activity::TABLE], - 's.' . Activity::PK . '= ac.' . Activity::PK + 's.' . Activity::PK . '= ac.' . Activity::PK, + [] ); $this->buildWhereClause($select); @@ -167,7 +171,7 @@ private function calculateSum(Select $select): void $sumSelect->reset($sumSelect::ORDER); $sumSelect->columns( [ - 'sum' => new Expression('SUM(payment_amount)') + 'sum' => new Expression('SUM(s.payment_amount)') ] ); @@ -194,10 +198,10 @@ private function buildWhereClause(Select $select): void try { switch ($this->filters->paid_filter) { case self::FILTER_PAID: - $select->where('is_paid = true'); + $select->where(['s.is_paid' => $this->zdb->isPostgres() ? 'true' : 1]); break; case self::FILTER_NOT_PAID: - $select->where('is_paid = false'); + $select->where(['s.is_paid' => $this->zdb->isPostgres() ? 'false' : 0]); break; case self::FILTER_DC_PAID: //nothing to do here. @@ -213,7 +217,7 @@ private function buildWhereClause(Select $select): void if ($this->filters->payment_type_filter != -1) { $select->where->equalTo( - 'payment_method', + 's.payment_method', $this->filters->payment_type_filter ); } @@ -230,11 +234,11 @@ private function buildWhereClause(Select $select): void $field = 's.creation_date'; break; case SubscriptionsList::DATE_SUBSCRIPTION: - $field = 'subscription_date'; + $field = 's.subscription_date'; break; case SubscriptionsList::DATE_END: default: - $field = 'end_date'; + $field = 's.end_date'; break; } @@ -255,7 +259,7 @@ private function buildWhereClause(Select $select): void } if (count($this->filters->selected)) { - $select->where([Subscription::PK => $this->filters->selected]); + $select->where(['s.' . Subscription::PK => $this->filters->selected]); } } catch (\Exception $e) { Analog::log( @@ -315,23 +319,23 @@ private function buildOrderClause(?array $fields = null): array break; case self::ORDERBY_SUBSCRIPTIONDATE: if ($this->canOrderBy('subscription_date', $fields)) { - $order[] = 'subscription_date ' . $this->filters->getDirection(); + $order[] = 's.subscription_date ' . $this->filters->getDirection(); } break; case self::ORDERBY_ENDDATE: if ($this->canOrderBy('end_date', $fields)) { - $order[] = 'end_date ' . $this->filters->getDirection(); + $order[] = 's.end_date ' . $this->filters->getDirection(); } break; case self::ORDERBY_PAID: - if ($this->canOrderBy('id_paid', $fields)) { - $order[] = 'is_paid ' . $this->filters->getDirection(); + if ($this->canOrderBy('is_paid', $fields)) { + $order[] = 's.is_paid ' . $this->filters->getDirection(); } break; case self::ORDERBY_AMOUNT: if ($this->canOrderBy('payment_amount', $fields)) { - $order[] = 'payment_amount ' . $this->filters->getDirection(); + $order[] = 's.payment_amount ' . $this->filters->getDirection(); } break; } diff --git a/templates/default/activities.html.twig b/templates/default/activities.html.twig index 78d124a..0c5f32a 100644 --- a/templates/default/activities.html.twig +++ b/templates/default/activities.html.twig @@ -17,7 +17,10 @@ {% block infoline %} {% set infoline = { - 'label': _Tn("%1$s activity", "%1$s activities", nb, "activities")|replace({"%1$s": nb}) + 'label': _Tn("%1$s activity", "%1$s activities", nb, "activities")|replace({"%1$s": nb}), + 'route': { + 'name': 'activities_filter-activitieslist' + } } %} {{ parent() }} {% endblock %} diff --git a/templates/default/activity.html.twig b/templates/default/activity.html.twig index c5469e6..dff8e6c 100644 --- a/templates/default/activity.html.twig +++ b/templates/default/activity.html.twig @@ -66,23 +66,3 @@ {% endblock %} - -{% block javascripts %} - {{ parent() }} - -{% endblock %} diff --git a/templates/default/subscription.html.twig b/templates/default/subscription.html.twig index c14f7f5..25143b1 100644 --- a/templates/default/subscription.html.twig +++ b/templates/default/subscription.html.twig @@ -105,11 +105,9 @@ {% block javascripts %} {% endblock %} diff --git a/templates/default/subscriptions.html.twig b/templates/default/subscriptions.html.twig index 3678100..f47adf3 100644 --- a/templates/default/subscriptions.html.twig +++ b/templates/default/subscriptions.html.twig @@ -83,7 +83,6 @@
{% include 'components/forms/member_dropdown.html.twig' with { - 'required': true, 'component_id': 'member_filter_elt', 'id': 'member_filter', 'label': _T("Member"), @@ -190,9 +189,6 @@ {% set bid = subscription.getId() %} - - - {{ subscription.getActivity().getName() }} @@ -209,13 +205,13 @@ {% if subscription.isPaid() %} {{ _T("Paid", "activities") }} {% else %} - {{ _T("Not paid", "activities") }}" + {{ _T("Not paid", "activities") }} {% endif %} {% set actions = [] %} - {% if login.isAdmin() or login.isStaff() or (login.isGroupManager() and subscription.getActivity().getGroup() in login.managed_groups) %} + {% if login.isAdmin() or login.isStaff() %} {% set actions = (actions ?? [])|merge([ { 'label': _T("Edit subscription", "activities"), diff --git a/tests/ActivitiesFixtures.php b/tests/ActivitiesFixtures.php index ad99da0..f625ce8 100644 --- a/tests/ActivitiesFixtures.php +++ b/tests/ActivitiesFixtures.php @@ -66,15 +66,16 @@ protected function createGroup(string $name, array $managers = [], array $member /** * Insert an activity * - * @param string $name Activity name - * @param int|null $id_group Group ID + * @param string $name Activity name + * @param int|null $id_group Group ID + * @param array $data Values to override * * @return int Activity ID */ - protected function insertActivity(string $name, ?int $id_group = null): int + protected function insertActivity(string $name, ?int $id_group = null, array $data = []): int { $insert = $this->zdb->insert(ACTIVITIES_PREFIX . Activity::TABLE); - $insert->values([ + $insert->values($data + [ 'name' => $name, 'price' => 10, Group::PK => $id_group, @@ -91,24 +92,31 @@ protected function insertActivity(string $name, ?int $id_group = null): int /** * Insert a subscription * - * @param int $activity Activity ID - * @param int $member Member ID + * @param int $activity Activity ID + * @param int $member Member ID + * @param array $data Values to override * * @return int Subscription ID */ - protected function insertSubscription(int $activity, int $member): int + protected function insertSubscription(int $activity, int $member, array $data = []): int { - $insert = $this->zdb->insert(ACTIVITIES_PREFIX . Subscription::TABLE); - $insert->values([ + $values = $data + [ Activity::PK => $activity, Adherent::PK => $member, - 'is_paid' => $this->zdb->isPostgres() ? 'false' : 0, + 'is_paid' => false, 'payment_method' => 0, 'creation_date' => date('Y-m-d'), 'subscription_date' => date('Y-m-d'), 'end_date' => date('Y-m-d', strtotime('+1 year')), 'comment' => '', - ]); + ]; + if ($this->zdb->isPostgres()) { + $values['is_paid'] = $values['is_paid'] ? 'true' : 'false'; + } else { + $values['is_paid'] = (int)$values['is_paid']; + } + $insert = $this->zdb->insert(ACTIVITIES_PREFIX . Subscription::TABLE); + $insert->values($values); $this->zdb->execute($insert); $select = $this->zdb->select(ACTIVITIES_PREFIX . Subscription::TABLE); diff --git a/tests/GaletteActivities/Controllers/Crud/tests/units/ActivitiesController.php b/tests/GaletteActivities/Controllers/Crud/tests/units/ActivitiesController.php new file mode 100644 index 0000000..d039b62 --- /dev/null +++ b/tests/GaletteActivities/Controllers/Crud/tests/units/ActivitiesController.php @@ -0,0 +1,163 @@ + + */ +class ActivitiesController extends GaletteRoutingTestCase +{ + use ActivitiesFixtures; + + protected int $seed = 20260926194512; + protected bool $load_plugins = true; + + /** + * Cleanup after each test method + */ + public function tearDown(): void + { + $this->login->logout(); + $this->cleanActivities(); + parent::tearDown(); + } + + /** + * Unknown activities are not edited + */ + public function testEditUnknownActivity(): void + { + $this->logSuperAdmin(); + $id = $this->insertActivity('Climbing') + 1000; + + $test_response = $this->app->handle($this->createRequest('activities_activity_edit', ['id' => (string)$id])); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('activities_activities')]], + $test_response->getHeaders() + ); + $this->assertSame(302, $test_response->getStatusCode()); + $this->expectNoLogEntry(); + $this->expectFlashData(['error_detected' => ['No activity #' . $id . '.']]); + } + + /** + * Storage errors are reported, not thrown + */ + public function testStoreError(): void + { + $this->logSuperAdmin(); + + $request = $this->createRequest('activities_storeactivity_add', [], 'POST') + ->withParsedBody(['name' => 'Climbing', 'id_group' => '999999', 'comment' => '']); + //on PostgreSQL, the failing query aborts the test transaction + $this->zdb->db->query('SAVEPOINT store_error', \Laminas\Db\Adapter\Adapter::QUERY_MODE_EXECUTE); + $test_response = $this->app->handle($request); + $this->zdb->db->query('ROLLBACK TO SAVEPOINT store_error', \Laminas\Db\Adapter\Adapter::QUERY_MODE_EXECUTE); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('activities_activity_add')]], + $test_response->getHeaders() + ); + $this->expectLogEntry(\Analog\Analog::ERROR, 'Query error'); + $this->expectLogEntry(\Analog\Analog::ERROR, 'Something went wrong'); + $this->expectNoLogEntry(); + $this->expectFlashData(['error_detected' => ['An error occurred while storing the activity.']]); + } + + /** + * Stored activities lead to the list + */ + public function testStoreActivity(): void + { + $this->logSuperAdmin(); + + $request = $this->createRequest('activities_storeactivity_add', [], 'POST') + ->withParsedBody(['name' => 'Climbing', 'price' => '12,50', 'comment' => '']); + $test_response = $this->app->handle($request); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('activities_activities')]], + $test_response->getHeaders() + ); + $this->assertSame(303, $test_response->getStatusCode()); + $this->expectNoLogEntry(); + $this->expectFlashData(['success_detected' => ['New activity has been successfully added.']]); + } + + /** + * Number of activities per page can be chosen + */ + public function testFilter(): void + { + $this->logSuperAdmin(); + + $request = $this->createRequest('activities_filter-activitieslist', [], 'POST') + ->withParsedBody(['nbshow' => '20']); + $test_response = $this->app->handle($request); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('activities_activities')]], + $test_response->getHeaders() + ); + $this->assertSame(303, $test_response->getStatusCode()); + $this->expectNoLogEntry(); + + $test_response = $this->app->handle($this->createRequest('activities_activities')); + $this->assertSame(200, $test_response->getStatusCode()); + $this->assertStringContainsString('name="nbshow" value="20"', (string)$test_response->getBody()); + $this->expectNoLogEntry(); + } + + /** + * Session keys do not collide with other plugins ones + */ + public function testSessionKeysArePrefixed(): void + { + $this->logSuperAdmin(); + //plugin-events stores its own entity and filters under these keys + $this->session->activity = new \stdClass(); + $this->session->activities_filter = new \stdClass(); + $this->session->subscription = new \stdClass(); + + //list first: pagination adds Twig globals, impossible once a page has been rendered + foreach (['activities_activities', 'activities_activity_add', 'activities_subscription_add'] as $route) { + $test_response = $this->app->handle($this->createRequest($route)); + $this->assertSame(200, $test_response->getStatusCode(), $route); + $this->expectNoLogEntry(); + } + unset($this->session->activity, $this->session->activities_filter, $this->session->subscription); + } + + /** + * Removal confirmation announces subscriptions removed with the activity + */ + public function testConfirmRemovalCountsSubscriptions(): void + { + $this->logSuperAdmin(); + $activity = $this->insertActivity('Climbing'); + + $body = (string)$this->app->handle( + $this->createRequest('activities_remove_activity', ['id' => (string)$activity]) + )->getBody(); + $this->assertStringNotContainsString('will be removed as well', $body); + $this->expectNoLogEntry(); + + $this->insertSubscription($activity, $this->getMemberOne()->id); + $this->insertSubscription($activity, $this->getMemberTwo()->id); + $body = (string)$this->app->handle( + $this->createRequest('activities_remove_activity', ['id' => (string)$activity]) + )->getBody(); + $this->assertStringContainsString('2 subscriptions to this activity will be removed as well.', $body); + $this->expectNoLogEntry(); + } +} diff --git a/tests/GaletteActivities/Controllers/Crud/tests/units/SubscriptionsController.php b/tests/GaletteActivities/Controllers/Crud/tests/units/SubscriptionsController.php index 553af88..2a38390 100644 --- a/tests/GaletteActivities/Controllers/Crud/tests/units/SubscriptionsController.php +++ b/tests/GaletteActivities/Controllers/Crud/tests/units/SubscriptionsController.php @@ -36,6 +36,15 @@ public function tearDown(): void parent::tearDown(); } + /** + * Get subscriptions list filters from session + */ + private function getSubscriptionsFilters(): \GaletteActivities\Filters\SubscriptionsList + { + $controller = $this->container->get(\GaletteActivities\Controllers\Crud\SubscriptionsController::class); + return $this->session->{$controller->getFilterName($controller::getDefaultFilterName())}; + } + /** * Post a new subscription * @@ -120,12 +129,85 @@ public function testStaffSubscribes(): void ['Location' => [$this->routeparser->urlFor('activities_subscriptions')]], $test_response->getHeaders() ); - $this->assertSame(301, $test_response->getStatusCode()); + $this->assertSame(303, $test_response->getStatusCode()); $this->expectNoLogEntry(); $this->expectFlashData(['success_detected' => [_T('New subscription has been successfully added.', 'activities')]]); + //list shows subscriptions of the stored activity + $this->assertSame($activity, $this->getSubscriptionsFilters()->activity_filter); $this->assertSame(1, $this->countSubscriptions($activity)); $this->assertTrue($this->isInGroup($group->getId(), $member_two->id)); $this->resetStaffStatus($staff, $member_two); } + + /** + * Unknown subscriptions are not edited + */ + public function testEditUnknownSubscription(): void + { + $this->logSuperAdmin(); + $id = $this->insertSubscription($this->insertActivity('Climbing'), $this->getMemberOne()->id) + 1000; + + $test_response = $this->app->handle($this->createRequest('activities_subscription_edit', ['id' => (string)$id])); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('activities_subscriptions')]], + $test_response->getHeaders() + ); + $this->assertSame(302, $test_response->getStatusCode()); + $this->expectNoLogEntry(); + $this->expectFlashData(['error_detected' => ['No subscription #' . $id . '.']]); + } + + /** + * Subscriptions are removed from their route ID, as activities + */ + public function testRemoveSubscription(): void + { + $this->logSuperAdmin(); + $activity = $this->insertActivity('Climbing'); + $id = $this->insertSubscription($activity, $this->getMemberOne()->id); + + $request = $this->createRequest('activities_do_remove_subscription', ['id' => (string)$id], 'POST') + ->withParsedBody(['confirm' => '1']); + $test_response = $this->app->handle($request); + $this->assertSame( + ['Location' => [$this->routeparser->urlFor('activities_subscriptions')]], + $test_response->getHeaders() + ); + $this->expectNoLogEntry(); + $this->expectFlashData(['success_detected' => ['Successfully deleted!']]); + $this->assertSame(0, $this->countSubscriptions($activity)); + } + + /** + * Member filter is optional, and can be cleared + */ + public function testMemberFilter(): void + { + $this->logSuperAdmin(); + $member_one = $this->getMemberOne(); + $activity = $this->insertActivity('Climbing'); + + $filter = function (array $data): void { + $request = $this->createRequest('activities_filter-subscriptionslist', [], 'POST') + ->withParsedBody($data); + $test_response = $this->app->handle($request); + $this->assertSame(303, $test_response->getStatusCode()); + $this->expectNoLogEntry(); + }; + + //activity alone + $filter(['activity_filter' => (string)$activity, 'member_filter' => '']); + $this->assertSame($activity, $this->getSubscriptionsFilters()->activity_filter); + $this->assertNull($this->getSubscriptionsFilters()->member_filter); + + $filter(['activity_filter' => (string)$activity, 'member_filter' => (string)$member_one->id]); + $this->assertSame($member_one->id, $this->getSubscriptionsFilters()->member_filter); + + //cleared + $filter(['activity_filter' => (string)$activity, 'member_filter' => '']); + $this->assertNull($this->getSubscriptionsFilters()->member_filter); + + $this->getSubscriptionsFilters()->reinit(); + } } diff --git a/tests/GaletteActivities/Controllers/tests/units/Routes.php b/tests/GaletteActivities/Controllers/tests/units/Routes.php index ac0428d..e4103e8 100644 --- a/tests/GaletteActivities/Controllers/tests/units/Routes.php +++ b/tests/GaletteActivities/Controllers/tests/units/Routes.php @@ -64,7 +64,7 @@ private function getPluginRequests(): array } //ensure routes have been found; a new route is checked without any change here - $this->assertGreaterThanOrEqual(15, count($requests)); + $this->assertGreaterThanOrEqual(16, count($requests)); return $requests; } diff --git a/tests/GaletteActivities/Entity/tests/units/Activity.php b/tests/GaletteActivities/Entity/tests/units/Activity.php index aade70b..2b54ec8 100644 --- a/tests/GaletteActivities/Entity/tests/units/Activity.php +++ b/tests/GaletteActivities/Entity/tests/units/Activity.php @@ -67,7 +67,7 @@ public function testCrud(): void $this->assertFalse($activity->check($data)); $this->assertSame(['Name is mandatory'], $activity->getErrors()); $this->expectLogEntry( - \Analog::ERROR, + \Analog\Analog::ERROR, 'Name is mandatory' ); @@ -80,7 +80,7 @@ public function testCrud(): void $this->assertFalse($activity->check($data)); $this->assertSame(['Type is too long'], $activity->getErrors()); $this->expectLogEntry( - \Analog::ERROR, + \Analog\Analog::ERROR, 'Type is too long' ); @@ -147,4 +147,51 @@ public function testLoadError(): void $activity = new \GaletteActivities\Entity\Activity($this->zdb); $this->assertFalse($activity->load(999)); } + + /** + * Assert activity data is refused + * + * @param array $data Activity data + * @param array $errors Expected errors + */ + private function expectInvalid(array $data, array $errors): void + { + $activity = new \GaletteActivities\Entity\Activity($this->zdb); + $this->assertFalse($activity->check($data)); + $this->assertSame($errors, $activity->getErrors()); + $this->expectLogEntry(\Analog\Analog::ERROR, 'Error(s) checking activity before store'); + } + + /** + * Names are limited to database length, types counted in characters + */ + public function testCheckLengths(): void + { + $this->expectInvalid(['name' => str_repeat('a', 151)], ['Name is too long']); + + $activity = new \GaletteActivities\Entity\Activity($this->zdb); + $this->assertTrue($activity->check(['name' => str_repeat('é', 150), 'type' => 'Éàü'])); + $this->assertTrue($activity->store()); + $this->assertTrue($activity->load((int)$activity->getId())); + $this->assertSame(str_repeat('é', 150), $activity->getName()); + $this->assertSame('Éàü', $activity->getType()); + } + + /** + * Prices accept comma decimal separator and zero, and can be cleared + */ + public function testCheckPrice(): void + { + $activity = new \GaletteActivities\Entity\Activity($this->zdb); + $this->assertTrue($activity->check(['name' => 'Climbing', 'price' => '12,50'])); + $this->assertSame(12.5, $activity->getPrice()); + + $this->assertTrue($activity->check(['name' => 'Climbing', 'price' => '0'])); + $this->assertSame(0.0, $activity->getPrice()); + + $this->assertTrue($activity->check(['name' => 'Climbing', 'price' => ''])); + $this->assertNull($activity->getPrice()); + + $this->expectInvalid(['name' => 'Climbing', 'price' => 'twelve'], ['Price must be a number.']); + } } diff --git a/tests/GaletteActivities/Entity/tests/units/Subscription.php b/tests/GaletteActivities/Entity/tests/units/Subscription.php index 7998199..5f45f60 100644 --- a/tests/GaletteActivities/Entity/tests/units/Subscription.php +++ b/tests/GaletteActivities/Entity/tests/units/Subscription.php @@ -11,6 +11,7 @@ namespace GaletteActivities\tests\units; use Galette\Tests\GaletteTestCase; +use GaletteActivities\tests\ActivitiesFixtures; /** * Subscription tests @@ -19,6 +20,8 @@ */ class Subscription extends GaletteTestCase { + use ActivitiesFixtures; + protected int $seed = 20240817102541; /** @@ -117,7 +120,7 @@ public function testCrud(): void $subscription->getErrors() ); $this->expectLogEntry( - \Analog::ERROR, + \Analog\Analog::ERROR, 'Activity is mandatory', ); @@ -134,7 +137,7 @@ public function testCrud(): void $subscription->getErrors() ); $this->expectLogEntry( - \Analog::ERROR, + \Analog\Analog::ERROR, 'Member is mandatory', ); @@ -151,7 +154,7 @@ public function testCrud(): void $subscription->getErrors() ); $this->expectLogEntry( - \Analog::ERROR, + \Analog\Analog::ERROR, 'Subscription date is mandatory', ); @@ -169,7 +172,7 @@ public function testCrud(): void $subscription->getErrors() ); $this->expectLogEntry( - \Analog::ERROR, + \Analog\Analog::ERROR, '- Wrong date format (Y-m-d) for Subscription date!', ); @@ -262,12 +265,6 @@ public function testCrud(): void ], $subscription->getErrors() ); - $this->expectLogEntry( - \Analog::ERROR, - $this->zdb->isPostgres() - ? 'duplicate key value violates unique constraint "galette_activities_subscriptions_id_activity_id_adh_key"' - : sprintf('Duplicate entry \'%1$s-%2$s\' for key', $gactivity_id, $member_one->id) - ); } /** @@ -278,4 +275,191 @@ public function testLoadError(): void $subscription = new \GaletteActivities\Entity\Subscription($this->zdb); $this->assertFalse($subscription->load(999)); } + + /** + * Check and store a subscription + * + * @param int $activity Activity ID + * @param int $member Member ID + * @param ?\GaletteActivities\Entity\Subscription $subscription Subscription to change, a new one if null + */ + private function storeSubscription( + int $activity, + int $member, + ?\GaletteActivities\Entity\Subscription $subscription = null + ): \GaletteActivities\Entity\Subscription { + $subscription ??= new \GaletteActivities\Entity\Subscription($this->zdb); + $this->assertTrue($subscription->check([ + 'activity' => $activity, + 'member' => $member, + 'subscription_date' => date('Y-m-d'), + 'end_date' => date('Y-m-d', strtotime('+1 year')), + ])); + $this->assertTrue($subscription->store()); + return $subscription; + } + + /** + * Members already in the activity group can subscribe + */ + public function testStoreMemberAlreadyInGroup(): void + { + $member_one = $this->getMemberOne(); + $group = $this->createGroup('Activity group', [], [$member_one]); + $activity = $this->insertActivity('Climbing', $group->getId()); + + $this->storeSubscription($activity, $member_one->id); + $this->assertSame(1, $this->countSubscriptions($activity)); + $this->assertTrue($this->isInGroup($group->getId(), $member_one->id)); + } + + /** + * Duplicates are refused without breaking the transaction opened by the caller + */ + public function testDuplicateInTransaction(): void + { + $member_one = $this->getMemberOne(); + $activity = $this->insertActivity('Climbing'); + $this->insertSubscription($activity, $member_one->id); + + $this->zdb->connection->beginTransaction(); + $subscription = new \GaletteActivities\Entity\Subscription($this->zdb); + $this->assertTrue($subscription->check([ + 'activity' => $activity, + 'member' => $member_one->id, + 'subscription_date' => date('Y-m-d'), + 'end_date' => date('Y-m-d', strtotime('+1 year')), + ])); + $this->assertFalse($subscription->store()); + $this->assertSame(['Subscription already exists for this member and activity'], $subscription->getErrors()); + + //transaction is still usable + $this->insertActivity('Hiking'); + $this->zdb->connection->commit(); + $this->assertSame(1, $this->countSubscriptions($activity)); + } + + /** + * Members join the group of their new activity, and stay in the previous one + */ + public function testChangeActivityJoinsGroup(): void + { + $member_one = $this->getMemberOne(); + $climbing_group = $this->createGroup('Climbing group'); + $hiking_group = $this->createGroup('Hiking group'); + $climbing = $this->insertActivity('Climbing', $climbing_group->getId()); + $hiking = $this->insertActivity('Hiking', $hiking_group->getId()); + + $subscription = $this->storeSubscription($climbing, $member_one->id); + $this->assertTrue($this->isInGroup($climbing_group->getId(), $member_one->id)); + $this->assertFalse($this->isInGroup($hiking_group->getId(), $member_one->id)); + + $subscription = new \GaletteActivities\Entity\Subscription($this->zdb, (int)$subscription->getId()); + $this->storeSubscription($hiking, $member_one->id, $subscription); + $this->assertTrue($this->isInGroup($hiking_group->getId(), $member_one->id)); + $this->assertTrue($this->isInGroup($climbing_group->getId(), $member_one->id)); + $this->assertSame(0, $this->countSubscriptions($climbing)); + + //storing again without changing activity does not join the group again + $this->zdb->execute($this->zdb->delete(\Galette\Entity\Group::GROUPSUSERS_TABLE)); + $subscription = new \GaletteActivities\Entity\Subscription($this->zdb, (int)$subscription->getId()); + $this->storeSubscription($hiking, $member_one->id, $subscription); + $this->assertFalse($this->isInGroup($hiking_group->getId(), $member_one->id)); + $this->assertSame(1, $this->countSubscriptions($hiking)); + } + + /** + * Assert subscription data is refused + * + * @param array $data Subscription data, merged with valid ones + * @param array $errors Expected errors + */ + private function expectInvalid(array $data, array $errors): void + { + $subscription = new \GaletteActivities\Entity\Subscription($this->zdb); + $this->assertFalse($subscription->check($data + [ + 'subscription_date' => date('Y-m-d'), + 'end_date' => date('Y-m-d', strtotime('+1 year')), + ])); + $this->assertSame($errors, $subscription->getErrors()); + $this->expectLogEntry(\Analog\Analog::ERROR, 'Some errors has been threw attempting to edit/store a subscription'); + } + + /** + * Activity and member must exist + */ + public function testCheckActivityAndMemberExist(): void + { + $member_one = $this->getMemberOne(); + $activity = $this->insertActivity('Climbing'); + + $this->expectInvalid(['activity' => $activity + 1000, 'member' => $member_one->id], ['No activity #' . ($activity + 1000) . '.']); + $this->expectInvalid(['activity' => $activity, 'member' => $member_one->id + 1000], ['No member #' . ($member_one->id + 1000) . '.']); + } + + /** + * End date cannot be before subscription date + */ + public function testCheckDates(): void + { + $member_one = $this->getMemberOne(); + $activity = $this->insertActivity('Climbing'); + + $this->expectInvalid( + [ + 'activity' => $activity, + 'member' => $member_one->id, + 'subscription_date' => '2026-09-26', + 'end_date' => '2026-09-25', + ], + ['End date must not be before subscription date.'] + ); + + //same day is fine + $subscription = new \GaletteActivities\Entity\Subscription($this->zdb); + $this->assertTrue($subscription->check([ + 'activity' => $activity, + 'member' => $member_one->id, + 'subscription_date' => '2026-09-26', + 'end_date' => '2026-09-26', + ])); + } + + /** + * Amounts accept comma decimal separator and zero; new subscriptions default to activity price, + * existing ones can be cleared + */ + public function testCheckAmount(): void + { + $member_one = $this->getMemberOne(); + $activity = $this->insertActivity('Climbing'); + $data = [ + 'activity' => $activity, + 'member' => $member_one->id, + 'subscription_date' => date('Y-m-d'), + 'end_date' => date('Y-m-d', strtotime('+1 year')), + 'save' => '1', + ]; + + $subscription = new \GaletteActivities\Entity\Subscription($this->zdb); + $this->assertTrue($subscription->check($data + ['payment_amount' => '12,50'])); + $this->assertSame(12.5, $subscription->getAmount()); + + $this->assertTrue($subscription->check($data + ['payment_amount' => '0'])); + $this->assertSame(0.0, $subscription->getAmount()); + + //activity price, from fixtures + $this->assertTrue($subscription->check($data + ['payment_amount' => ''])); + $this->assertSame(10.0, $subscription->getAmount()); + $this->assertTrue($subscription->store()); + + $subscription = new \GaletteActivities\Entity\Subscription($this->zdb, (int)$subscription->getId()); + $this->assertTrue($subscription->check($data + ['payment_amount' => ''])); + $this->assertNull($subscription->getAmount()); + $this->assertTrue($subscription->store()); + $subscription = new \GaletteActivities\Entity\Subscription($this->zdb, (int)$subscription->getId()); + $this->assertNull($subscription->getAmount()); + + $this->expectInvalid($data + ['payment_amount' => 'twelve'], ['Amount must be a number.']); + } } diff --git a/tests/GaletteActivities/Repository/tests/units/Subscriptions.php b/tests/GaletteActivities/Repository/tests/units/Subscriptions.php new file mode 100644 index 0000000..eece2b6 --- /dev/null +++ b/tests/GaletteActivities/Repository/tests/units/Subscriptions.php @@ -0,0 +1,117 @@ + + */ +class Subscriptions extends GaletteTestCase +{ + use ActivitiesFixtures; + + protected int $seed = 20260926190512; + + /** + * Cleanup after each test method + */ + public function tearDown(): void + { + $this->cleanActivities(); + parent::tearDown(); + } + + /** + * Get subscriptions IDs, in list order + * + * @param SubscriptionsList $filters Filters + * + * @return array + */ + private function getListIds(SubscriptionsList $filters): array + { + $subscriptions = new \GaletteActivities\Repository\Subscriptions($this->zdb, $filters); + $ids = []; + foreach ($subscriptions->getList() as $subscription) { + $ids[] = $subscription->getId(); + } + return $ids; + } + + /** + * Subscriptions keep their own values, not their activity ones + */ + public function testListKeepsSubscriptionValues(): void + { + $activity = $this->insertActivity( + 'Climbing', + null, + ['comment' => 'Activity comment', 'creation_date' => '2020-01-01'] + ); + $this->insertSubscription( + $activity, + $this->getMemberOne()->id, + ['comment' => 'Subscription comment', 'creation_date' => '2026-01-15'] + ); + + $subscriptions = new \GaletteActivities\Repository\Subscriptions($this->zdb); + $list = $subscriptions->getList(); + $this->assertCount(1, $list); + $this->assertSame('Subscription comment', $list[0]->getComment()); + $this->assertSame((new \DateTime('2026-01-15'))->format(__('Y-m-d')), $list[0]->getCreationDate()); + } + + /** + * Subscriptions are ordered and filtered by payment + */ + public function testPaid(): void + { + $member = $this->getMemberOne()->id; + $paid_one = $this->insertSubscription($this->insertActivity('Climbing'), $member, [ + 'is_paid' => true, + 'payment_amount' => 10, + 'end_date' => date('Y-m-d', strtotime('+1 year')), + ]); + $not_paid = $this->insertSubscription($this->insertActivity('Hiking'), $member, [ + 'end_date' => date('Y-m-d', strtotime('+2 years')), + ]); + $paid_two = $this->insertSubscription($this->insertActivity('Diving'), $member, [ + 'is_paid' => true, + 'payment_amount' => 5.5, + 'end_date' => date('Y-m-d', strtotime('+3 years')), + ]); + + //default order is on end date + $filters = new SubscriptionsList(); + $this->assertSame([$paid_two, $not_paid, $paid_one], $this->getListIds($filters)); + + //paid ones first + $filters->orderby = \GaletteActivities\Repository\Subscriptions::ORDERBY_PAID; + $ids = $this->getListIds($filters); + $this->assertCount(3, $ids); + $this->assertSame($not_paid, $ids[2]); + + $filters = new SubscriptionsList(); + $filters->paid_filter = \GaletteActivities\Repository\Subscriptions::FILTER_PAID; + $this->assertSame([$paid_two, $paid_one], $this->getListIds($filters)); + $subscriptions = new \GaletteActivities\Repository\Subscriptions($this->zdb, $filters); + $subscriptions->getList(); + $this->assertSame(15.5, $subscriptions->getSum()); + + $filters->paid_filter = \GaletteActivities\Repository\Subscriptions::FILTER_NOT_PAID; + $this->assertSame([$not_paid], $this->getListIds($filters)); + } +}