Commit 10525801 authored by Bertrand Gauthier's avatar Bertrand Gauthier
Browse files

Extraction Xlsx Formations : WIP

parent 896d7ecb
Loading
Loading
Loading
Loading
+169 −0
Original line number Diff line number Diff line
--
-- 9.x ?
--

drop view if exists v_extract_formation;

create or replace view v_extract_formation as
SELECT m.libelle                                                                    AS module_libelle,
       f.id                                                                         AS formation_id,
       f.libelle                                                                    AS formation_titre,
       sess.id                                                                      AS session_id,
       (SELECT string_agg((if.nom_patronymique::text || ' '::text) || if.prenom1::text,
                          ','::text || chr(10)) AS string_agg
        FROM formation_formateur ff
                 JOIN individu if ON if.id = ff.individu_id
        WHERE ff.histo_destruction IS NULL
          AND sess.id = ff.session_id)                                              AS session_intervenants,
       sess.modalite                                                                AS session_modalite,
       case sess.type
           when 'T' then 'Transversale'
           else 'Spécifique' end                                                    AS session_type,
       (SELECT string_agg(COALESCE(fsvs.sigle, fsvs.code)::text, ','::text || chr(10)) AS string_agg
        FROM formation_session_structure_valide fsv
                 JOIN structure fsvs ON fsv.structure_id = fsvs.id
        WHERE sess.id = fsv.session_id
          AND fsv.histo_destruction IS NULL)                                        AS session_structures_valides,
       ssr.sigle                                                                    AS session_etab_orga,
       COALESCE((sir.nom_patronymique::text || ' '::text) || sir.prenom1::text,
                (fir.nom_patronymique::text || ' '::text) || fir.prenom1::text)     AS session_responsable,
       (SELECT count(fi.id) AS count
        FROM formation_inscription fi
        WHERE sess.id = fi.session_id
          AND fi.histo_destruction IS NULL
          AND fi.liste IS NULL)                                                     AS session_nb_nonclasses,
       (SELECT count(fi.id) AS count
        FROM formation_inscription fi
        WHERE sess.id = fi.session_id
          AND fi.histo_destruction IS NULL
          AND fi.liste::text = 'P'::text)                                           AS session_nb_liste_princ,
       (SELECT count(fi.id) AS count
        FROM formation_inscription fi
        WHERE sess.id = fi.session_id
          AND fi.histo_destruction IS NULL
          AND fi.liste::text = 'C'::text)                                           AS session_nb_liste_compl,
       sea.id                                                                       AS seance_id,
       EXTRACT(year FROM sea.debut)                                                 AS seance_annee_debut,
       to_char(sea.debut, 'DD/MM/YYYY HH24:MI'::text)                               AS seance_debut,
       to_char(sea.fin, 'DD/MM/YYYY HH24:MI'::text)                                 AS seance_fin,
       date_part('hour'::text, sea.fin - sea.debut) +
       date_part('min'::text, sea.fin - sea.debut) / 60::double precision           AS seance_nb_h,
       (SELECT count(fp.id) AS count
        FROM formation_presence fp
        WHERE fp.seance_id = sea.id
          AND fp.histo_destruction IS NULL
          AND fp.temoin::text = 'O'::text)                                          AS seance_nb_presents,
       ((SELECT count(fi.id) AS count
         FROM formation_inscription fi
         WHERE sess.id = fi.session_id
           AND fi.histo_destruction IS NULL
           AND fi.liste::text = 'P'::text)) - ((SELECT count(fp.id) AS count
                                                FROM formation_presence fp
                                                WHERE sea.id = fp.seance_id
                                                  AND fp.histo_destruction IS NULL
                                                  AND fp.temoin::text = 'O'::text)) AS seance_nb_absents,
       (SELECT string_agg(tmp.str, ', '::text) AS string_agg
        FROM (SELECT (de.source_code::text || ':'::text) || count(*) AS str
              FROM formation_presence fp
                       JOIN formation_inscription fi ON fp.inscription_id = fi.id AND fi.histo_destruction IS NULL
                       JOIN doctorant d ON fi.doctorant_id = d.id
                       JOIN etablissement de ON d.etablissement_id = de.id
              WHERE fp.temoin::text = 'O'::text
                AND fp.seance_id = sea.id
              GROUP BY de.source_code
              ORDER BY de.source_code) tmp)                                              AS seance_nb_presents_etab
