]> git.agnieray.net Git - galette.git/commitdiff
Advanced search on contributions dynamic fields
authorJohan Cwiklinski <johan@x-tnd.be>
Fri, 15 Sep 2023 05:59:59 +0000 (07:59 +0200)
committerJohan Cwiklinski <johan@x-tnd.be>
Tue, 3 Oct 2023 18:26:07 +0000 (20:26 +0200)
fixes #1693

Display date dynamic fields searching contributions
Display boolean dynamic fields searching contributions
Handle dynamic booleans and date with advanced search within contributions
Fix existing values in advanced search for contribution dynamic fields
Add tests
Fixes on postgresql
Fix search on dynamic choice fields

galette/lib/Galette/Controllers/Crud/MembersController.php
galette/lib/Galette/Repository/Members.php
galette/templates/default/pages/advanced_search.html.twig
tests/Galette/Repository/tests/units/Members.php

index e9f91ba5cbb8a892bf041a5b77abc54228e4e803..f987beca73b298d323731fcefc45928717de0702 100644 (file)
@@ -498,14 +498,9 @@ class MembersController extends CrudController
     public function filter(Request $request, Response $response): Response
     {
         $post = $request->getParsedBody();
-        if (isset($this->session->filter_members)) {
-            //CAUTION: this one may be simple or advanced, display must change
-            $filters = $this->session->filter_members;
-        } else {
-            $filters = new MembersList();
-        }
+        $filters = $this->session->filter_members ?? new MembersList();
 
-        //reintialize filters
+        //reinitialize filters
         if (isset($post['clear_filter'])) {
             $filters = new MembersList();
         } elseif (isset($post['clear_adv_filter'])) {
index 5d79a94ff3945b0d545545b7091bf7976af5c652..3e2ee6fe51af68f32a939173c0fbd6e298f110eb 100644 (file)
@@ -714,7 +714,8 @@ class Members
 
                     //check if there are dynamic fields in the filter
                     foreach ($this->filters->contrib_dynamic as $k => $cd) {
-                        if (is_array($cd)) {
+                        $dyn_field = DynamicField::loadFieldType($zdb, (int)$k);
+                        if ($dyn_field instanceof \Galette\DynamicFields\Choice) {
                             $hasCdfc = true;
                             $cdfcs[] = $k;
                         }
@@ -754,24 +755,26 @@ class Members
 
             // choice dynamic fields
             if ($hasCdfc === true) {
-                $cdf_field = 'cdf.id';
-                if (TYPE_DB === 'pgsql') {
-                    $cdf_field .= '::text';
-                }
-
-                $cdf_field = 'cdfc.id';
-                if (TYPE_DB === 'pgsql') {
-                    $cdf_field .= '::text';
-                }
                 foreach ($cdfcs as $cdf) {
-                    $rcdf_field = str_replace(
-                        'cdfc.',
-                        'cdfc' . $cdf . '.',
-                        $cdf_field
+                    $rcdf_field = sprintf(
+                        '%s.%s',
+                        $zdb->platform->quoteIdentifier('cdfc' . $cdf),
+                        $zdb->platform->quoteIdentifier('id')
                     );
+                    if (TYPE_DB === 'pgsql') {
+                        $rcdf_field = $rcdf_field . '::text';
+                    }
+
                     $select->join(
                         array('cdfc' . $cdf => DynamicField::getFixedValuesTableName($cdf, true)),
-                        $rcdf_field . '=dfc.field_val',
+                        new Expression(
+                            sprintf(
+                                '%s = %s.%s',
+                                $rcdf_field,
+                                $zdb->platform->quoteIdentifier('dfc'),
+                                $zdb->platform->quoteIdentifier('field_val')
+                            )
+                        ),
                         array(),
                         $select::JOIN_LEFT
                     );
@@ -1360,9 +1363,6 @@ class Members
             && !isset($this->filters->contrib_dynamic['empty'])
         ) {
             foreach ($this->filters->contrib_dynamic as $k => $cd) {
-                $qry = '';
-                $prefix = 'a.';
-                $field = null;
                 $qop = ' LIKE ';
 
                 if (is_array($cd)) {
@@ -1379,9 +1379,34 @@ class Members
                     $qry = 'dfc.field_form = \'contrib\' AND ' .
                         'dfc.field_id = ' . $k . ' AND ';
                     $field = 'field_val';
-                    $qry .= 'LOWER(' . $prefix . $field . ') ' .
-                        $qop . ' ';
-                    $select->where($qry . $zdb->platform->quoteValue('%' . strtolower($cd) . '%'));
+
+                    $dyn_field = DynamicField::loadFieldType($zdb, (int)$k);
+
+                    if ($dyn_field instanceof \Galette\DynamicFields\Boolean) {
+                        if ($cd == 1) {
+                            $qry .= $field . ' = ' . (int)$cd;
+                        }
+                        $select->where($qry);
+                    } elseif ($dyn_field instanceof \Galette\DynamicFields\Date) {
+                        //dynamic dates are stored in their localized format :/
+                        //use current lang format to query for now
+                        //FIXME works with french formatted date only -_-
+                        if ($zdb->isPostgres()) {
+                            $qop = '=';
+                            $store_fmt = __("Y-m-d") === 'Y-m-d' ? 'YYYY-MM-DD' : 'DD/MM/YYYY';
+                            $cd = "to_date('" . $cd . "', '" . $store_fmt . "')";
+                            $qry .= "to_date(" . $prefix . $field . ", '$store_fmt')";
+                        } else {
+                            $store_fmt = __("Y-m-d") === 'Y-m-d' ? '%Y-%m-%d' : '%d/%m/%Y';
+                            $cd = "STR_TO_DATE('" . $cd . "', '" . $store_fmt . "')";
+                            $qry .= 'STR_TO_DATE(' . $prefix . $field . ', \'' . $store_fmt . '\') ';
+                        }
+                        $qry .= $qop . ' ' . $cd;
+                        $select->where($qry);
+                    } else {
+                        $qry .= 'LOWER(' . $prefix . $field . ') ' . $qop . ' ';
+                        $select->where($qry . $zdb->platform->quoteValue('%' . strtolower($cd) . '%'));
+                    }
                 }
             }
         }
@@ -1473,13 +1498,14 @@ class Members
                     } else {
                         //dynamic dates are stored in their localized format :/
                         //use current lang format to query for now
+                        //FIXME works with french formatted date only -_-
                         if ($zdb->isPostgres()) {
-                            $fs['search'] = "to_date('" . $fs['search'] . "', 'YYYY-MM-DD')";
                             $store_fmt = __("Y-m-d") === 'Y-m-d' ? 'YYYY-MM-DD' : 'DD/MM/YYYY';
+                            $fs['search'] = "to_date('" . $fs['search'] . "', '" . $store_fmt . "')";
                             $qry .= "to_date('" . $prefix . $fs['field'] . "', '$store_fmt')";
                         } else {
-                            $fs['search'] = "STR_TO_DATE('" . $fs['search'] . "', '%Y-%m-%d')";
                             $store_fmt = __("Y-m-d") === 'Y-m-d' ? '%Y-%m-%d' : '%d/%m/%Y';
+                            $fs['search'] = "STR_TO_DATE('" . $fs['search'] . "', '" . $store_fmt . "')";
                             $qry .= 'STR_TO_DATE(' . $prefix . $fs['field'] . ', \'' . $store_fmt . '\') ';
                         }
 
index fbf611215007cc03e45c5da7b5244760d998879d..9227afd1a8499cf72acf95e966adf27913e02a94 100644 (file)
     {% else %}
         {% set rid = "cds_" ~ fid %}
     {% endif %}
+    {% if get_class(field) != "Galette\\DynamicFields\\Boolean" %}
                         <label for="cds{% if get_class(field) == "Galette\\DynamicFields\\Choice" %}c{% endif %}_{{ field.getId() }}">{{ field.getName() }}</label>
+    {% endif %}
     {% if get_class(field) == "Galette\\DynamicFields\\Line" %}
-                        <input type="text" name="cds_{{ field.getId() }}" id="cds_{{ field.getId() }}" value="{% if attribute(filters.contrib_dynamic, rid) is defined %}{{ attribute(filters.contrib_dynamic, rid) }}{% endif %}" />
+                        <input type="text" name="cds_{{ field.getId() }}" id="cds_{{ field.getId() }}" value="{% if attribute(filters.contrib_dynamic, fid) is defined %}{{ attribute(filters.contrib_dynamic, fid) }}{% endif %}" />
     {% elseif get_class(field) == "Galette\\DynamicFields\\Text" %}
-                        <textarea name="cds_{{ field.getId() }}" id="cds_{{ field.getId() }}">{% if attribute(filters.contrib_dynamic, rid) is defined %}{{ attribute(filters.contrib_dynamic, rid) }}{% endif %}</textarea>
+                        <textarea name="cds_{{ field.getId() }}" id="cds_{{ field.getId() }}">{% if attribute(filters.contrib_dynamic, fid) is defined %}{{ attribute(filters.contrib_dynamic, fid) }}{% endif %}</textarea>
     {% elseif get_class(field) == "Galette\\DynamicFields\\Choice" %}
                         <select name="cdsc_{{ field.getId() }}[]" id="cdsc_{{ field.getId() }}" multiple="multiple" class="ui dropdown nochosen">
                             <option value="">{{ _T('Select') }}</option>
         {% for k, choice in field.getValues() %}
-                            <option value="{{ k }}"{% if cds.field is defined and cds.field == rid %} selected="selected"{% endif %}>{{ choice }}</option>
+                            <option value="{{ k }}"{% if attribute(filters.contrib_dynamic, fid) is defined and k in attribute(filters.contrib_dynamic, fid) %} selected="selected"{% endif %}>{{ choice }}</option>
         {% endfor %}
                         </select>
+    {% elseif get_class(field) == "Galette\\DynamicFields\\Boolean" %}
+        <div class="ui right aligned toggle checkbox">
+            <input type="checkbox" name="cds_{{ field.getId() }}" id="cds_{{ field.getId() }}" value="1" {% if attribute(filters.contrib_dynamic, fid) is defined %} checked="checked"{% endif %}/>
+            <label for="cds_{{ field.getId() }}">{{ field.getName() }}</label>
+        </div>
+    {% elseif get_class(field) == "Galette\\DynamicFields\\Date" %}
+                    <div class="ui calendar" id="cds_{{ field.getId() }}-rangeend">
+                        <div class="ui input left icon">
+                            <i class="calendar icon"></i>
+                            <input id="cds_{{ field.getId() }}" name="cds_{{ field.getId() }}" type="text" class="due_date" maxlength="10" size="10" value="{% if attribute(filters.contrib_dynamic, fid) is defined %}{{ attribute(filters.contrib_dynamic, fid) }}{% endif %}" placeholder="{{ _T('(yyyy-mm-dd format)') }}"/>
+                        </div>
+                    </div>
     {% endif %}
                     </div>
 {% endfor %}
index 51962a53904b0e79f64bf5da2c767258b2083e98..1c0ce498b3498bf961d14ea5a9551b4122fdb9eb 100644 (file)
@@ -55,6 +55,8 @@ class Members extends GaletteTestCase
     protected int $seed = 335689;
     private array $mids = [];
 
+    private ?string $contents_table = null;
+
     /**
      * Set up tests
      *
@@ -63,6 +65,7 @@ class Members extends GaletteTestCase
     public function setUp(): void
     {
         parent::setUp();
+        $this->contents_table = null;
         $this->createMembers();
     }
 
@@ -77,6 +80,25 @@ class Members extends GaletteTestCase
 
         $this->deleteGroups();
         $this->deleteMembers();
+
+        $delete = $this->zdb->delete(\Galette\Entity\DynamicFieldsHandle::TABLE);
+        $this->zdb->execute($delete);
+        $delete = $this->zdb->delete(\Galette\DynamicFields\DynamicField::TABLE);
+        $this->zdb->execute($delete);
+        //cleanup dynamic translations
+        $delete = $this->zdb->delete(\Galette\Core\L10n::TABLE);
+        $delete->where([
+            'text_orig' => [
+                'Dynamic choice field',
+                'Dynamic date field',
+                'Dynamic text field'
+            ]
+        ]);
+        $this->zdb->execute($delete);
+
+        if ($this->contents_table !== null) {
+            $this->zdb->drop($this->contents_table);
+        }
     }
 
     /**
@@ -131,7 +153,7 @@ class Members extends GaletteTestCase
             $this->assertTrue($member->store());
             $mids[] = $member->id;
 
-            //set first member displayed publically an active and up to date member
+            //set first member displayed publicly an active and up-to-date member
             if ($member->appearsInMembersList() && !$member->isDueFree() && $first === true) {
                 $first = false;
                 $contrib = new \Galette\Entity\Contribution($this->zdb, $this->login);
@@ -609,6 +631,177 @@ class Members extends GaletteTestCase
         $this->assertSame(0, $list->count());
     }
 
+    /**
+     * Test getList with contribution dynamic fields
+     *
+     * @return void
+     */
+    public function testGetListContributionDynamics()
+    {
+        // Advanced search on contributions dynamic fields
+
+        //add dynamic fields on contributions
+        $field_data = [
+            'form_name'         => 'contrib',
+            'field_name'        => 'Dynamic text field',
+            'field_perm'        => \Galette\DynamicFields\DynamicField::PERM_USER_WRITE,
+            'field_type'        => \Galette\DynamicFields\DynamicField::TEXT,
+            'field_required'    => 1,
+            'field_repeat'      => 1
+        ];
+
+        $tdf = \Galette\DynamicFields\DynamicField::getFieldType($this->zdb, $field_data['field_type']);
+
+        $stored = $tdf->store($field_data);
+        $error_detected = $tdf->getErrors();
+        $warning_detected = $tdf->getWarnings();
+        $this->assertTrue(
+            $stored,
+            implode(
+                ' ',
+                $tdf->getErrors() + $tdf->getWarnings()
+            )
+        );
+        $this->assertEmpty($error_detected, implode(' ', $tdf->getErrors()));
+        $this->assertEmpty($warning_detected, implode(' ', $tdf->getWarnings()));
+
+        //new dynamic field, of type choice.
+        $values = [
+            'First value',
+            'Second value',
+            'Third value'
+        ];
+        $field_data = [
+            'form_name'         => 'contrib',
+            'field_name'        => 'Dynamic choice field',
+            'field_perm'        => \Galette\DynamicFields\DynamicField::PERM_USER_WRITE,
+            'field_type'        => \Galette\DynamicFields\DynamicField::CHOICE,
+            'field_required'    => 0,
+            'field_repeat'      => 1,
+            'fixed_values'      => implode("\n", $values)
+        ];
+
+        $cdf = \Galette\DynamicFields\DynamicField::getFieldType($this->zdb, $field_data['field_type']);
+
+        $stored = $cdf->store($field_data);
+        $error_detected = $cdf->getErrors();
+        $warning_detected = $cdf->getWarnings();
+        $this->assertTrue(
+            $stored,
+            implode(
+                ' ',
+                $cdf->getErrors() + $cdf->getWarnings()
+            )
+        );
+        $this->assertEmpty($error_detected, implode(' ', $cdf->getErrors()));
+        $this->assertEmpty($warning_detected, implode(' ', $cdf->getWarnings()));
+        //cleanup dynamic choices table
+        $this->contents_table = $cdf->getFixedValuesTableName($cdf->getId());
+
+        //new dynamic field, of type date.
+        $field_data = [
+            'form_name'         => 'contrib',
+            'field_name'        => 'Dynamic date field',
+            'field_perm'        => \Galette\DynamicFields\DynamicField::PERM_USER_WRITE,
+            'field_type'        => \Galette\DynamicFields\DynamicField::DATE,
+            'field_required'    => 0,
+            'field_repeat'      => 1
+        ];
+
+        $ddf = \Galette\DynamicFields\DynamicField::getFieldType($this->zdb, $field_data['field_type']);
+
+        $stored = $ddf->store($field_data);
+        $error_detected = $ddf->getErrors();
+        $warning_detected = $ddf->getWarnings();
+        $this->assertTrue(
+            $stored,
+            implode(
+                ' ',
+                $ddf->getErrors() + $ddf->getWarnings()
+            )
+        );
+        $this->assertEmpty($error_detected, implode(' ', $ddf->getErrors()));
+        $this->assertEmpty($warning_detected, implode(' ', $ddf->getWarnings()));
+
+        //search on contribution dynamic text field
+        $filters = new \Galette\Filters\AdvancedMembersList();
+        $filters->contrib_dynamic = [$tdf->getId() => 'text value'];
+        $members = new \Galette\Repository\Members($filters);
+        $list = $members->getList();
+
+        $this->assertSame(0, $list->count());
+
+        $contrib = new \Galette\Entity\Contribution($this->zdb, $this->login);
+
+        $now = new \DateTime();
+        $begin_date = clone $now;
+        $begin_date->sub(new \DateInterval('P1D'));
+        $due_date = clone $begin_date;
+        $due_date->sub(new \DateInterval('P1D'));
+        $due_date->add(new \DateInterval('P1Y'));
+
+        $cdata = [
+            \Galette\Entity\Adherent::PK    => $this->mids[0],
+            'type_paiement_cotis'           => \Galette\Entity\PaymentType::CASH,
+            'montant_cotis'                 => 20,
+            'date_enreg'                    => $begin_date->format('Y-m-d'),
+            'date_debut_cotis'              => $begin_date->format('Y-m-d'),
+            'date_fin_cotis'                => $due_date->format('Y-m-d'),
+            \Galette\Entity\ContributionsTypes::PK  => 4, //donation in kind
+            'info_field_' . $tdf->getId() . '_1' => 'A contribution with a dynamic text value set on it'
+        ];
+        $this->assertTrue($contrib->check($cdata, [], []));
+        $this->assertTrue($contrib->store());
+
+        $list = $members->getList();
+        $this->assertSame(1, $list->count());
+
+        //search on contribution dynamic date field
+        $filters = new \Galette\Filters\AdvancedMembersList();
+        $ddate = new \DateTime('2020-01-01');
+        $filters->contrib_dynamic = [$ddf->getId() => $ddate->format(__('Y-m-d'))];
+        $members = new \Galette\Repository\Members($filters);
+        $list = $members->getList();
+
+        $this->assertSame(0, $list->count());
+
+        $cdata += [
+            'id_cotis' => $contrib->id,
+            'info_field_' . $ddf->getId() . '_1' => $ddate->format(__('Y-m-d'))
+        ];
+        $this->assertTrue($contrib->check($cdata, [], []));
+        $this->assertTrue($contrib->store());
+
+        $list = $members->getList();
+        $this->assertSame(1, $list->count());
+
+        //search on contribution dynamic choice field
+        $filters = new \Galette\Filters\AdvancedMembersList();
+        $filters->contrib_dynamic = [$cdf->getId() => 2]; //3rd options is selected
+        $members = new \Galette\Repository\Members($filters);
+        $list = $members->getList();
+
+        $this->assertSame(0, $list->count());
+
+        $cdata += [
+            'id_cotis' => $contrib->id,
+            'info_field_' . $cdf->getId() . '_1' => 2
+        ];
+        $this->assertTrue($contrib->check($cdata, [], []));
+        $this->assertTrue($contrib->store());
+
+        $list = $members->getList();
+        $this->assertSame(1, $list->count());
+
+        //search on multiple contribution dynamic choice field
+        $filters = new \Galette\Filters\AdvancedMembersList();
+        $filters->contrib_dynamic = [$cdf->getId() => [0, 2]]; //1st OR 3rd options are selected
+        $members = new \Galette\Repository\Members($filters);
+        $list = $members->getList();
+
+        $this->assertSame(1, $list->count());
+    }
+
     /**
      * Test getPublicList
      *