Overview
Comment:Modernisation des fiches membres + amélioration UX avec des transitions
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA1: 2bcf5d3c6a1f71e6b41d07582e37cdd0d6ebef20
User & Date: bohwaz on 2017-05-10 07:02:43
Other Links: branch diff | manifest | tags
Context
2017-05-11
05:14
Garradin a désormais besoin d'un secret unique pour chaque installation check-in: f2346dbb66 user: bohwaz tags: dev
2017-05-10
07:02
Modernisation des fiches membres + amélioration UX avec des transitions check-in: 2bcf5d3c6a user: bohwaz tags: dev
2017-05-09
07:03
Implémentation définitive vérification de signature à l'import de fichier, et plutôt que bloquer si le membre n'est plus admin, mettre toutes les catégories en admin. check-in: 623defddc2 user: bohwaz tags: dev
Changes

Modified src/include/init.php from [4bc86e819b] to [ba8a1afd8b].

113
114
115
116
117
118
119
120
121


122
123
124
125
126
127
128

    if (!defined($const))
    {
        define($const, $value);
    }
}

define('Garradin\WEBSITE', 'http://garradin.eu/');
define('Garradin\PLUGINS_URL', 'https://garradin.eu/plugins/list.json');



// PHP devrait être assez intelligent pour chopper la TZ système mais nan
// il sait pas faire (sauf sur Debian qui a le bon patch pour ça), donc pour 
// éviter le message d'erreur à la con on définit une timezone par défaut
// Pour utiliser une autre timezone, il suffit de définir date.timezone dans
// un .htaccess ou dans config.local.php
if (!ini_get('date.timezone'))







|
|
>
>







113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130

    if (!defined($const))
    {
        define($const, $value);
    }
}

const WEBSITE = 'http://garradin.eu/';
const PLUGINS_URL = 'https://garradin.eu/plugins/list.json';

const NTP_SERVER = 'fr.pool.ntp.org';

// PHP devrait être assez intelligent pour chopper la TZ système mais nan
// il sait pas faire (sauf sur Debian qui a le bon patch pour ça), donc pour 
// éviter le message d'erreur à la con on définit une timezone par défaut
// Pour utiliser une autre timezone, il suffit de définir date.timezone dans
// un .htaccess ou dans config.local.php
if (!ini_get('date.timezone'))

Modified src/include/lib/Garradin/Membres/Champs.php from [a9683b7b29] to [ae54e63bd0].

76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
...
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
...
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
...
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
...
376
377
378
379
380
381
382

383
384
385
386
387
388
389
...
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
        }

        return self::$presets;
    }

    static public function listUnusedPresets(Champs $champs)
    {
        return array_diff_key(self::importPresets(), $champs->getAll());
    }

	public function __construct($champs)
	{
		if ($champs instanceOf Champs)
		{
			$this->champs = $champs->getAll();
................................................................................
        }

        if (!property_exists($this->champs, $champ))
            return null;

        if ($key !== null)
        {
            if (property_exists($this->champs[$champ], $key))
                return $this->champs->$champ->$key;
            else
                return null;
        }

		return $this->champs->$champ;
	}