FROM formation_seance sea
         JOIN formation_session sess ON sea.session_id = sess.id AND sess.histo_destruction IS NULL
         JOIN formation_formation f ON sess.formation_id = f.id AND f.histo_destruction IS NULL
         JOIN formation_module m ON m.id = f.module_id AND m.histo_destruction IS NULL
         JOIN structure ssr ON ssr.id = sess.site_id
         LEFT JOIN individu fir ON fir.id = f.responsable_id
         LEFT JOIN individu sir ON sir.id = sess.responsable_id
WHERE sea.histo_destruction IS NULL;



-- drop view if exists v_extract_formation_enquete;
--
-- create or replace view v_extract_formation_enquete as
-- SELECT fs.formation_id,
--        ff.libelle                                                                      AS formation_libelle,
--        fs.id                                                                           AS session_id,
--        ec.libelle                                                                      AS question_categorie,
--        eq.libelle                                                                      AS question_libelle,
--        er.niveau                                                                       AS reponse_niveau,
--        er.description                                                                  AS reponse_commentaire,
--        to_char(COALESCE(er.histo_modification, er.histo_creation), 'DD/MM/YYYY'::text) AS reponse_date
-- FROM formation_enquete_reponse er
--          JOIN formation_inscription fi ON er.inscription_id = fi.id AND fi.histo_destruction IS NULL
--          JOIN formation_session fs ON fi.session_id = fs.id AND fs.histo_destruction IS NULL
--          JOIN formation_formation ff ON fs.formation_id = ff.id AND ff.histo_destruction IS NULL
--          JOIN formation_enquete_question eq ON er.question_id = eq.id AND eq.histo_destruction IS NULL
--          JOIN formation_enquete_categorie ec ON eq.categorie_id = ec.id AND eq.histo_destruction IS NULL
-- WHERE er.histo_destruction IS NULL
-- ORDER BY fs.formation_id DESC, ec.libelle, eq.libelle;



drop view if exists v_extract_formation_presence;
create or replace view v_extract_formation_presence as
with pre_these as (select 'NB : un doctorant peut avoir 2 thèses' as rem,
                          t.id,
                          t.doctorant_id,
                          t.ecole_doct_id,
                          t.unite_rech_id,
                          rank() OVER (PARTITION BY doctorant_id) as rank
                   from these t
                   where t.histo_destruction is null
                     and t.etat_these in ('E', 'S'))
select f.id                                                                                            as formation_id,
       f.libelle                                                                                       as formation_libelle,
       fsess.id                                                                                        as session_id,
       (select string_agg(nom_usuel || ' ' || prenom1, ', ')
        from formation_formateur ff
                 join individu i on ff.individu_id = i.id
        where ff.session_id = fsess.id)                                                                as session_intervenant,
       case fsess.modalite
           when 'P' then 'Présentiel'
           when 'D' then 'Distanciel'
           else 'Mixte' end                                                                            as session_modalite,
       fsess.type                                                                                      as session_type,
       eorga.source_code                                                                               as session_etab_orga,
       d.id                                                                                            as doctorant_id,
       di.nom_usuel                                                                                    as doctorant_nom_usuel,
       di.nom_patronymique                                                                             as doctorant_nom_patronymique,
       array_to_string(ARRAY [di.prenom1, di.prenom2, di.prenom3], ' ')                                as doctorant_prenoms,
       e.source_code                                                                                   as doctorant_etablissement,
       (select string_agg(distinct s.code, ', ')
        from ecole_doct ed
                 join structure s on s.id = ed.structure_id
                 join pre_these t on ed.id = t.ecole_doct_id and t.doctorant_id = d.id)                as these_ed,
       (select string_agg(distinct s.code, ', ')
        from unite_rech ed
                 join structure s on s.id = ed.structure_id
                 join pre_these t on ed.id = t.ecole_doct_id and t.rank = 1 and t.doctorant_id = d.id) as these_ur,
       'D' || (select count(distinct annee_univ)
        from these_annee_univ tau
                 join pre_these t on t.id = tau.these_id and t.rank = 1 and t.doctorant_id = d.id
        where tau.histo_destruction is null)                                                           as these_annee_doctorat,
       (select string_agg(distinct annee_univ::varchar, ', ')
        from these_annee_univ tau
                 join pre_these t on t.id = tau.these_id and t.rank = 1 and t.doctorant_id = d.id
        where tau.histo_destruction is null)                                                           as these_annees_inscription,
       fi.histo_creation                                                                               as session_date_inscription,
       fs.id                                                                                           as seance_id,
       fs.debut                                                                                        as seance_debut,
       case when fp.temoin = 'O' then 'Présent·e' else 'Absent·e' end                                  as seance_statut_presence
