Commit 543e5b8e authored by Bertrand Gauthier's avatar Bertrand Gauthier
Browse files

Merge branch 'criteres_recherche_texte' into release-1.4.6

parents 258568f0 c9d489ec
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -58,7 +58,7 @@ abstract class TheseFilter
     * @param array  $queryParams
     * @return string
     */
    private function paramFromQueryParams(array $queryParams)
    protected function paramFromQueryParams(array $queryParams)
    {
        $name = $this->getName();

@@ -116,7 +116,7 @@ abstract class TheseFilter
    }

    /**
     * @return string
     * @return string|string[]
     */
    public function getValue()
    {
+38 −0
Original line number Diff line number Diff line
@@ -12,6 +12,44 @@ use Doctrine\ORM\QueryBuilder;
class TheseTextFilter extends TheseFilter
{
    const NAME_text = 'text';
    const NAME_criteria = 'textCriteria';

    /**
     * Critères possibles sur lesquels faire porter la recherche sur texte libre.
     *
     * ATTENTION : les identifiants (clés) doivent être identiques à ceux utilisés dans
     *             le script de la vue matérialisée MV_RECHERCHE_THESE sollicitée pour la recherche.
     */
    const CRITERIA = [
        'titre' => "Titre de la thèse",
        'numero-doctorant' => "Numéro étudiant du doctorant",
        'nom-doctorant' => "Nom d'usage ou patronymique du doctorant",
        'prenom-doctorant' => "Prénom du doctorant",
        'nom-directeur' => "Nom d'usage ou patronymique du directeur ou co-directeur de thèse",
        'code-ed' => "Code national de l'école doctorale concernée (ex: 181)",
        'code-ur' => "Unité de recherche concernée (ex: umr6211)",
    ];

    /**
     * @param array $queryParams
     */
    public function processQueryParams(array $queryParams)
    {
        $filterValue = $this->paramFromQueryParams($queryParams);

        if (array_key_exists(self::NAME_criteria, $queryParams) && !empty($queryParams[self::NAME_criteria])) {
            $criteria = $queryParams[self::NAME_criteria];
        } else {
            $criteria = array_keys(self::CRITERIA);
        }

        $filterValue = [
            'text' => $filterValue,
            'criteria' => $criteria,
        ];

        $this->setValue($filterValue);
    }

    /**
     * @param QueryBuilder $qb
+70 −29
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ use Application\SourceCodeStringHelperAwareTrait;
use Application\View\Helper\Sortable;
use Doctrine\DBAL\DBALException;
use Doctrine\ORM\Query\Expr\Join;
use UnicaenApp\Exception\LogicException;
use UnicaenApp\Exception\RuntimeException;
use UnicaenApp\Util;

@@ -286,6 +287,14 @@ class TheseRechercheService
            $updated = true;
        }

        // Si aucun critère n'est sélectionné pour le champ de recherche texte, cela revient à les sélectionner tous
        $name = TheseTextFilter::NAME_criteria;
        $criteria = $this->paramFromQueryParams($name, $queryParams); // NB: null <=> filtre absent
        if ($criteria === null) {
            $queryParams = array_merge($queryParams, [$name => array_keys(TheseTextFilter::CRITERIA)]);
            $updated = true;
        }

        return $updated;
    }

@@ -345,9 +354,11 @@ class TheseRechercheService
        /**
         * Prise en compte du texte recherché éventuel.
         */
        $text = $this->getFilterValueByName(TheseTextFilter::NAME_text);
        $value = $this->getFilterValueByName(TheseTextFilter::NAME_text);
        $text = $value['text'];
        $criteria = $value['criteria'];
        if ($text !== null && strlen($text) > 1) {
            $results = $this->rechercherThese($text);
            $results = $this->rechercherThese($text, $criteria);
            $sourceCodes = array_unique(array_keys($results));
            if ($sourceCodes) {
                $qb
@@ -438,43 +449,73 @@ class TheseRechercheService
    }

    /**
     * Recherche de thèses à l'aide de la vue matérialisée MV_RECHERCHE_THESE.
     * Recherche textuelle de thèses dans la vue matérialisée MV_RECHERCHE_THESE.
     *
     * Le contenu de la colonne MV_RECHERCHE_THESE.HAYSTACK suit le format suivant :
     * <pre>
     * code-ed{...} code-ur{...} titre{...} numero-doctorant{...} nom-doctorant{...} prenom-doctorant{...} nom-directeur{...}
     * </pre>
     * Exemple :
     * <pre>
     * code-ed{591} code-ur{umr6614} titre{bivalve dreissena polymorpha} numero-doctorant{85982906} nom-doctorant{hochon hochon} prenom-doctorant{paule} nom-directeur{terieur}
     * </pre>
     *
     * @param string  $text
     * L'expression régulière utilisée est donc de la forme suivante :
     * <pre>
     * (<critere>|<critere>)\{[^{]*<terme>[^}]*\}
     * </pre>
     * Exemple :
     * <pre>
     * (nom-doctorant|nom-directeur)\{[^{]*hochon[^}]*\}
     * </pre>
     *
     * Lorsque le texte recherché est "hochon" par exemple, la requête SQL générée est la suivante :
     * <pre>
     *      SELECT * FROM MV_RECHERCHE_THESE MV WHERE rownum <= 100 AND (
     *          REGEXP_LIKE(haystack, q'[(nom-doctorant|nom-directeur)\{[^{]*hochon[^}]*\}]', 'i')
     *      )
     * </pre>
     *
     * Lorsque le texte recherché contient plusieurs mots séparés par des espaces, "hochon bivalve" par exemple,
     * la requête SQL générée est la suivante :
     * <pre>
     *      SELECT * FROM MV_RECHERCHE_THESE MV WHERE rownum <= 100 AND (
     *          REGEXP_LIKE(haystack, q'[(nom-doctorant|nom-directeur)\{[^{]*hochon[^}]*\}]', 'i') OR
     *          REGEXP_LIKE(haystack, q'[(nom-doctorant|nom-directeur)\{[^{]*bivalve[^}]*\}]', 'i')
     *      )
     * </pre>
     *
     * @param string  $text Texte recherché. Ex: "hochon", "hochon bivalve"
     * @param string[] $criteria Critères sur lesquels porte la recherche. EX: ['nom-doctorant', 'nom-directeur']
     * @param integer $limit
     *
     * @return array
     * @return array [<CODE_THESE> => ['code' => <CODE_THESE>, 'code-doctorant' => <CODE_DOCTORANT>]]
     */
    public function rechercherThese($text, $limit = 100)
    public function rechercherThese($text, array $criteria, $limit = 100)
    {
        if (strlen($text) < 2) return [];

        $text = Util::reduce($text);
        $criteres = explode(' ', $text);
        if ($unknown = array_diff($criteria, array_keys(TheseTextFilter::CRITERIA))) {
            throw new LogicException("Les critères de recherche suivants ne sont pas supportés : " . implode(', ', $unknown));
        }

        $sql     = sprintf('SELECT * FROM MV_RECHERCHE_THESE MV WHERE rownum <= %s ', (int)$limit);
        $sqlCri  = '';
        $criCode = 0;
        $words = explode(' ', $text);
        $words = array_map('trim', $words);
        $words = array_map([Util::class, 'reduce'], $words);

        foreach ($criteres as $c) {
            if (! is_numeric($c)) {
                if ($sqlCri != '') {
                    $sqlCri .= ' AND ';
                }
                $sqlCri .= "haystack LIKE LOWER(q'[%" . $c . "%]')"; // q'[] : double les quotes
            } else {
                $criCode = (int) $c;
            }
        }
        $orc = [];
        if ($sqlCri != '') {
            $orc[] = '(' . $sqlCri . ')';
        }
        if ($criCode) {
            $orc[] = "(code_doctorant like '%" . $criCode . "%' OR code_ecole_doct = '" . $criCode . "')";
        }
        $orc = implode(' OR ', $orc);
        $sql .= ' AND (' . $orc . ') ';
        foreach ($words as $word) {
            // regexp : (<critere>|<critere>)\{[^{]*<terme>[^}]*\}
            $regexp = '(' . implode('|', $criteria) . ')' . "\{[^{]*" . $word . "[^}]*\}";
            $orc[] = "    REGEXP_LIKE(haystack, q'[" . $regexp . "]', 'i')"; // la syntaxe q'[]' dispense de doubler les '
        }
        $orc = implode(' OR ' . PHP_EOL, $orc);

        $sql = <<<EOS
SELECT * FROM MV_RECHERCHE_THESE MV WHERE rownum <= $limit AND (
$orc
)
EOS;

        try {
            $stmt = $this->theseService->getEntityManager()->getConnection()->executeQuery($sql);
+3 −2
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@

namespace Application\View\Helper\SelectsFilterPanel;

use Application\Service\These\Filter\TheseSelectFilter;
use Application\View\Renderer\PhpRenderer;
use Zend\View\Helper\AbstractHelper;
use Zend\View\Resolver\TemplatePathStack;
@@ -9,7 +10,7 @@ use Zend\View\Resolver\TemplatePathStack;
class SelectsFilterPanelHelper extends AbstractHelper
{
    /**
     * @param \Application\Service\These\Filter\TheseSelectFilter[] $filters
     * @param TheseSelectFilter[] $filters
     * @return string
     */
    function __invoke(array $filters)
@@ -34,6 +35,6 @@ class SelectsFilterPanelHelper extends AbstractHelper
    {
        return
            ($optionValue !== '' && ((isset($queryParams[$optionName]) && $queryParams[$optionName] === $optionValue))) ||
            ($optionValue === '' && (!isset($queryParams[$optionName]) || !isset($queryParams[$optionName]) || $queryParams[$optionName] === ''));
            ($optionValue === '' && (!isset($queryParams[$optionName]) || $queryParams[$optionName] === ''));
    }
}
 No newline at end of file
+24 −26
Original line number Diff line number Diff line
<?php

use Application\Service\These\Filter\TheseSelectFilter;
use Application\View\Helper\SelectsFilterPanel\SelectsFilterPanelHelper;
use Application\Service\These\Filter\TheseTextFilter;
use Application\View\Renderer\PhpRenderer;

/**
 * @var PhpRenderer $this
 * @var \Application\Service\These\Filter\TheseSelectFilter $filter
 * @var TheseTextFilter $filter
 * @var array $queryParams
 */

@@ -16,24 +15,15 @@ $paramName = $filter->getName();
$width = isset($paramAttributes['width']) ? $paramAttributes['width'] : 'fit';
$liveSearch = isset($paramAttributes['liveSearch']) ? (bool)$paramAttributes['liveSearch'] : false;

$value = $filter->getValue();
$text = $value['text'];
$criteria = $value['criteria'];

$text = $filter->getValue();

$inputTitle = <<<EOS
<div class="these-search-input-help">
    Recherche possible sur
    <ul>
        <li>le titre de la thèse</li>
        <li>le numéro étudiant de l'auteur</li>
        <li>le nom de l'auteur</li>
        <li>le prénom de l'auteur</li>
        <li>le nom du directeur ou co-directeur de thèse</li>
        <li>le code national de l'école doctorale concernée (ex: 181)</li>
        <li>l'unité de recherche concernée (ex: umr6211)</li>
    </ul>
</div>
EOS;

$title = htmlspecialchars(
    "<div class=\"these-search-input-help\">" .
    $this->translate("La recherche de texte porte sur les critères sélectionnés dans la liste déroulante ci-contre...") .
    "</div>"
);
?>

<div class="form-group">
@@ -46,12 +36,20 @@ EOS;
               class="form-control filter"
               id="search-input"
               placeholder="<?php echo $this->translate("Entrez 2 caractères au moins...");?>"
               data-content="<?php echo htmlspecialchars(
                   "<div class=\"these-search-input-help\">".
                   $this->translate("Recherche possible sur<ul><li>le titre de la thèse</li><li>le numéro étudiant de l'auteur</li><li>le nom de l'auteur</li><li>le nom du directeur ou co-directeur de thèse</li><li>le code national de l'école doctorale concernée (ex: 181)</li><li>l'unité de recherche concernée (ex: umr6211)</li></ul>") .
                   "</div>"
               );
               ?>">
               data-content="<?php echo $title ?>">
        <div class="input-group-addon filter">
            <select multiple name="textCriteria[]" id="" class="selectpicker show-menu-arrow"
                    title="Sélectionnez un critère..."
                    data-selected-text-format="count"
                    data-count-selected-text="{0} critères sélectionnés"
                    data-actions-box="true"
                    data-header="Critères sur lesquels portera la recherche textuelle">
                <?php foreach (TheseTextFilter::CRITERIA as $value => $label): ?>
                <?php $isActive = in_array($value, $criteria); ?>
                    <option value="<?php echo $value ?>" <?php echo $isActive ? 'selected' : '' ?>><?php echo $label ?></option>
                <?php endforeach ?>
            </select>
        </div>
    </div>
</div>

Loading