................................................................................
        if (($config->type == 'multiple' || $config->type == 'select') && empty($config->options))
        {
            throw new UserException('Le champ "'.$name.'" nécessite de comporter au moins une option possible.');
        }

        if (!property_exists($config, 'editable'))
        {
            $config['editable'] = false;
        }

        if (!property_exists($config, 'mandatory'))
        {
            $config['mandatory'] = false;
        }

        if (!property_exists($config, 'private'))
        {
            $config['private'] = false;
        }

        return true;
    }

    /**
     * Ajouter un nouveau champ
................................................................................
        if (!preg_match('!^[a-z][a-z0-9]*(_[a-z0-9]+)*$!', $name))
        {
            throw new UserException('Le nom du champ est invalide : ne sont acceptés que les lettres minuscules et les chiffres (éventuellement séparés par un underscore).');
        }
        
        $this->_checkField($name, $config);

        $this->champs[$name] = $config;

        return true;
    }

    /**
     * Modifie un champ particulier
     * @param string $champ Nom du champ
     * @param string $key   Nom de la clé à modifier
     * @param mixed  $value Valeur à affecter
     * @return boolean true
     */
	public function set($champ, $key, $value)
	{
        if (!isset($this->champs[$champ]))
        {
            throw new \LogicException('Champ "'.$champ.'" inconnu.');
        }

        // Vérification
        $config = $this->champs[$champ];
        $config[$key] = $value;
        $this->_checkField($champ, $config);

		$this->champs[$champ] = $config;
		return true;
	}

    /**
     * Modifie les champs en interne en vérifiant que tout va bien
     * @param array $champs Liste des champs
     * @return boolean true
................................................................................
        {
            throw new UserException('Le champ '.$config->get('champ_identifiant')
                .' est défini comme identifiant à la connexion et ne peut donc être supprimé des fiches membres.');
        }

        foreach ($champs as $name=>&$config)
        {

            $this->_checkField($name, $config);
        }

        $this->champs = $champs;

        return true;
    }
................................................................................
                WHERE '.$config->get('champ_identifiant').' = "";');

            // Création de l'index unique
            $db->exec('CREATE UNIQUE INDEX membres_identifiant ON membres ('.$config->get('champ_identifiant').');');
        }

        // Création des index pour les champs affichés dans la liste des membres
        $listed_fields = array_keys($this->getListedFields());
        foreach ($listed_fields as $field)
        {
            if ($field === $config->get('champ_identifiant'))
            {
                // Il y a déjà un index
                continue;
            }







|







 







|







 







|




|




|







 







|













|





|
|


|







 







>







 







|







76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
...
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
...
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
...
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
...
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
...
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
        }

        return self::$presets;
    }

    static public function listUnusedPresets(Champs $champs)
    {
        return array_diff_key(self::importPresets(), (array) $champs->getAll());
    }

	public function __construct($champs)
	{
		if ($champs instanceOf Champs)
		{
			$this->champs = $champs->getAll();
................................................................................
        }

        if (!property_exists($this->champs, $champ))
            return null;

        if ($key !== null)
        {
            if (property_exists($this->champs->$champ, $key))
                return $this->champs->$champ->$key;
            else
                return null;
        }

		return $this->champs->$champ;
	}
................................................................................
        if (($config->type == 'multiple' || $config->type == 'select') && empty($config->options))
        {
            throw new UserException('Le champ "'.$name.'" nécessite de comporter au moins une option possible.');
        }

        if (!property_exists($config, 'editable'))
        {
            $config->editable = false;
        }

        if (!property_exists($config, 'mandatory'))
        {
            $config->mandatory = false;
        }

        if (!property_exists($config, 'private'))
        {
            $config->private = false;
        }

        return true;
    }

    /**
     * Ajouter un nouveau champ
................................................................................
        if (!preg_match('!^[a-z][a-z0-9]*(_[a-z0-9]+)*$!', $name))
        {
            throw new UserException('Le nom du champ est invalide : ne sont acceptés que les lettres minuscules et les chiffres (éventuellement séparés par un underscore).');
        }
        
        $this->_checkField($name, $config);

        $this->champs->$name = $config;

        return true;
    }

    /**
     * Modifie un champ particulier
     * @param string $champ Nom du champ
     * @param string $key   Nom de la clé à modifier
     * @param mixed  $value Valeur à affecter
     * @return boolean true
     */
	public function set($champ, $key, $value)
	{
        if (!isset($this->champs->$champ))
        {
            throw new \LogicException('Champ "'.$champ.'" inconnu.');
        }

        // Vérification
        $config = clone $this->champs->$champ;
        $config->$key = $value;
        $this->_checkField($champ, $config);

		$this->champs->$champ = $config;
		return true;
	}

    /**
     * Modifie les champs en interne en vérifiant que tout va bien
     * @param array $champs Liste des champs
     * @return boolean true
................................................................................
        {
            throw new UserException('Le champ '.$config->get('champ_identifiant')
                .' est défini comme identifiant à la connexion et ne peut donc être supprimé des fiches membres.');
        }

        foreach ($champs as $name=>&$config)
        {
            $config = (object) $config;
            $this->_checkField($name, $config);
        }

        $this->champs = $champs;

        return true;
    }
................................................................................
                WHERE '.$config->get('champ_identifiant').' = "";');

            // Création de l'index unique
            $db->exec('CREATE UNIQUE INDEX membres_identifiant ON membres ('.$config->get('champ_identifiant').');');
        }

        // Création des index pour les champs affichés dans la liste des membres
        $listed_fields = array_keys((array) $this->getListedFields());
        foreach ($listed_fields as $field)
        {
            if ($field === $config->get('champ_identifiant'))
            {
                // Il y a déjà un index
                continue;
            }

Modified src/include/lib/Garradin/Membres/Session.php from [2d466640b5] to [bded1148a6].

174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
...
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
			return false;
		}

		if (!Security_OTP::TOTP($user->secret, $code))
		{
			// Vérifier encore, mais avec le temps NTP
			// au cas où l'horloge du serveur n'est pas à l'heure
			$time = Security_OTP::getTimeFromNTP('fr.pool.ntp.org');

			if (!Security_OTP::TOTP($user->secret, $code, $time))
			{
				return false;
			}
		}

................................................................................
		}

		// On utilise le mot de passe: si l'utilisateur change de mot de passe
		// toutes les sessions précédentes sont invalidées
		$hash = hash(self::HASH_ALGO, $row->token . $row->passe);

		// Vérification du token
		if (!Security::hash_equals($cookie->token, $row->token))
		{
			// Le sélecteur est valide, mais pas le token ?
			// c'est probablement que le cookie a été volé, qu'un attaquant
			// a obtenu un nouveau token, et que l'utilisateur se représente 
			// avec un token qui n'est plus valide.
			// Dans ce cas supprimons toutes les sessions de ce membre pour 
			// le forcer à se re-connecter







|







 







|







174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
...
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
			return false;
		}

		if (!Security_OTP::TOTP($user->secret, $code))
		{
			// Vérifier encore, mais avec le temps NTP
			// au cas où l'horloge du serveur n'est pas à l'heure
			$time = Security_OTP::getTimeFromNTP(NTP_SERVER);

			if (!Security_OTP::TOTP($user->secret, $code, $time))
			{
				return false;
			}
		}

................................................................................
		}

		// On utilise le mot de passe: si l'utilisateur change de mot de passe
		// toutes les sessions précédentes sont invalidées
		$hash = hash(self::HASH_ALGO, $row->token . $row->passe);

		// Vérification du token
		if (!hash_equals($cookie->token, $row->token))
		{
			// Le sélecteur est valide, mais pas le token ?
			// c'est probablement que le cookie a été volé, qu'un attaquant
			// a obtenu un nouveau token, et que l'utilisateur se représente 
			// avec un token qui n'est plus valide.
			// Dans ce cas supprimons toutes les sessions de ce membre pour 
			// le forcer à se re-connecter

Modified src/include/lib/Garradin/Template.php from [ad4149b96e] to [bdc7529178].

104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
}

function tpl_strftime_fr($ts, $format)
{
    return Utils::strftime_fr($format, $ts);
}

function tpl_date_fr($ts, $format)
{
    return Utils::date_fr($format, $ts);
}

function tpl_format_droits($params)
{
    $droits = $params['droits'];







|







104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
}

function tpl_strftime_fr($ts, $format)
{
    return Utils::strftime_fr($format, $ts);
}

function tpl_date_fr($ts, $format = 'd/m/Y H:i:s')
{
    return Utils::date_fr($format, $ts);
}

function tpl_format_droits($params)
{
    $droits = $params['droits'];

Modified src/include/lib/Garradin/Utils.php from [9c5200104c] to [51958d7a51].

558
559
560
561
562
563
564
565
566
567

568
569
570
571
572
573
574
...
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
            {
                return $key . ' = ' . ($value ? 'true' : 'false');
            }
            elseif (is_numeric($value))
            {
                return $key . ' = ' . $value;
            }
            elseif (is_array($value))
            {
                $out = '';

                foreach ($value as $row)
                {
                    $out .= $get_ini_line($key . '[]', $row) . "\n";
                }

                return substr($out, 0, -1);
            }
................................................................................
            {
                return $key . ' = "' . str_replace('"', '\\"', $value) . '"';
            }
        };

        foreach ($in as $key=>$value)
        {
            if (is_array($value) && is_string($key))
            {
                $out .= '[' . $key . "]\n";

                foreach ($value as $row_key=>$row_value)
                {
                    $out .= $get_ini_line($row_key, $row_value) . "\n";
                }







|


>







 







|







558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
...
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
            {
                return $key . ' = ' . ($value ? 'true' : 'false');
            }
            elseif (is_numeric($value))
            {
                return $key . ' = ' . $value;
            }
            elseif (is_array($value) || is_object($value))
            {
                $out = '';
                $value = (array) $value;
                foreach ($value as $row)
                {
                    $out .= $get_ini_line($key . '[]', $row) . "\n";
                }

                return substr($out, 0, -1);
            }
................................................................................
            {
                return $key . ' = "' . str_replace('"', '\\"', $value) . '"';
            }
        };

        foreach ($in as $key=>$value)
        {
            if ((is_array($value) || is_object($value)) && is_string($key))
            {
                $out .= '[' . $key . "]\n";

                foreach ($value as $row_key=>$row_value)
                {
                    $out .= $get_ini_line($row_key, $row_value) . "\n";
                }

Modified src/templates/admin/config/index.tpl from [5b3c1849f9] to [c6633c21cb].

18
19
20
21
22
23
24
25





26
27
28
29
30
31
32

    <fieldset>
        <legend>Garradin</legend>
        <dl>
            <dt>Version installée</dt>
            <dd class="help">{$garradin_version} <a href="{$garradin_website}">[Vérifier la disponibilité d'une nouvelle version]</a></dd>
            <dt>Informations système</dt>
            <dd class="help">PHP version {$php_version} — SQLite version {$sqlite_version}</dd>





        </dl>
    </fieldset>

    <fieldset>
        <legend>Informations sur l'association</legend>
        <dl>
            <dt><label for="f_nom_asso">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>







|
>
>
>
>
>







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

    <fieldset>
        <legend>Garradin</legend>
        <dl>
            <dt>Version installée</dt>
            <dd class="help">{$garradin_version} <a href="{$garradin_website}">[Vérifier la disponibilité d'une nouvelle version]</a></dd>
            <dt>Informations système</dt>
            <dd class="help">
                Version PHP&nbsp;: {$php_version}<br />
                Version SQLite&nbsp;: {$sqlite_version}<br />
                Heure du serveur&nbsp;: {$server_time|date_fr} ({if $time_diff > -5 && $time_diff < 5}à l'heure{elseif $time_diff < 0}en retard de {$time_diff} secondes{else}en avance de {$time_diff} secondes{/if})<br />
                Chiffrement GnuPG&nbsp;: {if $has_gpg_support}disponible, module activé{else}non, module PHP gnupg non installé&nbsp;?{/if}<br />
            </dd>
        </dl>
    </fieldset>

    <fieldset>
        <legend>Informations sur l'association</legend>
        <dl>
            <dt><label for="f_nom_asso">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt>

Modified src/templates/admin/config/membres.tpl from [894ad69c3d] to [fae223707c].

1
2
3
4
5
6
7
8
..
15
16
17
18
19
20
21






22
23
24
25
26
27
28
...
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
...
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
...
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
...
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
...
287
288
289
290
291
292
293


294
295
296
297
298
299
300
301
{include file="admin/_head.tpl" title="Configuration — Fiche membres" current="config" js=1}

{include file="admin/config/_menu.tpl" current="membres"}

{if $error}
    {if $error == 'OK'}
    <p class="confirm">
        La configuration a bien été enregistrée.
................................................................................
    <p class="error">
        {$error}
    </p>
    {/if}
{/if}

{if $review}






    <fieldset>
        <legend>Fiche membre exemple</legend>
        <dl>
            {foreach from=$champs item="champ" key="nom"}
                {if $nom == 'passe'}{continue}{/if}
                {html_champ_membre config=$champ name=$nom disabled=true}
                {if empty($champ.editable) || !empty($champ.private)}
................................................................................
        {if $nom == 'passe'}{continue}{/if}
        <fieldset id="f_{$nom}">
            <legend>{$nom}</legend>
            <dl>
                <dt><label>Type</label></dt>
                <dd>{$champ.type|get_type}</dd>
                <dt><label for="f_{$nom}_title">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                <dd><input type="text" name="champs[{$nom}][title]" id="f_{$nom}_title" value="{form_field data=$champs[$nom] name=title}" size="60" required="required" /></dd>
                <dt><label for="f_{$nom}_help">Aide</label></dt>
                <dd><input type="text" name="champs[{$nom}][help]" id="f_{$nom}_help" value="{form_field data=$champs[$nom] name=help}" size="100" /></dd>
                <dt><label><input type="checkbox" name="champs[{$nom}][editable]" value="1" {form_field data=$champs[$nom] name=editable checked="1"} /> Modifiable par les membres</label></dt>
                <dd class="help">Si coché, les membres pourront changer cette information depuis leur espace personnel.</dd>
                <dt><label><input type="checkbox" name="champs[{$nom}][mandatory]" value="1" {form_field data=$champs[$nom] name=mandatory checked="1"} /> Champ obligatoire</label></dt>
                <dd class="help">Si coché, ce champ ne pourra rester vide.</dd>
                <dt><label><input type="checkbox" name="champs[{$nom}][private]" value="1" {form_field data=$champs[$nom] name=private checked="1"} /> Champ privé</label></dt>
                <dd class="help">Si coché, ce champ ne sera visible et modifiable que par les personnes pouvant gérer les membres, mais pas les membres eux-même.</dd>
                {if $champ.type == 'select' || $champ.type == 'multiple'}
                    <dt><label>Options disponibles</label></dt>
                    {if $champ.type == 'multiple'}
                        <dd class="help">Attention changer l'ordre des options peut avoir des effets indésirables.</dd>
                    {else}
                        <dd class="help">Attention renommer ou supprimer une option n'affecte pas ce qui a déjà été enregistré dans les fiches des membres.</dd>
................................................................................
                        {if $champ.type == 'select' || empty($champ.options) || count($champ.options) < 32}
                            <li><input type="text" name="champs[{$nom}][options][]" value="" size="50" /></li>
                        {/if}
                    </dd>
                {/if}
                <dt><label for="f_{$nom}_list_row">Numéro de colonne dans la liste des membres</label></dt>
                <dd class="help">Laisser vide ou indiquer le chiffre zéro pour que ce champ n'apparaisse pas dans la liste des membres. Inscrire un chiffre entre 1 et 10 pour indiquer l'ordre d'affichage du champ dans le tableau de la liste des membres.</dd>
                <dd><input type="number" id="f_{$nom}_list_row" name="champs[{$nom}][list_row]" min="0" max="10" value="{form_field data=$champs[$nom] name=list_row}" /></dd>
            </dl>
        </fieldset>
        {/foreach}
    </div>

    <fieldset id="f_passe">
        <legend>Mot de passe</legend>
................................................................................
    }

    var fields = document.querySelectorAll('#orderFields fieldset');

    for (i = 0; i < fields.length; i++)
    {
        var field = fields[i];
        field.querySelector('dl').style.display = 'none';

        var legend = field.querySelector('legend');

        legend.onclick = function () {
            var content = this.parentNode.querySelector('dl');
            if (content.style.display.toLowerCase() == 'none')
                content.style.display = 'block';
            else
                content.style.display = 'none';
        }

        legend.className = 'interactive';
        legend.title = 'Cliquer pour modifier ce champ';

        var actions = document.createElement('div');
        actions.className = 'actions';
................................................................................
        actions.appendChild(down);

        var edit = document.createElement('a');
        edit.className = 'icn edit';
        edit.innerHTML = '&#x270e;';
        edit.title = 'Modifier ce champ';
        edit.onclick = function (e) {
            var content = this.parentNode.parentNode.querySelector('dl');
            if (content.style.display.toLowerCase() == 'none')
                content.style.display = 'block';
            else
                content.style.display = 'none';
            return false;
        };
        actions.appendChild(edit);

        if (field.id != champ_identifiant && field.id != 'f_passe' && field.id != champ_identite)
        {
            var rem = document.createElement('a');
................................................................................
            rem.onclick = function (e) {
                if (!window.confirm('Êtes-vous sûr de supprimer ce champ des fiches de membre ?'))
                {
                    return false;
                }

                var field = this.parentNode.parentNode;


                field.parentNode.removeChild(field);
                return false;
            };
            actions.appendChild(rem);
        }

        if (field.querySelector('.options'))
        {
|







 







>
>
>
>
>
>







 







|

|
|

|

|







 







|







 







|




|
<
<
<
<







 







|
<
<
<
<







 







>
>
|







1
2
3
4
5
6
7
8
..
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
...
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
...
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
...
211
212
213
214
215
216
217
218
219
220
221
222
223




224
225
226
227
228
229
230
...
267
268
269
270
271
272
273
274




275
276
277
278
279
280
281
...
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
{include file="admin/_head.tpl" current="config" js=1}

{include file="admin/config/_menu.tpl" current="membres"}

{if $error}
    {if $error == 'OK'}
    <p class="confirm">
        La configuration a bien été enregistrée.
................................................................................
    <p class="error">
        {$error}
    </p>
    {/if}
{/if}

{if $review}
    <p class="help">
        Voici ce à quoi ressemblera la nouvelle fiche de membre, vérifiez vos modifications avant d'enregistrer les changements.
    </p>
    <p class="alert">
        Attention&nbsp;! Si vous avez supprimé un champ, les données liées à celui-ci seront supprimées de toutes les fiches de tous les membres.
    </p>
    <fieldset>
        <legend>Fiche membre exemple</legend>
        <dl>
            {foreach from=$champs item="champ" key="nom"}
                {if $nom == 'passe'}{continue}{/if}
                {html_champ_membre config=$champ name=$nom disabled=true}
                {if empty($champ.editable) || !empty($champ.private)}
................................................................................
        {if $nom == 'passe'}{continue}{/if}
        <fieldset id="f_{$nom}">
            <legend>{$nom}</legend>
            <dl>
                <dt><label>Type</label></dt>
                <dd>{$champ.type|get_type}</dd>
                <dt><label for="f_{$nom}_title">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt>
                <dd><input type="text" name="champs[{$nom}][title]" id="f_{$nom}_title" value="{form_field data=$champs->$nom name=title}" size="60" required="required" /></dd>
                <dt><label for="f_{$nom}_help">Aide</label></dt>
                <dd><input type="text" name="champs[{$nom}][help]" id="f_{$nom}_help" value="{form_field data=$champs->$nom name=help}" size="100" /></dd>
                <dt><label><input type="checkbox" name="champs[{$nom}][editable]" value="1" {form_field data=$champs->$nom name=editable checked="1"} /> Modifiable par les membres</label></dt>
                <dd class="help">Si coché, les membres pourront changer cette information depuis leur espace personnel.</dd>
                <dt><label><input type="checkbox" name="champs[{$nom}][mandatory]" value="1" {form_field data=$champs->$nom name=mandatory checked="1"} /> Champ obligatoire</label></dt>
                <dd class="help">Si coché, ce champ ne pourra rester vide.</dd>
                <dt><label><input type="checkbox" name="champs[{$nom}][private]" value="1" {form_field data=$champs->$nom name=private checked="1"} /> Champ privé</label></dt>
                <dd class="help">Si coché, ce champ ne sera visible et modifiable que par les personnes pouvant gérer les membres, mais pas les membres eux-même.</dd>
                {if $champ.type == 'select' || $champ.type == 'multiple'}
                    <dt><label>Options disponibles</label></dt>
                    {if $champ.type == 'multiple'}
                        <dd class="help">Attention changer l'ordre des options peut avoir des effets indésirables.</dd>
                    {else}
                        <dd class="help">Attention renommer ou supprimer une option n'affecte pas ce qui a déjà été enregistré dans les fiches des membres.</dd>
................................................................................
                        {if $champ.type == 'select' || empty($champ.options) || count($champ.options) < 32}
                            <li><input type="text" name="champs[{$nom}][options][]" value="" size="50" /></li>
                        {/if}
                    </dd>
                {/if}
                <dt><label for="f_{$nom}_list_row">Numéro de colonne dans la liste des membres</label></dt>
                <dd class="help">Laisser vide ou indiquer le chiffre zéro pour que ce champ n'apparaisse pas dans la liste des membres. Inscrire un chiffre entre 1 et 10 pour indiquer l'ordre d'affichage du champ dans le tableau de la liste des membres.</dd>
                <dd><input type="number" id="f_{$nom}_list_row" name="champs[{$nom}][list_row]" min="0" max="10" value="{form_field data=$champs->$nom name=list_row}" /></dd>
            </dl>
        </fieldset>
        {/foreach}
    </div>

    <fieldset id="f_passe">
        <legend>Mot de passe</legend>
................................................................................
    }

    var fields = document.querySelectorAll('#orderFields fieldset');

    for (i = 0; i < fields.length; i++)
    {
        var field = fields[i];
        field.querySelector('dl').classList.toggle('hidden');

        var legend = field.querySelector('legend');

        legend.onclick = function () {
            this.parentNode.querySelector('dl').classList.toggle('hidden');




        }

        legend.className = 'interactive';
        legend.title = 'Cliquer pour modifier ce champ';

        var actions = document.createElement('div');
        actions.className = 'actions';
................................................................................
        actions.appendChild(down);

        var edit = document.createElement('a');
        edit.className = 'icn edit';
        edit.innerHTML = '&#x270e;';
        edit.title = 'Modifier ce champ';
        edit.onclick = function (e) {
            this.parentNode.parentNode.querySelector('dl').classList.toggle('hidden');




            return false;
        };
        actions.appendChild(edit);

        if (field.id != champ_identifiant && field.id != 'f_passe' && field.id != champ_identite)
        {
            var rem = document.createElement('a');
................................................................................
            rem.onclick = function (e) {
                if (!window.confirm('Êtes-vous sûr de supprimer ce champ des fiches de membre ?'))
                {
                    return false;
                }

                var field = this.parentNode.parentNode;
                this.parentNode.parentNode.querySelector('dl').classList.add('hidden');
                field.classList.toggle('removed');
                window.setTimeout(function () { field.parentNode.removeChild(field); }, 1000);
                return false;
            };
            actions.appendChild(rem);
        }

        if (field.querySelector('.options'))
        {

Modified src/www/admin/config/index.php from [d0fb97ed05] to [1aa6f266e6].

43
44
45
46
47
48
49




50
51



52
53
54
55
56
57
58
59
60
61
62
63
            $error = $e->getMessage();
        }
    }
}

$tpl->assign('error', $error);





$tpl->assign('garradin_version', garradin_version() . ' [' . (garradin_manifest() ?: 'release') . ']');
$tpl->assign('php_version', phpversion());




$v = \SQLite3::version();
$tpl->assign('sqlite_version', $v['versionString']);

$tpl->assign('pays', Utils::getCountryList());

$cats = new Membres\Categories;
$tpl->assign('membres_cats', $cats->listSimple());

$tpl->assign('champs', $config->get('champs_membres')->getList(true));

$tpl->display('admin/config/index.tpl');







>
>
>
>


>
>
>












43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
            $error = $e->getMessage();
        }
    }
}

$tpl->assign('error', $error);

$server_time = time();
$ntp_time = \KD2\Security_OTP::getTimeFromNTP(NTP_SERVER);
$diff = $server_time - $ntp_time;

$tpl->assign('garradin_version', garradin_version() . ' [' . (garradin_manifest() ?: 'release') . ']');
$tpl->assign('php_version', phpversion());
$tpl->assign('has_gpg_support', \KD2\Security::canUseEncryption());
$tpl->assign('server_time', $server_time);
$tpl->assign('time_diff', $diff);

$v = \SQLite3::version();
$tpl->assign('sqlite_version', $v['versionString']);

$tpl->assign('pays', Utils::getCountryList());

$cats = new Membres\Categories;
$tpl->assign('membres_cats', $cats->listSimple());

$tpl->assign('champs', $config->get('champs_membres')->getList(true));

$tpl->display('admin/config/index.tpl');

Modified src/www/admin/config/membres.php from [6f25a6449e] to [a7dd2bb69f].

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
..
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
..
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
...
134
135
136
137
138
139
140
141


142
require_once __DIR__ . '/_inc.php';

$error = false;

$membres = new Membres;

// Restauration de ce qui était en session
if ($champs = $membres->sessionGet('champs_membres'))
{
    $champs = new Membres\Champs($champs);
}
else
{
    // Il est nécessaire de créer une nouvelle instance ici, sinon
    // l'enregistrement des modifs ne marchera pas car les deux instances seront identiques.
................................................................................
    {
        $error = 'Une erreur est survenue, merci de renvoyer le formulaire.';
    }
    else
    {
        if (!empty($_POST['reset']))
        {
            $membres->sessionStore('champs_membres', null);
            Utils::redirect('/admin/config/membres.php');
        }
        elseif (!empty($_POST['review']))
        {
            try {
                $nouveau_champs = Utils::post('champs');

                foreach ($nouveau_champs as $key=>&$cfg)
                {
                    $cfg['type'] = $champs->get($key, 'type');
                }
                
                $champs->setAll($nouveau_champs);
                $membres->sessionStore('champs_membres', (string)$champs);

                Utils::redirect('/admin/config/membres.php?review');
            }
            catch (UserException $e)
            {
                $error = $e->getMessage();
            }
................................................................................
                    {
                        $config['options'] = ['Première option'];
                    }

                    $champs->add($new, $config);
                }

                $membres->sessionStore('champs_membres', (string) $champs);

                Utils::redirect('/admin/config/membres.php?added');
            }
            catch (UserException $e)
            {
                $error = $e->getMessage();
            }
        }
        elseif (!empty($_POST['save']))
        {
            try {
                $champs->save();
                $membres->sessionStore('champs_membres', null);
                Utils::redirect('/admin/config/membres.php?ok');
            }
            catch (UserException $e)
            {
                $error = $e->getMessage();
            }
        }
................................................................................

$tpl->register_modifier('get_type', function ($type) use ($types) {
    return $types[$type];
});

$tpl->assign('csrf_name', Utils::CSRF_field_name('config_membres'));
$tpl->assign('csrf_value', Utils::CSRF_create('config_membres'));



$tpl->display('admin/config/membres.tpl');







|







 







|













|







 







|












|







 








>
>

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
..
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
..
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
...
134
135
136
137
138
139
140
141
142
143
144
require_once __DIR__ . '/_inc.php';

$error = false;

$membres = new Membres;

// Restauration de ce qui était en session
if ($champs = $session->sessionGet('champs_membres'))
{
    $champs = new Membres\Champs($champs);
}
else
{
    // Il est nécessaire de créer une nouvelle instance ici, sinon
    // l'enregistrement des modifs ne marchera pas car les deux instances seront identiques.
................................................................................
    {
        $error = 'Une erreur est survenue, merci de renvoyer le formulaire.';
    }
    else
    {
        if (!empty($_POST['reset']))
        {
            $session->sessionStore('champs_membres', null);
            Utils::redirect('/admin/config/membres.php');
        }
        elseif (!empty($_POST['review']))
        {
            try {
                $nouveau_champs = Utils::post('champs');

                foreach ($nouveau_champs as $key=>&$cfg)
                {
                    $cfg['type'] = $champs->get($key, 'type');
                }
                
                $champs->setAll($nouveau_champs);
                $session->sessionStore('champs_membres', (string)$champs);

                Utils::redirect('/admin/config/membres.php?review');
            }
            catch (UserException $e)
            {
                $error = $e->getMessage();
            }
................................................................................
                    {
                        $config['options'] = ['Première option'];
                    }

                    $champs->add($new, $config);
                }

                $session->sessionStore('champs_membres', (string) $champs);

                Utils::redirect('/admin/config/membres.php?added');
            }
            catch (UserException $e)
            {
                $error = $e->getMessage();
            }
        }
        elseif (!empty($_POST['save']))
        {
            try {
                $champs->save();
                $session->sessionStore('champs_membres', null);
                Utils::redirect('/admin/config/membres.php?ok');
            }
            catch (UserException $e)
            {
                $error = $e->getMessage();
            }
        }
................................................................................

$tpl->register_modifier('get_type', function ($type) use ($types) {
    return $types[$type];
});

$tpl->assign('csrf_name', Utils::CSRF_field_name('config_membres'));
$tpl->assign('csrf_value', Utils::CSRF_create('config_membres'));

$tpl->assign('title', 'Configuration — ' . (isset($_GET['review']) ? 'Confirmer les changements' : 'Fiche membres'));

$tpl->display('admin/config/membres.tpl');

Modified src/www/admin/static/admin.css from [b2e904a922] to [0a77a33694].

1141
1142
1143
1144
1145
1146
1147



1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165





















1166
1167
1168
1169
1170
1171
1172
    background: #eee;
    padding: .5em;
}

#orderFields fieldset {
    position: relative;
    min-height: 2em;



}

#orderFields fieldset legend {
    font-size: 1.2em;
    line-height: .8em;
    color: #666;
}

#orderFields fieldset .actions {
    display: block;
    position: absolute;
    top: 0em;
    right: 1em;
}

#orderFields fieldset .actions .icn {
    position: absolute;
}






















#orderFields fieldset .actions .remove { right: 0em; }
#orderFields fieldset .actions .edit { right: 1.5em; }
#orderFields fieldset .actions .down { right: 3em; }
#orderFields fieldset .actions .up { right: 4.5em; }

#orderFields fieldset:nth-child(1) .actions .up, #orderFields fieldset:nth-last-child(1) .actions .down {







>
>
>











|






>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
    background: #eee;
    padding: .5em;
}

#orderFields fieldset {
    position: relative;
    min-height: 2em;
    transition: all 1s;
    overflow: hidden;
    max-height: 1000px;
}

#orderFields fieldset legend {
    font-size: 1.2em;
    line-height: .8em;
    color: #666;
}

#orderFields fieldset .actions {
    display: block;
    position: absolute;
    top: 1em;
    right: 1em;
}

#orderFields fieldset .actions .icn {
    position: absolute;
}

#orderFields fieldset dl {
    overflow: hidden;
    transition: all .5s;
    opacity: 1;
    display: block;
    max-height: 1000px;
}

#orderFields fieldset dl.hidden {
    opacity: 0;
    max-height: 0;
}

#orderFields fieldset.removed {
    max-height: 0;
    opacity: 0;
    border-color: red;
    min-height: 0;
    height: 0;
}

#orderFields fieldset .actions .remove { right: 0em; }
#orderFields fieldset .actions .edit { right: 1.5em; }
#orderFields fieldset .actions .down { right: 3em; }
#orderFields fieldset .actions .up { right: 4.5em; }

#orderFields fieldset:nth-child(1) .actions .up, #orderFields fieldset:nth-last-child(1) .actions .down {