from formation_inscription fi
         join formation_session fsess on fi.session_id = fsess.id and fsess.histo_destruction is null
         join formation_formation f on fsess.formation_id = f.id and f.histo_destruction is null
         join formation_seance fs on fsess.id = fs.session_id and fs.histo_destruction is null
         join doctorant d on fi.doctorant_id = d.id and d.histo_destruction is null
         join individu di on di.id = d.individu_id and di.histo_destruction is null
         join etablissement e on d.etablissement_id = e.id
         join etablissement eorga on fsess.site_id = eorga.id
         left join formation_presence fp
                   on fi.id = fp.inscription_id and fp.seance_id = fs.id and fp.histo_destruction is null
-- where f.id = 87
order by f.id, fsess.id, fs.id, di.nom_patronymique, di.prenom1
;
+2 −2
Original line number Diff line number Diff line
@@ -21,10 +21,10 @@ return [
            ExcelDataTablesXlsxWriter::class => InvokableFactory::class,
        ],
    ],
    'xlsx_export' => [
    'xlsx_export_manager' => [
        'factories' => [],
        'aliases' => [],
        'config' => [],
        'exports' => [],
    ],

    'bjyauthorize'    => [
+60 −7
Original line number Diff line number Diff line
@@ -3,8 +3,10 @@
namespace Application\Export\Xlsx;

use Application\Export\Xlsx\Writer\XlsxWriterInterface;
use BadMethodCallException;
use DateTime;
use Exception;
use InvalidArgumentException;
use UnicaenApp\Traits\MessageAwareTrait;
use Webmozart\Assert\Assert;

@@ -12,6 +14,9 @@ abstract class AbstractXlsxExport implements XlsxExportInterface
{
    use MessageAwareTrait;

    const SOURCE_TYPE_DB = 'db';
    const SOURCE_TYPE_SERVICE = 'service';

    const LOG_TAB = 'LOG';

    protected string $code;
@@ -106,7 +111,9 @@ abstract class AbstractXlsxExport implements XlsxExportInterface
        header(sprintf('Content-Disposition: attachment;filename="%s"', $filename));
        header('Cache-Control: max-age=0');

        file_put_contents('php://output', $this->exportToString());
        $content = $this->exportToString();

        file_put_contents('php://output', $content);
    }

    protected function generate(string $outputFilePath): void
@@ -137,12 +144,40 @@ abstract class AbstractXlsxExport implements XlsxExportInterface
        }
    }

    protected function fetchDataForTab(string $tabName): array
    {
        $data = match ($this->getSourceTypeForTab($tabName)) {
            self::SOURCE_TYPE_DB => $this->fetchDbDataForTab($tabName),
            self::SOURCE_TYPE_SERVICE => $this->fetchServiceDataForTab($tabName),
            default => throw new InvalidArgumentException("Type de source inattendu !"),
        };

        foreach ($this->getDataProcessorsForTab($tabName) as $processor) {
            $data = $processor->processData($data);
        }

        return $data;
    }

    /**
     * Méthode chargée fournir les données à écrire dans l'onglet spécifié, à partir de la base de données.
     *
     * @return array Ex : ['col1' => 'val1', 'col2' => 'val2', ...]
     */
    protected function fetchDbDataForTab(string $tabName): array
    {
        throw new BadMethodCallException("Méthode non implémentée !");
    }

    /**
     * Méthode chargée fournir les données à écrire dans l'onglet spécifié.
     * Méthode chargée fournir les données à écrire dans l'onglet spécifié, à partir d'un tableau de données PHP.
     *
     * @return array Ex : ['col1' => 'val1', 'col2' => 'val2', ...]
     */
    abstract protected function fetchDataForTab(string $tabName): array;
    protected function fetchServiceDataForTab(string $tabName): array
    {
        throw new BadMethodCallException("Méthode non implémentée !");
    }


    protected function computeTempOutputFilePath(): string
