Add a tool to import a CSV file into a SQL table
authorx2003bruneau <x2003bruneau@839d8a87-29fc-0310-9880-83ba4fa771e5>
Sun, 17 Dec 2006 11:05:32 +0000 (11:05 +0000)
committerx2003bruneau <x2003bruneau@839d8a87-29fc-0310-9880-83ba4fa771e5>
Sun, 17 Dec 2006 11:05:32 +0000 (11:05 +0000)
git-svn-id: svn+ssh://murphy/home/svn/platal/trunk@1282 839d8a87-29fc-0310-9880-83ba4fa771e5

bin/csv2sql.php [new file with mode: 0755]
classes/csvimporter.php [new file with mode: 0644]
templates/include/csv-importer.tpl [new file with mode: 0644]

diff --git a/bin/csv2sql.php b/bin/csv2sql.php
new file mode 100755 (executable)
index 0000000..31fb94f
--- /dev/null
@@ -0,0 +1,60 @@
+#!/usr/bin/php5
+<?php
+
+// {{{ function showHelp()
+
+function showHelp($error = null) {
+    if ($error) {
+        echo 'Ligne de commande non-valide : ' . $error, "\n\n";
+    }
+    echo 'csv2sql.php -t table [-i source] [-r phpfile]', "\n\n";
+    echo 'options:', "\n";
+    echo ' -t table: table in which insertion is to be done', "\n";
+    echo ' -i source: CSV source file (stdin if not defined or if source is \'-\'', "\n";
+    echo ' -r phpfile: PHP file which define relations', "\n";
+}
+
+// }}}
+// {{{ function processArgs()
+
+function processArgs()
+{
+    global $sourceName, $table, $includedFile;
+    $opts = getopt('i:t:r:d:');
+    if ($opts['i'] == '-' || empty($opts['i'])) {
+        $sourceName = 'php://stdin';
+    } else {
+        $sourceName = $opts['i'];
+    }
+
+    if ($opts['r'] && !empty($opts['r'])) {
+        $includedFile = $opts['r'];
+    }
+
+    if (!$opts['t'] || empty($opts['t'])) {
+        showHelp('Table non définie');
+        exit;
+    }
+    $table = $opts['t'];
+}
+
+// }}}
+
+processArgs();
+require_once(dirname(__FILE__) . '/../classes/csvimporter.php');
+require_once(dirname(__FILE__) . '/../classes/xdb.php');
+
+$source    = file_get_contents($sourceName);
+$insert_relation = null;
+$update_relation = null;
+$debug           = false;
+$action          = CSV_INSERT;
+if (isset($includedFile)) {
+    require_once($includedFile);
+}
+
+$translater = new CSVImporter($table, $key, !$debug);
+$translater->setCSV($source);
+$translater->run($action, $insert_relation, $update_relation);
+
+?>
diff --git a/classes/csvimporter.php b/classes/csvimporter.php
new file mode 100644 (file)
index 0000000..8ac5aee
--- /dev/null
@@ -0,0 +1,272 @@
+<?php
+
+define('CSV_INSERT',  'insert'); // INSERT IGNORE
+define('CSV_REPLACE', 'replace'); // REPLACE
+define('CSV_UPDATE',  'update'); // INSERT and UPDATE on error
+
+class CSVImporter
+{
+    private $table;
+    private $key;
+    private $do_sql;
+
+    private $index;
+    private $separator;
+    private $data = array();
+
+    private $user_functions = array();
+
+    public function CSVImporter($table, $key = 'id', $do_sql = true)
+    {
+        $this->table     = $table;
+        $this->key       = $key;
+        $this->do_sql    = $do_sql;
+    }
+
+    private function processLine($line)
+    {
+        $array = split($this->separator, $line);
+        if (is_null($this->index)) {
+            $this->index = $array;
+            return true;
+        }
+
+        if (count($array) != count($this->index)) {
+            return false;
+        }
+        $assoc = array();
+        $i     = 0;
+        foreach ($this->index as $key) {
+            $assoc[$key] = $array[$i];
+            $i++;
+        }
+        $this->data[] = $assoc;
+        return true;
+    }
+
+    private function makeAssoc($line, $relation)
+    {
+        $ops = array();
+        foreach ($relation as $key=>$ref) {
+            if (@array_key_exists($ref, $line)) {
+                $value = $line[$ref];
+            } elseif (is_callable($ref, false)) {
+                $value = call_user_func($ref, $line, $key);
+            } else {
+                $value = $ref;
+            }
+            if (is_null($value) || $value == 'NULL') {
+                $value = 'NULL';
+            }
+            $ops[$key] = $value; 
+        }
+        return $ops;
+    }
+
+    private function makeRequestArgs($line, $relation)
+    {
+        $ops = array();
+        foreach ($relation as $key=>$ref) {
+            if (@array_key_exists($ref, $line)) {
+                $value = $line[$ref];
+            } elseif (is_callable($ref, false)) {
+                $value = call_user_func($ref, $line, $key);
+            } else {
+                $value = $ref;
+            }
+            if (is_null($value) || $value == 'NULL') {
+                $value = 'NULL';
+            } else {
+                $value = "'" . addslashes($value) . "'";
+            }
+            $ops[$key] = "$key = $value";
+        }
+        return $ops;
+    }
+
+    private function makeRelation()
+    {
+        $relation = array();
+        foreach ($this->index as $title) {
+            $relation[$title] = $title;
+        }
+        return $relation;
+    }
+
+    private function execute($query)
+    {
+        if (!$this->do_sql) {
+            echo "$query;\n";
+            return false;
+        }
+        return XDB::execute($query);
+    }
+
+    private function getFieldList()
+    {
+        $res = XDB::query("SHOW COLUMNS FROM {$this->table}");
+        if ($res->numRows()) {
+            return $res->fetchColumn();
+        }
+        return null;
+    }
+
+    public function setCSV($csv, $index = null, $separator = ';')
+    {
+        $this->index     = null;
+        $this->separator = $separator;
+        $csv   = preg_split("/(\r\n|\r|\n)/", $csv);
+
+        foreach ($csv as $line) {
+            $this->processLine($line);
+        }
+    }
+
+    public function run($action = CSV_UPDATE, $insert_relation = null, $update_relation = null)
+    {
+        if (is_null($insert_relation)) {
+            $insert_relation = $this->makeRelation();
+        }
+        if (is_null($update_relation)) {
+            $update_relation = $insert_relation;
+        }
+        foreach ($this->data as $line) {
+            $set = join(', ', $this->makeRequestArgs($line, $insert_relation));
+            switch ($action) {
+              case CSV_INSERT:
+                $this->execute("INSERT IGNORE INTO {$this->table} SET $set");
+                break;
+              case CSV_REPLACE:
+                $this->execute("REPLACE INTO {$this->table} SET $set");
+                break;
+              case CSV_UPDATE:
+                if (!$this->execute("INSERT INTO {$this->table} SET $set")) {
+                    $ops = $this->makeRequestArgs($line, $update_relation);
+                    $set = join(', ', $ops);
+                    $this->execute("UPDATE {$this->table} SET $set WHERE {$ops[$this->key]}");
+                }
+                break;
+            }
+        }
+    }
+
+    static public function dynamicCond($line, $key)
+    {
+        static $fields, $conds, $values, $thens, $elses;
+
+        if (!isset($fields)) {
+            $fields = Env::v('csv_cond_field');
+            $conds  = Env::v('csv_cond');
+            $values = Env::v('csv_cond_value');
+            $thens  = Env::v('csv_cond_then');
+            $elses  = Env::v('csv_cond_else');
+        }
+        $field = $line[$fields[$key]];
+        $cond  = $conds[$key];
+        $value = $values[$key];
+        if (is_numeric($field) && is_numeric($value)) {
+            $field = floatval($field);
+            $value = floatval($value);
+        }
+        switch ($cond) {
+            case 'defined':          $ok = (!empty($field)); break;
+            case 'equals':           $ok = ($field == $value); break;
+            case 'contains':         $ok = (strpos($field, $value) !== false); break;
+            case 'contained':        $ok = (strpos($value, $field) !== false); break;
+            case 'greater':          $ok = ($field > $value); break;
+            case 'greater_or_equal': $ok ($field >= $value); break;
+            case 'lower':            $ok = ($field < $value); break;
+            case 'lower_or_equal':   $ok = ($field <= $value); break;
+            default:                 $ok = false;
+        }
+        if ($ok) {
+            return $thens[$key];
+        } else {
+            return $elses[$key];
+        }
+    }
+
+    public function registerFunction($name, $desc, $callback)
+    {
+        if (is_callable($callback)) {
+            $this->user_functions['func_' . $name] = array('desc' => $desc, 'callback' => $callback);
+            return true;
+        }
+        return false;
+    }
+
+    /** Handle insertion form
+     * @param $page  PlatalPage to process
+     * @param $url   URI of the page
+     * @param $field Editable fields
+     */
+    public function apply(&$page, $url, $fields = null)
+    {
+        if (is_null($fields)) {
+            $fields = $this->getFieldList();
+        }
+        if (is_null($fields)) {
+            return false;
+        }
+
+        $current = Env::v('csv_page');
+        if (empty($current)) {
+            $current = 'source';
+        }
+        $next = Env::v('csv_next_page');
+        if (empty($next)) {
+            $next = $current;
+        }
+        $csv  = Env::v('csv');
+        if ($current == 'source' && Env::has('csv_valid')) {
+            $csv = Env::v('csv_source');
+            $next = 'values';
+        }
+        if ($csv) {
+            $this->setCSV($csv);
+        }
+        if ($current == 'values' && Env::has('csv_valid')) {
+            $next = 'valid';
+        }    
+        if (empty($csv)) {
+            $next = 'source';
+        }
+        if ($next == 'valid') {
+            $insert   = Env::v('csv_value');
+            $values   = Env::v('csv_user_value');
+            $update   = Env::v('csv_update');
+            foreach ($insert as $key=>$value) {
+                if (empty($value)) {
+                    $insert[$key] = null;
+                } elseif ($value == 'user_value') {
+                    $insert[$key] = $values[$key];
+                } elseif ($value == 'cond_value') {
+                    $insert[$key] = array($this, 'dynamicCond');
+                } elseif (array_key_exists($value, $this->user_functions)) {
+                    $insert[$key] = $this->user_functions[$value]['callback'];
+                }
+                if (isset($update[$key])) {
+                    $update[$key] = $insert[$key];
+                }
+            }
+            if ($current == 'valid' && Env::has('csv_valid')) {
+                $this->run(Env::v('csv_action'), $insert, $update);
+                $page->assign('csv_done', true);
+            } else {
+                $preview = array();
+                foreach ($this->data as $line) {
+                    $preview[] = $this->makeAssoc($line, $insert);
+                }
+                $page->assign('csv_preview', $preview);
+            }
+        }
+        $page->assign('csv_index', $this->index);
+        $page->assign('csv_funtions', $this->user_functions);
+        $page->assign('csv_page', $next);
+        $page->assign('csv_path', $url);
+        $page->assign('csv_fields', $fields);  
+        $page->assign('csv', $csv);
+    }
+}
+
+?>
diff --git a/templates/include/csv-importer.tpl b/templates/include/csv-importer.tpl
new file mode 100644 (file)
index 0000000..5765dc7
--- /dev/null
@@ -0,0 +1,261 @@
+{**************************************************************************}
+{*                                                                        *}
+{*  Copyright (C) 2003-2006 Polytechnique.org                             *}
+{*  http://opensource.polytechnique.org/                                  *}
+{*                                                                        *}
+{*  This program 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 2 of the License, or     *}
+{*  (at your option) any later version.                                   *}
+{*                                                                        *}
+{*  This program 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 this program; if not, write to the Free Software           *}
+{*  Foundation, Inc.,                                                     *}
+{*  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA               *}
+{*                                                                        *}
+{**************************************************************************}
+
+<script type="text/javascript">//<![CDATA[
+{literal}
+  function showValue(key, box)
+  {
+    var span_value = document.getElementById('csv_user_value_span[' + key + ']');
+    var span_cond  = document.getElementById('csv_cond_value_span[' + key + ']');
+    var i    = box.selectedIndex;
+    if (box.options[i].value == "user_value") {
+      span_value.style.display = "";
+      span_cond.style.display = "none";
+    } else if(box.options[i].value == "cond_value") {
+      span_value.style.display = "none";
+      span_cond.style.display = "";
+    } else {
+      span_value.style.display = "none";
+      span_conf.style.display = "none";
+    }
+  }
+  function showCond(key, box)
+  {
+    var line = document.getElementById('csv_cond_value[' + key + ']');
+    var i    = box.selectedIndex;
+    if (box.options[i].value == "defined") {
+      line.style.display = "none";
+    } else {
+      line.style.display = "";
+    }
+  }
+  function gotoPage(page)
+  {
+    document.getElementById('csv_next_page').value = page;
+    document.getElementById('csv_form').submit();
+    return false;
+  }
+{/literal}
+//]]></script>
+<form action="{$csv_path}" method="post" id="csv_form">
+<table class="cadre_a_onglet" cellpadding="0" cellspacing="0" style="width: 98%; margin-left:1%;">
+  <tr>
+    <td>
+      <ul id="onglet">
+        {if $csv_page eq 'source'}
+        <li class="actif">1 - Choisir<br />la source</li>
+        {else}
+        <li><a href="{$csv_path}" onclick="return gotoPage('source');">1 - Choisir<br />la source</a></li>
+        {/if}
+        {if $csv_page eq 'values'}
+        <li class="actif">2 - Définir<br />les valeurs</li>
+        {elseif $csv}
+        <li><a href="{$csv_path}" onclick="return gotoPage('values');">2 - Définir<br />les valeurs</a></li>
+        {else}
+        <li>2 - Définir<br />les valeurs</li>
+        {/if}
+        {if $csv_page eq 'valid'}
+        <li class="actif">3 - Vérifier<br />et valider</li>
+        {elseif $csv_action}
+        <li><a href="{$csv_path}" onclick="return gotoPage('valid');">3 - Vérifier<br />et valider</a></li>
+        {else}
+        <li>3 - Vérifier<br />et valider</li>
+        {/if}
+      </ul>
+    </td>
+  </tr>
+  <tr>
+    <td class="conteneur_tab">
+      <table style="width: 100%">
+        <tr>
+          <td>
+    {if $csv_page eq 'source'}
+      <textarea name="csv_source" rows="20" cols="80">{$csv|default:$smarty.request.csv_source}</textarea><br />
+      Entrez les données sous la forme :<br />
+      <pre class="center">TITRE1;TITRE2;...
+val1_1;val1_2;...
+val2_1;val2_2;...
+val3_1;val3_2;...</pre>
+    {elseif $csv_page eq 'values'}
+  <div class="center">
+    Action à effectuer si l'entrée existe : 
+    <select name="csv_action" onchange="this.form.submit()">
+      <option value="insert" {if $smarty.request.csv_action eq 'insert'}selected="selected"{/if}>
+        ne rien faire
+      </option>
+      <option value="replace" {if $smarty.request.csv_action eq 'replace'}selected="selected"{/if}>
+        remplacer par la nouvelle entrée
+      </option>
+      <option value="update" {if $smarty.request.csv_action eq 'update'}selected="selected"{/if}>
+        mettre à jour les champs sélectionnés
+      </option>
+    </select>
+  </div>
+  <table class="bicol">
+    <tr>
+      <th>Champ</th>
+      <th colspan="2">Valeur</th>
+      {if $smarty.request.csv_action eq 'update'}
+      <th>MàJ</th>
+    {/if}
+    </tr>
+    {foreach from=$csv_fields item=f}
+    <tr class="{cycle values="pair,impair"}">
+      <td>{$f}</td>
+      <td>
+        <select name="csv_value[{$f}]" onchange="showValue('{$f}', this);">
+          <option value="" {if !$smarty.request.csv_value[$f]}selected="selected"{/if}>
+            Vide
+          </option>
+          <option value="user_value" {if $smarty.request.csv_value[$f] eq "user_value"}selected="selected"{/if}>
+            Entrer la valeur
+          </option>
+          <option value="cond_value" {if $smarty.request.csv_value[$f] eq "cond_value"}selected="selected"{/if}>
+            Valeur conditionnelle
+          </option>
+          <optgroup label="Colonnes du CSV">
+            {foreach from=$csv_index item=col}
+            <option value="{$col}" {if $smarty.request.csv_value[$f] eq $col}selected="selected"{/if}>{$col}</option>
+            {/foreach}
+          </optgroup>
+          {if $csv_functions|count}
+          <optgroup label="Fonctions">
+            {foreach from=$csv_functions key=func item=desc}
+            <option value="{$func}" {if $smarty.request.csv_value[$f] eq $func}selected="selected"{/if}>{$desc.desc}</option>
+            {/foreach}
+          </optgroup>
+          {/if}
+        </select>
+      </td>
+      <td>
+        <span id="csv_user_value_span[{$f}]" {if $smarty.request.csv_value[$f] neq "user_value"}style="display: none"{/if}>
+          <input type="text" name="csv_user_value[{$f}]" value="{$smarty.request.csv_user_value[$f]}" />
+        </span>
+        <span id="csv_cond_value_span[{$f}]" {if $smarty.request.csv_value[$f] neq "cond_value"}style="display: none"{/if}>
+          Si
+          <select name="csv_cond_field[{$f}]">
+            {foreach from=$csv_index item=col}
+            <option value="{$col}" {if $smarty.request.csv_cond_field_value[$f] eq $col}selected="selected"{/if}>
+              {$col}
+            </option>
+            {/foreach}
+          </select>
+          <select name="csv_cond[{$f}]" onchange="showCond('{$f}', this)">
+            <option value="defined" {if $smarty.request.csv_cond[$f] eq "defined"}selected="selected"{/if}>
+              défini
+            </option>
+            <option value="equals" {if $smarty.request.csv_cond[$f] eq "equals"}selected="selected"{/if}>
+              est égale à
+            </option>
+            <option value="contains" {if $smarty.request.csv_cond[$f] eq "contains"}selected="selected"{/if}>
+              contient
+            </option>
+            <option value="contained" {if $smarty.request.csv_cond[$f] eq "contained"}selected="selected"{/if}>
+              est contenu dans
+            </option>
+            <option value="greater" {if $smarty.request.csv_cond[$f] eq "greater"}selected="selected"{/if}>
+              supérieur à
+            </option>
+            <option value="greater_or_equal" {if $smarty.request.csv_cond[$f] eq "greater_or_equal"}selected="selected"{/if}>
+              supérieur ou égal à
+            </option>
+            <option value="lower" {if $smarty.request.csv_cond[$f] eq "lower"}selected="selected"{/if}>
+              inférieur à
+            </option>
+            <option value="lower_or_equal" {if $smarty.request.csv_cond[$f] eq "lower_or_equal"}selected="selected"{/if}>
+              inférieur ou égal à
+            </option>
+          </select>
+          <span id="csv_cond_value[{$f}]" {if $smarty.request.csv_cond[$f] eq "defined" || !$smarty.request.csv_cond[$f]}style="display: none"{/if}>
+            <input type="text" name="csv_cond_value[{$f}]" value="{$smarty.request.csv_cond_value[$f]}" />
+          </span>
+          <br />Alors <input type="text" name="csv_cond_then[{$f}]" value="{$smarty.request.csv_cond_then[$f]}" />
+          <br />Sinon <input type="text" name="csv_cond_else[{$f}]" value="{$smarty.request.csv_cond_else[$f]}" />
+        </span>
+      </td>
+      {if $smarty.request.csv_action eq 'update'}
+      <td class="center">
+        <input type="checkbox" name="csv_update[{$f}]" {if $smarty.request.csv_update[$f]}checked="checked"{/if} />
+      </td>
+      {/if}
+    </tr>
+    {/foreach}
+  </table>
+  {elseif $csv_page eq 'valid'}
+  {if !$csv_done}
+  <table class="bicol">
+    <tr>
+      {foreach from=$csv_fields item=f}
+      <th>{$f}</th>
+      {/foreach}
+    </tr>
+    {foreach from=$csv_preview item=assoc}
+    <tr class="{cycle values="pair,impair"}">
+      {foreach from=$csv_fields item=f}
+      <td>{$assoc[$f]}</td>
+      {/foreach}
+    <tr>
+    {/foreach}
+  </table>
+  {else}
+  Les données ont été ajoutées.
+  {/if}
+  {/if}
+    </td>
+  </tr>
+
+  {if !$csv_done}
+  <tr>
+    <td class="center">
+    <input type="hidden" name="csv_page" value="{$csv_page}" />
+    <input type="hidden" id="csv_next_page" name="csv_next_page" value="{$csv_page}" />
+    <input type="hidden" name="csv" value="{$csv}" />
+    {if $csv_page neq 'values'}
+      <input type="hidden" name="csv_action" value="{$smarty.request.csv_action}" />
+    {foreach from=$csv_fields item=f}
+      <input type="hidden" name="csv_value[{$f}]" value="{$smarty.request.csv_value[$f]}" />
+      <input type="hidden" name="csv_user_value[{$f}]" value="{$smarty.request.csv_user_value[$f]}" />
+      <input type="hidden" name="csv_cond_field[{$f}]" value="{$smarty.request.csv_cond_field[$f]}" />
+      <input type="hidden" name="csv_cond[{$f}]" value="{$smarty.request.csv_cond[$f]}" />
+      <input type="hidden" name="csv_cond_value[{$f}]" value="{$smarty.request.csv_cond_value[$f]}" />
+      <input type="hidden" name="csv_cond_then[{$f}]" value="{$smarty.request.csv_cond_then[$f]}" />
+      <input type="hidden" name="csv_cond_else[{$f}]" value="{$smarty.request.csv_cond_else[$f]}" />
+      <input type="hidden" name="csv_update[{$f}]" value="{$smarty.request.csv_update[$f]}" />
+    {/foreach}
+    {/if}
+    {if $csv_page eq 'source'}
+    <input type="submit" name="csv_valid" value="Changer le CSV" />
+    {elseif $csv_page eq 'values'}
+    <input type="submit" name="csv_valid" value="Aperçu" />
+    {elseif $csv_page eq 'valid'}
+    <input type="submit" name="csv_valid" value="Valider" />
+    {/if}
+    </td>
+  </tr>
+  {/if}
+  </table>
+    </td>
+  </tr>
+</table>
+</form>
+
+{* vim:set et sws=2 sts=2 sw=2: *}