]> git.agnieray.net Git - galette.git/commitdiff
Merge branch 'hotfix/1.0.3'
authorJohan Cwiklinski <johan@x-tnd.be>
Fri, 1 Mar 2024 10:08:16 +0000 (11:08 +0100)
committerJohan Cwiklinski <johan@x-tnd.be>
Fri, 1 Mar 2024 10:08:16 +0000 (11:08 +0100)
43 files changed:
README.md
bin/release
galette/config/paths.inc.php
galette/config/versions.inc.php
galette/docs/CHANGES
galette/includes/dependencies.php
galette/includes/galette.inc.php
galette/includes/main.inc.php
galette/includes/routes/main.routes.php
galette/install/steps/check.php
galette/lib/Galette/Controllers/AjaxController.php
galette/lib/Galette/Controllers/AuthController.php
galette/lib/Galette/Controllers/Crud/ContributionsController.php
galette/lib/Galette/Controllers/Crud/DynamicFieldsController.php
galette/lib/Galette/Controllers/Crud/MembersController.php
galette/lib/Galette/Core/GaletteMail.php
galette/lib/Galette/Core/Preferences.php
galette/lib/Galette/Entity/Adherent.php
galette/lib/Galette/Entity/Contribution.php
galette/lib/Galette/Features/HasEvent.php [new file with mode: 0644]
galette/lib/Galette/Features/Replacements.php
galette/lib/Galette/Middleware/UpdateAndMaintenance.php
galette/templates/default/components/dynamic_fields.html.twig
galette/templates/default/elements/ajax_messages.html.twig [deleted file]
galette/templates/default/elements/js/messages.js.twig
galette/templates/default/elements/js/modal_action.js.twig
galette/templates/default/elements/js/removal.js.twig
galette/templates/default/elements/logged_user.html.twig
galette/templates/default/elements/messages_inline.html.twig
galette/templates/default/elements/navigation/navigation_aside.html.twig
galette/templates/default/elements/scripts.html.twig
galette/templates/default/page.html.twig
galette/templates/default/pages/configuration_dynamic_field_form.html.twig
galette/templates/default/pages/configuration_dynamic_fields.html.twig
galette/templates/default/pages/members_list.html.twig
galette/templates/default/pages/preferences.html.twig
galette/templates/default/public_page.html.twig
semantic.json
tests/Galette/Core/tests/units/Preferences.php
tests/Galette/Entity/tests/units/PdfModel.php
tests/Galette/Features/tests/units/HasEvent.php [new file with mode: 0644]
tests/TestsBootstrap.php
ui/semantic/galette/globals/site.variables

index 0d9fc5042ead8a63e1fc5e6b72a51c2464745f3e..ac660e36440e4b42151c3de1ae77a77083f144a9 100644 (file)
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
 
 ### English
 