@@ -165,19 +200,37 @@ abstract class AbstractXlsxExport implements XlsxExportInterface
        return $config;
    }

    protected function getViewNameForTab(string$tabName): string
    protected function getSourceTypeForTab(string $tabName): string
    {
        $tabConfig = $this->getConfigForTab($tabName);
        Assert::keyExists($tabConfig, $k = 'source_type', "Clé %s introuvable dans le tableau de config de l'onglet $tabName");

        return $tabConfig[$k];
    }

    protected function getSourceForTab(string $tabName): string|object
    {
        $tabConfig = $this->getConfigForTab($tabName);
        Assert::keyExists($tabConfig, $k = 'view_name', "Clé %s introuvable dans le tableau de config de l'onglet $tabName");
        Assert::keyExists($tabConfig, $k = 'source', "Clé %s introuvable dans le tableau de config de l'onglet $tabName");

        return $tabConfig[$k];
    }

    protected function getViewColumnsForTab(string$tabName): array
    /**
     * @return array<\Application\Export\Xlsx\XlsxExportDataProcessorInterface>
     */
    protected function getDataProcessorsForTab(string $tabName): array
    {
        $tabConfig = $this->getConfigForTab($tabName);

        return $tabConfig['data_processors'] ?? [];
    }

    protected function getColumnsForTab(string $tabName): array
    {
        $tabConfig = $this->getConfigForTab($tabName);

        Assert::keyExists($tabConfig, $k = 'view_columns', "Clé %s introuvable dans le tableau de config de l'onglet $tabName");
        Assert::keyExists($tabConfig, $k = 'columns', "Clé %s introuvable dans le tableau de config de l'onglet $tabName");
        $viewColumns = $tabConfig[$k];

        if (is_string($viewColumns)) {
+170 −0
Original line number Diff line number Diff line
<?php

namespace Application\Export\Xlsx;

use Webmozart\Assert\Assert;

/**
 * Date processor chargé d'exploser des colonnes de données en plusieurs parce qu'elles contiennent des données agrégées.
 *
 * - Exemple avec les colonnes `['nb_presents']` et une liste de "codes" `['INSA','ULHN']` : dans la ligne de données
 * `['seance_id'=>123, 'nb_presents'=>'INSA:12, ULHN:5']`, la colonne 'nb_presents' sera explosée en 2 colonnes,
 * ce qui transformerait la ligne en ceci :
 * `['seance_id'=>123, 'nb_presents_INSA'=>'12', 'nb_presents_ULHN'=>'5']`.
 *
 * - Même exemple mais avec la liste de "codes" `['INSA','UCN','ULHN','URN']`, la ligne deviendrait ceci :
 * `['seance_id'=>123, 'nb_presents_INSA'=>'12', 'nb_presents_UCN'=>'', 'nb_presents_ULHN'=>'5', 'nb_presents_URN'=>'']`.
 *
 *  - Même exemple mais en préservant les colonnes, la ligne deviendrait ceci :
 *  `['seance_id'=>123, 'nb_presents'=>'INSA:12, ULHN:5', 'nb_presents_INSA'=>'12', 'nb_presents_UCN'=>'', 'nb_presents_ULHN'=>'5', 'nb_presents_URN'=>'']`.
 */
class ExplodeDataProcessor implements XlsxExportDataProcessorInterface
{
    protected array $columns;

    /**
     * Spécifie les noms de colonnes concernées par l'explosion.
     * @param array $columns Ex : ['nb_presents', 'nb_absents']
     */
    public function setColumns(array $columns): void
    {
        $this->columns = $columns;
    }

    /**
     * @var array<string>
     */
    protected array $codes;

    /**
     * Spécifie l'esemble des codes possibles. Ex : `['INSA','UCN','ULHN','URN']`.
     * @param array<string> $codes
     */
    public function setCodes(array $codes): void
    {
        $this->codes = $codes;
    }

    protected bool $preserveColumns = false;

    /**
     * Spécifie si les colonnes faisant l'objet d'une explosion doivent être conservées ou pas.
     */
    public function setPreserveColumns(bool $preserveColumns): void
    {
        $this->preserveColumns = $preserveColumns;
    }

    protected string $outerPregSplitPattern = "/[\s,]+/";

    public function setOuterPregSplitPattern(string $outerPregSplitPattern): void
    {
        $this->outerPregSplitPattern = $outerPregSplitPattern;
    }

    protected string $innerPregSplitPattern = "/[\s:]+/";

    public function setInnerPregSplitPattern(string $innerPregSplitPattern): void
    {
        $this->innerPregSplitPattern = $innerPregSplitPattern;
    }

    public function __construct(array $options = null)
    {
        if (isset($options['columns'])) {
            $this->setColumns($options['columns']);
        }
        if (isset($options['codes'])) {
            $this->setCodes($options['codes']);
        }
        if (isset($options['preserve_columns'])) {
            $this->setPreserveColumns((bool)$options['preserve_columns']);
        }
        if (isset($options['outer_preg_split_pattern'])) {
            $this->setOuterPregSplitPattern($options['outer_preg_split_pattern']);
        }
        if (isset($options['inner_preg_split_pattern'])) {
            $this->setInnerPregSplitPattern($options['inner_preg_split_pattern']);
        }
    }

    public function processData(array $data): array
    {
        if (empty($data)) {
            return [];
        }
        foreach ($this->columns as $column) {
            Assert::keyExists($data[0], $column, "La colonne %s n'existe pas dans les données");
        }

        return array_map(
            fn($row) => $this->processColumns($row, $this->columns),
            $data
        );
    }

    /**
     * @param array $row Ex : ['seance_id'=>123, 'nb_presents'=>'INSA:12, ULHN:5']
     * @param array<string> $columnsToTransform Ex : ['nb_presents']
     * @return array Ex : ['seance_id'=>123, 'nb_presents_INSA'=>'12', 'nb_presents_UCN'=>'', 'nb_presents_ULHN'=>'5', 'nb_presents_URN'=>'']
     */
    protected function processColumns(array $row, array $columnsToTransform): array
    {
        $transformedRow = [];

        // parcours des colonnes pour en exploser/décliner certaines par établissement
        foreach ($row as $col => $value) {
            if (in_array($col, $columnsToTransform)) {
                if ($this->preserveColumns) {
                    $transformedRow[$col] = $value;
                }
                // explosion/déclinaison par établissement
                $array = $this->explodeColumn($value, $col);
                $transformedRow += $array;
            } else {
                $transformedRow[$col] = $value;
            }
        }

        return $transformedRow;
    }

    /**
     * @param string|null $value Ex : 'INSA:12, ULHN:5' ou 'INSA:12,ULHN:5'
     * @param string $column Ex : 'nb_presents'
     * @return array Ex : ['nb_presents_INSA'=>'12', 'nb_presents_UCN'=>'', 'nb_presents_ULHN'=>'5', 'nb_presents_URN'=>'']
     */
    protected function explodeColumn(?string $value, string $column): array
    {
        // ex si 4 codes : ['nb_presents_INSA'=>'', 'nb_presents_UCN'=>'', 'nb_presents_ULHN'=>'', 'nb_presents_URN'=>'']
        $array = array_fill_keys(
            array_map(
                fn(string $code) => $this->generateColumnName($column, $code),
                $this->codes
            ),
            ''
        );

        if ($value === null) {
            return $array;
        }

        $parts = preg_split($this->outerPregSplitPattern, $value);
        Assert::notSame(false, $parts, "Le preg_split avec le pattern $this->outerPregSplitPattern a échoué sur la valeur suivante : " . $value);
        foreach ($parts as $part) {
            $split = preg_split($this->innerPregSplitPattern, $part);
            Assert::notSame(false, $split, "Le preg_split avec le pattern $this->innerPregSplitPattern a échoué sur la valeur suivante : " . $part);
            [$code, $val] = $split;
            Assert::inArray($code, $this->codes, "Valeur rencontrée ne correspondant à aucun code connu : %s");
            $col = $this->generateColumnName($column, $code);
            $array[$col] = $val;
        }

        return $array;
    }

    protected function generateColumnName(string $column, string $code): string
    {
        return $column. '_' . $code;
    }
}
 No newline at end of file
+54 −4

File changed.

Preview size limit exceeded, changes collapsed.

Loading