Comment: | Implement dynamic field delete and edit |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | dev |
Files: | files | file ages | folders |
SHA3-256: |
3e93b9feec29274e0a053e19e7aec3fe |
User & Date: | bohwaz on 2022-03-15 00:42:53 |
Other Links: | branch diff | manifest | tags |
2022-03-16
| ||
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 | |
00:42 | Remove uninstalled presets check-in: 0c4a61cd92 user: bohwaz tags: dev | |
Modified src/include/data/1.2.0_migration.sql from [72795b0652] to [66b98794ee].
︙ | ︙ | |||
50 51 52 53 54 55 56 | DELETE FROM config WHERE key IN ('champs_membres', 'champ_identite', 'champ_identifiant'); -- Seems that some installations had this leftover? Let's just drop it. DROP TABLE IF EXISTS srs_old; -- Drop membres DROP TABLE IF EXISTS membres; | < < < < < | 50 51 52 53 54 55 56 | DELETE FROM config WHERE key IN ('champs_membres', 'champ_identite', 'champ_identifiant'); -- Seems that some installations had this leftover? Let's just drop it. DROP TABLE IF EXISTS srs_old; -- Drop membres DROP TABLE IF EXISTS membres; |
Modified src/include/init.php from [dcc287710a] to [ddc6c0aeef].
︙ | ︙ | |||
339 340 341 342 343 344 345 | if (ERRORS_REPORT_URL) { ErrorManager::setRemoteReporting(ERRORS_REPORT_URL, true); } ErrorManager::setProductionErrorTemplate(defined('Garradin\ERRORS_TEMPLATE') && ERRORS_TEMPLATE ? ERRORS_TEMPLATE : '<!DOCTYPE html><html><head><title>Erreur interne</title> <style type="text/css"> | | | | 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 | if (ERRORS_REPORT_URL) { ErrorManager::setRemoteReporting(ERRORS_REPORT_URL, true); } ErrorManager::setProductionErrorTemplate(defined('Garradin\ERRORS_TEMPLATE') && ERRORS_TEMPLATE ? ERRORS_TEMPLATE : '<!DOCTYPE html><html><head><title>Erreur interne</title> <style type="text/css"> body {font-family: sans-serif; background: #fff; } code, p, h1 { max-width: 400px; margin: 1em auto; display: block; } code { text-align: right; color: #666; } a { color: blue; } form { text-align: center; } </style></head><body><h1>Erreur interne</h1><p>Désolé mais le serveur a rencontré une erreur interne et ne peut répondre à votre requête. Merci de ré-essayer plus tard.</p> <p>Si vous suspectez un bug dans Garradin, vous pouvez suivre <a href="https://fossil.kd2.org/garradin/wiki?name=Rapporter+un+bug&p">ces instructions</a> pour le rapporter.</p> <if(sent)><p>Un-e responsable a été notifié-e et cette erreur sera corrigée dès que possible.</p></if> <if(logged)><code>L\'erreur a été enregistrée dans les journaux système (error.log) sous la référence : <b>{$ref}</b></code></if> <p><a href="' . WWW_URL . '">← Retour à la page d\'accueil</a></p> </body></html>'); ErrorManager::setHtmlHeader('<!DOCTYPE html><meta charset="utf-8" /><style type="text/css"> body { font-family: sans-serif; background: #fff; } * { margin: 0; padding: 0; } u, code b, i, h3 { font-style: normal; font-weight: normal; text-decoration: none; } #icn { color: #fff; font-size: 2em; float: right; margin: 1em; padding: 1em; background: #900; border-radius: 50%; } section header { background: #fdd; padding: 1em; } section article { margin: 1em; } section article h3, section article h4 { font-size: 1em; font-family: mono; } code { border: 1px dotted #ccc; display: block; } code b { margin-right: 1em; color: #999; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Users/DynamicField.php from [7162a58dd1] to [0e567ce101].
︙ | ︙ | |||
46 47 48 49 50 51 52 | * Index of row in users list */ protected ?int $list_row; /** * Multiple options (JSON) for select and multiple fields */ | | | 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | * Index of row in users list */ protected ?int $list_row; /** * Multiple options (JSON) for select and multiple fields */ protected ?array $options = []; /** * Default value */ protected ?string $default_value; /** |
︙ | ︙ | |||
73 74 75 76 77 78 79 80 81 82 83 84 85 86 | const TYPES = [ 'email' => 'Adresse E-Mail', 'url' => 'Adresse URL', 'checkbox' => 'Case à cocher', 'date' => 'Date', 'datetime' => 'Date et heure', 'file' => 'Fichier', 'password' => 'Mot de passe', 'number' => 'Nombre', 'tel' => 'Numéro de téléphone', 'select' => 'Sélecteur à choix unique', 'multiple' => 'Sélecteur à choix multiple', 'country' => 'Sélecteur de pays', | > | 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | const TYPES = [ 'email' => 'Adresse E-Mail', 'url' => 'Adresse URL', 'checkbox' => 'Case à cocher', 'date' => 'Date', 'datetime' => 'Date et heure', 'month' => 'Mois et année', 'file' => 'Fichier', 'password' => 'Mot de passe', 'number' => 'Nombre', 'tel' => 'Numéro de téléphone', 'select' => 'Sélecteur à choix unique', 'multiple' => 'Sélecteur à choix multiple', 'country' => 'Sélecteur de pays', |
︙ | ︙ | |||
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | 'textarea' => 'TEXT', ]; const SQL_CONSTRAINTS = [ 'checkbox' => '%1s = 1 OR %1s = 0', 'date' => '%1s IS NULL OR (date(%1$s) IS NOT NULL AND date(%1s) = %1$s)', 'datetime' => '%1s IS NULL OR (date(%1$s) IS NOT NULL AND date(%1s) = %1$s)', ]; const SYSTEM_FIELDS = [ 'id' => '?int', 'id_category' => 'int', 'pgp_key' => '?string', 'otp_secret' => '?string', 'date_login' => '?DateTime', 'date_created' => '?date', ]; public function delete(): bool { if ($this->system) { throw new ValidationException('Ce champ est utilisé en interne, il n\'est pas possible de le supprimer'); } return parent::delete(); } public function selfCheck(): void { $this->name = strtolower($this->name); | > > > > > > > | 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | 'textarea' => 'TEXT', ]; const SQL_CONSTRAINTS = [ 'checkbox' => '%1s = 1 OR %1s = 0', 'date' => '%1s IS NULL OR (date(%1$s) IS NOT NULL AND date(%1s) = %1$s)', 'datetime' => '%1s IS NULL OR (date(%1$s) IS NOT NULL AND date(%1s) = %1$s)', 'month' => '%1s IS NULL OR (date(%1s || \'-03\') = %1$s || \'-03\')', // Use 3rd day to avoid any potential issue with timezones ]; const SYSTEM_FIELDS = [ 'id' => '?int', 'id_category' => 'int', 'pgp_key' => '?string', 'otp_secret' => '?string', 'date_login' => '?DateTime', 'date_created' => '?date', ]; public function delete(): bool { if ($this->system) { throw new ValidationException('Ce champ est utilisé en interne, il n\'est pas possible de le supprimer'); } if ($this->type == 'file') { foreach (Files::glob(File::CONTEXT_USER . '/*/' . $this->name) as $file) { $file->delete(); } } return parent::delete(); } public function selfCheck(): void { $this->name = strtolower($this->name); |
︙ | ︙ |
Modified src/include/lib/Garradin/Entities/Users/User.php from [0932410200] to [73f2b6b410].
︙ | ︙ | |||
106 107 108 109 110 111 112 113 114 115 116 117 118 119 | if ($session->isLogged()) { $user = $session->getUser(); if ($user->id == $this->id) { throw new UserException('Il n\'est pas possible de supprimer son propre compte. Merci de demander à un administrateur de le faire.'); } } return parent::delete(); } public function category(): Category { return Categories::get($this->id_category); | > > | 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | if ($session->isLogged()) { $user = $session->getUser(); if ($user->id == $this->id) { throw new UserException('Il n\'est pas possible de supprimer son propre compte. Merci de demander à un administrateur de le faire.'); } } Files::delete($this->attachementsDirectory()); return parent::delete(); } public function category(): Category { return Categories::get($this->id_category); |
︙ | ︙ |
Modified src/include/lib/Garradin/Upgrade.php from [70de025310] to [95ffb05e25].
︙ | ︙ | |||
270 271 272 273 274 275 276 | $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(); // Migrate other stuff | < < < < < | 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | $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(); // Migrate other stuff $db->import(ROOT . '/include/data/1.2.0_migration.sql'); $db->commitSchemaUpdate(); } // Réinstaller les plugins système si nécessaire Plugin::checkAndInstallSystemPlugins(); |
︙ | ︙ |
Modified src/templates/admin/config/_menu.tpl from [ab9107d0d2] to [b82fb8771c].
1 2 3 4 5 6 7 | <nav class="tabs"> <ul> <li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">Général</a></li> <li{if $current == 'custom'} class="current"{/if}><a href="{$admin_url}config/custom.php">Personnalisation</a></li> <li{if $current == 'categories'} class="current"{/if}><a href="{$admin_url}config/categories/">Catégories de membres</a></li> <li{if $current == 'fields'} class="current"{/if}><a href="{$admin_url}config/fields/">Fiche des membres</a></li> <li{if $current == 'backup'} class="current"{/if}><a href="{$admin_url}config/backup/">Sauvegardes</a></li> | > | 1 2 3 4 5 6 7 8 | {if !$dialog} <nav class="tabs"> <ul> <li{if $current == 'index'} class="current"{/if}><a href="{$admin_url}config/">Général</a></li> <li{if $current == 'custom'} class="current"{/if}><a href="{$admin_url}config/custom.php">Personnalisation</a></li> <li{if $current == 'categories'} class="current"{/if}><a href="{$admin_url}config/categories/">Catégories de membres</a></li> <li{if $current == 'fields'} class="current"{/if}><a href="{$admin_url}config/fields/">Fiche des membres</a></li> <li{if $current == 'backup'} class="current"{/if}><a href="{$admin_url}config/backup/">Sauvegardes</a></li> |
︙ | ︙ | |||
18 19 20 21 22 23 24 | {if SQL_DEBUG} <li{if $sub_current == 'sql_debug'} class="current"{/if}><a href="{$admin_url}config/advanced/sql_debug.php">Journal SQL</a></li> {/if} {/if} </ul> {/if} </nav> | > | 19 20 21 22 23 24 25 26 | {if SQL_DEBUG} <li{if $sub_current == 'sql_debug'} class="current"{/if}><a href="{$admin_url}config/advanced/sql_debug.php">Journal SQL</a></li> {/if} {/if} </ul> {/if} </nav> {/if} |
Modified src/templates/admin/config/categories/index.tpl from [885e1ad798] to [1057f70315].
︙ | ︙ | |||
15 16 17 18 19 20 21 | <th>{$cat.name}</th> <td class="num">{$cat.count}</td> <td class="permissions"> {display_permissions permissions=$cat} </td> <td class="actions"> {if $cat.id != $logged_user.id_category} | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <th>{$cat.name}</th> <td class="num">{$cat.count}</td> <td class="permissions"> {display_permissions permissions=$cat} </td> <td class="actions"> {if $cat.id != $logged_user.id_category} {linkbutton shape="delete" label="Supprimer" href="supprimer.php?id=%d"|args:$cat.id target="_dialog"} {/if} {linkbutton shape="edit" label="Modifier" href="modifier.php?id=%d"|args:$cat.id} {linkbutton shape="users" label="Liste des membres" href="!membres/?cat=%d"|args:$cat.id} </td> </tr> {/foreach} </tbody> |
︙ | ︙ |
Added src/templates/admin/config/fields/delete.tpl version [79517ab2b0].
> > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 | {include file="admin/_head.tpl" title="Supprimer un champ" current="config"} {include file="common/delete_form.tpl" legend="Supprimer ce champ ?" confirm="Cocher cette case pour supprimer le champ, cela effacera de manière permanente cette donnée de toutes les fiches membres." warning="Êtes-vous sûr de vouloir supprimer le champ « %s » ?"|args:$field.label alert="Attention, ce champ ainsi que les données qu'il contient seront supprimés de toutes les fiches membres existantes." } {include file="admin/_foot.tpl"} |
Added src/templates/admin/config/fields/edit.tpl version [4e4c6cc5a2].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | <?php $title = $field->exists() ? 'Modifier un champ' : 'Ajouter un champ'; ?> {include file="admin/_head.tpl" current="config" title=$title} {include file="admin/config/_menu.tpl" current="fields"} {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>{$title}</legend> {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" help="Il ne sera plus possible de modifier le type une fois le champ créé." required=true} {else} {input type="select" name="type" options=$field::TYPES source=$field label="Type" help="Il n'est plus possible de modifier le titre." disabled=true} {/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 comme 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"} <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"} <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 aux personnes qui gèrent les membres" source=$field help="Le membre pourra modifier cette information en se connectant"} </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> <p class="submit"> {csrf_field key=$csrf_key} {linkbutton label="Annuler les changements" shape="left" href="./"} {button type="submit" name="save" label="Enregistrer les changements" shape="right" class="main"} </p> </form> <script type="text/javascript"> {literal} 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); } } {/literal} </script> {include file="admin/_foot.tpl"} |
Modified src/templates/admin/config/fields/index.tpl from [aba9e76cf1] to [2b943be0f1].
1 2 3 4 5 | {include file="admin/_head.tpl" current="config" title="Fiche des membres"} {include file="admin/config/_menu.tpl" current="fields"} <nav class="tabs"> | | | | | | > > > > | 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 | {include file="admin/_head.tpl" current="config" title="Fiche des membres"} {include file="admin/config/_menu.tpl" current="fields"} <nav class="tabs"> {linkbutton shape="plus" label="Ajouter un champ" href="edit.php"} </nav> {if $_GET.msg == 'ORDER_SAVED'} <p class="block confirm"> L'ordre a bien été enregistré. </p> {elseif $_GET.msg == 'SAVED'} <p class="block confirm"> Les modifications ont bien été enregistrées. </p> {elseif $_GET.msg == 'DELETED'} <p class="block confirm"> Le champ a bien été supprimé. </p> {/if} {form_errors} <form method="post" action="{$self_url_no_qs}"> <table class="list"> |
︙ | ︙ | |||
43 44 45 46 47 48 49 50 51 52 | {/if} {linkbutton shape="edit" label="Modifier" href="edit.php?id=%d"|args:$field.id target="_dialog"} </td> </tr> {/foreach} </tbody> </table> <p class="submit"> {csrf_field key=$csrf_key} | > > > > | < < < < | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | {/if} {linkbutton shape="edit" label="Modifier" href="edit.php?id=%d"|args:$field.id target="_dialog"} </td> </tr> {/foreach} </tbody> </table> <p class="help"> Cliquer et glisser-déposer sur une ligne pour en changer l'ordre. </p> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="save" label="Enregistrer l'ordre" shape="right"} </p> </form> <script type="text/javascript" src="{$admin_url}static/scripts/dragdrop-table.js"></script> {include file="admin/_foot.tpl"} |
Added src/www/admin/config/fields/delete.php version [bbd2f22de5].
> > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | <?php namespace Garradin; use Garradin\Users\DynamicFields; require_once __DIR__ . '/../_inc.php'; $csrf_key = 'change_fields_delete'; $fields = DynamicFields::getInstance(); $field = $fields->fieldById((int)qg('id')); if (!$field) { throw new UserException('Le champ indiqué n\'existe pas.'); } $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'); |
Added src/www/admin/config/fields/edit.php version [4f4bd678e4].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | <?php namespace Garradin; use Garradin\Entities\Users\DynamicField; use Garradin\Users\DynamicFields; require_once __DIR__ . '/../_inc.php'; $csrf_key = 'change_fields_edit_' . (int)qg('id'); $fields = DynamicFields::getInstance(); if (qg('id')) { $field = $fields->fieldById((int)qg('id')); } else { $field = new DynamicField; } if (!$field) { throw new UserException('Le champ indiqué n\'existe pas.'); } $form->runIf('save', function () use ($field) { $field->importForm(); $field->save(); }, $csrf_key, '!config/fields/?msg=SAVED'); $tpl->assign(compact('csrf_key', 'field')); $tpl->display('admin/config/fields/edit.tpl'); |