Comment: | Implement edit and adding new fields |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | dev |
Files: | files | file ages | folders |
SHA3-256: |
c2c72edead74bb8ac2f81a68890d3062 |
User & Date: | bohwaz on 2022-03-16 02:03:12 |
Other Links: | branch diff | manifest | tags |
2022-03-16
| ||
02:41 | Rename config properties to English check-in: 8b6354df02 user: bohwaz tags: dev | |
02:03 | Implement edit and adding new fields check-in: c2c72edead user: bohwaz tags: dev | |
2022-03-15
| ||
00:42 | Implement dynamic field delete and edit check-in: 3e93b9feec user: bohwaz tags: dev | |
Modified src/include/data/1.2.0_schema.sql from [deb0df031b] to [bbc725b503].
︙ | ︙ | |||
14 15 16 17 18 19 20 | sort_order INTEGER NOT NULL, type TEXT NOT NULL, label TEXT NOT NULL, help TEXT NULL, required INTEGER NOT NULL DEFAULT 0, read_access INTEGER NOT NULL DEFAULT 0, write_access INTEGER NOT NULL DEFAULT 1, | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | sort_order INTEGER NOT NULL, type TEXT NOT NULL, label TEXT NOT NULL, help TEXT NULL, required INTEGER NOT NULL DEFAULT 0, read_access INTEGER NOT NULL DEFAULT 0, write_access INTEGER NOT NULL DEFAULT 1, list_table INTEGER NOT NULL DEFAULT 0, options TEXT NULL, default_value TEXT NULL, system TEXT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS config_users_fields_name ON config_users_fields (name); |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Users/DynamicField.php from [0e567ce101] to [0e64db5237].
︙ | ︙ | |||
39 40 41 42 43 44 45 | /** * 0 = only admins can write this field * 1 = admins + the user themselves can change it */ protected int $write_access; /** | | | | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | /** * 0 = only admins can write this field * 1 = admins + the user themselves can change it */ protected int $write_access; /** * Use in users list table? */ protected bool $list_table; /** * Multiple options (JSON) for select and multiple fields */ protected ?array $options = []; /** |
︙ | ︙ | |||
179 180 181 182 183 184 185 | $this->assert(!$db->test(self::TABLE, 'name = ?', $this->name), 'Ce nom de champ est déjà utilisé par un autre champ: ' . $this->name); } else { $this->assert(!$db->test(self::TABLE, 'name = ? AND id != ?', $this->name, $this->id()), 'Ce nom de champ est déjà utilisé par un autre champ.'); } $this->assert($this->exists() || $this->system & self::PRESET || !array_key_exists($this->name, DynamicFields::getInstance()->getPresets()), 'Ce nom de champ est déjà utilisé par un champ pré-défini.'); | | > > > | > > > > > > > > > > > > > > | 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | $this->assert(!$db->test(self::TABLE, 'name = ?', $this->name), 'Ce nom de champ est déjà utilisé par un autre champ: ' . $this->name); } else { $this->assert(!$db->test(self::TABLE, 'name = ? AND id != ?', $this->name, $this->id()), 'Ce nom de champ est déjà utilisé par un autre champ.'); } $this->assert($this->exists() || $this->system & self::PRESET || !array_key_exists($this->name, DynamicFields::getInstance()->getPresets()), 'Ce nom de champ est déjà utilisé par un champ pré-défini.'); if ($this->exists()) { $this->assert(!isset($this->_modified['type'])); $this->assert(!isset($this->_modified['name'])); } } public function importForm(array $source = null) { if (null === $source) { $source = $_POST; } $source['required'] = !empty($source['required']) ? true : false; $source['list_table'] = !empty($source['list_table']) ? true : false; return parent::importForm($source); } } |
Modified src/include/lib/Garradin/Upgrade.php from [95ffb05e25] to [439017fa89].
︙ | ︙ | |||
267 268 269 270 271 272 273 | $db->beginSchemaUpdate(); // Create config_users_fields table $db->import(ROOT . '/include/data/1.2.0_schema.sql'); // Migrate users table $df = \Garradin\Users\DynamicFields::fromOldINI($config->champs_membres, $config->champ_identifiant, $config->champ_identite, 'numero'); | | | 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | $db->beginSchemaUpdate(); // Create config_users_fields table $db->import(ROOT . '/include/data/1.2.0_schema.sql'); // Migrate users table $df = \Garradin\Users\DynamicFields::fromOldINI($config->champs_membres, $config->champ_identifiant, $config->champ_identite, 'numero'); $df->save(false); // Migrate other stuff $db->import(ROOT . '/include/data/1.2.0_migration.sql'); $db->commitSchemaUpdate(); } // Réinstaller les plugins système si nécessaire |
︙ | ︙ |
Modified src/include/lib/Garradin/Users/DynamicFields.php from [4fb2df2bc7] to [a863dfd6c8].
︙ | ︙ | |||
25 26 27 28 29 30 31 | protected $_fields_by_system_use = [ 'login' => [], 'password' => [], 'name' => [], 'number' => [], ]; | | > > | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | protected $_fields_by_system_use = [ 'login' => [], 'password' => [], 'name' => [], 'number' => [], ]; protected array $_presets = []; protected array $_deleted = []; static protected $_instance; static public function getInstance() { if (null === self::$_instance) { self::$_instance = new self; |
︙ | ︙ | |||
311 312 313 314 315 316 317 | $field->set('name', $name); $field->set('label', $data['title']); $field->set('type', $data['type']); $field->set('help', empty($data['help']) ? null : $data['help']); $field->set('read_access', $data['private'] ? $field::ACCESS_ADMIN : $field::ACCESS_USER); $field->set('write_access', $data['editable'] ? $field::ACCESS_ADMIN : $field::ACCESS_USER); $field->set('required', (bool) $data['mandatory']); | | | 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | $field->set('name', $name); $field->set('label', $data['title']); $field->set('type', $data['type']); $field->set('help', empty($data['help']) ? null : $data['help']); $field->set('read_access', $data['private'] ? $field::ACCESS_ADMIN : $field::ACCESS_USER); $field->set('write_access', $data['editable'] ? $field::ACCESS_ADMIN : $field::ACCESS_USER); $field->set('required', (bool) $data['mandatory']); $field->set('list_table', (bool) $data['list_row']); $field->set('sort_order', $i++); $self->add($field); if ($field->type == 'checkbox' || $field->type == 'multiple') { // A checkbox/multiple checkbox can either be 0 or 1, not NULL $db->exec(sprintf('UPDATE membres SET %s = 0 WHERE %1$s IS NULL;', $field->name)); } |
︙ | ︙ | |||
387 388 389 390 391 392 393 | $fields = array_filter( $this->_fields, function ($a, $b) use ($name_fields) { if (in_array($b, $name_fields)) { return false; } | | | | | 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 | $fields = array_filter( $this->_fields, function ($a, $b) use ($name_fields) { if (in_array($b, $name_fields)) { return false; } return empty($a->list_table) ? false : true; }, ARRAY_FILTER_USE_BOTH ); uasort($fields, function ($a, $b) { if ($a->sort_order == $b->sort_order) return 0; return ($a->sort_order > $b->sort_order) ? 1 : -1; }); return $fields; } public function getSQLSchema(string $table_name = User::TABLE): string { |
︙ | ︙ | |||
455 456 457 458 459 460 461 | public function getSQLSearchSchema(string $table_name = User::TABLE): ?string { $search_table = $table_name . '_search'; $columns = []; foreach ($this->_fields as $key => $cfg) { | | | 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 | public function getSQLSearchSchema(string $table_name = User::TABLE): ?string { $search_table = $table_name . '_search'; $columns = []; foreach ($this->_fields as $key => $cfg) { if ($cfg->type == 'text' || $cfg->list_table) { $columns[] = $key; } } if (!count($columns)) { return null; } |
︙ | ︙ | |||
592 593 594 595 596 597 598 | { $this->_fields[$df->name] = $df; $this->reloadCache(); } public function delete(string $name) { | < < < < | | < < | < | < | | 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 | { $this->_fields[$df->name] = $df; $this->reloadCache(); } public function delete(string $name) { $this->_deleted[] = $this->_fields[$name]; unset($this->_fields[$name]); $this->reloadCache(); } public function save(bool $allow_rebuild = true) { if (empty($this->_fields_by_system_use['number'])) { throw new ValidationException('Aucun champ de numéro de membre n\'existe'); } if (count($this->_fields_by_system_use['number']) != 1) { throw new ValidationException('Un seul champ peut être défini comme numéro'); |
︙ | ︙ | |||
635 636 637 638 639 640 641 642 643 | if (empty($this->_fields_by_system_use['password'])) { throw new ValidationException('Aucun champ de mot de passe n\'existe'); } if (count($this->_fields_by_system_use['password']) != 1) { throw new ValidationException('Un seul champ peut être défini comme mot de passe'); } foreach ($this->_fields as $field) { | > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > | > > > > > | 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 | if (empty($this->_fields_by_system_use['password'])) { throw new ValidationException('Aucun champ de mot de passe n\'existe'); } if (count($this->_fields_by_system_use['password']) != 1) { throw new ValidationException('Un seul champ peut être défini comme mot de passe'); } $rebuild = false; foreach ($this->_fields as $field) { if (!$field->exists()) { $rebuild = true; } if ($field->isModified()) { $field->save(); } } foreach ($this->_deleted as $f) { $f->delete(); $rebuild = true; } $this->_deleted = []; if ($rebuild && $allow_rebuild) { $db = DB::getInstance(); $db->begin(); // FIXME/TODO: use ALTER TABLE ... DROP COLUMN for SQLite 3.35.0+ // some conditions apply // https://www.sqlite.org/lang_altertable.html#altertabdropcol $this->rebuildUsersTable(); $db->commit(); $this->reload(); } } public function setOrderAll(array $order) { foreach (array_values($order) as $sort => $key) { if (!array_key_exists($key, $this->_fields)) { throw new \InvalidArgumentException('Unknown field name: ' . $key); } $this->_fields[$key]->set('sort_order', $sort); } } public function getLastOrderIndex() { return count($this->_fields); } } |
Modified src/templates/admin/config/fields/edit.tpl from [4e4c6cc5a2] to [a1b7b7cc77].
︙ | ︙ | |||
13 14 15 16 17 18 19 | {if !$field->exists()} <p class="help block">Avant de demander une information personnelle à vos membres… en avez-vous vraiment besoin ?<br /> La loi demande à minimiser au strict minimum les données collectées. Pensez également aux risques de sécurité : si vous demandez la date de naissance complète, cela pourrait être utilisé pour de l'usurpation d'identité, il serait donc plus sage de ne demander que le mois et l'année de naissance, si ces données sont nécessaires afin d'avoir l'âge de la personne. </p> {/if} <dl> {if !$field->exists()} | | > > | > | < < < | < < < > > > > > > > > > > > > > | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | {if !$field->exists()} <p class="help block">Avant de demander une information personnelle à vos membres… en avez-vous vraiment besoin ?<br /> La loi demande à minimiser au strict minimum les données collectées. Pensez également aux risques de sécurité : si vous demandez la date de naissance complète, cela pourrait être utilisé pour de l'usurpation d'identité, il serait donc plus sage de ne demander que le mois et l'année de naissance, si ces données sont nécessaires afin d'avoir l'âge de la personne. </p> {/if} <dl> {if !$field->exists()} {input type="select" name="type" options=$field::TYPES source=$field label="Type" default="text" help="Il ne sera plus possible de modifier le type une fois le champ créé." required=true} {input type="text" name="name" pattern="[a-z](_?[a-z0-9]+)*" label="Nom unique" required=true source=$field help="Ne peut comporter que des lettres minuscules et des tirets bas. Par exemple pour un champ demandant l'adresse, on peut utiliser 'adresse_postale'. Ce nom ne peut plus être modifié ensuite."} {else} <dd class="help">Le type et le nom unique ne sont pas modifiables.</dd> {input type="select" name="type" options=$field::TYPES source=$field label="Type" disabled=true} {input type="text" name="name" disabled=true label="Nom unique" source=$field} {/if} {input type="text" name="label" label="Libellé" required=true source=$field} {input type="text" name="help" label="Texte d'aide" help="Apparaîtra dans les formulaires de manière identique à ce texte." source=$field} {input type="checkbox" name="required" value=1 label="Champ obligatoire" help="Si coché, une fiche membre ne pourra pas être enregistrée si ce champ n'est pas renseigné." source=$field} {input type="text" name="default" source=$field label="Valeur par défaut" help="Si renseigné, le champ aura cette valeur par défaut lors de l'ajout d'un nouveau membre"} {input type="checkbox" name="list_table" value=1 label="Afficher dans la liste des membres" source=$field} </dl> </fieldset> <fieldset class="type-select type-multiple"> <legend>Options possibles</legend> <p class="alert block type-select">Attention renommer ou supprimer une option n'affecte pas ce qui a déjà été enregistré dans les fiches des membres.</p> <p class="alert block type-multiple">Attention changer l'ordre des options peut avoir des effets indésirables.</p> <dl class="type-multiple type-select options"> {foreach from=$field.options item="option"} <dd>{input type="text" name="options[]" default=$option}</dd> {/foreach} <dd>{input type="text" name="options[]"}</dd> </dl> </fieldset> <fieldset> <legend>Accès</legend> <dl> <dt>Le champ est visible…</dt> {input type="radio" name="read_access" value=$field::ACCESS_ADMIN label="Seulement aux personnes qui gèrent les membres" source=$field} {input type="radio" name="read_access" value=$field::ACCESS_USER label="Au membre lui-même, et aux personnes qui gèrent les membres" source=$field help="Le membre pourra voir cette information en se connectant" default=$field::ACCESS_USER} <dt>Le champ peut être modifié…</dt> {input type="radio" name="write_access" value=$field::ACCESS_ADMIN label="Par les personnes qui gèrent les membres" source=$field} {input type="radio" name="write_access" value=$field::ACCESS_USER label="Par le membre lui-même, et les personnes qui gèrent les membres" source=$field help="Le membre pourra modifier cette information en se connectant" default=$field::ACCESS_USER} </dl> </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {linkbutton label="Annuler" shape="left" href="./" target="_parent"} {button type="submit" name="save" label="Enregistrer" shape="right" class="main"} </p> </form> <script type="text/javascript" src="{$admin_url}static/scripts/config_fields.js"></script> {include file="admin/_foot.tpl"} |
Modified src/templates/admin/config/fields/index.tpl from [2b943be0f1] to [283d7f900d].
︙ | ︙ | |||
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <form method="post" action="{$self_url_no_qs}"> <table class="list"> <thead> <tr> <td>Ordre</td> <th>Libellé</th> <td>Liste des membres</td> <td></td> </tr> </thead> <tbody> {foreach from=$fields item="field"} <tr> <td> {button shape="menu" title="Cliquer, glisser et déposer pour modifier l'ordre"} <input type="hidden" name="sort_order[]" value="{$field.name}" /> </td> <th>{$field.label}</th> | > > > | > > > | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | <form method="post" action="{$self_url_no_qs}"> <table class="list"> <thead> <tr> <td>Ordre</td> <th>Libellé</th> <td>Liste des membres</td> <td>Obligatoire ?</td> <td>Visibilité</td> <td>Modification</td> <td></td> </tr> </thead> <tbody> {foreach from=$fields item="field"} <tr> <td> {button shape="menu" title="Cliquer, glisser et déposer pour modifier l'ordre"} <input type="hidden" name="sort_order[]" value="{$field.name}" /> </td> <th>{$field.label}</th> <td>{if $field.list_table}Oui{/if}</td> <td>{if $field.required}Obligatoire{/if}</td> <td>{if $field.read_access == $field::ACCESS_USER}Membre{else}Gestionnaires{/if}</td> <td>{if $field.write_access == $field::ACCESS_USER}Membre{else}Gestionnaires{/if}</td> <td class="actions"> {if !$field.system || ($field.system && !($field.system | $field::PRESET))} {linkbutton shape="delete" label="Supprimer" href="delete.php?id=%d"|args:$field.id target="_dialog"} {/if} {linkbutton shape="edit" label="Modifier" href="edit.php?id=%d"|args:$field.id target="_dialog"} </td> </tr> |
︙ | ︙ |
Modified src/www/admin/config/fields/delete.php from [bbd2f22de5] to [9f47781249].
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 27 | $form->runIf('delete', function () use ($field, $fields) { if (!f('confirm_delete')) { throw new UserException('Merci de bien vouloir cocher la case pour confirmer la suppression.'); } $fields->delete($field->name); }, $csrf_key, '!config/fields/?msg=DELETED'); $tpl->assign(compact('csrf_key', 'field')); $tpl->display('admin/config/fields/delete.tpl'); | > | 16 17 18 19 20 21 22 23 24 25 26 27 28 | $form->runIf('delete', function () use ($field, $fields) { if (!f('confirm_delete')) { throw new UserException('Merci de bien vouloir cocher la case pour confirmer la suppression.'); } $fields->delete($field->name); $fields->save(); }, $csrf_key, '!config/fields/?msg=DELETED'); $tpl->assign(compact('csrf_key', 'field')); $tpl->display('admin/config/fields/delete.tpl'); |
Modified src/www/admin/config/fields/edit.php from [4f4bd678e4] to [eeda1a9d98].
︙ | ︙ | |||
16 17 18 19 20 21 22 | $field = new DynamicField; } if (!$field) { throw new UserException('Le champ indiqué n\'existe pas.'); } | | > > > > > > | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | $field = new DynamicField; } if (!$field) { throw new UserException('Le champ indiqué n\'existe pas.'); } $form->runIf('save', function () use ($field, $fields) { $field->importForm(); if (!$field->exists()) { $field->sort_order = $fields->getLastOrderIndex(); $fields->add($field); } $fields->save(); }, $csrf_key, '!config/fields/?msg=SAVED'); $tpl->assign(compact('csrf_key', 'field')); $tpl->display('admin/config/fields/edit.tpl'); |
Modified src/www/admin/config/fields/index.php from [87b5ad26e4] to [6e011c28dd].
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php namespace Garradin; use Garradin\Users\DynamicFields; require_once __DIR__ . '/../_inc.php'; $csrf_key = 'change_fields_order'; $fields = DynamicFields::getInstance(); $form->runIf('save', function () use ($fields) { $fields->setOrderAll(f('sort_order')); $fields->save(); | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php namespace Garradin; use Garradin\Users\DynamicFields; require_once __DIR__ . '/../_inc.php'; $csrf_key = 'change_fields_order'; $fields = DynamicFields::getInstance(); $form->runIf('save', function () use ($fields) { $fields->setOrderAll(f('sort_order')); $fields->save(); }, $csrf_key, '!config/fields/?msg=SAVED_ORDER'); $tpl->assign('fields', $fields->all()); $tpl->assign(compact('csrf_key')); $tpl->display('admin/config/fields/index.tpl'); |
Added src/www/admin/static/scripts/config_fields.js version [209338fde7].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | function changeType() { var type = $('#f_type').value; g.toggle('.type-select, .type-multiple', false); g.toggle('.type-' + type, true); } $('#f_type').onchange = changeType; changeType(); var addBtn = document.createElement('button'); addBtn.type = "button"; addBtn.dataset.icon = "➕"; addBtn.className = "icn add"; addBtn.title = "Ajouter une option"; var delBtn = document.createElement('button'); delBtn.type = "button"; delBtn.dataset.icon = "➖"; delBtn.className = "icn delete"; delBtn.title = "Enlever cette option"; var options = $('.options dd'); options.forEach((o, i) => { if (i == 0) { return; } let btn = delBtn.cloneNode(true); btn.onclick = delOption; o.appendChild(btn); }); addPlusButton(); function addOption(e) { var options = $('.options dd'); var target = e.target; var new_option = target.parentNode.cloneNode(true); new_option.querySelector('input').value = ''; new_option.querySelectorAll('button').forEach((b) => b.remove()); var btn = delBtn.cloneNode(); btn.onclick = delOption; new_option.appendChild(btn); target.parentNode.parentNode.appendChild(new_option); target.remove(); // Remove add button from previous line addPlusButton(); } function delOption(e) { var options = $('.options dd'); if (options.length == 1) { return; } e.target.parentNode.remove(); addPlusButton(); } function addPlusButton () { var options = $('.options dd'); var btn = addBtn.cloneNode(); btn.onclick = addOption; if (options.length < 30) { let last = options[options.length - 1]; if (last.querySelector('.add')) { return; } last.appendChild(btn); } } |