-[![Download most recent Galette release (1.0.2)](https://img.shields.io/badge/1.0.2-Latest_Galette-ffb619.svg?logo=php&logoColor=white&style=for-the-badge)](https://download.tuxfamily.org/galette/galette-1.0.2.tar.bz2)
+[![Download most recent Galette release (1.0.3)](https://img.shields.io/badge/1.0.3-Latest_Galette-ffb619.svg?logo=php&logoColor=white&style=for-the-badge)](https://download.tuxfamily.org/galette/galette-1.0.3.tar.bz2)
 [![Download Galette development (nightly) build](https://img.shields.io/badge/nightly-Galette_development-ffb619.svg?logo=php&logoColor=white&style=for-the-badge)](https://download.tuxfamily.org/galette/galette-dev.tar.bz2)
 
 Galette is a membership management web application towards non profit organizations; released under GPLv3.
@@ -29,7 +29,7 @@ To use Galette, you can either:
 
 ### Français
 
-[![Télécharger la version de Galette la plus récente (1.0.2)](https://img.shields.io/badge/1.0.2-Dernière_Galette-ffb619.svg?logo=php&logoColor=white&style=for-the-badge)](https://download.tuxfamily.org/galette/galette-1.0.2.tar.bz2)
+[![Télécharger la version de Galette la plus récente (1.0.3)](https://img.shields.io/badge/1.0.3-Dernière_Galette-ffb619.svg?logo=php&logoColor=white&style=for-the-badge)](https://download.tuxfamily.org/galette/galette-1.0.3.tar.bz2)
 [![Télécharger la version de développement (nighly) de Galette](https://img.shields.io/badge/nightly-Galette_développement-ffb619.svg?logo=php&logoColor=white&style=for-the-badge)](https://download.tuxfamily.org/galette/galette-dev.tar.bz2)
 
 Galette est un outil de gestion d’adhérents et de cotisations en ligne à destination des associations, sous license GPLV3.
index 412bb84236d10ce8844938e9a1e0bbb78f51d605..39bd1e6cb402905c156ec35b4361adad6c300f54 100755 (executable)
@@ -314,11 +314,11 @@ def add_libs(rel_name, galette_archive):
     galette.close()
 
     #set galette nightly version
-    includes_dir = os.path.join(src_dir, rel_name, 'galette', 'includes')
+    config_dir = os.path.join(src_dir, rel_name, 'galette', 'config')
     if nightly_version != None:
-        sed_cmd = 'sed -e "s/GALETTE_NIGHTLY\', false/GALETTE_NIGHTLY\', \'%s\'/" -i galette.inc.php' % nightly_version
+        sed_cmd = 'sed -e "s/GALETTE_NIGHTLY\', false/GALETTE_NIGHTLY\', \'%s\'/" -i versions.inc.php' % nightly_version
         print(sed_cmd)
-        p1 = subprocess.Popen(sed_cmd, shell=True, cwd=includes_dir)
+        p1 = subprocess.Popen(sed_cmd, shell=True, cwd=config_dir)
         p1.wait()
 
     #install npm modules
index 5d1e3cea83df2c01ad599143b0ee0459139e860a..559d528abcf2aa34e978082631ad9c94d7c38ab8 100644 (file)
@@ -97,7 +97,7 @@ if (!defined('GALETTE_COMPILE_DIR')) {
     define('GALETTE_COMPILE_DIR', GALETTE_DATA_PATH . 'templates_c/');
 }
 if (!defined('GALETTE_CACHE_DIR')) {
-    define('GALETTE_CACHE_DIR', GALETTE_DATA_PATH . 'cache/');
+    define('GALETTE_CACHE_DIR', GALETTE_DATA_PATH . 'cache/' . GALETTE_VERSION . '/');
 }
 if (!defined('GALETTE_EXPORTS_PATH')) {
     define('GALETTE_EXPORTS_PATH', GALETTE_DATA_PATH . 'exports/');
index d7f8c149f9412ac3e906326ea80664a6f4853a0c..27afb314bc40574bba1fe2d65525099306fa1ba6 100644 (file)
@@ -38,4 +38,8 @@
 define('GALETTE_PHP_MIN', '8.1');
 define('GALETTE_MYSQL_MIN', '5.7');
 define('GALETTE_MARIADB_MIN', '10.4');
-define('GALETTE_PGSQL_MIN', '11')  ;
\ No newline at end of file
+define('GALETTE_PGSQL_MIN', '11')  ;
+define('GALETTE_NIGHTLY', false);
+define('GALETTE_VERSION', 'v1.0.3');
+define('GALETTE_COMPAT_VERSION', '1.0.0');
+define('GALETTE_DB_VERSION', '0.960');
index c95a4ed4031b876e3e9cd7f7194d87cc82bb3acf..0c0f5ad4226eebf83475ab433dea646df7415d73 100644 (file)
@@ -1,6 +1,17 @@
 Changes
 -------
 
+1.0.2 -> 1.0.3
+
+* Logo in mail signature is not shown
+* Missing HTML editor for dynamic fields information field
+* Update and maintainance pages are no longer working
+* Do not throw events on mass edition
+* Make cache version dependent
+* Check preferences website is valid
+* Link to asso website from logo
+* Rework UI messages
+
 1.0.1 -> 1.0.2
 
 - Public pages access restriction (CVE-2024-24761)
index 8a751a9c00e9fa16e7532549be5b55b00225e501..7275eb494df41e70d25fa74bd4c52c4eabf544e9 100644 (file)
@@ -71,7 +71,7 @@ $container->set('Slim\Views\Twig', function (ContainerInterface $c) {
     $view = Twig::create(
         $templates,
         [
-            'cache' => rtrim(GALETTE_CACHE_DIR, DIRECTORY_SEPARATOR),
+            'cache' => rtrim(GALETTE_CACHE_DIR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'templates',
             'debug' => \Galette\Core\Galette::isDebugEnabled(),
             'strict_variables' => \Galette\Core\Galette::isDebugEnabled()
         ]
index 6a83526f542c7afc95de0b96a91d03986ee542aa..e69824a95a54dadd20e038ce4a228e268d80c063 100644 (file)
@@ -99,16 +99,11 @@ if (
     $profiler->start();
 }
 
-define('GALETTE_NIGHTLY', false);
-define('GALETTE_VERSION', 'v1.0.2');
-
 //Version to display
 if (!defined('GALETTE_HIDE_VERSION')) {
     define('GALETTE_DISPLAY_VERSION', \Galette\Core\Galette::gitVersion(false));
 }
 
-define('GALETTE_COMPAT_VERSION', '1.0.0');
-define('GALETTE_DB_VERSION', '0.960');
 if (!defined('GALETTE_MODE')) {
     define('GALETTE_MODE', \Galette\Core\Galette::MODE_PROD);
 }
index 0609aca80fd32614fa7ac91d01f5d4b8b76dd508..c48b63d238f4fb47af8ce1c4723b6a1e2c975630 100644 (file)
@@ -117,10 +117,18 @@ $app->add($session);
 require GALETTE_ROOT . '/includes/dependencies.php';
 $app->add($app->getContainer()->get('csrf'));
 
+/**
+ * Authentication middleware
+ */
+$authenticate = new Authenticate($container);
+
+require_once GALETTE_ROOT . 'includes/routes/main.routes.php';
+
 if ($needs_update) {
     $app->add(
         new UpdateAndMaintenance(
             $container->get('i18n'),
+            $container->get(RouteParser::class),
             UpdateAndMaintenance::NEED_UPDATE
         )
     );
@@ -129,11 +137,6 @@ if ($needs_update) {
     die();
 }
 
-/**
- * Authentication middleware
- */
-$authenticate = new Authenticate($container);
-
 //FIXME: remove in 1.1.0; routes/groups should call middleware directly
 $showPublicPages = new \Galette\Middleware\PublicPages($container);
 
@@ -141,7 +144,8 @@ $showPublicPages = new \Galette\Middleware\PublicPages($container);
 if (Galette::MODE_MAINT === GALETTE_MODE && !$container->get('login')->isSuperAdmin()) {
     $app->add(
         new UpdateAndMaintenance(
-            $i18n,
+            $container->get('i18n'),
+            $container->get(RouteParser::class),
             UpdateAndMaintenance::MAINTENANCE
         )
     );
@@ -157,7 +161,6 @@ $app->add(Language::class);
 //Telemetry update middleware
 $app->add(Telemetry::class);
 
-require_once GALETTE_ROOT . 'includes/routes/main.routes.php';
 require_once GALETTE_ROOT . 'includes/routes/authentication.routes.php';
 require_once GALETTE_ROOT . 'includes/routes/management.routes.php';
 require_once GALETTE_ROOT . 'includes/routes/members.routes.php';
index 24a09ff77df2b9b819956f157416a968868e148a..cbd41cf15abd5330a01abba8eedd6718c21226ce 100644 (file)
@@ -69,7 +69,8 @@ $app->get(
 //system information - keep old route with typo ('s' on 'information') for now (0.9.4)
 $app->get(
     '/system-informations',
-    function ($request, $response) use ($routeparser) {
+    function ($request, $response) use ($container) {
+        $routeparser = $container->get(\Slim\Routing\RouteParser::class);
         return $response
             ->withStatus(302)
             ->withHeader('Location', $routeparser->urlFor('sysinfos'));
index 87f6d4eda3a3e3948fd76dc2190f341527d650fa..614246bf6d411ae0c6284402c70eac7c429e9e24 100644 (file)
@@ -66,8 +66,8 @@ $perms_ok = true;
 $files_need_rw = array(
     _T("Compilation")       => GALETTE_COMPILE_DIR,
     _T("Photos")            => GALETTE_PHOTOS_PATH,
-    _T("Cache")             => GALETTE_CACHE_DIR,
-    _T("Temporary images")   => GALETTE_TEMPIMAGES_PATH,
+    _T("Cache")             => str_replace(GALETTE_VERSION, '', GALETTE_CACHE_DIR),
+    _T("Temporary images")  => GALETTE_TEMPIMAGES_PATH,
     _T("Configuration")     => GALETTE_CONFIG_PATH,
     _T("Exports")           => GALETTE_EXPORTS_PATH,
     _T("Imports")           => GALETTE_IMPORTS_PATH,
index 504e710a665013ca7745dda1c2990f1c4b856689..955bfe7d10a803bc4404b0491f918a36f525c577 100644 (file)
@@ -63,7 +63,7 @@ use Throwable;
 class AjaxController extends AbstractController
 {
     /**
-     * Messages
+     * Messages as JSON array
      *
      * @param Request  $request  PSR Request
      * @param Response $response PSR Response
@@ -72,11 +72,54 @@ class AjaxController extends AbstractController
      */
     public function messages(Request $request, Response $response): Response
     {
-        $this->view->render(
-            $response,
-            'elements/ajax_messages.html.twig'
-        );
-        return $response;
+        $messages = [];
+
+        $errors = $this->flash->getMessage('loginfault') ?? [];
+        $errors = array_merge($errors, $this->flash->getMessage('error_detected') ?? []);
+        $errors = array_merge($errors, $this->flash->getMessage('error') ?? []);
+
+        if (count($errors) > 0) {
+            $messages['error'] = [
+                'title' => _T('- ERROR -'),
+                'icon' => 'exclamation circle',
+                'messages' => $errors
+            ];
+        }
+
+        $warnings = $this->flash->getMessage('warning_detected') ?? [];
+        $warnings = array_merge($warnings, $this->flash->getMessage('warning') ?? []);
+
+        if (count($warnings) > 0) {
+            $messages['warning'] = [
+                'title' => _T('- WARNING -'),
+                'icon' => 'exclamation triangle',
+                'messages' => $warnings
+            ];
+        }
+
+        $info = $this->flash->getMessage('info_detected') ?? [];
+        $info = array_merge($info, $this->flash->getMessage('info') ?? []);
+
+        if (count($info) > 0) {
+            $messages['info'] = [
+                'title' => '',
+                'icon' => 'info',
+                'messages' => $info
+            ];
+        }
+
+        $success = $this->flash->getMessage('success_detected') ?? [];
+        $success = array_merge($success, $this->flash->getMessage('succes') ?? []);
+
+        if (count($success) > 0) {
+            $messages['success'] = [
+                'title' => '',
+                'icon' => 'check circle outline',
+                'messages' => $success
+            ];
+        }
+
+        return $this->withJson($response, $messages);
     }
 
     /**
index e812ea18662998d3d6713134e282657566ebb869..92850a9b72259affc7ab004e89899793c3dd9667 100644 (file)
@@ -136,6 +136,13 @@ class AuthController extends AbstractController
         }
 
         if ($this->login->isLogged()) {
+            if (defined('NON_UTF_DBCONNECT')) {
+                $this->flash->addMessage(
+                    'warning',
+                    'It appears you are using NON_UTF_DBCONNECT constant, it will be in next major release.'
+                );
+            }
+
             if (!$checkpass->isValid($password)) {
                 //password is no longer valid with current rules, must be changed
                 $this->flash->addMessage(
index 146e37fe9addf2574787d41c5b3ac0b64317f6bc..ebee1834eb064bbc4f0be95c21d57fe0960c5c1b 100644 (file)
@@ -337,6 +337,7 @@ class ContributionsController extends CrudController
         foreach ($members_ids as $member_id) {
             $post[Adherent::PK] = (int)$member_id;
             $contrib = new Contribution($this->zdb, $this->login);
+            $contrib->disableEvents();
 
             // regular fields
             $valid = $contrib->check($post, $contrib->getRequired(), $disabled);
index 6814648387f8c25be5d5c7d5cba195080c6cbeff..e275bec0d2d218e5f8f573c54e4bbe8fcc548569 100644 (file)
@@ -235,7 +235,10 @@ class DynamicFieldsController extends CrudController
             'fields_list'       => $fields_list,
             'form_name'         => $form_name,
             'form_title'        => DynamicField::getFormTitle($form_name),
-            'page_title'        => _T("Dynamic fields configuration")
+            'page_title'        => _T("Dynamic fields configuration"),
+            'html_editor'       => true,
+            'html_editor_active' => $this->preferences->pref_editor_enabled
+
         ];
 
         $tpl = 'pages/configuration_dynamic_fields.html.twig';
@@ -429,7 +432,9 @@ class DynamicFieldsController extends CrudController
             'form_name'     => $form_name,
             'perm_names'    => DynamicField::getPermsNames(),
             'mode'          => (($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ? 'ajax' : ''),
-            'df'            => $df
+            'df'            => $df,
+            'html_editor'   => true,
+            'html_editor_active' => $this->preferences->pref_editor_enabled
         ];
 
         // display page
index d9b1fcaf3f7d1dabb1e46f7f4b2d39936bf3e871..6891ee25da4d6b4592e907260358c826e5867ef9 100644 (file)
@@ -1307,7 +1307,9 @@ class MembersController extends CrudController
                         && !$this->login->isStaff()
                         && $this->login->isGroupManager();
                     $member = new Adherent($this->zdb);
-                    $member->disableAllDeps();
+                    $member
+                        ->disableAllDeps()
+                        ->disableEvents();
                     if ($is_manager) {
                         $member->enableDep('groups');
                     }
index 18517c8dd2b0b521df3755b566d480c2da49613c..db30634db12587906d2b9a17d8e8f4a6b0f8e779 100644 (file)
@@ -294,7 +294,7 @@ class GaletteMail
             );
         }
 
-        $signature = $this->preferences->getMailSignature();
+        $signature = $this->preferences->getMailSignature($this->mail);
         if ($signature != '') {
             if ($this->html) {
                 //we are sending html message
index 504486573f499a55462d0fddeeb65456c595d031..dd10a3fb2eb7e0793ae4a96b73e3a5d13f017bac 100644 (file)
@@ -39,6 +39,7 @@ use Galette\Entity\PaymentType;
 use Galette\Entity\Social;
 use Galette\Features\Replacements;
 use Galette\Features\Socials;
+use PHPMailer\PHPMailer\PHPMailer;
 use Throwable;
 use Analog\Analog;
 use Galette\Entity\Adherent;
@@ -767,6 +768,11 @@ class Preferences
             case 'pref_footer':
                 $value = $this->cleanHtmlValue($value);
                 break;
+            case 'pref_website':
+                if (!isValidWebUrl($value)) {
+                    $this->errors[] = _T("- Invalid website URL.");
+                }
+                break;
         }
 
         return $value;
@@ -1228,9 +1234,11 @@ class Preferences
     /**
      * Get email signature
      *
+     * @param PHPMailer $mail PHPMailer instance
+     *
      * @return string
      */
-    public function getMailSignature(): string
+    public function getMailSignature(PHPMailer $mail): string
     {
         global $routeparser;
 
@@ -1245,6 +1253,7 @@ class Preferences
             $this->getMainPatterns() + $this->getSignaturePatterns()
         );
         $this
+            ->setMail($mail)
             ->setMain()
             ->setSocialReplacements();
 
@@ -1349,7 +1358,11 @@ class Preferences
     public function cleanHtmlValue(string $value): string
     {
         $config = \HTMLPurifier_Config::createDefault();
-        $config->set('Cache.SerializerPath', GALETTE_CACHE_DIR);
+        $cache_dir = rtrim(GALETTE_CACHE_DIR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'htmlpurifier';
+        if (!file_exists($cache_dir)) {
+            mkdir($cache_dir, 0755, true);
+        }
+        $config->set('Cache.SerializerPath', $cache_dir);
         $purifier = new \HTMLPurifier($config);
         return $purifier->purify($value);
     }
index 9b844fc231ac148872596412f6bb271251f5e888..e6d67b4d94282f039c845efa3ac25c9182bf9686 100644 (file)
@@ -38,6 +38,7 @@ namespace Galette\Entity;
 
 use ArrayObject;
 use Galette\Events\GaletteEvent;
+use Galette\Features\HasEvent;
 use Galette\Features\Socials;
 use Throwable;
 use Analog\Analog;
@@ -126,6 +127,7 @@ class Adherent
 {
     use Dynamics;
     use Socials;
+    use HasEvent;
 
     public const TABLE = 'adherents';
     public const PK = 'id_adh';
@@ -237,6 +239,12 @@ class Adherent
             }
         }
 
+        $this
+            ->withAddEvent()
+            ->withEditEvent()
+            ->withoutDeleteEvent()
+            ->activateEvents();
+
         if ($args == null || is_int($args)) {
             if (is_int($args) && $args > 0) {
                 $this->load($args);
@@ -1569,7 +1577,7 @@ class Adherent
                         );
                     }
 
-                    $event = 'member.add';
+                    $event = $this->getAddEventName();
                 } else {
                     $hist->add(_T("Fail to add new member."));
                     throw new \Exception(
@@ -1605,7 +1613,7 @@ class Adherent
                         $this->sname
                     );
                 }
-                $event = 'member.edit';
+                $event = $this->getEditEventName();
             }
 
             //dynamic fields
@@ -1613,7 +1621,7 @@ class Adherent
             $this->storeSocials($this->id);
 
             //send event at the end of process, once all has been stored
-            if ($event !== null) {
+            if ($event !== null && $this->areEventsEnabled()) {
                 $emitter->dispatch(new GaletteEvent($event, $this));
             }
             return true;
@@ -2267,4 +2275,14 @@ class Adherent
         $this->loadParent();
         return $this;
     }
+
+    /**
+     * Get prefix for events
+     *
+     * @return string
+     */
+    protected function getEventsPrefix(): string
+    {
+        return 'member';
+    }
 }
index f8cc8bd0db1e93ca8cf37ee7a0a2e1a896b65759..3457da44e01dc8437c3eeeb0d82943f3376dc18a 100644 (file)
@@ -40,6 +40,7 @@ namespace Galette\Entity;
 use ArrayObject;
 use DateTime;
 use Galette\Events\GaletteEvent;
+use Galette\Features\HasEvent;
 use Throwable;
 use Analog\Analog;
 use Laminas\Db\Sql\Expression;
@@ -85,6 +86,7 @@ use Galette\Features\Dynamics;
 class Contribution
 {
     use Dynamics;
+    use HasEvent;
 
     public const TABLE = 'cotisations';
     public const PK = 'id_cotis';
@@ -135,6 +137,12 @@ class Contribution
         global $preferences;
         $this->_payment_type = (int)$preferences->pref_default_paymenttype;
 
+        $this
+            ->withAddEvent()
+            ->withEditEvent()
+            ->withoutDeleteEvent()
+            ->activateEvents();
+
         /*
          * Fields configuration. Each field is an array and must reflect:
          * array(
@@ -683,7 +691,7 @@ class Contribution
                         _T("Contribution added"),
                         Adherent::getSName($this->zdb, $this->_member)
                     );
-                    $event = 'contribution.add';
+                    $event = $this->getAddEventName();
                 } else {
                     $hist->add(_T("Fail to add new contribution."));
                     throw new \Exception(
@@ -705,7 +713,7 @@ class Contribution
                     );
                 }
 
-                $event = 'contribution.edit';
+                $event = $this->getEditEventName();
             }
             //update deadline
             if ($this->isFee()) {
@@ -719,7 +727,7 @@ class Contribution
             $this->_orig_amount = $this->_amount;
 
             //send event at the end of process, once all has been stored
-            if ($event !== null) {
+            if ($event !== null && $this->areEventsEnabled()) {
                 $emitter->dispatch(new GaletteEvent($event, $this));
             }
 
@@ -1505,4 +1513,14 @@ class Contribution
 
         return false;
     }
+
+    /**
+     * Get prefix for events
+     *
+     * @return string
+     */
+    protected function getEventsPrefix(): string
+    {
+        return 'contribution';
+    }
 }
diff --git a/galette/lib/Galette/Features/HasEvent.php b/galette/lib/Galette/Features/HasEvent.php
new file mode 100644 (file)
index 0000000..b1d0765
--- /dev/null
@@ -0,0 +1,237 @@
+<?php
+
+/**
+ * Has events trait
+ *
+ * PHP version 5
+ *
+ * Copyright © 2024 The Galette Team
+ *
+ * This file is part of Galette (http://galette.tuxfamily.org).
+ *
+ * Galette is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Galette is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Galette. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Features
+ * @package   Galette
+ *
+ * @author    Johan Cwiklinski <johan@x-tnd.be>
+ * @copyright 2024 The Galette Team
+ * @license   https://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
+ * @link      https://galette.eu
+ */
+
+namespace Galette\Features;
+
+/**
+ * Has events trait
+ *
+ * @category  Features
+ * @name      HasEvents
+ * @package   Galette
+ * @author    Johan Cwiklinski <johan@x-tnd.be>
+ * @copyright 2024 The Galette Team
+ * @license   https://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
+ * @link      https://galette.eu
+ */
+
+trait HasEvent
+{
+    private bool $has_add_event = false;
+    private bool $has_edit_event = false;
+    private bool $has_delete_event = false;
+    protected bool $events_active = true;
+
+    /**
+     * Get prefix for events
+     *
+     * @return string
+     */
+    abstract protected function getEventsPrefix(): string;
+
+    /**
+     * Activate events
+     *
+     * @return self
+     */
+    public function activateEvents(): self
+    {
+        $this->events_active = true;
+        return $this;
+    }
+
+    /**
+     * Disable events
+     *
+     * @return self
+     */
+    public function disableEvents(): self
+    {
+        $this->events_active = false;
+        return $this;
+    }
+
+    /**
+     * Are events enabled
+     *
+     * @return bool
+     */
+    public function areEventsEnabled(): bool
+    {
+        return $this->events_active;
+    }
+
+    /**
+     * Activate add event
+     *
+     * @return self
+     */
+    public function withAddEvent(): self
+    {
+        $this->has_add_event = true;
+        return $this;
+    }
+
+    /**
+     * Disable add event
+     *
+     * @return self
+     */
+    public function withoutAddEvent(): self
+    {
+        $this->has_add_event = false;
+        return $this;
+    }
+
+    /**
+     * Get add event name
+     *
+     * @return ?string
+     */
+    public function getAddEventName(): ?string
+    {
+        if (!$this->hasAddEvent()) {
+            return null;
+        }
+        return sprintf(
+            '%1$s.add',
+            $this->getEventsPrefix()
+        );
+    }
+
+    /**
+     * Has add event
+     *
+     * @return bool
+     */
+    public function hasAddEvent(): bool
+    {
+        return $this->areEventsEnabled() && $this->has_add_event;
+    }
+
+    /**
+     * Activate edit event
+     *
+     * @return self
+     */
+    public function withEditEvent(): self
+    {
+        $this->has_edit_event = true;
+        return $this;
+    }
+
+    /**
+     * Disable edit event
+     *
+     * @return self
+     */
+    public function withoutEditEvent(): self
+    {
+        $this->has_edit_event = false;
+        return $this;
+    }
+
+    /**
+     * Get edit event name
+     *
+     * @return ?string
+     */
+    public function getEditEventName(): ?string
+    {
+        if (!$this->hasEditEvent()) {
+            return null;
+        }
+        return sprintf(
+            '%1$s.edit',
+            $this->getEventsPrefix()
+        );
+    }
+
+    /**
+     * Has edit event
+     *
+     * @return bool
+     */
+    public function hasEditEvent(): bool
+    {
+        return $this->areEventsEnabled() && $this->has_edit_event;
+    }
+
+    /**
+     * Activate add event
+     *
+     * @return self
+     */
+    public function withDeleteEvent(): self
+    {
+        $this->has_delete_event = true;
+        return $this;
+    }
+
+    /**
+     * Disable delete event
+     *
+     * @return self
+     */
+    public function withoutDeleteEvent(): self
+    {
+        $this->has_delete_event = false;
+        return $this;
+    }
+
+    /**
+     * Get edit event name
+     *
+     * @return ?string
+     */
+    public function getDeleteEventName(): ?string
+    {
+        if (!$this->hasDeleteEvent()) {
+            return null;
+        }
+        return sprintf(
+            '%1$s.delete',
+            $this->getEventsPrefix()
+        );
+    }
+
+    /**
+     * Has delete event
+     *
+     * @return bool
+     */
+    public function hasDeleteEvent(): bool
+    {
+        return $this->areEventsEnabled() && $this->has_delete_event;
+    }
+}
index 767c52cdf7c268e20de1c3b45e502d673e16adac..8f25d717138c20b578edc2dff161b25cf4520217 100644 (file)
@@ -51,6 +51,7 @@ use Galette\Repository\DynamicFieldsSet;
 use Galette\DynamicFields\DynamicField;
 use Analog\Analog;
 use NumberFormatter;
+use PHPMailer\PHPMailer\PHPMailer;
 use Slim\Routing\RouteParser;
 
 /**
@@ -71,6 +72,7 @@ trait Replacements
     private $patterns = [];
     private $replaces = [];
     private $dynamic_patterns = [];
+    private ?PHPMailer $mail = null;
 
     /**
      * @var Db
@@ -466,6 +468,19 @@ trait Replacements
         return $c_patterns + $dynamic_patterns;
     }
 
+    /**
+     * Set mail instance
+     *
+     * @param PHPMailer $mail PHPMailer instance
+     *
+     * @return self
+     */
+    public function setMail(PHPMailer $mail): self
+    {
+        $this->mail = $mail;
+        return $this;
+    }
+
     /**
      * Set main replacements
      *
@@ -483,10 +498,14 @@ trait Replacements
         }
 
         $logo = new Logo();
-
+        if ($this->mail !== null) {
+            $logo_content = $this->preferences->getURL() . $this->routeparser->urlFor('logo');
+        } else {
+            $logo_content = '@' . base64_encode(file_get_contents($logo->getPath()));
+        }
         $logo_elt = sprintf(
-            '<img src="%1$s" width="%2$s" height="%3$s" />',
-            '@' . base64_encode(file_get_contents($logo->getPath())),
+            '<img src="%1$s" width="%2$s" height="%3$s" alt="" />',
+            $logo_content,
             $logo->getOptimalWidth(),
             $logo->getOptimalHeight()
         );
@@ -913,7 +932,7 @@ trait Replacements
             $replaced
         );
 
-        return $replaced;
+        return trim($replaced);
     }
 
     /**
index 8f3f60f6cc3eedaba5a32587b5fa598b58a33912..19343d9975ad33cafeee7e722e960d9c0a2cb0c6 100644 (file)
@@ -43,7 +43,7 @@ use Galette\Core\I18n;
 use Psr\Http\Message\ServerRequestInterface as Request;
 use Psr\Http\Message\ResponseInterface as Response;
 use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
-use Slim\Routing\RouteContext;
+use Slim\Routing\RouteParser;
 
 /**
  * Galette's Slim middleware for Update and Maintenance
@@ -74,15 +74,22 @@ class UpdateAndMaintenance
      */
     protected $i18n;
 
+    /**
+     * @var RouteParser
+     */
+    protected RouteParser $routeParser;
+
     /**
      * Constructor
      *
-     * @param I18n         $i18n     I18n instance
-     * @param callable|int $callback Callable or local constant
+     * @param I18n         $i18n        I18n instance
+     * @param RouteParser  $routeParser Route parser
+     * @param callable|int $callback    Callable or local constant
      */
-    public function __construct(I18n $i18n, $callback = self::MAINTENANCE)
+    public function __construct(I18n $i18n, RouteParser $routeParser, callable|int $callback = self::MAINTENANCE)
     {
         $this->i18n = $i18n;
+        $this->routeParser = $routeParser;
 
         if ($callback === self::MAINTENANCE) {
             $this->callback = array($this, 'maintenancePage');
@@ -118,16 +125,14 @@ class UpdateAndMaintenance
     /**
      * Renders the page
      *
-     * @param \Psr\Http\Message\ServerRequestInterface $request  PSR7 request
-     * @param string                                   $contents HTML page contents
+     * @param Request $request  PSR7 request
+     * @param string  $contents HTML page contents
      *
      * @return string
      */
     private function renderPage(Request $request, $contents)
     {
-        $routeContext = RouteContext::fromRequest($request);
-        $routeParser = $routeContext->getRouteParser();
-        $path = $routeParser->urlFor('slash');
+        $path = $this->routeParser->urlFor('slash');
 
         //add ending / if missing
         if (
@@ -145,7 +150,6 @@ class UpdateAndMaintenance
     <head>
         <title>" . _T("Galette needs update!") . "</title>
         <meta charset=\"UTF-8\"/>
-        <link rel=\"stylesheet\" type=\"text/css\" href=\"" . $theme_path . "../../assets/css/galette-main.bundle.min.css\"/>
         <link rel=\"stylesheet\" type=\"text/css\" href=\"" . $theme_path . "ui/semantic.min.css\"/>
         <link rel=\"shortcut icon\" href=\"" . $theme_path . "images/favicon.png\"/>
     </head>
@@ -168,7 +172,7 @@ class UpdateAndMaintenance
     /**
      * Displays maintenance page
      *
-     * @param \Psr\Http\Message\ServerRequestInterface $request PSR7 request
+     * @param Request $request PSR7 request
      *
      * @return string
      */
@@ -182,7 +186,7 @@ class UpdateAndMaintenance
     /**
      * Displays needs update page
      *
-     * @param \Psr\Http\Message\ServerRequestInterface $request PSR7 request
+     * @param Request $request PSR7 request
      *
      * @return string
      */
index 43589910bdcf75e1a51fd088ea1e271492536338..43a12aa1268f6763b459459ff98bdd087bcc54ba 100644 (file)
                                 <div class="field{% if field.isRequired() %} required{% endif %}{% if get_class(field) == 'Galette\\DynamicFields\\File' %} wide{% endif %}">
                                     {{ _self.draw_field(field, field_data, disabled, loop.index, object, masschange) }}
                                     {% if field.getInformation() %}
-                                        <p class="exemple">{{ field.getInformation()|raw }}</p>
+                                        <div class="exemple">{{ field.getInformation()|raw }}</div>
                                     {% endif %}
                                 </div>
                             {% endfor %}
diff --git a/galette/templates/default/elements/ajax_messages.html.twig b/galette/templates/default/elements/ajax_messages.html.twig
deleted file mode 100644 (file)
index 61b940f..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-{% extends 'ajax.html.twig' %}
-
-{% block content %}
-    {% include 'elements/messages_inline.html.twig' %}
-{% endblock %}
index ffd0cc36d4ae7dfe7f1a83386fb5dbec5a4d42de..e1c9ca8b4665e5af071d89116bb0b8931b492d3e 100644 (file)
@@ -14,19 +14,24 @@ $('.message.with-transition')
 ;
 
 {# Let's see if there are success messages to show #}
-{% set successes = flash.getMessage('success_detected') %}
+{% set success = flash.getMessage('success_detected') ?? [] %}
+{% set success = success|merge(flash.getMessage('success') ?? []) %}
 {% if success_detected is defined and success_detected is iterable %}
-    {% for s in success_detected %}
-        {% set successes = successes|merge([s]) %}
+    {% for entry in success_detected %}
+        {% set success = success|merge([entry]) %}
     {% endfor %}
 {% endif %}
-{% if successes is iterable and successes|length > 0 %}
-    {% for success in successes %}
+{% if success is iterable and success|length > 0 %}
+    {% for entry in success %}
         $('body')
           .toast({
             displayTime: 'auto',
-            position: 'bottom right',
-            message: '{{ success|e('js') }}',
+            minDisplayTime: 5000,
+            wordsPerMinute: 80,
+            showProgress: 'bottom',
+            closeIcon: true,
+            position: 'top attached',
+            message: '{{ entry|e('js') }}',
             showIcon: 'check circle outline',
             class: 'success'
           })
@@ -34,6 +39,94 @@ $('.message.with-transition')
     {% endfor %}
 {% endif %}
 
+{# Let's see if there are info messages to show #}
+{% set info = flash.getMessage('info_detected') ?? [] %}
+{% set info = info|merge(flash.getMessage('info') ?? []) %}
+{% if info_detected is defined and info_detected is iterable %}
+    {% for entry in info_detected %}
+        {% set info = info|merge([entry]) %}
+    {% endfor %}
+{% endif %}
+{% if info is iterable and info|length > 0 %}
+    {% for entry in info %}
+        {% set info_title = '' %}
+        {% set info_message = entry %}
+        {% if entry is iterable %}
+            {% set info_title = entry.title %}
+            {% set info_message = entry.message %}
+        {% endif %}
+        $('body')
+          .toast({
+            displayTime: 'auto',
+            minDisplayTime: 5000,
+            wordsPerMinute: 80,
+            showProgress: 'bottom',
+            closeIcon: true,
+            position: 'top attached',
+            title: '{{ info_title|e('js') }}',
+            message: '{{ info_message|e('js') }}',
+            showIcon: 'info',
+            class: 'info'
+          })
+        ;
+    {% endfor %}
+{% endif %}
+
+{# Let's see if there are loginfault messages to show #}
+{% set loginfaults = flash.getMessage('loginfault') %}
+{% if loginfault_detected is defined and loginfault_detected is iterable %}
+    {% for l in loginfault_detected %}
+        {% set loginfaults = loginfaults|merge([l]) %}
+    {% endfor %}
+{% endif %}
+
+{# Let's see if there are error messages to show #}
+{% set errors = flash.getMessage('error_detected') ?? [] %}
+{% set errors = errors|merge(flash.getMessage('error') ?? []) %}
+{% set errors = errors|merge(loginfaults ?? []) %}
+{% if error_detected is defined and error_detected is iterable %}
+    {% for e in error_detected %}
+        {% set errors = errors|merge([e]) %}
+    {% endfor %}
+{% endif %}
+{% if errors is iterable and errors|length > 0 %}
+    {% for error in errors %}
+        $('body')
+          .toast({
+            displayTime: 0,
+            closeIcon: true,
+            position: 'top attached',
+            message: '{{ error|e('js') }}',
+            showIcon: 'exclamation circle',
+            class: 'error'
+          })
+        ;
+    {% endfor %}
+{% endif %}
+
+{# Let's see if there are warning messages to show #}
+{% set warnings = flash.getMessage('warning_detected') ?? [] %}
+{% set warnings = warnings|merge(flash.getMessage('warning') ?? []) %}
+{% if warning_detected is defined and warning_detected is iterable %}
+    {% for w in warning_detected %}
+        {% set warnings = warnings|merge([w]) %}
+    {% endfor %}
+{% endif %}
+{% if warnings is iterable and warnings|length > 0 %}
+    {% for warning in warnings %}
+        $('body')
+          .toast({
+            displayTime: 0,
+            closeIcon: true,
+            position: 'top attached',
+            message: '{{ warning|e('js') }}',
+            showIcon: 'exclamation triangle',
+            class: 'warning'
+          })
+        ;
+    {% endfor %}
+{% endif %}
+
 {# Renew telemetry #}
 {% if renew_telemetry is defined and renew_telemetry %}
     $('body')
index 97ef6e8e2eb8fd0b9dcd452e4403f355de2f2bea..e7ebb208a9f2b98d79b4a1df2bb0fb7b40d72c9a 100644 (file)
@@ -52,6 +52,7 @@
  * - modal_approve_text: modal's approve button's text.
  * - modal_approve_icon: modal's approve button's icon.
  * - modal_cancel_text: modal's cancel button's text.
+ * - modal_action_onshow: additionnal code to execute on modal's onShow event.
  *
  * @see loader.js.twig
  * @see modal.js.twig
                 } %}
             },
             error: function() {
+                {# Use "only" keyword to prevent known but not explicitiely defined variables to be passed #}
                 {% include "elements/js/modal.js.twig" with {
                     modal_title_twig: _T("An error occurred :(")|e("js"),
                     modal_without_content: true,
                     modal_deny_only: true,
                     modal_cancel_text: _T("Close")|e("js"),
                     modal_classname: "redalert",
-                } %}
+                } only %}
             }
         });
     });
             inline: false,
             addTouchEvents: false,
         });
+{% if modal_action_onshow is defined %}
+        {{ modal_action_onshow|raw }}
+{% endif %}
     }
index 59d12ead7b4dd07bbfcd75ae518bcd1fc2f88cb5..9067e15019bc6bb2ce01e8ea783f362dbee04ee4 100644 (file)
                                     $.ajax({
                                         url: '{{ url_for("ajaxMessages") }}',
                                         method: "GET",
-                                        success: function (message) {
-                                            $('.main-content .message').remove();
-                                            $('.main-content').prepend(message);
+                                        success: function (values) {
+                                            for (var type in values) {
+                                                var dtime = 0;
+                                                if (type == 'success') {
+                                                    dtime = 'auto';
+                                                }
+                                                $('body')
+                                                    .toast({
+                                                        displayTime: dtime,
+                                                        minDisplayTime: 5000,
+                                                        wordsPerMinute: 80,
+                                                        showProgress: 'bottom',
+                                                        closeIcon: true,
+                                                        position: 'top attached',
+                                                        title: values[type]['title'],
+                                                        message: values[type]['messages'].join('<br/>'),
+                                                        showIcon: values[type]['icon'],
+                                                        class: type
+                                                    })
+                                                ;
+                                            }
                                         }
                                     });
                                 }
index 723f5cfb342904fd0a62405e85b3000bb72f4430..b5366db09d836d2047ff327b8600412bf113419f 100644 (file)
                     <div class="menu">
                         <div class="item">
                             <div class="ui basic center aligned fitted segment">
+            {% if preferences.pref_website is not empty %}
+                                <a href="{{ preferences.pref_website }}" target="_blank">
+                                    <img src="{{ url_for('logo') }}" width="{{ logo.getOptimalWidth() }}" height="{{ logo.getOptimalHeight() }}" alt="{{ preferences.pref_nom }}" class="icon" title="{{ _T("Open '%s' in a new window")|replace({"%s": preferences.pref_website}) }}"/>
+                                </a>
+            {% else %}
                                 <img src="{{ url_for('logo') }}" width="{{ logo.getOptimalWidth() }}" height="{{ logo.getOptimalHeight() }}" alt="{{ preferences.pref_nom }}" class="icon"/>
+            {% endif %}
                                 <div class="ui block huge brand header">
                                     {{ preferences.pref_nom }}
                                     {% if preferences.pref_slogan %}<div class="sub tiny header">{{ __(preferences.pref_slogan) }}</div>{% endif %}
index 8645aef37cdaa612a692b1cd0b2ef39d9dae3911..a078ae270bcffda74d8a8d12dbe23d82acdbf2dc 100644 (file)
@@ -7,83 +7,89 @@
 {% endif %}
 
 {# Let's see if there are error messages to show #}
-{% set errors = flash.getMessage('error_detected') ?? []|merge(flash.getMessage('error') ?? [])|merge(loginfaults ?? []) %}
+{% set errors = flash.getMessage('error_detected') ?? [] %}
+{% set errors = errors|merge(flash.getMessage('error') ?? []) %}
+{% set errors = errors|merge(loginfaults ?? []) %}
 {% if error_detected is defined and error_detected is iterable %}
     {% for e in error_detected %}
         {% set errors = errors|merge([e]) %}
     {% endfor %}
 {% endif %}
 {% if errors is iterable and errors|length > 0 %}
-    <div class="ui error icon message with-transition">
-        <i class="times icon" aria-hidden="true"></i>
-        <i class="window close outline icon" aria-hidden="true"></i>
-        <div class="content">
-            <div class="header">{{ _T("- ERROR -") }}</div>
-            {% if errors|length > 1 %}
-                <ul class="list">
-                {% for error in errors %}
-                    <li>{{ error|raw }}</li>
-                {% endfor %}
-                </ul>
-            {% else %}
-                {% for error in errors %}
-                    <p>{{ error|raw }}</p>
-                {% endfor %}
-            {% endif %}
+    <noscript>
+        <div class="ui error icon message">
+            <i class="exclamation circle icon" aria-hidden="true"></i>
+            <div class="content">
+                <div class="header">{{ _T("- ERROR -") }}</div>
+                {% if errors|length > 1 %}
+                    <ul class="list">
+                    {% for error in errors %}
+                        <li>{{ error|raw }}</li>
+                    {% endfor %}
+                    </ul>
+                {% else %}
+                    {% for error in errors %}
+                        <p>{{ error|raw }}</p>
+                    {% endfor %}
+                {% endif %}
+            </div>
         </div>
-    </div>
+    </noscript>
 {% endif %}
 
 {# Let's see if there are warning messages to show #}
-{% set warnings = flash.getMessage('warning_detected') ?? []|merge(flash.getMessage('warning') ?? []) %}
+{% set warnings = flash.getMessage('warning_detected') ?? [] %}
+{% set warnings = warnings|merge(flash.getMessage('warning') ?? []) %}
 {% if warning_detected is defined and warning_detected is iterable %}
     {% for w in warning_detected %}
         {% set warnings = warnings|merge([w]) %}
     {% endfor %}
 {% endif %}
 {% if warnings is iterable and warnings|length > 0 %}
-    <div class="ui warning icon message with-transition">
-        <i class="exclamation triangle icon" aria-hidden="true"></i>
-        <i class="window close outline icon" aria-hidden="true"></i>
-        <div class="content">
-            <div class="header">{{ _T("- WARNING -") }}</div>
-            {% if warnings|length > 1 %}
-                <ul class="list">
-                {% for warning in warnings %}
-                    <li>{{ warning|raw }}</li>
-                {% endfor %}
-                </ul>
-            {% else %}
-                {% for warning in warnings %}
-                    <p>{{ warning|raw }}</p>
-                {% endfor %}
-            {% endif %}
+    <noscript>
+        <div class="ui warning icon message">
+            <i class="exclamation triangle icon" aria-hidden="true"></i>
+            <div class="content">
+                <div class="header">{{ _T("- WARNING -") }}</div>
+                {% if warnings|length > 1 %}
+                    <ul class="list">
+                    {% for warning in warnings %}
+                        <li>{{ warning|raw }}</li>
+                    {% endfor %}
+                    </ul>
+                {% else %}
+                    {% for warning in warnings %}
+                        <p>{{ warning|raw }}</p>
+                    {% endfor %}
+                {% endif %}
+            </div>
         </div>
-    </div>
+    </noscript>
 {% endif %}
 
 {# Let's see if there are success messages to show #}
-{% set successs = flash.getMessage('success_detected') ?? []|merge(flash.getMessage('success') ?? []) %}
+{% set success = flash.getMessage('success_detected') ?? [] %}
+{% set success = success|merge(flash.getMessage('success') ?? []) %}
 {% if success_detected is defined and success_detected is iterable %}
-    {% for s in success_detected %}
-        {% set successs = successs|merge([s]) %}
+    {% for entry in success_detected %}
+        {% set success = success|merge([entry]) %}
     {% endfor %}
 {% endif %}
-{% if successs is iterable and successs|length > 0 %}
+{% if success is iterable and success|length > 0 %}
     <noscript>
         <div class="ui success icon message">
             <i class="check circle outline icon" aria-hidden="true"></i>
             <i class="window close outline icon" aria-hidden="true"></i>
             <div class="content">
-            {% if successs|length > 1 %}
+            {% if success|length > 1 %}
                 <ul class="list">
-                {% for success in successs %}
-                    <li>{{ success|raw }}</li>
+                {% for entry in success %}
+                    <li>{{ entry|raw }}</li>
                 {% endfor %}
                 </ul>
             {% else %}
-                {% for success in successs %}
-                    <p>{{ success|raw }}</p>
+                {% for entry in success %}
+                    <p>{{ entry|raw }}</p>
                 {% endfor %}
             {% endif %}
             </div>
     </noscript>
 {% endif %}
 
+{# Let's see if there are info messages to show #}
+{% set info = flash.getMessage('info_detected') ?? [] %}
+{% set info = info|merge(flash.getMessage('info') ?? []) %}
+{% if info_detected is defined and info_detected is iterable %}
+    {% for i in info_detected %}
+        {% set info = info|merge([i]) %}
+    {% endfor %}
+{% endif %}
+{% if info is iterable and info|length > 0 %}
+    <noscript>
+        <div class="ui info icon message">
+            <i class="info icon" aria-hidden="true"></i>
+            <div class="content">
+                {% if info|length > 1 %}
+                    <ul class="list">
+                        {% for i in info %}
+                            <li>{{ i|raw }}</li>
+                        {% endfor %}
+                    </ul>
+                {% else %}
+                    {% for entry in info %}
+                        {% set info_title = '' %}
+                        {% set info_message = entry %}
+                        {% if entry is iterable %}
+                            {% set info_title = entry.title %}
+                            <p class="header">{{ info_title }}</p>
+                            {% set info_message = entry.message %}
+                        {% endif %}
+                        <p>{{ entry|raw }}</p>
+                    {% endfor %}
+                {% endif %}
+            </div>
+        </div>
+    </noscript>
+{% endif %}
+
 {# Renew telemetry #}
 {% if renew_telemetry is defined and renew_telemetry %}
     {% include "modals/telemetry.html.twig" with {part: "dialog"} %}
index fee09dcea6d56225057d56a777f2a51cf647a00a..39de2449a44c2d8d9aafd8a40b8a7dd7fab7acd7 100644 (file)
@@ -5,7 +5,13 @@
 
 {% if not login.getCompactMenu() %}
     <div class="ui basic center aligned fitted segment">
+    {% if preferences.pref_website is not empty %}
+        <a href="{{ preferences.pref_website }}" target="_blank">
+            <img src="{{ url_for('logo') }}" width="{{ logo.getOptimalWidth() }}" height="{{ logo.getOptimalHeight() }}" alt="{{ preferences.pref_nom }}" class="icon" title="{{ _T("Open '%s' in a new window")|replace({"%s": preferences.pref_website}) }}"/>
+        </a>
+    {% else %}
         <img src="{{ url_for('logo') }}" width="{{ logo.getOptimalWidth() }}" height="{{ logo.getOptimalHeight() }}" alt="{{ preferences.pref_nom }}" class="icon"/>
+    {% endif %}
         <div class="ui block huge brand header">
             {{ preferences.pref_nom }}
             {% if preferences.pref_slogan %}<div class="sub tiny header">{{ __(preferences.pref_slogan) }}</div>{% endif %}
index 89c0ef72c98a477cd9574db2d87f48363a7646b5..544a39ece2411e76b9d36e5f1caf993cab8f4b9b 100644 (file)
         <script type="text/javascript" src="{{ base_path() }}/assets/js/formatting.js"></script>
         <script type="text/javascript" src="{{ base_path() }}/assets/js/summernote.min.js"></script>
         <script type="text/javascript" src="{{ base_path() }}/assets/js/lang/summernote-{{ i18n.getID()|replace({'_': '-'}) }}.min.js"></script>
-        <script language="javascript">
-            function activateMailingEditor() {
-                if(!$('#mailing_html').attr('checked')){
-                    $('#mailing_html').attr('checked', true);
-                }
-
-                $('input#html_editor_active').attr('value', '1');
-                $('#activate_editor').remove();
-                $('#summernote_toggler').html('<a class="ui blue tertiary button" href="javascript:deactivateMailingEditor();" id="deactivate_editor">{{ _T("Deactivate HTML editor") }}</a>');
-
-                $('#mailing_corps').summernote({
-                    lang: '{{ i18n.getID()|replace({'_': '-'}) }}',
-                    disableDragAndDrop: true,
-                    height: 240,
-                    toolbar: [
+        <script type="text/javascript">
+            function activateHtmlEditor(elt, basic) {
+                if (basic === true) {
+                    var _toolbar = [
+                        ['font', ['bold', 'italic', 'strikethrough', 'clear']],
+                        ['para', ['ul', 'ol']],
+                        ['insert', ['link']],
+                        ['view', ['codeview']]
+                    ];
+                } else {
+                    var _toolbar = [
                         ['style', ['style']],
                         ['font', ['bold', 'italic', 'strikethrough', 'clear']],
                         ['para', ['ul', 'ol', 'paragraph']],
                         ['insert', ['link', 'picture']],
-                        ['view', ['codeview', 'help']]
-                    ],
+                        ['view', ['codeview']]
+                    ];
+                }
+                elt.summernote({
+                    lang: '{{ i18n.getID()|replace({'_': '-'}) }}',
+                    disableDragAndDrop: true,
+                    height: 240,
+                    toolbar: _toolbar,
                     styleTags: [
                         'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
                     ],
                         }
                     }
                 });
-                $('#mailing_corps').summernote('focus');
+                elt.summernote('focus');
+            }
+
+            function deactivateHtmlEditor(elt) {
+                elt.summernote('destroy');
+            }
+
+            function activateMailingEditor() {
+                if(!$('#mailing_html').attr('checked')){
+                    $('#mailing_html').attr('checked', true);
+                }
+
+                $('input#html_editor_active').attr('value', '1');
+                $('#activate_editor').remove();
+                $('#summernote_toggler').html('<a class="ui blue tertiary button" href="javascript:deactivateMailingEditor();" id="deactivate_editor">{{ _T("Deactivate HTML editor") }}</a>');
+
+                activateHtmlEditor($('#mailing_corps'));
             }
             function deactivateMailingEditor() {
-                $('#mailing_corps').summernote('destroy');
+                deactivateHtmlEditor($('#mailing_corps'));
                 $('#deactivate_editor').remove();
                 $('#summernote_toggler').html('<a class="ui blue tertiary button" href="javascript:activateMailingEditor();" id="activate_editor">{{ _T("Activate HTML editor") }}</a>');
             }
index 9c6ee07fa46786358474ba8b47c3657b68d29ec6..2e6567246825cf30e615eb551d7c3272f8047950 100644 (file)
                 <section class="content{% if contentcls is defined %} {{ contentcls }}{% endif %}{% if login.getCompactMenu() %} extended{% endif %}">
 {% if not login.isLogged() %}
                     <div class="ui basic center aligned fitted segment">
+    {% if preferences.pref_website is not empty %}
+                        <a href="{{ preferences.pref_website }}" target="_blank">
+                            <img src="{{ url_for("logo") }}" width="{{ logo.getOptimalWidth() }}" height="{{ logo.getOptimalHeight() }}" alt="{{ preferences.pref_nom }}" class="icon" title="{{ _T("Open '%s' in a new window")|replace({"%s": preferences.pref_website}) }}"/>
+                        </a>
+    {% else %}
                         <img src="{{ url_for("logo") }}" width="{{ logo.getOptimalWidth() }}" height="{{ logo.getOptimalHeight() }}" alt="{{ preferences.pref_nom }}" class="icon"/>
+    {% endif %}
                         <div class="ui large header">
                             {{ preferences.pref_nom }}
                             <div class="sub header">{% if preferences.pref_slogan %}{{ __(preferences.pref_slogan) }}{% endif %}</div>
index fc0eeec77f1105b0751226619744ab4d9e48b2fb..c49d0e98e400e6cb38c10b7d68a9b291585abe66 100644 (file)
 
 {% block javascripts %}
     <script>
-        $('#field_information').summernote({
-            lang: '{{ i18n.getID()|replace({'_': '-'}) }}',
-            height: 240,
-            toolbar: [
-                ['style', ['style']],
-                ['font', ['bold', 'italic', 'strikethrough', 'clear']],
-                ['para', ['ul', 'ol', 'paragraph']],
-                ['insert', ['link', 'picture']],
-                ['view', ['codeview', 'help']]
-            ],
-            styleTags: [
-                'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
-            ]
+        $(function() {
+            activateHtmlEditor($('#field_information'), true);
         });
-        $('#field_information').summernote('focus');
-
     </script>
 {% endblock %}
index bcc2b9aef8de3a151e7f377448a022544d74d51a..6c0dad989f9efeca0fdbf4d4b820cfe0cdf7d201 100644 (file)
@@ -96,7 +96,8 @@
                     modal_title_twig: _T("Edit field")|e("js"),
                     modal_class: "tiny",
                     modal_content_class: "scrolling",
-                    modal_onapprove: modal_onapprove
+                    modal_onapprove: modal_onapprove,
+                    modal_action_onshow: "activateHtmlEditor($('#field_information'), true);"
                 } %}
             }
             _editDynField();
index 605d336c7d88c2193213f932b9b6e26df40fbe8a..be0488be09bf67ceb7a02adb2f203e27348ceec1 100644 (file)
                                 $.ajax({
                                     url: '{{ url_for('ajaxMessages') }}',
                                     method: "GET",
-                                    success: function (message) {
-                                        $('#asso_name').after(message);
+                                    success: function (values) {
+                                        for (var type in values) {
+                                            var dtime = 0;
+                                            if (type == 'success') {
+                                                dtime = 'auto';
+                                            }
+                                            $('body')
+                                                .toast({
+                                                    displayTime: dtime,
+                                                    minDisplayTime: 5000,
+                                                    wordsPerMinute: 80,
+                                                    showProgress: 'bottom',
+                                                    closeIcon: true,
+                                                    position: 'top attached',
+                                                    title: values[type]['title'],
+                                                    message: values[type]['messages'].join('<br/>'),
+                                                    showIcon: values[type]['icon'],
+                                                    class: type
+                                                })
+                                            ;
+                                        }
                                     }
                                 });
                             }
index 5a8eef96bc56a06d5edcaab4067460a487290dd1..8459ffb88da3c01e10796c068b81ccbbc6635174 100644 (file)
                                     $.ajax({
                                         url: '{{ url_for('ajaxMessages') }}',
                                         method: "GET",
-                                        success: function(message) {
-                                            var message_inline = new DOMParser().parseFromString(message, 'text/html');
-                                            var message_content = message_inline.body.querySelectorAll('div.content');
-                                            $('body').toast({
-                                                position: 'bottom right',
-                                                message: message_content,
-                                                showIcon: 'check circle outline',
-                                                class: 'success'
-                                            });
+                                        success: function (values) {
+                                            for (var type in values) {
+                                                var dtime = 0;
+                                                if (type == 'success') {
+                                                    dtime = 'auto';
+                                                }
+                                                $('body')
+                                                    .toast({
+                                                        displayTime: dtime,
+                                                        minDisplayTime: 5000,
+                                                        wordsPerMinute: 80,
+                                                        showProgress: 'bottom',
+                                                        closeIcon: true,
+                                                        position: 'top attached',
+                                                        title: values[type]['title'],
+                                                        message: values[type]['messages'].join('<br/>'),
+                                                        showIcon: values[type]['icon'],
+                                                        class: type
+                                                    })
+                                                ;
+                                            }
                                         }
                                     });
                                 },
index 07002d19bda6eef86c7d6c10051afb38713f6bcb..93378d260e3df062fb7611de93efed4e139cfc47 100644 (file)
                 <section class="{% if login.isLogged() %}content{% else %}vertically centered{% endif %}">
 {% if not login.isLogged() %}
                     <div class="ui basic center aligned fitted segment">
+    {% if preferences.pref_website is not empty %}
+                        <a href="{{ preferences.pref_website }}" target="_blank">
+                            <img src="{{ url_for("logo") }}" width="{{ logo.getOptimalWidth() }}" height="{{ logo.getOptimalHeight() }}" alt="{{ preferences.pref_nom }}" class="logo" title="{{ _T("Open '%s' in a new window")|replace({"%s": preferences.pref_website}) }}"/>
+                        </a>
+    {% else %}
                         <img src="{{ url_for("logo") }}" width="{{ logo.getOptimalWidth() }}" height="{{ logo.getOptimalHeight() }}" alt="{{ preferences.pref_nom }}" class="logo"/>
+    {% endif %}
                         <div class="ui large header">
                             {{ preferences.pref_nom }}
                             <div class="sub header">{% if preferences.pref_slogan %}{{ __(preferences.pref_slogan) }}{% endif %}</div>
index 7b8ea16ac18e67aeb7ea034b70625850d09f82f3..ef2301e26b0f8543d85c74fce682f04acfb4aade 100644 (file)
@@ -18,6 +18,6 @@
   "permission": "644",
   "autoInstall": true,
   "rtl": "both",
-  "components": ["reset", "site", "button", "container", "divider", "header", "icon", "input", "label", "list", "loader", "segment", "step", "form", "grid", "menu", "message", "table", "card", "item", "accordion", "checkbox", "dimmer", "dropdown", "popup", "sidebar", "tab", "transition", "text", "calendar", "toast", "modal", "api", "search", "emoji"],
+  "components": ["reset", "site", "button", "container", "divider", "header", "icon", "input", "label", "list", "loader", "segment", "step", "form", "grid", "menu", "message", "table", "card", "item", "accordion", "checkbox", "dimmer", "dropdown", "popup", "sidebar", "tab", "transition", "text", "calendar", "progress", "toast", "modal", "api", "search", "emoji"],
   "version": "2.9.3"
 }
\ No newline at end of file
index afe9f5b63a989eb6c09ab0fb68c50b22e0939117..5b92752ef4acf36b309c09c12691f50ddb3c773a 100644 (file)
@@ -36,6 +36,7 @@
 
 namespace Galette\Core\test\units;
 
+use PHPMailer\PHPMailer\PHPMailer;
 use PHPUnit\Framework\TestCase;
 
 /**
@@ -563,14 +564,21 @@ class Preferences extends TestCase
      */
     public function testGetMailSignature()
     {
-        $this->assertSame("\r\n-- \r\nGalette\r\n\r\n", $this->preferences->getMailSignature());
+        $mail = new PHPMailer();
+        $this->assertSame("\r\n-- \r\nGalette", $this->preferences->getMailSignature($mail));
 
         $this->preferences->pref_website = 'https://galette.eu';
-        $this->assertSame("\r\n-- \r\nGalette\r\n\r\nhttps://galette.eu", $this->preferences->getMailSignature());
+        $this->assertSame(
+            "\r\n-- \r\nGalette\r\n\r\nhttps://galette.eu",
+            $this->preferences->getMailSignature($mail)
+        );
 
         //with legacy values
-        $this->preferences->pref_mailsign = "NAME}\r\n\r\n{WEBSITE}\r\n{GOOGLEPLUS}\r\n{FACEBOOK}\r\n{TWITTER}\r\n{LINKEDIN}\r\n{VIADEO}";
-        $this->assertSame("\r\n-- \r\nGalette\r\n\r\nhttps://galette.eu", $this->preferences->getMailSignature());
+        $this->preferences->pref_mail_sign = "{NAME}\r\n\r\n{WEBSITE}\r\n{FACEBOOK}\r\n{TWITTER}\r\n{LINKEDIN}\r\n{VIADEO}";
+        $this->assertSame(
+            "\r\n-- \r\nGalette\r\n\r\nhttps://galette.eu",
+            $this->preferences->getMailSignature($mail)
+        );
 
         $social = new \Galette\Entity\Social($this->zdb);
         $this->assertTrue(
@@ -586,7 +594,10 @@ class Preferences extends TestCase
         );
 
         $this->preferences->pref_mail_sign = "{ASSO_NAME}\r\n\r\n{ASSO_WEBSITE} - {ASSO_SOCIAL_MASTODON}";
-        $this->assertSame("\r\n-- \r\nGalette\r\n\r\nhttps://galette.eu - https://framapiaf.org/@galette", $this->preferences->getMailSignature());
+        $this->assertSame(
+            "\r\n-- \r\nGalette\r\n\r\nhttps://galette.eu - https://framapiaf.org/@galette",
+            $this->preferences->getMailSignature($mail)
+        );
 
         $social = new \Galette\Entity\Social($this->zdb);
         $this->assertTrue(
@@ -600,7 +611,10 @@ class Preferences extends TestCase
             2,
             \Galette\Entity\Social::getListForMember(null, \Galette\Entity\Social::MASTODON)
         );
-        $this->assertSame("\r\n-- \r\nGalette\r\n\r\nhttps://galette.eu - https://framapiaf.org/@galette, Galette mastodon URL - the return", $this->preferences->getMailSignature());
+        $this->assertSame(
+            "\r\n-- \r\nGalette\r\n\r\nhttps://galette.eu - https://framapiaf.org/@galette, Galette mastodon URL - the return",
+            $this->preferences->getMailSignature($mail)
+        );
     }
 
     /**
@@ -643,4 +657,30 @@ class Preferences extends TestCase
             $legend['socials']['patterns']['asso_social_mynewtype']
         );
     }
+
+    /**
+     * Test website URL
+     *
+     * @return void
+     */
+    public function testWebsiteURL(): void
+    {
+        $preferences = [];
+        foreach ($this->preferences->getDefaults() as $key => $value) {
+            $preferences[$key] = $value;
+        }
+
+        $post = array_merge($preferences, ['pref_website' => 'https://galette.eu']);
+        $this->assertTrue(
+            $this->preferences->check($post, $this->login),
+            print_r($this->preferences->getErrors(), true)
+        );
+
+        $post = array_merge($preferences, ['pref_website' => 'galette.eu']);
+        $this->assertFalse(
+            $this->preferences->check($post, $this->login),
+            print_r($this->preferences->getErrors(), true)
+        );
+        $this->assertSame(['- Invalid website URL.'], $this->preferences->getErrors());
+    }
 }
index 07d097a5ca3ca18ba4e0f4565368ec0363870749..9221379cd1f79bb3750beb22395f0781fdcbe735 100644 (file)
@@ -334,7 +334,7 @@ class PdfModel extends GaletteTestCase
         );
 
         $this->assertMatchesRegularExpression(
-            '/<td id="pdf_logo"><img src="@.+" width="129" height="60" \/><\/td>/',
+            '/<td id="pdf_logo"><img src="@.+" width="129" height="60" alt="" \/><\/td>/',
             $model->hheader
         );
 
diff --git a/tests/Galette/Features/tests/units/HasEvent.php b/tests/Galette/Features/tests/units/HasEvent.php
new file mode 100644 (file)
index 0000000..2f17592
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * HasEvents tests
+ *
+ * PHP version 5
+ *
+ * Copyright © 2024 The Galette Team
+ *
+ * This file is part of Galette (http://galette.tuxfamily.org).
+ *
+ * Galette is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Galette is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Galette. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Features
+ * @package   GaletteTests
+ *
+ * @author    Johan Cwiklinski <johan@x-tnd.be>
+ * @copyright 2024 The Galette Team
+ * @license   https://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
+ * @link      https://galette.eu
+ */
+
+namespace Galette\Entity\test\units;
+
+use Galette\GaletteTestCase;
+
+/**
+ * HasEvent tests class
+ *
+ * @category  Features
+ * @name      HasEvents
+ * @package   GaletteTests
+ * @author    Johan Cwiklinski <johan@x-tnd.be>
+ * @copyright 2024 The Galette Team
+ * @license   https://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
+ * @link      https://galette.eu
+ */
+class HasEvent extends GaletteTestCase
+{
+    protected int $seed = 20240223092214;
+
+    /**
+     * Test HasEvent capacities
+     *
+     * @return void
+     */
+    public function testCapacities(): void
+    {
+        $this->adh = new \Galette\Entity\Adherent($this->zdb);
+
+        //per default, add and edit events are active on contributions
+        $contrib = new \Galette\Entity\Contribution($this->zdb, $this->login);
+        $this->assertTrue($contrib->areEventsEnabled());
+        $this->assertTrue($contrib->hasAddEvent());
+        $this->assertTrue($contrib->hasEditEvent());
+        $this->assertFalse($contrib->hasDeleteEvent());
+        $this->assertEquals('contribution.add', $contrib->getAddEventName());
+        $this->assertEquals('contribution.edit', $contrib->getEditEventName());
+        $this->assertNull($contrib->getDeleteEventName());
+
+        //per default, add and edit events are active on members
+        $this->assertTrue($this->adh->areEventsEnabled());
+        $this->assertTrue($this->adh->hasAddEvent());
+        $this->assertTrue($this->adh->hasEditEvent());
+        $this->assertFalse($this->adh->hasDeleteEvent());
+        $this->assertEquals('member.add', $this->adh->getAddEventName());
+        $this->assertEquals('member.edit', $this->adh->getEditEventName());
+        $this->assertNull($this->adh->getDeleteEventName());
+
+        //disable add event
+        $this->adh->withoutAddEvent();
+        $this->assertFalse($this->adh->hasAddEvent());
+        $this->assertNull($this->adh->getAddEventName());
+        $this->assertTrue($this->adh->hasEditEvent());
+        //enable add event
+        $this->adh->withAddEvent();
+        $this->assertTrue($this->adh->hasAddEvent());
+
+        //disable edit event
+        $this->adh->withoutEditEvent();
+        $this->assertTrue($this->adh->hasAddEvent());
+        $this->assertFalse($this->adh->hasEditEvent());
+        $this->assertNull($this->adh->getEditEventName());
+        //enable edit event
+        $this->adh->withEditEvent();
+        $this->assertTrue($this->adh->hasEditEvent());
+
+        //enable delete event
+        $this->adh->withDeleteEvent();
+        $this->assertTrue($this->adh->hasDeleteEvent());
+        $this->assertEquals('member.delete', $this->adh->getDeleteEventName());
+        //disable delete event
+        $this->adh->withoutDeleteEvent();
+        $this->assertFalse($this->adh->hasDeleteEvent());
+
+        // disable all events
+        $this->adh->disableEvents();
+        $this->assertFalse($this->adh->areEventsEnabled());
+        $this->assertFalse($this->adh->hasAddEvent());
+        $this->assertFalse($this->adh->hasEditEvent());
+        $this->assertFalse($this->adh->hasDeleteEvent());
+        $this->assertNull($this->adh->getAddEventName());
+        $this->assertNull($this->adh->getEditEventName());
+        $this->assertNull($this->adh->getDeleteEventName());
+
+        //reactivate events
+        $this->adh->activateEvents();
+        $this->assertTrue($this->adh->areEventsEnabled());
+        $this->assertTrue($this->adh->hasAddEvent());
+        $this->assertTrue($this->adh->hasEditEvent());
+        $this->assertFalse($this->adh->hasDeleteEvent());
+    }
+}
index 8fc67d729b982478f6ea5fdd13b1549cf4315861..9763de76fc17c501d5ab88c108b3cc6e40097773 100755 (executable)
@@ -47,6 +47,7 @@ define('GALETTE_PLUGINS_PATH', GALETTE_TESTS_PATH . '/plugins/');
 define('GALETTE_TPL_SUBDIR', 'templates/default/');
 define('GALETTE_THEME', 'themes/default/');
 define('GALETTE_DATA_PATH', GALETTE_TESTS_PATH . '/tests-data/');
+define('GALETTE_CACHE_DIR', GALETTE_DATA_PATH . 'cache/');
 if (is_dir(GALETTE_DATA_PATH)) {
     $files = new RecursiveIteratorIterator(
         new RecursiveDirectoryIterator(
index 60c41127aaefaeda16eaa7664c92f1b4f62bbb99..38e1da91567097d40a0399723ab68be87486c6a8 100644 (file)
@@ -58,3 +58,6 @@
 
 @pluginsActiveBackground    : #d9f7d8;
 @pluginsInactiveBackground  : #ffead8;
+
+@infoColor: @blue;
+@warningColor: #e35a00;